A framework for building convergent cross-platform Nextcloud clients using Flutter.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

391 lines
12 KiB

import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:dynamite_runtime/http_client.dart';
import 'package:meta/meta.dart';
import 'package:nextcloud/src/webdav/props.dart';
import 'package:nextcloud/src/webdav/webdav.dart';
import 'package:universal_io/io.dart';
import 'package:xml/xml.dart' as xml;
2 years ago
/// Base path used on the server
const String webdavBasePath = '/remote.php/webdav';
2 years ago
/// WebDavClient class
class WebDavClient {
// ignore: public_member_api_docs
WebDavClient(this.rootClient);
2 years ago
// ignore: public_member_api_docs
final DynamiteClient rootClient;
2 years ago
Future<HttpClientResponse> _send(
final String method,
final Uri url, {
final Stream<Uint8List>? dataStream,
final Uint8List? data,
2 years ago
final Map<String, String>? headers,
}) async {
assert(dataStream == null || data == null, 'Only one of dataStream or data can be specified.');
final request = await HttpClient().openUrl(method, url)
2 years ago
..persistentConnection = true;
for (final header in {
HttpHeaders.contentTypeHeader: 'application/xml',
...?rootClient.baseHeaders,
...?headers,
...?rootClient.authentications.firstOrNull?.headers,
2 years ago
}.entries) {
request.headers.add(header.key, header.value);
}
if (data != null) {
request.add(data);
}
if (dataStream != null) {
await request.addStream(dataStream);
2 years ago
}
final response = await request.close();
if (response.statusCode > 299) {
throw DynamiteApiException(
response.statusCode,
response.responseHeaders,
await response.body,
);
2 years ago
}
return response;
}
Uri _constructUri([final Uri? path]) => constructUri(rootClient.baseURL, path);
@visibleForTesting
// ignore: public_member_api_docs
static Uri constructUri(final Uri baseURL, [final Uri? path]) {
assert(
path == null || path.path == '/' || !path.path.startsWith('/'),
"The path should not start a '/' unless indicating the root folder.",
);
assert(!baseURL.path.endsWith('/'), "The baseURL should not end with a '/'.");
final pathBuffer = StringBuffer(baseURL.path)..write(webdavBasePath);
if (path != null && path.path != '/') {
pathBuffer
..write('/')
..write(path.path);
}
return baseURL.replace(path: pathBuffer.toString());
}
2 years ago
Future<WebDavMultistatus> _parseResponse(final HttpClientResponse response) async =>
WebDavMultistatus.fromXmlElement(xml.XmlDocument.parse(await response.body).rootElement);
Map<String, String> _getUploadHeaders({
required final DateTime? lastModified,
required final DateTime? created,
required final int? contentLength,
}) =>
{
if (lastModified != null) 'X-OC-Mtime': (lastModified.millisecondsSinceEpoch ~/ 1000).toString(),
if (created != null) 'X-OC-CTime': (created.millisecondsSinceEpoch ~/ 1000).toString(),
if (contentLength != null) HttpHeaders.contentLengthHeader: contentLength.toString(),
};
/// Gets the WebDAV capabilities of the server.
Future<WebDavOptions> options() async {
2 years ago
final response = await _send(
'OPTIONS',
_constructUri(),
2 years ago
);
final davCapabilities = response.headers['dav']?.first ?? '';
final davSearchCapabilities = response.headers['dasl']?.first ?? '';
return WebDavOptions(
2 years ago
davCapabilities.split(',').map((final e) => e.trim()).where((final e) => e.isNotEmpty).toSet(),
davSearchCapabilities.split(',').map((final e) => e.trim()).where((final e) => e.isNotEmpty).toSet(),
);
}
/// Creates a collection at [path].
///
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL for more information.
Future<HttpClientResponse> mkcol(final Uri path) async => _send(
'MKCOL',
_constructUri(path),
);
2 years ago
/// Deletes the resource at [path].
///
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_DELETE for more information.
Future<HttpClientResponse> delete(final Uri path) => _send(
2 years ago
'DELETE',
_constructUri(path),
2 years ago
);
/// Puts a new file at [path] with [localData] as content.
///
/// [lastModified] sets the date when the file was last modified on the server.
/// [created] sets the date when the file was created on the server.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information.
Future<HttpClientResponse> put(
final Uint8List localData,
final Uri path, {
final DateTime? lastModified,
final DateTime? created,
}) =>
_send(
'PUT',
_constructUri(path),
data: localData,
headers: _getUploadHeaders(
lastModified: lastModified,
created: created,
contentLength: null,
),
2 years ago
);
/// Puts a new file at [path] with [localData] as content.
///
/// [lastModified] sets the date when the file was last modified on the server.
/// [created] sets the date when the file was created on the server.
/// [contentLength] sets the length of the [localData] that is uploaded.
/// [onProgress] can be used to watch the upload progress. Possible values range from 0.0 to 1.0. [contentLength] needs to be set for it to work.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information.
Future<HttpClientResponse> putStream(
final Stream<Uint8List> localData,
final Uri path, {
final DateTime? lastModified,
final DateTime? created,
final int? contentLength,
final void Function(double progress)? onProgress,
}) async {
var uploaded = 0;
return _send(
'PUT',
_constructUri(path),
dataStream: contentLength != null
? localData.map((final chunk) {
uploaded += chunk.length;
onProgress?.call(uploaded / contentLength);
return chunk;
})
: localData,
headers: _getUploadHeaders(
lastModified: lastModified,
created: created,
contentLength: contentLength,
),
);
}
2 years ago
/// Puts a new file at [path] with [file] as content.
///
/// [lastModified] sets the date when the file was last modified on the server.
/// [created] sets the date when the file was created on the server.
/// [onProgress] can be used to watch the upload progress. Possible values range from 0.0 to 1.0.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information.
Future<HttpClientResponse> putFile(
final File file,
final FileStat fileStat,
final Uri path, {
final DateTime? lastModified,
final DateTime? created,
final void Function(double progress)? onProgress,
}) async =>
putStream(
file.openRead().cast<Uint8List>(),
path,
lastModified: lastModified,
created: created,
contentLength: fileStat.size,
onProgress: onProgress,
);
/// Gets the content of the file at [path].
Future<Uint8List> get(final Uri path) async => (await getStream(path)).bodyBytes;
2 years ago
/// Gets the content of the file at [path].
Future<HttpClientResponse> getStream(final Uri path) async => _send(
2 years ago
'GET',
_constructUri(path),
2 years ago
);
/// Gets the content of the file at [path].
Future<void> getFile(
final Uri path,
final File file, {
final void Function(double progress)? onProgress,
}) async {
final sink = file.openWrite();
final response = await getStream(path);
if (response.contentLength > 0) {
final completer = Completer<void>();
var downloaded = 0;
response.listen((final chunk) async {
sink.add(chunk);
downloaded += chunk.length;
onProgress?.call(downloaded / response.contentLength);
if (downloaded >= response.contentLength) {
completer.complete();
}
});
await completer.future;
}
await sink.close();
}
/// Retrieves the props for the resource at [path].
2 years ago
///
/// Optionally populates the given [prop]s on the returned resources.
/// [depth] can be used to limit scope of the returned resources.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPFIND for more information.
Future<WebDavMultistatus> propfind(
final Uri path, {
final WebDavPropWithoutValues? prop,
final WebDavDepth? depth,
}) async =>
_parseResponse(
await _send(
'PROPFIND',
_constructUri(path),
data: utf8.encode(
WebDavPropfind(prop: prop ?? WebDavPropWithoutValues()).toXmlElement(namespaces: namespaces).toXmlString(),
) as Uint8List,
headers: depth != null ? {'Depth': depth.value} : null,
),
);
2 years ago
/// Runs the filter-files report with the [filterRules] on the resource at [path].
2 years ago
///
/// Optionally populates the [prop]s on the returned resources.
/// See https://github.com/owncloud/docs/issues/359 for more information.
Future<WebDavMultistatus> report(
final Uri path,
final WebDavOcFilterRules filterRules, {
final WebDavPropWithoutValues? prop,
}) async =>
_parseResponse(
await _send(
'REPORT',
_constructUri(path),
data: utf8.encode(
WebDavOcFilterFiles(
filterRules: filterRules,
prop: prop ?? WebDavPropWithoutValues(), // coverage:ignore-line
).toXmlElement(namespaces: namespaces).toXmlString(),
) as Uint8List,
),
);
2 years ago
/// Updates the props of the resource at [path].
2 years ago
///
/// The props in [set] will be updated.
/// The props in [remove] will be removed.
2 years ago
/// Returns true if the update was successful.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPPATCH for more information.
Future<bool> proppatch(
final Uri path, {
final WebDavProp? set,
final WebDavPropWithoutValues? remove,
}) async {
2 years ago
final response = await _send(
'PROPPATCH',
_constructUri(path),
data: utf8.encode(
WebDavPropertyupdate(
set: set != null ? WebDavSet(prop: set) : null,
remove: remove != null ? WebDavRemove(prop: remove) : null,
).toXmlElement(namespaces: namespaces).toXmlString(),
) as Uint8List,
2 years ago
);
final data = await _parseResponse(response);
for (final a in data.responses) {
for (final b in a.propstats) {
if (!b.status.contains('200')) {
return false;
}
}
}
return true;
2 years ago
}
/// Moves the resource from [sourcePath] to [destinationPath].
///
/// If [overwrite] is set any existing resource will be replaced.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_MOVE for more information.
2 years ago
Future<HttpClientResponse> move(
final Uri sourcePath,
final Uri destinationPath, {
2 years ago
final bool overwrite = false,
}) =>
_send(
'MOVE',
_constructUri(sourcePath),
2 years ago
headers: {
'Destination': _constructUri(destinationPath).toString(),
2 years ago
'Overwrite': overwrite ? 'T' : 'F',
},
);
/// Copies the resource from [sourcePath] to [destinationPath].
///
/// If [overwrite] is set any existing resource will be replaced.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_COPY for more information.
2 years ago
Future<HttpClientResponse> copy(
final Uri sourcePath,
final Uri destinationPath, {
2 years ago
final bool overwrite = false,
}) =>
_send(
'COPY',
_constructUri(sourcePath),
2 years ago
headers: {
'Destination': _constructUri(destinationPath).toString(),
2 years ago
'Overwrite': overwrite ? 'T' : 'F',
},
);
}
/// WebDAV capabilities
class WebDavOptions {
2 years ago
/// Creates a new WebDavStatus.
WebDavOptions(
2 years ago
this.capabilities,
this.searchCapabilities,
);
/// DAV capabilities as advertised by the server in the 'dav' header.
Set<String> capabilities;
/// DAV search and locating capabilities as advertised by the server in the 'dasl' header.
2 years ago
Set<String> searchCapabilities;
}
/// Depth used for [WebDavClient.propfind].
///
/// See http://www.webdav.org/specs/rfc2518.html#HEADER_Depth for more information.
enum WebDavDepth {
/// Returns props of the resource.
zero('0'),
/// Returns props of the resource and its immediate children.
///
/// Only works on collections and returns the same as [WebDavDepth.zero] for other resources.
one('1'),
/// Returns props of the resource and all its progeny.
///
/// Only works on collections and returns the same as [WebDavDepth.zero] for other resources.
infinity('infinity');
const WebDavDepth(this.value);
// ignore: public_member_api_docs
final String value;
}