part of '../../nextcloud.dart'; /// WebDavClient class class WebDavClient { // ignore: public_member_api_docs WebDavClient( this.rootClient, this.basePath, ); // ignore: public_member_api_docs final Client rootClient; /// Base path used on the server final String basePath; /// XML namespaces supported by this client. /// /// For Nextcloud namespaces see [WebDav/Requesting properties](https://docs.nextcloud.com/server/latest/developer_manual/client_apis/WebDAV/basic.html#requesting-properties). final Map namespaces = { 'DAV:': 'd', 'http://owncloud.org/ns': 'oc', 'http://nextcloud.org/ns': 'nc', 'http://open-collaboration-services.org/ns': 'ocs', 'http://open-cloud-mesh.org/ns': 'ocm', 'http://sabredav.org/ns': 's', // mostly used in error responses }; Future _send( final String method, final String url, final List expectedCodes, { final Stream? data, final Map? headers, }) async { final request = await HttpClient().openUrl( method, Uri.parse(url), ) ..followRedirects = false ..persistentConnection = true; for (final header in { HttpHeaders.contentTypeHeader: 'application/xml', ...rootClient.baseHeaders, if (headers != null) ...headers, }.entries) { request.headers.add(header.key, header.value); } if (data != null) { await request.addStream(data); } final response = await request.close(); if (!expectedCodes.contains(response.statusCode)) { throw ApiException( response.statusCode, {}, // TODO await response.bodyBytes, ); } return response; } /// Registers a custom namespace for properties. /// /// Requires a unique [namespaceUri] and [prefix]. void registerNamespace(final String namespaceUri, final String prefix) => namespaces.putIfAbsent(namespaceUri, () => prefix); String _constructPath([final String? path]) => [ rootClient.baseURL, basePath, if (path != null) ...[ path, ], ] .map((part) { while (part.startsWith('/')) { part = part.substring(1); } while (part.endsWith('/')) { part = part.substring(0, part.length - 1); } return part; }) .where((final part) => part.isNotEmpty) .join('/'); String _buildPropsRequest(final Set props) { final builder = xml.XmlBuilder(); builder ..processing('xml', 'version="1.0"') ..element( 'd:propfind', nest: () { namespaces.forEach(builder.namespace); builder.element( 'd:prop', nest: () { props.forEach(builder.element); }, ); }, ); return builder.buildDocument().toString(); } /// returns the WebDAV capabilities of the server Future status() async { final response = await _send( 'OPTIONS', _constructPath(), [200], ); final davCapabilities = response.headers['dav']?.cast().first ?? ''; final davSearchCapabilities = response.headers['dasl']?.cast().first ?? ''; return WebDavStatus( 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(), ); } /// make a dir with [remotePath] under current dir Future mkdir( final String remotePath, { final bool safe = true, }) async { final expectedCodes = [ 201, if (safe) ...[ 301, 405, ], ]; return _send( 'MKCOL', _constructPath(remotePath), expectedCodes, ); } /// just like mkdir -p Future mkdirs( final String remotePath, { final bool safe = true, }) async { final dirs = remotePath.trim().split('/')..removeWhere((final value) => value == ''); if (dirs.isEmpty) { return null; } if (remotePath.trim().startsWith('/')) { dirs[0] = '/${dirs[0]}'; } final prevPath = StringBuffer(); late HttpClientResponse response; for (final dir in dirs) { response = await mkdir( '$prevPath/$dir', safe: safe, ); prevPath.write('/$dir'); } return response; } /// remove dir with given [path] Future delete(final String path) => _send( 'DELETE', _constructPath(path), [204], ); /// upload a new file with [localData] as content to [remotePath] Future upload(final Uint8List localData, final String remotePath) => _send( 'PUT', _constructPath(remotePath), [200, 201, 204], data: Stream.value(localData), ); /// upload a new file with [localData] as content to [remotePath] Future uploadStream(final Stream localData, final String remotePath) async => _send( 'PUT', _constructPath(remotePath), [200, 201, 204], data: localData, ); /// download [remotePath] and store the response file contents to String Future download(final String remotePath) async => Uint8List.fromList( (await (await _send( 'GET', _constructPath(remotePath), [200], )) .join()) .codeUnits, ); /// download [remotePath] and store the response file contents to ByteStream Future downloadStream(final String remotePath) async => _send( 'GET', _constructPath(remotePath), [200], ); /// list the directories and files under given [remotePath]. /// /// Optionally populates the given [props] on the returned files. Future> ls( final String remotePath, { final Set? props, }) async { final response = await _send( 'PROPFIND', _constructPath(remotePath), [207, 301], data: Stream.value(Uint8List.fromList(utf8.encode(_buildPropsRequest(props ?? {})))), ); if (response.statusCode == 301) { return ls(response.headers['location']!.first); } return treeFromWebDavXml( basePath, namespaces, await response.body, )..removeAt(0); } /// Runs the filter-files report with the given [propFilters] on the /// [remotePath]. /// /// Optionally populates the given [props] on the returned files. Future> filter( final String remotePath, final Map propFilters, { final Set props = const {}, }) async { final builder = xml.XmlBuilder(); builder ..processing('xml', 'version="1.0"') ..element( 'oc:filter-files', nest: () { namespaces.forEach(builder.namespace); builder ..element( 'oc:filter-rules', nest: () { propFilters.forEach((final key, final value) { builder.element( key, nest: () { builder.text(value); }, ); }); }, ) ..element( 'd:prop', nest: () { props.forEach(builder.element); }, ); }, ); final response = await _send( 'REPORT', _constructPath(remotePath), [200, 207], data: Stream.value(Uint8List.fromList(utf8.encode(builder.buildDocument().toString()))), ); return treeFromWebDavXml( basePath, namespaces, await response.body, ); } /// Retrieves properties for the given [remotePath]. /// /// Populates all available properties by default, but a reduced set can be /// specified via [props]. Future getProps( final String remotePath, { final Set? props, }) async { final response = await _send( 'PROPFIND', _constructPath(remotePath), [200, 207], data: Stream.value(Uint8List.fromList(utf8.encode(_buildPropsRequest(props ?? {})))), headers: {'Depth': '0'}, ); return fileFromWebDavXml( basePath, namespaces, await response.body, ); } /// Update (string) properties of the given [remotePath]. /// /// Returns true if the update was successful. Future updateProps( final String remotePath, final Map props, ) async { final builder = xml.XmlBuilder(); builder ..processing('xml', 'version="1.0"') ..element( 'd:propertyupdate', nest: () { namespaces.forEach(builder.namespace); builder.element( 'd:set', nest: () { builder.element( 'd:prop', nest: () { props.forEach((final key, final value) { builder.element( key, nest: () { builder.text(value); }, ); }); }, ); }, ); }, ); final response = await _send( 'PROPPATCH', _constructPath(remotePath), [200, 207], data: Stream.value(Uint8List.fromList(utf8.encode(builder.buildDocument().toString()))), ); return checkUpdateFromWebDavXml(await response.body); } /// Move a file from [sourcePath] to [destinationPath] Future move( final String sourcePath, final String destinationPath, { final bool overwrite = false, }) => _send( 'MOVE', _constructPath(sourcePath), [200, 201, 204], headers: { 'Destination': _constructPath(destinationPath), 'Overwrite': overwrite ? 'T' : 'F', }, ); /// Copy a file from [sourcePath] to [destinationPath] Future copy( final String sourcePath, final String destinationPath, { final bool overwrite = false, }) => _send( 'COPY', _constructPath(sourcePath), [200, 201, 204], headers: { 'Destination': _constructPath(destinationPath), 'Overwrite': overwrite ? 'T' : 'F', }, ); } /// WebDAV server status. class WebDavStatus { /// Creates a new WebDavStatus. WebDavStatus( this.capabilities, this.searchCapabilities, ); /// DAV capabilities as advertised by the server in the 'dav' header. Set capabilities; /// DAV search and locating capabilities as advertised by the server in the /// 'dasl' header. Set searchCapabilities; }