From dd5ed6ec08b81252fed22bc3165c615c1c213a9b 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 | 23 +- .../neon_files/lib/dialogs/choose_folder.dart | 19 +- .../neon_files/lib/dialogs/create_folder.dart | 2 +- .../neon_files/lib/models/file_details.dart | 7 +- .../neon/neon_files/lib/pages/details.dart | 8 +- packages/neon/neon_files/lib/utils/task.dart | 6 +- .../neon/neon_files/lib/widgets/actions.dart | 35 ++- .../neon_files/lib/widgets/browser_view.dart | 23 +- .../lib/widgets/file_list_tile.dart | 2 +- .../neon_files/lib/widgets/file_preview.dart | 9 +- .../neon_files/lib/widgets/navigator.dart | 14 +- packages/nextcloud/lib/src/webdav/client.dart | 37 +-- packages/nextcloud/lib/src/webdav/file.dart | 9 +- .../nextcloud/lib/src/webdav/path_uri.dart | 76 ++++++ packages/nextcloud/lib/webdav.dart | 1 + packages/nextcloud/test/webdav_test.dart | 217 +++++++++++------- 18 files changed, 377 insertions(+), 203 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..266fcae3949 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()); @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..7b08085aa6d 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, + PathUri(pathSegments: List.from(path.pathSegments)..last = 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.pathSegments.last)); 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..f3af1aba1db 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,15 @@ class _FilesChooseCreateDialogState extends State { } } } - widget.bloc.uploadFile([...widget.basePath, p.basename(file.path)], file.path); + widget.bloc.uploadFile( + PathUri( + pathSegments: [ + ...widget.basePath.pathSegments, + p.basename(file.path), + ], + ), + file.path, + ); } @override @@ -104,12 +112,19 @@ 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( + PathUri( + pathSegments: [ + ...widget.basePath.pathSegments, + 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..8d98773eeec 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,26 @@ 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( + PathUri( + pathSegments: [ + ...pathSnapshot.requireData.pathSegments, + result, + ], + ), + ); } }, child: Text(FilesLocalizations.of(context).folderCreate), ), ElevatedButton( - onPressed: !(const ListEquality().equals(originalPath, pathSnapshot.data)) - ? () => Navigator.of(context).pop(pathSnapshot.data) + onPressed: originalPath.path != pathSnapshot.requireData.path + ? () => 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..e3bd0e9f848 100644 --- a/packages/neon/neon_files/lib/models/file_details.dart +++ b/packages/neon/neon_files/lib/models/file_details.dart @@ -15,8 +15,7 @@ class FileDetails { FileDetails.fromWebDav({ required final WebDavFile file, - required final List path, - }) : path = List.from(path)..add(file.name), + }) : path = file.path, isDirectory = file.isDirectory, size = file.size, etag = file.etag, @@ -64,9 +63,7 @@ class FileDetails { } } - String get name => path.last; - - final List path; + final PathUri path; final bool isDirectory; diff --git a/packages/neon/neon_files/lib/pages/details.dart b/packages/neon/neon_files/lib/pages/details.dart index 36d50c1f9bc..089ab05d7cd 100644 --- a/packages/neon/neon_files/lib/pages/details.dart +++ b/packages/neon/neon_files/lib/pages/details.dart @@ -14,7 +14,7 @@ class FilesDetailsPage extends StatelessWidget { Widget build(final BuildContext context) => Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( - title: Text(details.name), + title: Text(details.path.basename), ), body: SafeArea( child: ListView( @@ -42,9 +42,9 @@ class FilesDetailsPage extends StatelessWidget { for (final entry in { 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('/'), + : FilesLocalizations.of(context).detailsFileName: details.path.basename, + if (details.path.pathSegments.isNotEmpty) + FilesLocalizations.of(context).detailsParentFolder: details.path.dirname.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..74e13d2a99a 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({ @@ -43,7 +44,7 @@ class FileActions extends StatelessWidget { title: details.isDirectory ? FilesLocalizations.of(context).folderRename : FilesLocalizations.of(context).fileRename, - value: details.name, + value: details.path.basename, ); if (result != null) { bloc.rename(details.path, result); @@ -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.dirname; 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,23 @@ class FileActions extends StatelessWidget { ); b.dispose(); if (result != null) { - bloc.move(details.path, result..add(details.name)); + bloc.move( + details.path, + PathUri( + pathSegments: [ + ...result.pathSegments, + details.path.basename, + ], + ), + ); } case FilesFileAction.copy: if (!context.mounted) { return; } - final originalPath = details.path.sublist(0, details.path.length - 1); + final originalPath = details.path.dirname; final b = bloc.getNewFilesBrowserBloc(initialPath: originalPath); - final result = await showDialog?>( + final result = await showDialog( context: context, builder: (final context) => FilesChooseFolderDialog( bloc: b, @@ -82,7 +91,15 @@ class FileActions extends StatelessWidget { ); b.dispose(); if (result != null) { - bloc.copy(details.path, result..add(details.name)); + bloc.copy( + details.path, + PathUri( + pathSegments: [ + ...result.pathSegments, + details.path.basename, + ], + ), + ); } case FilesFileAction.sync: if (!context.mounted) { @@ -108,8 +125,8 @@ class FileActions extends StatelessWidget { if (await showConfirmationDialog( context, details.isDirectory - ? FilesLocalizations.of(context).folderDeleteConfirm(details.name) - : FilesLocalizations.of(context).fileDeleteConfirm(details.name), + ? FilesLocalizations.of(context).folderDeleteConfirm(details.path.basename) + : FilesLocalizations.of(context).fileDeleteConfirm(details.path.basename), )) { bloc.delete(details.path); } diff --git a/packages/neon/neon_files/lib/widgets/browser_view.dart b/packages/neon/neon_files/lib/widgets/browser_view.dart index e8f138611d8..a8aa68ac041 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 pathSegments = pathSnapshot.requireData.pathSegments; + if (pathSegments.isNotEmpty) { + widget.bloc.setPath(PathUri(pathSegments: pathSegments.sublist(0, pathSegments.length - 1))); return true; } return false; @@ -87,7 +87,7 @@ 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]; @@ -101,7 +101,6 @@ class _FilesBrowserViewState extends State { ) : FileDetails.fromWebDav( file: file, - path: widget.bloc.path.value, ); return FileListTile( @@ -148,8 +147,12 @@ class _FilesBrowserViewState extends State { } } - bool _pathMatchesFile(final List path, final String name) => const ListEquality().equals( - [...widget.bloc.path.value, name], - path, - ); + bool _pathMatchesFile(final PathUri path, final String name) => + PathUri( + pathSegments: [ + ...widget.bloc.path.value.pathSegments, + name, + ], + ) == + path; } diff --git a/packages/neon/neon_files/lib/widgets/file_list_tile.dart b/packages/neon/neon_files/lib/widgets/file_list_tile.dart index 85426f9fec5..6c9cf098dbf 100644 --- a/packages/neon/neon_files/lib/widgets/file_list_tile.dart +++ b/packages/neon/neon_files/lib/widgets/file_list_tile.dart @@ -47,7 +47,7 @@ class FileListTile extends StatelessWidget { // When the ETag is null it means we are uploading this file right now onTap: details.isDirectory || details.etag != null ? () async => _onTap(context, details) : null, title: Text( - details.name, + details.path.basename, overflow: TextOverflow.ellipsis, ), subtitle: Row( diff --git a/packages/neon/neon_files/lib/widgets/file_preview.dart b/packages/neon/neon_files/lib/widgets/file_preview.dart index 02865546ac0..79f6cfc365e 100644 --- a/packages/neon/neon_files/lib/widgets/file_preview.dart +++ b/packages/neon/neon_files/lib/widgets/file_preview.dart @@ -57,7 +57,7 @@ class FilePreview extends StatelessWidget { } return FileIcon( - details.name, + details.path.basename, color: color, size: size.shortestSide, ); @@ -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..c24fdc0e500 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,19 @@ class FilesBrowserNavigator extends StatelessWidget { tooltip: FilesLocalizations.of(context).goToPath(''), icon: const Icon(Icons.house), onPressed: () { - bloc.setPath([]); + bloc.setPath(PathUri()); }, ); } - final path = this.path.sublist(0, index); - final label = path.join('/'); - + final path = PathUri(pathSegments: this.path.pathSegments.sublist(0, index)); return TextButton( onPressed: () { bloc.setPath(path); }, child: Text( - path.last, - semanticsLabel: FilesLocalizations.of(context).goToPath(label), + path.path, + semanticsLabel: FilesLocalizations.of(context).goToPath(path.path), ), ); }, diff --git a/packages/nextcloud/lib/src/webdav/client.dart b/packages/nextcloud/lib/src/webdav/client.dart index 9a2e9869a04..5ae1b8a895e 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(path: '/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..0a9a35f2d51 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,8 @@ 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 = + PathUri(pathSegments: PathUri(path: _response.href).pathSegments.sublist(webdavBase.pathSegments.length)); /// The fileid namespaced by the instance id, globally unique late final String? id = props.ocid; @@ -79,11 +80,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.basename; /// 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..12b5a64551c --- /dev/null +++ b/packages/nextcloud/lib/src/webdav/path_uri.dart @@ -0,0 +1,76 @@ +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. + /// + /// Only one of [path] and [pathSegments] must be given. + PathUri({final String? path, final List? pathSegments}) + : assert( + (path == null) != (pathSegments == null) || (path == null && pathSegments == null), + 'Only one of path and pathSegments must be given', + ), + _uri = Uri( + path: path, + pathSegments: pathSegments, + ).normalizePath(); + + final Uri _uri; + + /// Returns the path as a string. + /// + /// See [pathSegments] for getting the path as a list of its segments. + String get path => _uri.path; + + /// Returns the path as a list of its segments. + /// + /// See [path] for getting the path as a string. + List get pathSegments => _uri.pathSegments; + + /// Returns whether the path is a directory. + /// + /// If the whole path is empty it is considered to be a directory. + bool get isDirectory => pathSegments.lastOrNull?.isEmpty ?? true; + + /// Returns the basename of the path. + String get basename => pathSegments.where((final s) => s.isNotEmpty).lastOrNull ?? ''; + + /// Returns the dirname of the path. + PathUri get dirname { + var segments = normalize().pathSegments; + if (segments.isNotEmpty) { + if (isDirectory) { + if (segments.length > 2) { + segments = segments.sublist(0, segments.length - 2)..add(''); + } else { + segments = ['']; + } + } else { + segments = segments.sublist(0, segments.length - 1); + } + } + return PathUri(pathSegments: segments); + } + + /// Removes all duplicate slashes. + /// + /// Keeps the trailing slash if present. + PathUri normalize() => PathUri( + pathSegments: pathSegments + .whereIndexed( + (final i, final s) => i == pathSegments.length - 1 || s.isNotEmpty, + ) + .toList(), + ); + + @override + bool operator ==(final Object other) => other is PathUri && _uri == other._uri; + + @override + int get hashCode => _uri.hashCode; + + @override + String toString() => _uri.toString(); +} 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..2b1e94689d5 100644 --- a/packages/nextcloud/test/webdav_test.dart +++ b/packages/nextcloud/test/webdav_test.dart @@ -19,17 +19,78 @@ 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(path: '/')).toString(), + '$sanitizedBaseURL$webdavBase', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri(path: 'test')).toString(), + '$sanitizedBaseURL$webdavBase/test', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri(path: 'test/')).toString(), + '$sanitizedBaseURL$webdavBase/test', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri(path: '/test')).toString(), + '$sanitizedBaseURL$webdavBase/test', + ); + expect( + WebDavClient.constructUri(baseURL, PathUri(path: '/test/')).toString(), + '$sanitizedBaseURL$webdavBase/test', + ); }); } }); + group('PathUri', () { + test('path', () { + expect(PathUri(path: 'test').path, 'test'); + expect(PathUri(path: '/test/').path, '/test/'); + }); + + test('pathSegments', () { + expect(PathUri(path: 'test').pathSegments, ['test']); + expect(PathUri(path: '/test/').pathSegments, ['test', '']); + }); + + test('isDirectory', () { + expect(PathUri().isDirectory, true); + expect(PathUri(path: '').isDirectory, true); + expect(PathUri(path: 'test').isDirectory, false); + expect(PathUri(path: '/test/').isDirectory, true); + }); + + test('basename', () { + expect(PathUri().basename, ''); + expect(PathUri(path: '').basename, ''); + expect(PathUri(path: 'test').basename, 'test'); + expect(PathUri(path: '/test/').basename, 'test'); + expect(PathUri(path: 'abc/test').basename, 'test'); + expect(PathUri(path: '/abc/test/').basename, 'test'); + }); + + test('dirname', () { + expect(PathUri().dirname, PathUri()); + expect(PathUri(path: '').dirname, PathUri()); + expect(PathUri(path: 'test/abc').dirname, PathUri(path: 'test')); + expect(PathUri(path: '/test/abc/').dirname, PathUri(path: 'test/')); + }); + + test('normalize', () { + expect(PathUri().normalize(), PathUri()); + expect(PathUri(path: '').normalize(), PathUri()); + expect(PathUri(path: 'test/abc').normalize(), PathUri(path: 'test/abc')); + expect(PathUri(path: '/test/abc/').normalize(), PathUri(path: 'test/abc/')); + expect(PathUri(path: '//test//abc//').normalize(), PathUri(path: 'test/abc/')); + }); + }); + group( 'webdav', () { @@ -44,7 +105,7 @@ void main() { test('List directory', () async { final responses = (await client.webdav.propfind( - Uri(path: '/'), + PathUri(path: '/'), prop: WebDavPropWithoutValues.fromBools( nchaspreview: true, davgetcontenttype: true, @@ -64,7 +125,7 @@ void main() { test('List directory recursively', () async { final responses = (await client.webdav.propfind( - Uri(path: '/'), + PathUri(path: '/'), depth: WebDavDepth.infinity, )) .responses; @@ -73,7 +134,7 @@ void main() { test('Get file props', () async { final response = (await client.webdav.propfind( - Uri(path: 'Nextcloud.png'), + PathUri(path: 'Nextcloud.png'), prop: WebDavPropWithoutValues.fromBools( davgetlastmodified: true, davgetetag: true, @@ -107,7 +168,7 @@ void main() { .toWebDavFiles() .single; - expect(response.path, Uri(path: 'Nextcloud.png')); + expect(response.path, PathUri(path: 'Nextcloud.png')); expect(response.id, isNotEmpty); expect(response.fileId, isNotEmpty); expect(response.isCollection, isFalse); @@ -156,11 +217,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(path: 'test')); + await client.webdav.put(data, PathUri(path: 'test/test.txt')); final response = (await client.webdav.propfind( - Uri(path: 'test'), + PathUri(path: 'test'), prop: WebDavPropWithoutValues.fromBools( davgetcontenttype: true, davgetlastmodified: true, @@ -172,7 +233,7 @@ void main() { .toWebDavFiles() .single; - expect(response.path, Uri(path: 'test/')); + expect(response.path, PathUri(path: 'test/')); expect(response.isCollection, isTrue); expect(response.mimeType, isNull); expect(response.size, data.lengthInBytes); @@ -187,17 +248,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(path: 'test.txt')); final id = response.headers['oc-fileid']!.first; await client.webdav.proppatch( - Uri(path: 'test.txt'), + PathUri(path: 'test.txt'), set: WebDavProp( ocfavorite: 1, ), ); final responses = (await client.webdav.report( - Uri(path: '/'), + PathUri(path: '/'), WebDavOcFilterRules( ocfavorite: 1, ), @@ -221,13 +282,13 @@ void main() { await client.webdav.put( utf8.encode('test') as Uint8List, - Uri(path: 'test.txt'), + PathUri(path: 'test.txt'), lastModified: lastModifiedDate, created: createdDate, ); final updated = await client.webdav.proppatch( - Uri(path: 'test.txt'), + PathUri(path: 'test.txt'), set: WebDavProp( ocfavorite: 1, ), @@ -235,7 +296,7 @@ void main() { expect(updated, isTrue); final props = (await client.webdav.propfind( - Uri(path: 'test.txt'), + PathUri(path: 'test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, davgetlastmodified: true, @@ -255,10 +316,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(path: 'test.txt')); var updated = await client.webdav.proppatch( - Uri(path: 'test.txt'), + PathUri(path: 'test.txt'), set: WebDavProp( ocfavorite: 1, ), @@ -266,7 +327,7 @@ void main() { expect(updated, isTrue); var props = (await client.webdav.propfind( - Uri(path: 'test.txt'), + PathUri(path: 'test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, nccreationtime: true, @@ -281,7 +342,7 @@ void main() { expect(props.ocfavorite, 1); updated = await client.webdav.proppatch( - Uri(path: 'test.txt'), + PathUri(path: 'test.txt'), remove: WebDavPropWithoutValues.fromBools( ocfavorite: true, ), @@ -289,7 +350,7 @@ void main() { expect(updated, isFalse); props = (await client.webdav.propfind( - Uri(path: 'test.txt'), + PathUri(path: 'test.txt'), prop: WebDavPropWithoutValues.fromBools( ocfavorite: true, ), @@ -311,11 +372,11 @@ void main() { await client.webdav.putFile( source, source.statSync(), - Uri(path: 'test.png'), + PathUri(path: 'test.png'), onProgress: progressValues.add, ); await client.webdav.getFile( - Uri(path: 'test.png'), + PathUri(path: 'test.png'), destination, onProgress: progressValues.add, ); @@ -343,32 +404,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(path: path)); expect(response.statusCode, 201); - final downloadedContent = await client.webdav.get(Uri(path: path)); + final downloadedContent = await client.webdav.get(PathUri(path: 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(path: '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(path: 'test.txt')); - final response = await client.webdav.delete(Uri(path: 'test.txt')); + final response = await client.webdav.delete(PathUri(path: 'test.txt')); expect(response.statusCode, 204); }); test('delete_null', () async { expect( - () => client.webdav.delete(Uri(path: 'test.txt')), + () => client.webdav.delete(PathUri(path: 'test.txt')), throwsA(predicate((final e) => e.statusCode == 404)), ); }); @@ -376,29 +437,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(path: 'test')); expect(response.statusCode, 201); }); test('mkcol_again', () async { - await client.webdav.mkcol(Uri(path: 'test')); + await client.webdav.mkcol(PathUri(path: 'test')); expect( - () => client.webdav.mkcol(Uri(path: 'test')), + () => client.webdav.mkcol(PathUri(path: '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(path: 'test')); - response = await client.webdav.delete(Uri(path: 'test')); + response = await client.webdav.delete(PathUri(path: 'test')); expect(response.statusCode, 204); }); test('mkcol_no_parent', () async { expect( - () => client.webdav.mkcol(Uri(path: '409me/noparent')), + () => client.webdav.mkcol(PathUri(path: '409me/noparent')), throwsA(predicate((final e) => e.statusCode == 409)), ); }); @@ -408,110 +469,110 @@ void main() { group('copymove', () { test('copy_simple', () async { - await client.webdav.mkcol(Uri(path: 'src')); + await client.webdav.mkcol(PathUri(path: 'src')); - final response = await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst')); + final response = await client.webdav.copy(PathUri(path: 'src'), PathUri(path: '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(path: 'src')); + await client.webdav.mkcol(PathUri(path: 'dst')); expect( - () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst')), + () => client.webdav.copy(PathUri(path: 'src'), PathUri(path: '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(path: 'src'), PathUri(path: 'dst'), overwrite: true); expect(response.statusCode, 204); }); test('copy_nodestcoll', () async { - await client.webdav.mkcol(Uri(path: 'src')); + await client.webdav.mkcol(PathUri(path: 'src')); expect( - () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'nonesuch/dst')), + () => client.webdav.copy(PathUri(path: 'src'), PathUri(path: '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(path: 'src')); + await client.webdav.mkcol(PathUri(path: '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(path: '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(path: 'src'), PathUri(path: 'dst1')); + await client.webdav.copy(PathUri(path: 'src'), PathUri(path: 'dst2')); expect( - () => client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst1')), + () => client.webdav.copy(PathUri(path: 'src'), PathUri(path: '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(path: 'src'), PathUri(path: '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(path: 'dst1/$i.txt')); expect(response.statusCode, 204); } - response = await client.webdav.delete(Uri(path: 'dst1/sub')); + response = await client.webdav.delete(PathUri(path: 'dst1/sub')); expect(response.statusCode, 204); - response = await client.webdav.delete(Uri(path: 'dst2')); + response = await client.webdav.delete(PathUri(path: '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(path: 'src1.txt')); + await client.webdav.put(Uint8List(0), PathUri(path: 'src2.txt')); + await client.webdav.mkcol(PathUri(path: 'coll')); - var response = await client.webdav.move(Uri(path: 'src1.txt'), Uri(path: 'dst.txt')); + var response = await client.webdav.move(PathUri(path: 'src1.txt'), PathUri(path: 'dst.txt')); expect(response.statusCode, 201); expect( - () => client.webdav.move(Uri(path: 'src2.txt'), Uri(path: 'dst.txt')), + () => client.webdav.move(PathUri(path: 'src2.txt'), PathUri(path: '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(path: 'src2.txt'), PathUri(path: '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(path: 'src')); + await client.webdav.mkcol(PathUri(path: '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(path: '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(path: 'noncoll')); + await client.webdav.copy(PathUri(path: 'src'), PathUri(path: 'dst2')); + await client.webdav.move(PathUri(path: 'src'), PathUri(path: 'dst1')); expect( - () => client.webdav.move(Uri(path: 'dst1'), Uri(path: 'dst2')), + () => client.webdav.move(PathUri(path: 'dst1'), PathUri(path: '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(path: 'dst2'), PathUri(path: 'dst1'), overwrite: true); + await client.webdav.copy(PathUri(path: 'dst1'), PathUri(path: '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(path: 'dst1/$i.txt')); expect(response.statusCode, 204); } - final response = await client.webdav.delete(Uri(path: 'dst1/sub')); + final response = await client.webdav.delete(PathUri(path: 'dst1/sub')); expect(response.statusCode, 204); expect( - () => client.webdav.move(Uri(path: 'dst2'), Uri(path: 'noncoll')), + () => client.webdav.move(PathUri(path: 'dst2'), PathUri(path: 'noncoll')), throwsA(predicate((final e) => e.statusCode == 412)), ); }); @@ -523,10 +584,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(path: 'test.txt')); expect(response.statusCode, 201); - final downloadedContent = await client.webdav.get(Uri(path: 'test.txt')); + final downloadedContent = await client.webdav.get(PathUri(path: 'test.txt')); expect(downloadedContent, hasLength(largefileSize)); }); });