diff --git a/packages/neon/neon_files/lib/blocs/browser.dart b/packages/neon/neon_files/lib/blocs/browser.dart index 8b379847..933a1464 100644 --- a/packages/neon/neon_files/lib/blocs/browser.dart +++ b/packages/neon/neon_files/lib/blocs/browser.dart @@ -44,9 +44,9 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents client.id, 'files-${path.value.join('/')}', files, - () async => client.webdav.ls( + () async => client.webdav.propfind( path.value.join('/'), - prop: WebDavPropfindProp.fromBools( + prop: WebDavPropWithoutValues.fromBools( davgetcontenttype: true, davgetetag: true, davgetlastmodified: true, @@ -69,11 +69,6 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents @override void createFolder(final List path) { - wrapAction( - () async => client.webdav.mkdir( - path.join('/'), - safe: false, - ), - ); + wrapAction(() async => client.webdav.mkcol(path.join('/'))); } } diff --git a/packages/neon/neon_files/lib/blocs/files.dart b/packages/neon/neon_files/lib/blocs/files.dart index 47213f79..005d74f6 100644 --- a/packages/neon/neon_files/lib/blocs/files.dart +++ b/packages/neon/neon_files/lib/blocs/files.dart @@ -65,7 +65,12 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta @override void addFavorite(final List path) { - wrapAction(() async => client.webdav.updateProps(path.join('/'), WebDavProp(ocfavorite: 1))); + wrapAction( + () async => client.webdav.proppatch( + path.join('/'), + set: WebDavProp(ocfavorite: 1), + ), + ); } @override @@ -119,9 +124,9 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta @override void removeFavorite(final List path) { wrapAction( - () async => client.webdav.updateProps( + () async => client.webdav.proppatch( path.join('/'), - WebDavProp(ocfavorite: 0), + set: WebDavProp(ocfavorite: 0), ), ); } diff --git a/packages/neon/neon_files/lib/utils/download_task.dart b/packages/neon/neon_files/lib/utils/download_task.dart index 0e509f2c..424fc04a 100644 --- a/packages/neon/neon_files/lib/utils/download_task.dart +++ b/packages/neon/neon_files/lib/utils/download_task.dart @@ -13,7 +13,7 @@ class DownloadTask { Future execute(final NextcloudClient client, final IOSink sink) async { final completer = Completer(); - final response = await client.webdav.downloadStream(path.join('/')); + final response = await client.webdav.getStream(path.join('/')); var downloaded = 0; response.listen((final chunk) async { diff --git a/packages/neon/neon_files/lib/utils/upload_task.dart b/packages/neon/neon_files/lib/utils/upload_task.dart index 9299b0d2..905f9caf 100644 --- a/packages/neon/neon_files/lib/utils/upload_task.dart +++ b/packages/neon/neon_files/lib/utils/upload_task.dart @@ -16,7 +16,7 @@ class UploadTask { Future execute(final NextcloudClient client, final Stream> stream) async { var uploaded = 0; - await client.webdav.uploadStream( + await client.webdav.putStream( stream.map((final chunk) { uploaded += chunk.length; _streamController.add((uploaded / size * 100).toInt()); diff --git a/packages/nextcloud/bin/generate_props.dart b/packages/nextcloud/bin/generate_props.dart index cec196e0..35c79e82 100644 --- a/packages/nextcloud/bin/generate_props.dart +++ b/packages/nextcloud/bin/generate_props.dart @@ -29,7 +29,7 @@ void main() { "part 'props.g.dart';", '', ...generateClass( - 'WebDavPropfindProp', + 'WebDavPropWithoutValues', 'prop', 'namespaceDav', findProps, diff --git a/packages/nextcloud/lib/src/webdav/client.dart b/packages/nextcloud/lib/src/webdav/client.dart index e3bf1c9a..108ec64f 100644 --- a/packages/nextcloud/lib/src/webdav/client.dart +++ b/packages/nextcloud/lib/src/webdav/client.dart @@ -20,8 +20,7 @@ class WebDavClient { Future _send( final String method, - final String url, - final List expectedCodes, { + final String url, { final Stream? data, final Map? headers, }) async { @@ -29,7 +28,6 @@ class WebDavClient { method, Uri.parse(url), ) - ..followRedirects = false ..persistentConnection = true; for (final header in { HttpHeaders.contentTypeHeader: 'application/xml', @@ -46,7 +44,7 @@ class WebDavClient { final response = await request.close(); - if (!expectedCodes.contains(response.statusCode)) { + if (response.statusCode > 299) { throw DynamiteApiException( response.statusCode, response.responseHeaders, @@ -76,73 +74,10 @@ class WebDavClient { .where((final part) => part.isNotEmpty) .join('/'); - /// 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]}'; // coverage:ignore-line - } - 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], - ); + Future _parseResponse(final HttpClientResponse response) async => + WebDavMultistatus.fromXmlElement(xml.XmlDocument.parse(await response.body).rootElement); - Map? _generateUploadHeaders({ + Map? _getUploadHeaders({ required final DateTime? lastModified, required final DateTime? created, required final int? contentLength, @@ -161,135 +96,168 @@ class WebDavClient { return headers.isNotEmpty ? headers : null; } - /// upload a new file with [localData] as content to [remotePath] - Future upload( + /// Gets the WebDAV capabilities of the server. + Future options() async { + final response = await _send( + 'OPTIONS', + _constructPath(), + ); + final davCapabilities = response.headers['dav']?.cast().first ?? ''; + final davSearchCapabilities = response.headers['dasl']?.cast().first ?? ''; + return WebDavOptions( + 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 mkcol(final String path) async => _send( + 'MKCOL', + _constructPath(path), + ); + + /// Deletes the resource at [path]. + /// + /// See http://www.webdav.org/specs/rfc2518.html#METHOD_DELETE for more information. + Future delete(final String path) => _send( + 'DELETE', + _constructPath(path), + ); + + /// 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 put( final Uint8List localData, - final String remotePath, { + final String path, { final DateTime? lastModified, final DateTime? created, }) => - uploadStream( + putStream( Stream.value(localData), - remotePath, + path, lastModified: lastModified, created: created, contentLength: localData.lengthInBytes, ); - /// upload a new file with [localData] as content to [remotePath] - Future uploadStream( + /// 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. + /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information. + Future putStream( final Stream localData, - final String remotePath, { + final String path, { final DateTime? lastModified, final DateTime? created, final int? contentLength, }) async => _send( 'PUT', - _constructPath(remotePath), - [200, 201, 204], + _constructPath(path), data: localData, - headers: _generateUploadHeaders( + headers: _getUploadHeaders( lastModified: lastModified, created: created, contentLength: contentLength, ), ); - /// download [remotePath] and store the response file contents to String - Future download(final String remotePath) async => (await downloadStream(remotePath)).bodyBytes; + /// Gets the content of the file at [path]. + Future get(final String path) async => (await getStream(path)).bodyBytes; - /// download [remotePath] and store the response file contents to ByteStream - Future downloadStream(final String remotePath) async => _send( + /// Gets the content of the file at [path]. + Future getStream(final String path) async => _send( 'GET', - _constructPath(remotePath), - [200], + _constructPath(path), ); - Future _parseResponse(final HttpClientResponse response) async => - WebDavMultistatus.fromXmlElement(xml.XmlDocument.parse(await response.body).rootElement); - - /// list the directories and files under given [remotePath]. + /// Retrieves the props for the resource at [path]. /// /// Optionally populates the given [prop]s on the returned files. /// [depth] can be '0', '1' or 'infinity'. - Future ls( - final String remotePath, { - final WebDavPropfindProp? prop, + /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPFIND for more information. + Future propfind( + final String path, { + final WebDavPropWithoutValues? prop, final String? depth, }) async { assert(depth == null || ['0', '1', 'infinity'].contains(depth), 'Depth has to be 0, 1 or infinity'); - final response = await _send( - 'PROPFIND', - _constructPath(remotePath), - [207, 301], - data: Stream.value( - Uint8List.fromList( - utf8.encode( - WebDavPropfind(prop: prop ?? WebDavPropfindProp()).toXmlElement(namespaces: namespaces).toXmlString(), + return _parseResponse( + await _send( + 'PROPFIND', + _constructPath(path), + data: Stream.value( + Uint8List.fromList( + utf8.encode( + WebDavPropfind(prop: prop ?? WebDavPropWithoutValues()) + .toXmlElement(namespaces: namespaces) + .toXmlString(), + ), ), ), - ), - headers: { - if (depth != null) ...{ - 'Depth': depth, + headers: { + if (depth != null) ...{ + 'Depth': depth, + }, }, - }, + ), ); - if (response.statusCode == 301) { - // coverage:ignore-start - return ls( - response.headers['location']!.first, - prop: prop, - depth: depth, - ); - // coverage:ignore-end - } - return _parseResponse(response); } - /// Runs the filter-files report with the given [filterRules] on the - /// [remotePath]. + /// Runs the filter-files report with the [filterRules] on the resource at [path]. /// - /// Optionally populates the given [prop]s on the returned files. - Future filter( - final String remotePath, + /// Optionally populates the [prop]s on the returned files. + /// See https://github.com/owncloud/docs/issues/359 for more information. + Future report( + final String path, final WebDavOcFilterRules filterRules, { - final WebDavPropfindProp? prop, - }) async { - final response = await _send( - 'REPORT', - _constructPath(remotePath), - [200, 207], - data: Stream.value( - Uint8List.fromList( - utf8.encode( - WebDavOcFilterFiles( - filterRules: filterRules, - prop: prop ?? WebDavPropfindProp(), // coverage:ignore-line - ).toXmlElement(namespaces: namespaces).toXmlString(), + final WebDavPropWithoutValues? prop, + }) async => + _parseResponse( + await _send( + 'REPORT', + _constructPath(path), + data: Stream.value( + Uint8List.fromList( + utf8.encode( + WebDavOcFilterFiles( + filterRules: filterRules, + prop: prop ?? WebDavPropWithoutValues(), // coverage:ignore-line + ).toXmlElement(namespaces: namespaces).toXmlString(), + ), + ), ), ), - ), - ); - return _parseResponse(response); - } + ); - /// Update (string) properties of the given [remotePath]. + /// Updates the props of the resource at [path]. /// + /// The props in [set] will be updated. + /// The props in [remove] will be removed. /// Returns true if the update was successful. - Future updateProps( - final String remotePath, - final WebDavProp prop, - ) async { + /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPPATCH for more information. + Future proppatch( + final String path, { + final WebDavProp? set, + final WebDavPropWithoutValues? remove, + }) async { final response = await _send( 'PROPPATCH', - _constructPath(remotePath), - [200, 207], + _constructPath(path), data: Stream.value( Uint8List.fromList( utf8.encode( - WebDavPropertyupdate(set: WebDavSet(prop: prop)).toXmlElement(namespaces: namespaces).toXmlString(), + WebDavPropertyupdate( + set: set != null ? WebDavSet(prop: set) : null, + remove: remove != null ? WebDavRemove(prop: remove) : null, + ).toXmlElement(namespaces: namespaces).toXmlString(), ), ), ), @@ -305,7 +273,10 @@ class WebDavClient { return true; } - /// Move a file from [sourcePath] to [destinationPath] + /// 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. Future move( final String sourcePath, final String destinationPath, { @@ -314,14 +285,16 @@ class WebDavClient { _send( 'MOVE', _constructPath(sourcePath), - [200, 201, 204], headers: { 'Destination': _constructPath(destinationPath), 'Overwrite': overwrite ? 'T' : 'F', }, ); - /// Copy a file from [sourcePath] to [destinationPath] + /// 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. Future copy( final String sourcePath, final String destinationPath, { @@ -330,7 +303,6 @@ class WebDavClient { _send( 'COPY', _constructPath(sourcePath), - [200, 201, 204], headers: { 'Destination': _constructPath(destinationPath), 'Overwrite': overwrite ? 'T' : 'F', @@ -338,10 +310,10 @@ class WebDavClient { ); } -/// WebDAV server status. -class WebDavStatus { +/// WebDAV capabilities +class WebDavOptions { /// Creates a new WebDavStatus. - WebDavStatus( + WebDavOptions( this.capabilities, this.searchCapabilities, ); @@ -349,7 +321,6 @@ class WebDavStatus { /// 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. + /// DAV search and locating capabilities as advertised by the server in the 'dasl' header. Set searchCapabilities; } diff --git a/packages/nextcloud/lib/src/webdav/props.dart b/packages/nextcloud/lib/src/webdav/props.dart index e35e9506..0346fc28 100644 --- a/packages/nextcloud/lib/src/webdav/props.dart +++ b/packages/nextcloud/lib/src/webdav/props.dart @@ -7,8 +7,8 @@ part 'props.g.dart'; @annotation.XmlSerializable(createMixin: true) @annotation.XmlRootElement(name: 'prop', namespace: namespaceDav) -class WebDavPropfindProp with _$WebDavPropfindPropXmlSerializableMixin { - WebDavPropfindProp({ +class WebDavPropWithoutValues with _$WebDavPropWithoutValuesXmlSerializableMixin { + WebDavPropWithoutValues({ this.davgetlastmodified, this.davgetetag, this.davgetcontenttype, @@ -38,7 +38,7 @@ class WebDavPropfindProp with _$WebDavPropfindPropXmlSerializableMixin { this.ocmsharepermissions, }); - WebDavPropfindProp.fromBools({ + WebDavPropWithoutValues.fromBools({ final bool davgetlastmodified = false, final bool davgetetag = false, final bool davgetcontenttype = false, @@ -94,7 +94,8 @@ class WebDavPropfindProp with _$WebDavPropfindPropXmlSerializableMixin { ocssharepermissions = ocssharepermissions ? [null] : null, ocmsharepermissions = ocmsharepermissions ? [null] : null; - factory WebDavPropfindProp.fromXmlElement(final XmlElement element) => _$WebDavPropfindPropFromXmlElement(element); + factory WebDavPropWithoutValues.fromXmlElement(final XmlElement element) => + _$WebDavPropWithoutValuesFromXmlElement(element); @annotation.XmlElement( name: 'getlastmodified', diff --git a/packages/nextcloud/lib/src/webdav/props.g.dart b/packages/nextcloud/lib/src/webdav/props.g.dart index 3e842c96..b1d201a7 100644 --- a/packages/nextcloud/lib/src/webdav/props.g.dart +++ b/packages/nextcloud/lib/src/webdav/props.g.dart @@ -6,7 +6,7 @@ part of 'props.dart'; // XmlSerializableGenerator // ************************************************************************** -void _$WebDavPropfindPropBuildXmlChildren(WebDavPropfindProp instance, XmlBuilder builder, +void _$WebDavPropWithoutValuesBuildXmlChildren(WebDavPropWithoutValues instance, XmlBuilder builder, {Map namespaces = const {}}) { final davgetlastmodified = instance.davgetlastmodified; final davgetlastmodifiedSerialized = davgetlastmodified; @@ -308,14 +308,14 @@ void _$WebDavPropfindPropBuildXmlChildren(WebDavPropfindProp instance, XmlBuilde } } -void _$WebDavPropfindPropBuildXmlElement(WebDavPropfindProp instance, XmlBuilder builder, +void _$WebDavPropWithoutValuesBuildXmlElement(WebDavPropWithoutValues instance, XmlBuilder builder, {Map namespaces = const {}}) { builder.element('prop', namespace: 'DAV:', namespaces: namespaces, nest: () { instance.buildXmlChildren(builder, namespaces: namespaces); }); } -WebDavPropfindProp _$WebDavPropfindPropFromXmlElement(XmlElement element) { +WebDavPropWithoutValues _$WebDavPropWithoutValuesFromXmlElement(XmlElement element) { final davgetlastmodified = element.getElements('getlastmodified', namespace: 'DAV:')?.map((e) => e.getText()).whereType(); final davgetetag = element.getElements('getetag', namespace: 'DAV:')?.map((e) => e.getText()).whereType(); @@ -401,7 +401,7 @@ WebDavPropfindProp _$WebDavPropfindPropFromXmlElement(XmlElement element) { .getElements('share-permissions', namespace: 'http://open-cloud-mesh.org/ns') ?.map((e) => e.getText()) .whereType(); - return WebDavPropfindProp( + return WebDavPropWithoutValues( davgetlastmodified: davgetlastmodified?.toList(), davgetetag: davgetetag?.toList(), davgetcontenttype: davgetcontenttype?.toList(), @@ -431,13 +431,13 @@ WebDavPropfindProp _$WebDavPropfindPropFromXmlElement(XmlElement element) { ocmsharepermissions: ocmsharepermissions?.toList()); } -List _$WebDavPropfindPropToXmlAttributes(WebDavPropfindProp instance, +List _$WebDavPropWithoutValuesToXmlAttributes(WebDavPropWithoutValues instance, {Map namespaces = const {}}) { final attributes = []; return attributes; } -List _$WebDavPropfindPropToXmlChildren(WebDavPropfindProp instance, +List _$WebDavPropWithoutValuesToXmlChildren(WebDavPropWithoutValues instance, {Map namespaces = const {}}) { final children = []; final davgetlastmodified = instance.davgetlastmodified; @@ -638,28 +638,29 @@ List _$WebDavPropfindPropToXmlChildren(WebDavPropfindProp instance, return children; } -XmlElement _$WebDavPropfindPropToXmlElement(WebDavPropfindProp instance, {Map namespaces = const {}}) { +XmlElement _$WebDavPropWithoutValuesToXmlElement(WebDavPropWithoutValues instance, + {Map namespaces = const {}}) { return XmlElement( XmlName('prop', namespaces['DAV:']), [...namespaces.toXmlAttributes(), ...instance.toXmlAttributes(namespaces: namespaces)], instance.toXmlChildren(namespaces: namespaces)); } -mixin _$WebDavPropfindPropXmlSerializableMixin { +mixin _$WebDavPropWithoutValuesXmlSerializableMixin { void buildXmlChildren(XmlBuilder builder, {Map namespaces = const {}}) => - _$WebDavPropfindPropBuildXmlChildren(this as WebDavPropfindProp, builder, namespaces: namespaces); + _$WebDavPropWithoutValuesBuildXmlChildren(this as WebDavPropWithoutValues, builder, namespaces: namespaces); void buildXmlElement(XmlBuilder builder, {Map namespaces = const {}}) => - _$WebDavPropfindPropBuildXmlElement(this as WebDavPropfindProp, builder, namespaces: namespaces); + _$WebDavPropWithoutValuesBuildXmlElement(this as WebDavPropWithoutValues, builder, namespaces: namespaces); List toXmlAttributes({Map namespaces = const {}}) => - _$WebDavPropfindPropToXmlAttributes(this as WebDavPropfindProp, namespaces: namespaces); + _$WebDavPropWithoutValuesToXmlAttributes(this as WebDavPropWithoutValues, namespaces: namespaces); List toXmlChildren({Map namespaces = const {}}) => - _$WebDavPropfindPropToXmlChildren(this as WebDavPropfindProp, namespaces: namespaces); + _$WebDavPropWithoutValuesToXmlChildren(this as WebDavPropWithoutValues, namespaces: namespaces); XmlElement toXmlElement({Map namespaces = const {}}) => - _$WebDavPropfindPropToXmlElement(this as WebDavPropfindProp, namespaces: namespaces); + _$WebDavPropWithoutValuesToXmlElement(this as WebDavPropWithoutValues, namespaces: namespaces); } void _$WebDavPropBuildXmlChildren(WebDavProp instance, XmlBuilder builder, diff --git a/packages/nextcloud/lib/src/webdav/webdav.dart b/packages/nextcloud/lib/src/webdav/webdav.dart index 981c520b..664cdb7d 100644 --- a/packages/nextcloud/lib/src/webdav/webdav.dart +++ b/packages/nextcloud/lib/src/webdav/webdav.dart @@ -76,11 +76,15 @@ class WebDavPropstat with _$WebDavPropstatXmlSerializableMixin { @annotation.XmlRootElement(name: 'propertyupdate', namespace: namespaceDav) class WebDavPropertyupdate with _$WebDavPropertyupdateXmlSerializableMixin { WebDavPropertyupdate({ - required this.set, + this.set, + this.remove, }); - @annotation.XmlElement(name: 'set', namespace: namespaceDav) - final WebDavSet set; + @annotation.XmlElement(name: 'set', namespace: namespaceDav, includeIfNull: false) + final WebDavSet? set; + + @annotation.XmlElement(name: 'remove', namespace: namespaceDav, includeIfNull: false) + final WebDavRemove? remove; } @annotation.XmlSerializable(createMixin: true) @@ -96,6 +100,19 @@ class WebDavSet with _$WebDavSetXmlSerializableMixin { final WebDavProp prop; // coverage:ignore-line } +@annotation.XmlSerializable(createMixin: true) +@annotation.XmlRootElement(name: 'remove', namespace: namespaceDav) +class WebDavRemove with _$WebDavRemoveXmlSerializableMixin { + WebDavRemove({ + required this.prop, + }); + + factory WebDavRemove.fromXmlElement(final XmlElement element) => _$WebDavRemoveFromXmlElement(element); + + @annotation.XmlElement(name: 'prop', namespace: namespaceDav) + final WebDavPropWithoutValues prop; // coverage:ignore-line +} + @annotation.XmlSerializable(createMixin: true) @annotation.XmlRootElement(name: 'propfind', namespace: namespaceDav) class WebDavPropfind with _$WebDavPropfindXmlSerializableMixin { @@ -104,7 +121,7 @@ class WebDavPropfind with _$WebDavPropfindXmlSerializableMixin { }); @annotation.XmlElement(name: 'prop', namespace: namespaceDav) - final WebDavPropfindProp prop; + final WebDavPropWithoutValues prop; } @annotation.XmlSerializable(createMixin: true) @@ -119,7 +136,7 @@ class WebDavOcFilterFiles with _$WebDavOcFilterFilesXmlSerializableMixin { final WebDavOcFilterRules filterRules; @annotation.XmlElement(name: 'prop', namespace: namespaceDav) - final WebDavPropfindProp prop; + final WebDavPropWithoutValues prop; } @annotation.XmlSerializable(createMixin: true) diff --git a/packages/nextcloud/lib/src/webdav/webdav.g.dart b/packages/nextcloud/lib/src/webdav/webdav.g.dart index 1007981f..85806c17 100644 --- a/packages/nextcloud/lib/src/webdav/webdav.g.dart +++ b/packages/nextcloud/lib/src/webdav/webdav.g.dart @@ -221,9 +221,18 @@ void _$WebDavPropertyupdateBuildXmlChildren(WebDavPropertyupdate instance, XmlBu {Map namespaces = const {}}) { final set = instance.set; final setSerialized = set; - builder.element('set', namespace: 'DAV:', nest: () { - setSerialized.buildXmlChildren(builder, namespaces: namespaces); - }); + if (setSerialized != null) { + builder.element('set', namespace: 'DAV:', nest: () { + setSerialized.buildXmlChildren(builder, namespaces: namespaces); + }); + } + final remove = instance.remove; + final removeSerialized = remove; + if (removeSerialized != null) { + builder.element('remove', namespace: 'DAV:', nest: () { + removeSerialized.buildXmlChildren(builder, namespaces: namespaces); + }); + } } void _$WebDavPropertyupdateBuildXmlElement(WebDavPropertyupdate instance, XmlBuilder builder, @@ -234,8 +243,11 @@ void _$WebDavPropertyupdateBuildXmlElement(WebDavPropertyupdate instance, XmlBui } WebDavPropertyupdate _$WebDavPropertyupdateFromXmlElement(XmlElement element) { - final set = element.getElement('set', namespace: 'DAV:')!; - return WebDavPropertyupdate(set: WebDavSet.fromXmlElement(set)); + final set = element.getElement('set', namespace: 'DAV:'); + final remove = element.getElement('remove', namespace: 'DAV:'); + return WebDavPropertyupdate( + set: set != null ? WebDavSet.fromXmlElement(set) : null, + remove: remove != null ? WebDavRemove.fromXmlElement(remove) : null); } List _$WebDavPropertyupdateToXmlAttributes(WebDavPropertyupdate instance, @@ -249,9 +261,22 @@ List _$WebDavPropertyupdateToXmlChildren(WebDavPropertyupdate instance, final children = []; final set = instance.set; final setSerialized = set; - final setConstructed = XmlElement(XmlName('set', namespaces['DAV:']), - setSerialized.toXmlAttributes(namespaces: namespaces), setSerialized.toXmlChildren(namespaces: namespaces)); - children.add(setConstructed); + final setConstructed = setSerialized != null + ? XmlElement(XmlName('set', namespaces['DAV:']), setSerialized.toXmlAttributes(namespaces: namespaces), + setSerialized.toXmlChildren(namespaces: namespaces)) + : null; + if (setConstructed != null) { + children.add(setConstructed); + } + final remove = instance.remove; + final removeSerialized = remove; + final removeConstructed = removeSerialized != null + ? XmlElement(XmlName('remove', namespaces['DAV:']), removeSerialized.toXmlAttributes(namespaces: namespaces), + removeSerialized.toXmlChildren(namespaces: namespaces)) + : null; + if (removeConstructed != null) { + children.add(removeConstructed); + } return children; } @@ -338,6 +363,66 @@ mixin _$WebDavSetXmlSerializableMixin { _$WebDavSetToXmlElement(this as WebDavSet, namespaces: namespaces); } +void _$WebDavRemoveBuildXmlChildren(WebDavRemove instance, XmlBuilder builder, + {Map namespaces = const {}}) { + final prop = instance.prop; + final propSerialized = prop; + builder.element('prop', namespace: 'DAV:', nest: () { + propSerialized.buildXmlChildren(builder, namespaces: namespaces); + }); +} + +void _$WebDavRemoveBuildXmlElement(WebDavRemove instance, XmlBuilder builder, + {Map namespaces = const {}}) { + builder.element('remove', namespace: 'DAV:', namespaces: namespaces, nest: () { + instance.buildXmlChildren(builder, namespaces: namespaces); + }); +} + +WebDavRemove _$WebDavRemoveFromXmlElement(XmlElement element) { + final prop = element.getElement('prop', namespace: 'DAV:')!; + return WebDavRemove(prop: WebDavPropWithoutValues.fromXmlElement(prop)); +} + +List _$WebDavRemoveToXmlAttributes(WebDavRemove instance, {Map namespaces = const {}}) { + final attributes = []; + return attributes; +} + +List _$WebDavRemoveToXmlChildren(WebDavRemove instance, {Map namespaces = const {}}) { + final children = []; + final prop = instance.prop; + final propSerialized = prop; + final propConstructed = XmlElement(XmlName('prop', namespaces['DAV:']), + propSerialized.toXmlAttributes(namespaces: namespaces), propSerialized.toXmlChildren(namespaces: namespaces)); + children.add(propConstructed); + return children; +} + +XmlElement _$WebDavRemoveToXmlElement(WebDavRemove instance, {Map namespaces = const {}}) { + return XmlElement( + XmlName('remove', namespaces['DAV:']), + [...namespaces.toXmlAttributes(), ...instance.toXmlAttributes(namespaces: namespaces)], + instance.toXmlChildren(namespaces: namespaces)); +} + +mixin _$WebDavRemoveXmlSerializableMixin { + void buildXmlChildren(XmlBuilder builder, {Map namespaces = const {}}) => + _$WebDavRemoveBuildXmlChildren(this as WebDavRemove, builder, namespaces: namespaces); + + void buildXmlElement(XmlBuilder builder, {Map namespaces = const {}}) => + _$WebDavRemoveBuildXmlElement(this as WebDavRemove, builder, namespaces: namespaces); + + List toXmlAttributes({Map namespaces = const {}}) => + _$WebDavRemoveToXmlAttributes(this as WebDavRemove, namespaces: namespaces); + + List toXmlChildren({Map namespaces = const {}}) => + _$WebDavRemoveToXmlChildren(this as WebDavRemove, namespaces: namespaces); + + XmlElement toXmlElement({Map namespaces = const {}}) => + _$WebDavRemoveToXmlElement(this as WebDavRemove, namespaces: namespaces); +} + void _$WebDavPropfindBuildXmlChildren(WebDavPropfind instance, XmlBuilder builder, {Map namespaces = const {}}) { final prop = instance.prop; @@ -356,7 +441,7 @@ void _$WebDavPropfindBuildXmlElement(WebDavPropfind instance, XmlBuilder builder WebDavPropfind _$WebDavPropfindFromXmlElement(XmlElement element) { final prop = element.getElement('prop', namespace: 'DAV:')!; - return WebDavPropfind(prop: WebDavPropfindProp.fromXmlElement(prop)); + return WebDavPropfind(prop: WebDavPropWithoutValues.fromXmlElement(prop)); } List _$WebDavPropfindToXmlAttributes(WebDavPropfind instance, @@ -424,7 +509,7 @@ WebDavOcFilterFiles _$WebDavOcFilterFilesFromXmlElement(XmlElement element) { final filterRules = element.getElement('filter-rules', namespace: 'http://owncloud.org/ns')!; final prop = element.getElement('prop', namespace: 'DAV:')!; return WebDavOcFilterFiles( - filterRules: WebDavOcFilterRules.fromXmlElement(filterRules), prop: WebDavPropfindProp.fromXmlElement(prop)); + filterRules: WebDavOcFilterRules.fromXmlElement(filterRules), prop: WebDavPropWithoutValues.fromXmlElement(prop)); } List _$WebDavOcFilterFilesToXmlAttributes(WebDavOcFilterFiles instance, diff --git a/packages/nextcloud/test/webdav_test.dart b/packages/nextcloud/test/webdav_test.dart index e737a55d..6c2f1b7a 100644 --- a/packages/nextcloud/test/webdav_test.dart +++ b/packages/nextcloud/test/webdav_test.dart @@ -24,15 +24,15 @@ void main() { tearDown(() => container.destroy()); test('Get status', () async { - final status = await client.webdav.status(); - expect(status.capabilities, containsAll(['1', '3'])); - expect(status.searchCapabilities, hasLength(0)); + final options = await client.webdav.options(); + expect(options.capabilities, containsAll(['1', '3'])); + expect(options.searchCapabilities, hasLength(0)); }); test('List directory', () async { - final responses = (await client.webdav.ls( + final responses = (await client.webdav.propfind( '/', - prop: WebDavPropfindProp.fromBools( + prop: WebDavPropWithoutValues.fromBools( nchaspreview: true, davgetcontenttype: true, davgetlastmodified: true, @@ -50,7 +50,7 @@ void main() { }); test('List directory recursively', () async { - final responses = (await client.webdav.ls( + final responses = (await client.webdav.propfind( '/', depth: 'infinity', )) @@ -59,32 +59,23 @@ void main() { }); test('Create directory', () async { - final response = await client.webdav.mkdir('test'); + final response = await client.webdav.mkcol('test'); expect(response.statusCode, equals(201)); }); - test('Create directory recursively', () async { - final response = await client.webdav.mkdirs('test/bla'); - expect(response!.statusCode, equals(201)); - - final responses = (await client.webdav.ls('/test')).responses; - expect(responses, hasLength(2)); - expect(responses[1].href, endsWith('/test/bla/')); - }); - test('Upload files', () async { final pngBytes = File('test/files/test.png').readAsBytesSync(); final txtBytes = File('test/files/test.txt').readAsBytesSync(); - var response = await client.webdav.upload(pngBytes, 'test.png'); + var response = await client.webdav.put(pngBytes, 'test.png'); expect(response.statusCode, equals(201)); - response = await client.webdav.upload(txtBytes, 'test.txt'); + response = await client.webdav.put(txtBytes, 'test.txt'); expect(response.statusCode, equals(201)); - final responses = (await client.webdav.ls( + final responses = (await client.webdav.propfind( '/', - prop: WebDavPropfindProp.fromBools( + prop: WebDavPropWithoutValues.fromBools( ocsize: true, ), )) @@ -105,7 +96,7 @@ void main() { final created = lastModified.subtract(const Duration(hours: 1)); final txtBytes = File('test/files/test.txt').readAsBytesSync(); - final response = await client.webdav.upload( + final response = await client.webdav.put( txtBytes, 'test.txt', lastModified: lastModified, @@ -113,9 +104,9 @@ void main() { ); expect(response.statusCode, equals(201)); - final props = (await client.webdav.ls( + final props = (await client.webdav.propfind( '/', - prop: WebDavPropfindProp.fromBools( + prop: WebDavPropWithoutValues.fromBools( davgetlastmodified: true, nccreationtime: true, ), @@ -136,14 +127,14 @@ void main() { }); test('Download file', () async { - final response = await client.webdav.download('Nextcloud.png'); + final response = await client.webdav.get('Nextcloud.png'); expect(response, isNotEmpty); }); test('Delete file', () async { final response = await client.webdav.delete('Nextcloud.png'); expect(response.statusCode, 204); - final responses = (await client.webdav.ls('/')).responses; + final responses = (await client.webdav.propfind('/')).responses; expect(responses.where((final response) => response.href!.endsWith('/Nextcloud.png')), hasLength(0)); }); @@ -153,14 +144,14 @@ void main() { 'test.png', ); expect(response.statusCode, 201); - final responses = (await client.webdav.ls('/')).responses; + final responses = (await client.webdav.propfind('/')).responses; expect(responses.where((final response) => response.href!.endsWith('/Nextcloud.png')), hasLength(1)); expect(responses.where((final response) => response.href!.endsWith('/test.png')), hasLength(1)); }); test('Copy file (overwrite fail)', () async { - await client.webdav.upload(Uint8List.fromList(utf8.encode('1')), '1.txt'); - await client.webdav.upload(Uint8List.fromList(utf8.encode('2')), '2.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('1')), '1.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('2')), '2.txt'); expect( () => client.webdav.copy('1.txt', '2.txt'), @@ -169,8 +160,8 @@ void main() { }); test('Copy file (overwrite success)', () async { - await client.webdav.upload(Uint8List.fromList(utf8.encode('1')), '1.txt'); - await client.webdav.upload(Uint8List.fromList(utf8.encode('2')), '2.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('1')), '1.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('2')), '2.txt'); final response = await client.webdav.copy( '1.txt', @@ -186,14 +177,14 @@ void main() { 'test.png', ); expect(response.statusCode, 201); - final responses = (await client.webdav.ls('/')).responses; + final responses = (await client.webdav.propfind('/')).responses; expect(responses.where((final response) => response.href!.endsWith('/Nextcloud.png')), hasLength(0)); expect(responses.where((final response) => response.href!.endsWith('/test.png')), hasLength(1)); }); test('Move file (overwrite fail)', () async { - await client.webdav.upload(Uint8List.fromList(utf8.encode('1')), '1.txt'); - await client.webdav.upload(Uint8List.fromList(utf8.encode('2')), '2.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('1')), '1.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('2')), '2.txt'); expect( () => client.webdav.move('1.txt', '2.txt'), @@ -202,8 +193,8 @@ void main() { }); test('Move file (overwrite success)', () async { - await client.webdav.upload(Uint8List.fromList(utf8.encode('1')), '1.txt'); - await client.webdav.upload(Uint8List.fromList(utf8.encode('2')), '2.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('1')), '1.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('2')), '2.txt'); final response = await client.webdav.move( '1.txt', @@ -214,9 +205,9 @@ void main() { }); test('Get file props', () async { - final response = (await client.webdav.ls( + final response = (await client.webdav.propfind( 'Nextcloud.png', - prop: WebDavPropfindProp.fromBools( + prop: WebDavPropWithoutValues.fromBools( davgetlastmodified: true, davgetetag: true, davgetcontenttype: true, @@ -298,12 +289,12 @@ void main() { test('Get directory props', () async { final data = Uint8List.fromList(utf8.encode('test')); - await client.webdav.mkdir('test'); - await client.webdav.upload(data, 'test/test.txt'); + await client.webdav.mkcol('test'); + await client.webdav.put(data, 'test/test.txt'); - final response = (await client.webdav.ls( + final response = (await client.webdav.propfind( 'test', - prop: WebDavPropfindProp.fromBools( + prop: WebDavPropWithoutValues.fromBools( davgetcontenttype: true, davgetlastmodified: true, davresourcetype: true, @@ -329,21 +320,21 @@ void main() { }); test('Filter files', () async { - final response = await client.webdav.upload(Uint8List.fromList(utf8.encode('test')), 'test.txt'); + final response = await client.webdav.put(Uint8List.fromList(utf8.encode('test')), 'test.txt'); final id = response.headers['oc-fileid']!.first; - await client.webdav.updateProps( + await client.webdav.proppatch( 'test.txt', - WebDavProp( + set: WebDavProp( ocfavorite: 1, ), ); - final responses = (await client.webdav.filter( + final responses = (await client.webdav.report( '/', WebDavOcFilterRules( ocfavorite: 1, ), - prop: WebDavPropfindProp.fromBools( + prop: WebDavPropWithoutValues.fromBools( ocid: true, ocfavorite: true, ), @@ -361,20 +352,20 @@ void main() { final createdEpoch = createdDate.millisecondsSinceEpoch ~/ 1000; final uploadTime = DateTime.now(); - await client.webdav.upload(Uint8List.fromList(utf8.encode('test')), 'test.txt'); + await client.webdav.put(Uint8List.fromList(utf8.encode('test')), 'test.txt'); - final updated = await client.webdav.updateProps( + final updated = await client.webdav.proppatch( 'test.txt', - WebDavProp( + set: WebDavProp( ocfavorite: 1, nccreationtime: createdEpoch, ), ); expect(updated, isTrue); - final props = (await client.webdav.ls( + final props = (await client.webdav.propfind( 'test.txt', - prop: WebDavPropfindProp.fromBools( + prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, nccreationtime: true, ncuploadtime: true, @@ -389,5 +380,53 @@ void main() { expect(DateTime.fromMillisecondsSinceEpoch(props.nccreationtime! * 1000).isAtSameMomentAs(createdDate), isTrue); expectDateInReasonableTimeRange(DateTime.fromMillisecondsSinceEpoch(props.ncuploadtime! * 1000), uploadTime); }); + + test('Remove properties', () async { + await client.webdav.put(Uint8List.fromList(utf8.encode('test')), 'test.txt'); + + var updated = await client.webdav.proppatch( + 'test.txt', + set: WebDavProp( + ocfavorite: 1, + ), + ); + expect(updated, isTrue); + + var props = (await client.webdav.propfind( + 'test.txt', + prop: WebDavPropWithoutValues.fromBools( + ocfavorite: true, + nccreationtime: true, + ncuploadtime: true, + ), + )) + .responses + .single + .propstats + .first + .prop; + expect(props.ocfavorite, 1); + + updated = await client.webdav.proppatch( + 'test.txt', + remove: WebDavPropWithoutValues.fromBools( + ocfavorite: true, + ), + ); + expect(updated, isFalse); + + props = (await client.webdav.propfind( + 'test.txt', + prop: WebDavPropWithoutValues.fromBools( + ocfavorite: true, + ), + )) + .responses + .single + .propstats + .first + .prop; + expect(props.ocfavorite, 0); + }); }); }