From 61c8c8969b2b74209055ab897ac2a0a7989875f7 Mon Sep 17 00:00:00 2001 From: jld3103 Date: Fri, 3 Nov 2023 15:54:16 +0100 Subject: [PATCH] refactor(nextcloud,neon_files): Introduce PathUri for WebDAV path handling Signed-off-by: jld3103 --- .../neon/neon_files/lib/blocs/browser.dart | 22 +- packages/neon/neon_files/lib/blocs/files.dart | 70 ++--- .../neon_files/lib/dialogs/choose_create.dart | 11 +- .../neon_files/lib/dialogs/choose_folder.dart | 12 +- .../neon_files/lib/dialogs/create_folder.dart | 2 +- .../neon_files/lib/models/file_details.dart | 13 +- .../neon/neon_files/lib/pages/details.dart | 4 +- packages/neon/neon_files/lib/utils/task.dart | 6 +- .../neon/neon_files/lib/widgets/actions.dart | 13 +- .../neon_files/lib/widgets/browser_view.dart | 21 +- .../neon_files/lib/widgets/file_preview.dart | 7 +- .../neon_files/lib/widgets/navigator.dart | 20 +- packages/nextcloud/lib/src/webdav/client.dart | 37 +-- packages/nextcloud/lib/src/webdav/file.dart | 15 +- .../nextcloud/lib/src/webdav/path_uri.dart | 151 +++++++++++ packages/nextcloud/lib/webdav.dart | 1 + packages/nextcloud/test/webdav_test.dart | 249 ++++++++++++------ 17 files changed, 450 insertions(+), 204 deletions(-) create mode 100644 packages/nextcloud/lib/src/webdav/path_uri.dart diff --git a/packages/neon/neon_files/lib/blocs/browser.dart b/packages/neon/neon_files/lib/blocs/browser.dart index 80c970621c5..db14b80b6b6 100644 --- a/packages/neon/neon_files/lib/blocs/browser.dart +++ b/packages/neon/neon_files/lib/blocs/browser.dart @@ -1,22 +1,22 @@ part of '../neon_files.dart'; abstract interface class FilesBrowserBlocEvents { - void setPath(final List path); + void setPath(final PathUri path); - void createFolder(final List path); + void createFolder(final PathUri path); } abstract interface class FilesBrowserBlocStates { BehaviorSubject>> get files; - BehaviorSubject> get path; + BehaviorSubject get path; } class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents, FilesBrowserBlocStates { FilesBrowserBloc( this.options, this.account, { - final List? initialPath, + final PathUri? initialPath, }) { if (initialPath != null) { path.add(initialPath); @@ -39,16 +39,16 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents BehaviorSubject>> files = BehaviorSubject>>(); @override - BehaviorSubject> path = BehaviorSubject>.seeded([]); + BehaviorSubject path = BehaviorSubject.seeded(PathUri.cwd()); @override Future refresh() async { await RequestManager.instance.wrapWebDav>( account.id, - 'files-${path.value.join('/')}', + 'files-${path.value.path}', files, () => account.client.webdav.propfind( - Uri(pathSegments: path.value), + path.value, prop: WebDavPropWithoutValues.fromBools( davgetcontenttype: true, davgetetag: true, @@ -65,13 +65,13 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents } @override - void setPath(final List p) { - path.add(p); + void setPath(final PathUri path) { + this.path.add(path); unawaited(refresh()); } @override - void createFolder(final List path) { - wrapAction(() async => account.client.webdav.mkcol(Uri(pathSegments: path))); + void createFolder(final PathUri path) { + wrapAction(() async => account.client.webdav.mkcol(path)); } } diff --git a/packages/neon/neon_files/lib/blocs/files.dart b/packages/neon/neon_files/lib/blocs/files.dart index 2711c40be75..9b9d85755b5 100644 --- a/packages/neon/neon_files/lib/blocs/files.dart +++ b/packages/neon/neon_files/lib/blocs/files.dart @@ -1,25 +1,25 @@ part of '../neon_files.dart'; abstract interface class FilesBlocEvents { - void uploadFile(final List path, final String localPath); + void uploadFile(final PathUri path, final String localPath); - void syncFile(final List path); + void syncFile(final PathUri path); - void openFile(final List path, final String etag, final String? mimeType); + void openFile(final PathUri path, final String etag, final String? mimeType); - void shareFileNative(final List path, final String etag); + void shareFileNative(final PathUri path, final String etag); - void delete(final List path); + void delete(final PathUri path); - void rename(final List path, final String name); + void rename(final PathUri path, final String name); - void move(final List path, final List destination); + void move(final PathUri path, final PathUri destination); - void copy(final List path, final List destination); + void copy(final PathUri path, final PathUri destination); - void addFavorite(final List path); + void addFavorite(final PathUri path); - void removeFavorite(final List path); + void removeFavorite(final PathUri path); } abstract interface class FilesBlocStates { @@ -58,32 +58,32 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta BehaviorSubject> tasks = BehaviorSubject>.seeded([]); @override - void addFavorite(final List path) { + void addFavorite(final PathUri path) { wrapAction( () async => account.client.webdav.proppatch( - Uri(pathSegments: path), + path, set: WebDavProp(ocfavorite: 1), ), ); } @override - void copy(final List path, final List destination) { - wrapAction(() async => account.client.webdav.copy(Uri(pathSegments: path), Uri(pathSegments: destination))); + void copy(final PathUri path, final PathUri destination) { + wrapAction(() async => account.client.webdav.copy(path, destination)); } @override - void delete(final List path) { - wrapAction(() async => account.client.webdav.delete(Uri(pathSegments: path))); + void delete(final PathUri path) { + wrapAction(() async => account.client.webdav.delete(path)); } @override - void move(final List path, final List destination) { - wrapAction(() async => account.client.webdav.move(Uri(pathSegments: path), Uri(pathSegments: destination))); + void move(final PathUri path, final PathUri destination) { + wrapAction(() async => account.client.webdav.move(path, destination)); } @override - void openFile(final List path, final String etag, final String? mimeType) { + void openFile(final PathUri path, final String etag, final String? mimeType) { wrapAction( () async { final file = await _cacheFile(path, etag); @@ -98,7 +98,7 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta } @override - void shareFileNative(final List path, final String etag) { + void shareFileNative(final PathUri path, final String etag) { wrapAction( () async { final file = await _cacheFile(path, etag); @@ -115,36 +115,36 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta } @override - void removeFavorite(final List path) { + void removeFavorite(final PathUri path) { wrapAction( () async => account.client.webdav.proppatch( - Uri(pathSegments: path), + path, set: WebDavProp(ocfavorite: 0), ), ); } @override - void rename(final List path, final String name) { + void rename(final PathUri path, final String name) { wrapAction( () async => account.client.webdav.move( - Uri(pathSegments: path), - Uri(pathSegments: List.from(path)..last = name), + path, + path.rename(name), ), ); } @override - void syncFile(final List path) { + void syncFile(final PathUri path) { wrapAction( () async { final file = File( - p.join( + p.joinAll([ await NeonPlatform.instance.userAccessibleAppDataPath, account.humanReadableID, 'files', - path.join(Platform.pathSeparator), - ), + ...path.pathSegments, + ]), ); if (!file.parent.existsSync()) { file.parent.createSync(recursive: true); @@ -156,7 +156,7 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta } @override - void uploadFile(final List path, final String localPath) { + void uploadFile(final PathUri path, final String localPath) { wrapAction( () async { final task = FilesUploadTask( @@ -171,12 +171,12 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta ); } - Future _cacheFile(final List path, final String etag) async { + Future _cacheFile(final PathUri path, final String etag) async { final cacheDir = await getApplicationCacheDirectory(); - final file = File(p.join(cacheDir.path, 'files', etag.replaceAll('"', ''), path.last)); + final file = File(p.join(cacheDir.path, 'files', etag.replaceAll('"', ''), path.name)); if (!file.existsSync()) { - debugPrint('Downloading ${Uri(pathSegments: path)} since it does not exist'); + debugPrint('Downloading $path since it does not exist'); if (!file.parent.existsSync()) { await file.parent.create(recursive: true); } @@ -187,7 +187,7 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta } Future _downloadFile( - final List path, + final PathUri path, final File file, ) async { final task = FilesDownloadTask( @@ -200,7 +200,7 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta } FilesBrowserBloc getNewFilesBrowserBloc({ - final List? initialPath, + final PathUri? initialPath, }) => FilesBrowserBloc( options, diff --git a/packages/neon/neon_files/lib/dialogs/choose_create.dart b/packages/neon/neon_files/lib/dialogs/choose_create.dart index 38ad27d58b5..683fc80df78 100644 --- a/packages/neon/neon_files/lib/dialogs/choose_create.dart +++ b/packages/neon/neon_files/lib/dialogs/choose_create.dart @@ -8,7 +8,7 @@ class FilesChooseCreateDialog extends StatefulWidget { }); final FilesBloc bloc; - final List basePath; + final PathUri basePath; @override State createState() => _FilesChooseCreateDialogState(); @@ -43,7 +43,10 @@ class _FilesChooseCreateDialogState extends State { } } } - widget.bloc.uploadFile([...widget.basePath, p.basename(file.path)], file.path); + widget.bloc.uploadFile( + widget.basePath.join(PathUri.parse(p.basename(file.path))), + file.path, + ); } @override @@ -104,12 +107,12 @@ class _FilesChooseCreateDialogState extends State { onTap: () async { Navigator.of(context).pop(); - final result = await showDialog>( + final result = await showDialog( context: context, builder: (final context) => const FilesCreateFolderDialog(), ); if (result != null) { - widget.bloc.browser.createFolder([...widget.basePath, ...result]); + widget.bloc.browser.createFolder(widget.basePath.join(PathUri.parse(result))); } }, ), diff --git a/packages/neon/neon_files/lib/dialogs/choose_folder.dart b/packages/neon/neon_files/lib/dialogs/choose_folder.dart index da5f2e7a423..2d833ba7059 100644 --- a/packages/neon/neon_files/lib/dialogs/choose_folder.dart +++ b/packages/neon/neon_files/lib/dialogs/choose_folder.dart @@ -11,7 +11,7 @@ class FilesChooseFolderDialog extends StatelessWidget { final FilesBrowserBloc bloc; final FilesBloc filesBloc; - final List originalPath; + final PathUri originalPath; @override Widget build(final BuildContext context) => AlertDialog( @@ -28,7 +28,7 @@ class FilesChooseFolderDialog extends StatelessWidget { mode: FilesBrowserMode.selectDirectory, ), ), - StreamBuilder>( + StreamBuilder( stream: bloc.path, builder: (final context, final pathSnapshot) => pathSnapshot.hasData ? Container( @@ -38,19 +38,19 @@ class FilesChooseFolderDialog extends StatelessWidget { children: [ ElevatedButton( onPressed: () async { - final result = await showDialog>( + final result = await showDialog( context: context, builder: (final context) => const FilesCreateFolderDialog(), ); if (result != null) { - bloc.createFolder([...pathSnapshot.requireData, ...result]); + bloc.createFolder(pathSnapshot.requireData.join(PathUri.parse(result))); } }, child: Text(FilesLocalizations.of(context).folderCreate), ), ElevatedButton( - onPressed: !(const ListEquality().equals(originalPath, pathSnapshot.data)) - ? () => Navigator.of(context).pop(pathSnapshot.data) + onPressed: originalPath != pathSnapshot.requireData + ? () => Navigator.of(context).pop(pathSnapshot.requireData) : null, child: Text(FilesLocalizations.of(context).folderChoose), ), diff --git a/packages/neon/neon_files/lib/dialogs/create_folder.dart b/packages/neon/neon_files/lib/dialogs/create_folder.dart index 45083184915..3a00d968e98 100644 --- a/packages/neon/neon_files/lib/dialogs/create_folder.dart +++ b/packages/neon/neon_files/lib/dialogs/create_folder.dart @@ -22,7 +22,7 @@ class _FilesCreateFolderDialogState extends State { void submit() { if (formKey.currentState!.validate()) { - Navigator.of(context).pop(controller.text.split('/')); + Navigator.of(context).pop(controller.text); } } diff --git a/packages/neon/neon_files/lib/models/file_details.dart b/packages/neon/neon_files/lib/models/file_details.dart index c0d61b28bcf..e5f63b3a3c7 100644 --- a/packages/neon/neon_files/lib/models/file_details.dart +++ b/packages/neon/neon_files/lib/models/file_details.dart @@ -4,7 +4,6 @@ part of '../neon_files.dart'; class FileDetails { const FileDetails({ required this.path, - required this.isDirectory, required this.size, required this.etag, required this.mimeType, @@ -15,9 +14,7 @@ class FileDetails { FileDetails.fromWebDav({ required final WebDavFile file, - required final List path, - }) : path = List.from(path)..add(file.name), - isDirectory = file.isDirectory, + }) : path = file.path, size = file.size, etag = file.etag, mimeType = file.mimeType, @@ -31,7 +28,6 @@ class FileDetails { }) : path = task.path, size = task.stat.size, lastModified = task.stat.modified, - isDirectory = false, etag = null, mimeType = null, hasPreview = null, @@ -41,7 +37,6 @@ class FileDetails { required FilesDownloadTask this.task, required final WebDavFile file, }) : path = task.path, - isDirectory = file.isDirectory, size = file.size, etag = file.etag, mimeType = file.mimeType, @@ -64,11 +59,11 @@ class FileDetails { } } - String get name => path.last; + String get name => path.name; - final List path; + bool get isDirectory => path.isDirectory; - final bool isDirectory; + final PathUri path; final int? size; diff --git a/packages/neon/neon_files/lib/pages/details.dart b/packages/neon/neon_files/lib/pages/details.dart index 36d50c1f9bc..73a51e93666 100644 --- a/packages/neon/neon_files/lib/pages/details.dart +++ b/packages/neon/neon_files/lib/pages/details.dart @@ -43,8 +43,8 @@ class FilesDetailsPage extends StatelessWidget { details.isDirectory ? FilesLocalizations.of(context).detailsFolderName : FilesLocalizations.of(context).detailsFileName: details.name, - FilesLocalizations.of(context).detailsParentFolder: - details.path.length == 1 ? '/' : details.path.sublist(0, details.path.length - 1).join('/'), + if (details.path.parent != null) + FilesLocalizations.of(context).detailsParentFolder: details.path.parent!.path, if (details.size != null) ...{ details.isDirectory ? FilesLocalizations.of(context).detailsFolderSize diff --git a/packages/neon/neon_files/lib/utils/task.dart b/packages/neon/neon_files/lib/utils/task.dart index f4324237abf..2ff5346e559 100644 --- a/packages/neon/neon_files/lib/utils/task.dart +++ b/packages/neon/neon_files/lib/utils/task.dart @@ -6,7 +6,7 @@ sealed class FilesTask { required this.file, }); - final List path; + final PathUri path; final File file; @@ -25,7 +25,7 @@ class FilesDownloadTask extends FilesTask { Future execute(final NextcloudClient client) async { await client.webdav.getFile( - Uri(pathSegments: path), + path, file, onProgress: streamController.add, ); @@ -46,7 +46,7 @@ class FilesUploadTask extends FilesTask { await client.webdav.putFile( file, stat, - Uri(pathSegments: path), + path, lastModified: stat.modified, onProgress: streamController.add, ); diff --git a/packages/neon/neon_files/lib/widgets/actions.dart b/packages/neon/neon_files/lib/widgets/actions.dart index aa9eb789101..002f4362f6d 100644 --- a/packages/neon/neon_files/lib/widgets/actions.dart +++ b/packages/neon/neon_files/lib/widgets/actions.dart @@ -4,6 +4,7 @@ import 'package:neon/platform.dart'; import 'package:neon/utils.dart'; import 'package:neon_files/l10n/localizations.dart'; import 'package:neon_files/neon_files.dart'; +import 'package:nextcloud/webdav.dart'; class FileActions extends StatelessWidget { const FileActions({ @@ -52,9 +53,9 @@ class FileActions extends StatelessWidget { if (!context.mounted) { return; } - final originalPath = details.path.sublist(0, details.path.length - 1); + final originalPath = details.path.parent!; final b = bloc.getNewFilesBrowserBloc(initialPath: originalPath); - final result = await showDialog?>( + final result = await showDialog( context: context, builder: (final context) => FilesChooseFolderDialog( bloc: b, @@ -64,15 +65,15 @@ class FileActions extends StatelessWidget { ); b.dispose(); if (result != null) { - bloc.move(details.path, result..add(details.name)); + bloc.move(details.path, result.join(PathUri.parse(details.name))); } case FilesFileAction.copy: if (!context.mounted) { return; } - final originalPath = details.path.sublist(0, details.path.length - 1); + final originalPath = details.path.parent!; final b = bloc.getNewFilesBrowserBloc(initialPath: originalPath); - final result = await showDialog?>( + final result = await showDialog( context: context, builder: (final context) => FilesChooseFolderDialog( bloc: b, @@ -82,7 +83,7 @@ class FileActions extends StatelessWidget { ); b.dispose(); if (result != null) { - bloc.copy(details.path, result..add(details.name)); + bloc.copy(details.path, result.join(PathUri.parse(details.name))); } case FilesFileAction.sync: if (!context.mounted) { diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index e8f138611d8..7f37552061d 100644 --- a/packages/neon/neon_files/lib/widgets/browser_view.dart +++ b/packages/neon/neon_files/lib/widgets/browser_view.dart @@ -43,7 +43,7 @@ class _FilesBrowserViewState extends State { @override Widget build(final BuildContext context) => ResultBuilder>.behaviorSubject( subject: widget.bloc.files, - builder: (final context, final filesSnapshot) => StreamBuilder>( + builder: (final context, final filesSnapshot) => StreamBuilder( stream: widget.bloc.path, builder: (final context, final pathSnapshot) => StreamBuilder>( stream: widget.filesBloc.tasks, @@ -68,9 +68,9 @@ class _FilesBrowserViewState extends State { return BackButtonListener( onBackButtonPressed: () async { - final path = pathSnapshot.requireData; - if (path.isNotEmpty) { - widget.bloc.setPath(path.sublist(0, path.length - 1)); + final parent = pathSnapshot.requireData.parent; + if (parent != null) { + widget.bloc.setPath(parent); return true; } return false; @@ -87,12 +87,13 @@ class _FilesBrowserViewState extends State { final uploadingTaskTiles = buildUploadTasks(tasksSnapshot.requireData, sorted); return NeonListView( - scrollKey: 'files-${pathSnapshot.requireData.join('/')}', + scrollKey: 'files-${pathSnapshot.requireData.path}', itemCount: sorted.length, itemBuilder: (final context, final index) { final file = sorted[index]; - final matchingTask = tasksSnapshot.requireData - .firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); + final matchingTask = tasksSnapshot.requireData.firstWhereOrNull( + (final task) => widget.bloc.path.value.join(PathUri.parse(file.name)) == task.path, + ); final details = matchingTask != null ? FileDetails.fromTask( @@ -101,7 +102,6 @@ class _FilesBrowserViewState extends State { ) : FileDetails.fromWebDav( file: file, - path: widget.bloc.path.value, ); return FileListTile( @@ -147,9 +147,4 @@ class _FilesBrowserViewState extends State { ); } } - - bool _pathMatchesFile(final List path, final String name) => const ListEquality().equals( - [...widget.bloc.path.value, name], - path, - ); } diff --git a/packages/neon/neon_files/lib/widgets/file_preview.dart b/packages/neon/neon_files/lib/widgets/file_preview.dart index 02865546ac0..57dbeb16c24 100644 --- a/packages/neon/neon_files/lib/widgets/file_preview.dart +++ b/packages/neon/neon_files/lib/widgets/file_preview.dart @@ -76,14 +76,12 @@ class FilePreviewImage extends NeonApiImage { }) { final width = size.width.toInt(); final height = size.height.toInt(); - final path = file.path.join('/'); - final cacheKey = 'preview-$path-$width-$height'; + final cacheKey = 'preview-${file.path.path}-$width-$height'; return FilePreviewImage._( file: file, size: size, cacheKey: cacheKey, - path: path, width: width, height: height, ); @@ -93,12 +91,11 @@ class FilePreviewImage extends NeonApiImage { required final FileDetails file, required Size super.size, required super.cacheKey, - required final String path, required final int width, required final int height, }) : super( getImage: (final client) async => client.core.preview.getPreview( - file: path, + file: file.path.path, x: width, y: height, ), diff --git a/packages/neon/neon_files/lib/widgets/navigator.dart b/packages/neon/neon_files/lib/widgets/navigator.dart index 2e079b43122..08f33fba731 100644 --- a/packages/neon/neon_files/lib/widgets/navigator.dart +++ b/packages/neon/neon_files/lib/widgets/navigator.dart @@ -7,7 +7,7 @@ class FilesBrowserNavigator extends StatelessWidget { super.key, }); - final List path; + final PathUri path; final FilesBrowserBloc bloc; @override @@ -18,7 +18,7 @@ class FilesBrowserNavigator extends StatelessWidget { horizontal: 10, ), scrollDirection: Axis.horizontal, - itemCount: path.length + 1, + itemCount: path.pathSegments.length + 1, itemBuilder: (final context, final index) { if (index == 0) { return IconButton( @@ -30,21 +30,23 @@ class FilesBrowserNavigator extends StatelessWidget { tooltip: FilesLocalizations.of(context).goToPath(''), icon: const Icon(Icons.house), onPressed: () { - bloc.setPath([]); + bloc.setPath(PathUri.cwd()); }, ); } - final path = this.path.sublist(0, index); - final label = path.join('/'); - + final partialPath = PathUri( + isAbsolute: path.isAbsolute, + isDirectory: path.isDirectory, + pathSegments: path.pathSegments.sublist(0, index), + ); return TextButton( onPressed: () { - bloc.setPath(path); + bloc.setPath(partialPath); }, child: Text( - path.last, - semanticsLabel: FilesLocalizations.of(context).goToPath(label), + partialPath.name, + semanticsLabel: FilesLocalizations.of(context).goToPath(partialPath.name), ), ); }, diff --git a/packages/nextcloud/lib/src/webdav/client.dart b/packages/nextcloud/lib/src/webdav/client.dart index 52ce8bcf91c..5288c506a57 100644 --- a/packages/nextcloud/lib/src/webdav/client.dart +++ b/packages/nextcloud/lib/src/webdav/client.dart @@ -4,13 +4,14 @@ import 'dart:typed_data'; import 'package:dynamite_runtime/http_client.dart'; import 'package:meta/meta.dart'; +import 'package:nextcloud/src/webdav/path_uri.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; /// Base path used on the server -final webdavBase = Uri(path: '/remote.php/webdav'); +final webdavBase = PathUri.parse('/remote.php/webdav'); /// WebDavClient class class WebDavClient { @@ -60,11 +61,11 @@ class WebDavClient { return response; } - Uri _constructUri([final Uri? path]) => constructUri(rootClient.baseURL, path); + Uri _constructUri([final PathUri? path]) => constructUri(rootClient.baseURL, path); @visibleForTesting // ignore: public_member_api_docs - static Uri constructUri(final Uri baseURL, [final Uri? path]) { + static Uri constructUri(final Uri baseURL, [final PathUri? path]) { final segments = baseURL.pathSegments.toList()..addAll(webdavBase.pathSegments); if (path != null) { segments.addAll(path.pathSegments); @@ -103,7 +104,7 @@ class WebDavClient { /// Creates a collection at [path]. /// /// See http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL for more information. - Future mkcol(final Uri path) async => _send( + Future mkcol(final PathUri path) async => _send( 'MKCOL', _constructUri(path), ); @@ -111,7 +112,7 @@ class WebDavClient { /// Deletes the resource at [path]. /// /// See http://www.webdav.org/specs/rfc2518.html#METHOD_DELETE for more information. - Future delete(final Uri path) => _send( + Future delete(final PathUri path) => _send( 'DELETE', _constructUri(path), ); @@ -123,7 +124,7 @@ class WebDavClient { /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information. Future put( final Uint8List localData, - final Uri path, { + final PathUri path, { final DateTime? lastModified, final DateTime? created, }) => @@ -147,7 +148,7 @@ class WebDavClient { /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information. Future putStream( final Stream localData, - final Uri path, { + final PathUri path, { final DateTime? lastModified, final DateTime? created, final int? contentLength, @@ -181,7 +182,7 @@ class WebDavClient { Future putFile( final File file, final FileStat fileStat, - final Uri path, { + final PathUri path, { final DateTime? lastModified, final DateTime? created, final void Function(double progress)? onProgress, @@ -196,17 +197,17 @@ class WebDavClient { ); /// Gets the content of the file at [path]. - Future get(final Uri path) async => (await getStream(path)).bytes; + Future get(final PathUri path) async => (await getStream(path)).bytes; /// Gets the content of the file at [path]. - Future getStream(final Uri path) async => _send( + Future getStream(final PathUri path) async => _send( 'GET', _constructUri(path), ); /// Gets the content of the file at [path]. Future getFile( - final Uri path, + final PathUri path, final File file, { final void Function(double progress)? onProgress, }) async { @@ -236,7 +237,7 @@ class WebDavClient { /// [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 propfind( - final Uri path, { + final PathUri path, { final WebDavPropWithoutValues? prop, final WebDavDepth? depth, }) async => @@ -256,7 +257,7 @@ class WebDavClient { /// Optionally populates the [prop]s on the returned resources. /// See https://github.com/owncloud/docs/issues/359 for more information. Future report( - final Uri path, + final PathUri path, final WebDavOcFilterRules filterRules, { final WebDavPropWithoutValues? prop, }) async => @@ -280,7 +281,7 @@ class WebDavClient { /// Returns true if the update was successful. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPPATCH for more information. Future proppatch( - final Uri path, { + final PathUri path, { final WebDavProp? set, final WebDavPropWithoutValues? remove, }) async { @@ -310,8 +311,8 @@ class WebDavClient { /// 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 Uri sourcePath, - final Uri destinationPath, { + final PathUri sourcePath, + final PathUri destinationPath, { final bool overwrite = false, }) => _send( @@ -328,8 +329,8 @@ class WebDavClient { /// 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 Uri sourcePath, - final Uri destinationPath, { + final PathUri sourcePath, + final PathUri destinationPath, { final bool overwrite = false, }) => _send( diff --git a/packages/nextcloud/lib/src/webdav/file.dart b/packages/nextcloud/lib/src/webdav/file.dart index 7013c898c51..27139337397 100644 --- a/packages/nextcloud/lib/src/webdav/file.dart +++ b/packages/nextcloud/lib/src/webdav/file.dart @@ -1,4 +1,5 @@ import 'package:nextcloud/src/webdav/client.dart'; +import 'package:nextcloud/src/webdav/path_uri.dart'; import 'package:nextcloud/src/webdav/props.dart'; import 'package:nextcloud/src/webdav/webdav.dart'; @@ -25,8 +26,14 @@ class WebDavFile { _response.propstats.singleWhere((final propstat) => propstat.status.contains('200')).prop; /// The path of file - late final Uri path = - Uri(pathSegments: Uri(path: _response.href).pathSegments.sublist(webdavBase.pathSegments.length)); + late final PathUri path = () { + final href = PathUri.parse(_response.href!); + return PathUri( + isAbsolute: false, + isDirectory: href.isDirectory, + pathSegments: href.pathSegments.sublist(webdavBase.pathSegments.length), + ); + }(); /// The fileid namespaced by the instance id, globally unique late final String? id = props.ocid; @@ -79,11 +86,11 @@ class WebDavFile { late final bool? hasPreview = props.nchaspreview; /// Returns the decoded name of the file / folder without the whole path - late final String name = path.pathSegments.where((final s) => s.isNotEmpty).lastOrNull ?? ''; + late final String name = path.name; /// Whether the file is hidden. late final bool isHidden = name.startsWith('.'); /// Whether the file is a directory - late final bool isDirectory = (isCollection ?? false) || path.pathSegments.last.isEmpty; + late final bool isDirectory = (isCollection ?? false) || path.isDirectory; } diff --git a/packages/nextcloud/lib/src/webdav/path_uri.dart b/packages/nextcloud/lib/src/webdav/path_uri.dart new file mode 100644 index 00000000000..51698f822cb --- /dev/null +++ b/packages/nextcloud/lib/src/webdav/path_uri.dart @@ -0,0 +1,151 @@ +import 'package:collection/collection.dart'; +import 'package:meta/meta.dart'; + +/// A [Uri] that only exposes the path handling. +@immutable +class PathUri { + /// Creates a new path URI. + const PathUri({ + required this.isAbsolute, + required this.isDirectory, + required this.pathSegments, + }); + + /// Parses a path URI from a given [path]. + /// + /// An empty [path] is considered to be the current working directory. + factory PathUri.parse(final String path) { + var isAbsolute = false; + var isDirectory = false; + final pathSegments = []; + + final parts = path.split('/'); + if (parts.length == 1 && parts.single.isEmpty) { + return const PathUri( + isAbsolute: false, + isDirectory: true, + pathSegments: [], + ); + } + for (var i = 0; i < parts.length; i++) { + final part = parts[i]; + if (i == 0 && part.isEmpty) { + isAbsolute = true; + } + if (i == parts.length - 1 && part.isEmpty) { + isDirectory = true; + } + if (part.isNotEmpty) { + pathSegments.add(part); + } + } + + return PathUri( + isAbsolute: isAbsolute, + isDirectory: isDirectory, + pathSegments: pathSegments, + ); + } + + /// Creates a new empty path URI representing the current working directory. + factory PathUri.cwd() => const PathUri( + isAbsolute: false, + isDirectory: true, + pathSegments: [], + ); + + /// Whether the path is an absolute path. + /// + /// If `true` [path] will start with a slash. + final bool isAbsolute; + + /// Whether the path is a directory. + /// + /// If `true` [path] will end with a slash. + final bool isDirectory; + + /// Returns the path as a list of its segments. + /// + /// See [path] for getting the path as a string. + final List pathSegments; + + /// Returns the path as a string. + /// + /// See [pathSegments] for getting the path as a list of its segments. + String get path { + if (pathSegments.isEmpty) { + if (isAbsolute) { + return '/'; + } + return ''; + } + final buffer = StringBuffer(); + if (isAbsolute) { + buffer.write('/'); + } + buffer.writeAll(pathSegments, '/'); + if (isDirectory) { + buffer.write('/'); + } + return buffer.toString(); + } + + /// Returns the name of the last element in path. + String get name => pathSegments.lastOrNull ?? ''; + + /// Returns the parent of the path. + PathUri? get parent { + if (pathSegments.isNotEmpty) { + return PathUri( + isAbsolute: isAbsolute, + isDirectory: true, + pathSegments: pathSegments.sublist(0, pathSegments.length - 1), + ); + } + + return null; + } + + /// Joins the current path with another [path]. + /// + /// If the current path is not a directory a [StateError] will be thrown. + /// See [isDirectory] for checking if the current path is a directory. + PathUri join(final PathUri other) { + if (!isDirectory) { + throw StateError('$this is not a directory.'); + } + + return PathUri( + isAbsolute: isAbsolute, + isDirectory: other.isDirectory, + pathSegments: [ + ...pathSegments, + ...other.pathSegments, + ], + ); + } + + /// Renames the last path segment and returns a new path URI. + PathUri rename(final String name) => PathUri( + isAbsolute: isAbsolute, + isDirectory: isDirectory, + pathSegments: pathSegments.isNotEmpty ? [...pathSegments.sublist(0, pathSegments.length - 1), name] : [], + ); + + @override + bool operator ==(final Object other) => + other is PathUri && + isAbsolute == other.isAbsolute && + isDirectory == other.isDirectory && + const ListEquality().equals(pathSegments, other.pathSegments); + + @override + int get hashCode => Object.hashAll([ + isAbsolute, + isDirectory, + pathSegments, + ]); + + @override + String toString() => path; +} diff --git a/packages/nextcloud/lib/webdav.dart b/packages/nextcloud/lib/webdav.dart index ecce9168d78..c604cc3eb99 100644 --- a/packages/nextcloud/lib/webdav.dart +++ b/packages/nextcloud/lib/webdav.dart @@ -4,6 +4,7 @@ import 'package:nextcloud/src/webdav/client.dart'; export 'src/webdav/client.dart'; export 'src/webdav/file.dart'; +export 'src/webdav/path_uri.dart'; export 'src/webdav/props.dart'; export 'src/webdav/webdav.dart'; diff --git a/packages/nextcloud/test/webdav_test.dart b/packages/nextcloud/test/webdav_test.dart index 0cba20be659..e6bf91d6ee1 100644 --- a/packages/nextcloud/test/webdav_test.dart +++ b/packages/nextcloud/test/webdav_test.dart @@ -19,17 +19,110 @@ void main() { final baseURL = Uri.parse(values.$1); final sanitizedBaseURL = Uri.parse(values.$2); - test('$baseURL', () { - expect(WebDavClient.constructUri(baseURL).toString(), '$sanitizedBaseURL$webdavBase'); - expect(WebDavClient.constructUri(baseURL, Uri(path: '/')).toString(), '$sanitizedBaseURL$webdavBase'); - expect(WebDavClient.constructUri(baseURL, Uri(path: 'test')).toString(), '$sanitizedBaseURL$webdavBase/test'); - expect(WebDavClient.constructUri(baseURL, Uri(path: 'test/')).toString(), '$sanitizedBaseURL$webdavBase/test'); - expect(WebDavClient.constructUri(baseURL, Uri(path: '/test')).toString(), '$sanitizedBaseURL$webdavBase/test'); - expect(WebDavClient.constructUri(baseURL, Uri(path: '/test/')).toString(), '$sanitizedBaseURL$webdavBase/test'); + test(baseURL, () { + expect( + WebDavClient.constructUri(baseURL).toString(), + '$sanitizedBaseURL$webdavBase', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri.parse('/')).toString(), + '$sanitizedBaseURL$webdavBase', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri.parse('test')).toString(), + '$sanitizedBaseURL$webdavBase/test', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri.parse('test/')).toString(), + '$sanitizedBaseURL$webdavBase/test', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri.parse('/test')).toString(), + '$sanitizedBaseURL$webdavBase/test', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri.parse('/test/')).toString(), + '$sanitizedBaseURL$webdavBase/test', + ); }); } }); + group('PathUri', () { + test('isAbsolute', () { + expect(PathUri.parse('').isAbsolute, false); + expect(PathUri.parse('/').isAbsolute, true); + expect(PathUri.parse('test').isAbsolute, false); + expect(PathUri.parse('test/').isAbsolute, false); + expect(PathUri.parse('/test').isAbsolute, true); + expect(PathUri.parse('/test/').isAbsolute, true); + }); + + test('isDirectory', () { + expect(PathUri.parse('').isDirectory, true); + expect(PathUri.parse('/').isDirectory, true); + expect(PathUri.parse('test').isDirectory, false); + expect(PathUri.parse('test/').isDirectory, true); + expect(PathUri.parse('/test').isDirectory, false); + expect(PathUri.parse('/test/').isDirectory, true); + }); + + test('pathSegments', () { + expect(PathUri.parse('').pathSegments, isEmpty); + expect(PathUri.parse('/').pathSegments, isEmpty); + expect(PathUri.parse('test').pathSegments, ['test']); + expect(PathUri.parse('test/').pathSegments, ['test']); + expect(PathUri.parse('/test').pathSegments, ['test']); + expect(PathUri.parse('/test/').pathSegments, ['test']); + }); + + test('path', () { + expect(PathUri.parse('').path, ''); + expect(PathUri.parse('/').path, '/'); + expect(PathUri.parse('test').path, 'test'); + expect(PathUri.parse('test/').path, 'test/'); + expect(PathUri.parse('/test').path, '/test'); + expect(PathUri.parse('/test/').path, '/test/'); + }); + + test('normalization', () { + expect(PathUri.parse('/test/abc/').path, '/test/abc/'); + expect(PathUri.parse('//test//abc//').path, '/test/abc/'); + expect(PathUri.parse('///test///abc///').path, '/test/abc/'); + }); + + test('name', () { + expect(PathUri.parse('').name, ''); + expect(PathUri.parse('test').name, 'test'); + expect(PathUri.parse('/test/').name, 'test'); + expect(PathUri.parse('abc/test').name, 'test'); + expect(PathUri.parse('/abc/test/').name, 'test'); + }); + + test('parent', () { + expect(PathUri.parse('').parent, null); + expect(PathUri.parse('/').parent, null); + expect(PathUri.parse('test').parent, PathUri.parse('')); + expect(PathUri.parse('test/abc').parent, PathUri.parse('test/')); + expect(PathUri.parse('test/abc/').parent, PathUri.parse('test/')); + expect(PathUri.parse('/test/abc').parent, PathUri.parse('/test/')); + expect(PathUri.parse('/test/abc/').parent, PathUri.parse('/test/')); + }); + + test('join', () { + expect(PathUri.parse('').join(PathUri.parse('test')), PathUri.parse('test')); + expect(PathUri.parse('test/').join(PathUri.parse('abc')), PathUri.parse('test/abc')); + expect(PathUri.parse('test/').join(PathUri.parse('abc/123')), PathUri.parse('test/abc/123')); + expect(() => PathUri.parse('test').join(PathUri.parse('abc')), throwsA(isA())); + }); + + test('rename', () { + expect(PathUri.parse('').rename('test'), PathUri.parse('')); + expect(PathUri.parse('test').rename('abc'), PathUri.parse('abc')); + expect(PathUri.parse('test/abc').rename('123'), PathUri.parse('test/123')); + }); + }); + group( 'webdav', () { @@ -44,7 +137,7 @@ void main() { test('List directory', () async { final responses = (await client.webdav.propfind( - Uri(path: '/'), + PathUri.parse('/'), prop: WebDavPropWithoutValues.fromBools( nchaspreview: true, davgetcontenttype: true, @@ -64,7 +157,7 @@ void main() { test('List directory recursively', () async { final responses = (await client.webdav.propfind( - Uri(path: '/'), + PathUri.parse('/'), depth: WebDavDepth.infinity, )) .responses; @@ -73,7 +166,7 @@ void main() { test('Get file props', () async { final response = (await client.webdav.propfind( - Uri(path: 'Nextcloud.png'), + PathUri.parse('Nextcloud.png'), prop: WebDavPropWithoutValues.fromBools( davgetlastmodified: true, davgetetag: true, @@ -107,7 +200,7 @@ void main() { .toWebDavFiles() .single; - expect(response.path, Uri(path: 'Nextcloud.png')); + expect(response.path, PathUri.parse('Nextcloud.png')); expect(response.id, isNotEmpty); expect(response.fileId, isNotEmpty); expect(response.isCollection, isFalse); @@ -156,11 +249,11 @@ void main() { test('Get directory props', () async { final data = utf8.encode('test') as Uint8List; - await client.webdav.mkcol(Uri(path: 'test')); - await client.webdav.put(data, Uri(path: 'test/test.txt')); + await client.webdav.mkcol(PathUri.parse('test')); + await client.webdav.put(data, PathUri.parse('test/test.txt')); final response = (await client.webdav.propfind( - Uri(path: 'test'), + PathUri.parse('test'), prop: WebDavPropWithoutValues.fromBools( davgetcontenttype: true, davgetlastmodified: true, @@ -172,7 +265,7 @@ void main() { .toWebDavFiles() .single; - expect(response.path, Uri(path: 'test/')); + expect(response.path, PathUri.parse('test/')); expect(response.isCollection, isTrue); expect(response.mimeType, isNull); expect(response.size, data.lengthInBytes); @@ -187,17 +280,17 @@ void main() { }); test('Filter files', () async { - final response = await client.webdav.put(utf8.encode('test') as Uint8List, Uri(path: 'test.txt')); + final response = await client.webdav.put(utf8.encode('test') as Uint8List, PathUri.parse('test.txt')); final id = response.headers['oc-fileid']!.first; await client.webdav.proppatch( - Uri(path: 'test.txt'), + PathUri.parse('test.txt'), set: WebDavProp( ocfavorite: 1, ), ); final responses = (await client.webdav.report( - Uri(path: '/'), + PathUri.parse('/'), WebDavOcFilterRules( ocfavorite: 1, ), @@ -221,13 +314,13 @@ void main() { await client.webdav.put( utf8.encode('test') as Uint8List, - Uri(path: 'test.txt'), + PathUri.parse('test.txt'), lastModified: lastModifiedDate, created: createdDate, ); final updated = await client.webdav.proppatch( - Uri(path: 'test.txt'), + PathUri.parse('test.txt'), set: WebDavProp( ocfavorite: 1, ), @@ -235,7 +328,7 @@ void main() { expect(updated, isTrue); final props = (await client.webdav.propfind( - Uri(path: 'test.txt'), + PathUri.parse('test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, davgetlastmodified: true, @@ -255,10 +348,10 @@ void main() { }); test('Remove properties', () async { - await client.webdav.put(utf8.encode('test') as Uint8List, Uri(path: 'test.txt')); + await client.webdav.put(utf8.encode('test') as Uint8List, PathUri.parse('test.txt')); var updated = await client.webdav.proppatch( - Uri(path: 'test.txt'), + PathUri.parse('test.txt'), set: WebDavProp( ocfavorite: 1, ), @@ -266,7 +359,7 @@ void main() { expect(updated, isTrue); var props = (await client.webdav.propfind( - Uri(path: 'test.txt'), + PathUri.parse('test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, nccreationtime: true, @@ -281,7 +374,7 @@ void main() { expect(props.ocfavorite, 1); updated = await client.webdav.proppatch( - Uri(path: 'test.txt'), + PathUri.parse('test.txt'), remove: WebDavPropWithoutValues.fromBools( ocfavorite: true, ), @@ -289,7 +382,7 @@ void main() { expect(updated, isFalse); props = (await client.webdav.propfind( - Uri(path: 'test.txt'), + PathUri.parse('test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, ), @@ -311,11 +404,11 @@ void main() { await client.webdav.putFile( source, source.statSync(), - Uri(path: 'test.png'), + PathUri.parse('test.png'), onProgress: progressValues.add, ); await client.webdav.getFile( - Uri(path: 'test.png'), + PathUri.parse('test.png'), destination, onProgress: progressValues.add, ); @@ -343,32 +436,32 @@ void main() { test(name, () async { final content = utf8.encode('This is a test file') as Uint8List; - final response = await client.webdav.put(content, Uri(path: path)); + final response = await client.webdav.put(content, PathUri.parse(path)); expect(response.statusCode, 201); - final downloadedContent = await client.webdav.get(Uri(path: path)); + final downloadedContent = await client.webdav.get(PathUri.parse(path)); expect(downloadedContent, equals(content)); }); } test('put_no_parent', () async { expect( - () => client.webdav.put(Uint8List(0), Uri(path: '409me/noparent.txt')), + () => client.webdav.put(Uint8List(0), PathUri.parse('409me/noparent.txt')), // https://github.com/nextcloud/server/issues/39625 throwsA(predicate((final e) => e.statusCode == 409)), ); }); test('delete', () async { - await client.webdav.put(Uint8List(0), Uri(path: 'test.txt')); + await client.webdav.put(Uint8List(0), PathUri.parse('test.txt')); - final response = await client.webdav.delete(Uri(path: 'test.txt')); + final response = await client.webdav.delete(PathUri.parse('test.txt')); expect(response.statusCode, 204); }); test('delete_null', () async { expect( - () => client.webdav.delete(Uri(path: 'test.txt')), + () => client.webdav.delete(PathUri.parse('test.txt')), throwsA(predicate((final e) => e.statusCode == 404)), ); }); @@ -376,29 +469,29 @@ void main() { // delete_fragment: This test is not applicable because the fragment is already removed on the client side test('mkcol', () async { - final response = await client.webdav.mkcol(Uri(path: 'test')); + final response = await client.webdav.mkcol(PathUri.parse('test')); expect(response.statusCode, 201); }); test('mkcol_again', () async { - await client.webdav.mkcol(Uri(path: 'test')); + await client.webdav.mkcol(PathUri.parse('test')); expect( - () => client.webdav.mkcol(Uri(path: 'test')), + () => client.webdav.mkcol(PathUri.parse('test')), throwsA(predicate((final e) => e.statusCode == 405)), ); }); test('delete_coll', () async { - var response = await client.webdav.mkcol(Uri(path: 'test')); + var response = await client.webdav.mkcol(PathUri.parse('test')); - response = await client.webdav.delete(Uri(path: 'test')); + response = await client.webdav.delete(PathUri.parse('test')); expect(response.statusCode, 204); }); test('mkcol_no_parent', () async { expect( - () => client.webdav.mkcol(Uri(path: '409me/noparent')), + () => client.webdav.mkcol(PathUri.parse('409me/noparent')), throwsA(predicate((final e) => e.statusCode == 409)), ); }); @@ -408,110 +501,110 @@ void main() { group('copymove', () { test('copy_simple', () async { - await client.webdav.mkcol(Uri(path: 'src')); + await client.webdav.mkcol(PathUri.parse('src')); - final response = await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst')); + final response = await client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst')); expect(response.statusCode, 201); }); test('copy_overwrite', () async { - await client.webdav.mkcol(Uri(path: 'src')); - await client.webdav.mkcol(Uri(path: 'dst')); + await client.webdav.mkcol(PathUri.parse('src')); + await client.webdav.mkcol(PathUri.parse('dst')); expect( - () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst')), + () => client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst')), throwsA(predicate((final e) => e.statusCode == 412)), ); - final response = await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst'), overwrite: true); + final response = await client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst'), overwrite: true); expect(response.statusCode, 204); }); test('copy_nodestcoll', () async { - await client.webdav.mkcol(Uri(path: 'src')); + await client.webdav.mkcol(PathUri.parse('src')); expect( - () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'nonesuch/dst')), + () => client.webdav.copy(PathUri.parse('src'), PathUri.parse('nonesuch/dst')), throwsA(predicate((final e) => e.statusCode == 409)), ); }); test('copy_coll', () async { - await client.webdav.mkcol(Uri(path: 'src')); - await client.webdav.mkcol(Uri(path: 'src/sub')); + await client.webdav.mkcol(PathUri.parse('src')); + await client.webdav.mkcol(PathUri.parse('src/sub')); for (var i = 0; i < 10; i++) { - await client.webdav.put(Uint8List(0), Uri(path: 'src/$i.txt')); + await client.webdav.put(Uint8List(0), PathUri.parse('src/$i.txt')); } - await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst1')); - await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst2')); + await client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst1')); + await client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst2')); expect( - () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst1')), + () => client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst1')), throwsA(predicate((final e) => e.statusCode == 412)), ); - var response = await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst2'), overwrite: true); + var response = await client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst2'), overwrite: true); expect(response.statusCode, 204); for (var i = 0; i < 10; i++) { - response = await client.webdav.delete(Uri(path: 'dst1/$i.txt')); + response = await client.webdav.delete(PathUri.parse('dst1/$i.txt')); expect(response.statusCode, 204); } - response = await client.webdav.delete(Uri(path: 'dst1/sub')); + response = await client.webdav.delete(PathUri.parse('dst1/sub')); expect(response.statusCode, 204); - response = await client.webdav.delete(Uri(path: 'dst2')); + response = await client.webdav.delete(PathUri.parse('dst2')); expect(response.statusCode, 204); }); // copy_shallow: Does not work on litmus, let's wait for https://github.com/nextcloud/server/issues/39627 test('move', () async { - await client.webdav.put(Uint8List(0), Uri(path: 'src1.txt')); - await client.webdav.put(Uint8List(0), Uri(path: 'src2.txt')); - await client.webdav.mkcol(Uri(path: 'coll')); + await client.webdav.put(Uint8List(0), PathUri.parse('src1.txt')); + await client.webdav.put(Uint8List(0), PathUri.parse('src2.txt')); + await client.webdav.mkcol(PathUri.parse('coll')); - var response = await client.webdav.move(Uri(path: 'src1.txt'), Uri(path: 'dst.txt')); + var response = await client.webdav.move(PathUri.parse('src1.txt'), PathUri.parse('dst.txt')); expect(response.statusCode, 201); expect( - () => client.webdav.move(Uri(path: 'src2.txt'), Uri(path: 'dst.txt')), + () => client.webdav.move(PathUri.parse('src2.txt'), PathUri.parse('dst.txt')), throwsA(predicate((final e) => e.statusCode == 412)), ); - response = await client.webdav.move(Uri(path: 'src2.txt'), Uri(path: 'dst.txt'), overwrite: true); + response = await client.webdav.move(PathUri.parse('src2.txt'), PathUri.parse('dst.txt'), overwrite: true); expect(response.statusCode, 204); }); test('move_coll', () async { - await client.webdav.mkcol(Uri(path: 'src')); - await client.webdav.mkcol(Uri(path: 'src/sub')); + await client.webdav.mkcol(PathUri.parse('src')); + await client.webdav.mkcol(PathUri.parse('src/sub')); for (var i = 0; i < 10; i++) { - await client.webdav.put(Uint8List(0), Uri(path: 'src/$i.txt')); + await client.webdav.put(Uint8List(0), PathUri.parse('src/$i.txt')); } - await client.webdav.put(Uint8List(0), Uri(path: 'noncoll')); - await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst2')); - await client.webdav.move(Uri(path: 'src'), Uri(path: 'dst1')); + await client.webdav.put(Uint8List(0), PathUri.parse('noncoll')); + await client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst2')); + await client.webdav.move(PathUri.parse('src'), PathUri.parse('dst1')); expect( - () => client.webdav.move(Uri(path: 'dst1'), Uri(path: 'dst2')), + () => client.webdav.move(PathUri.parse('dst1'), PathUri.parse('dst2')), throwsA(predicate((final e) => e.statusCode == 412)), ); - await client.webdav.move(Uri(path: 'dst2'), Uri(path: 'dst1'), overwrite: true); - await client.webdav.copy(Uri(path: 'dst1'), Uri(path: 'dst2')); + await client.webdav.move(PathUri.parse('dst2'), PathUri.parse('dst1'), overwrite: true); + await client.webdav.copy(PathUri.parse('dst1'), PathUri.parse('dst2')); for (var i = 0; i < 10; i++) { - final response = await client.webdav.delete(Uri(path: 'dst1/$i.txt')); + final response = await client.webdav.delete(PathUri.parse('dst1/$i.txt')); expect(response.statusCode, 204); } - final response = await client.webdav.delete(Uri(path: 'dst1/sub')); + final response = await client.webdav.delete(PathUri.parse('dst1/sub')); expect(response.statusCode, 204); expect( - () => client.webdav.move(Uri(path: 'dst2'), Uri(path: 'noncoll')), + () => client.webdav.move(PathUri.parse('dst2'), PathUri.parse('noncoll')), throwsA(predicate((final e) => e.statusCode == 412)), ); }); @@ -523,10 +616,10 @@ void main() { // large_put: Already covered by large_get test('large_get', () async { - final response = await client.webdav.put(Uint8List(largefileSize), Uri(path: 'test.txt')); + final response = await client.webdav.put(Uint8List(largefileSize), PathUri.parse('test.txt')); expect(response.statusCode, 201); - final downloadedContent = await client.webdav.get(Uri(path: 'test.txt')); + final downloadedContent = await client.webdav.get(PathUri.parse('test.txt')); expect(downloadedContent, hasLength(largefileSize)); }); });