Browse Source

Merge pull request #1103 from nextcloud/feature/nextcloud/webdav-pathuri

pull/1136/head
Kate 1 year ago committed by GitHub
parent
commit
d720dc8e08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 26
      packages/neon/neon_files/lib/blocs/browser.dart
  2. 84
      packages/neon/neon_files/lib/blocs/files.dart
  3. 11
      packages/neon/neon_files/lib/dialogs/choose_create.dart
  4. 16
      packages/neon/neon_files/lib/dialogs/choose_folder.dart
  5. 2
      packages/neon/neon_files/lib/dialogs/create_folder.dart
  6. 19
      packages/neon/neon_files/lib/models/file_details.dart
  7. 4
      packages/neon/neon_files/lib/pages/details.dart
  8. 2
      packages/neon/neon_files/lib/pages/main.dart
  9. 12
      packages/neon/neon_files/lib/utils/task.dart
  10. 29
      packages/neon/neon_files/lib/widgets/actions.dart
  11. 29
      packages/neon/neon_files/lib/widgets/browser_view.dart
  12. 4
      packages/neon/neon_files/lib/widgets/file_list_tile.dart
  13. 7
      packages/neon/neon_files/lib/widgets/file_preview.dart
  14. 22
      packages/neon/neon_files/lib/widgets/navigator.dart
  15. 37
      packages/nextcloud/lib/src/webdav/client.dart
  16. 15
      packages/nextcloud/lib/src/webdav/file.dart
  17. 138
      packages/nextcloud/lib/src/webdav/path_uri.dart
  18. 1
      packages/nextcloud/lib/webdav.dart
  19. 261
      packages/nextcloud/test/webdav_test.dart

26
packages/neon/neon_files/lib/blocs/browser.dart

@ -1,25 +1,25 @@
part of '../neon_files.dart'; part of '../neon_files.dart';
abstract interface class FilesBrowserBlocEvents { abstract interface class FilesBrowserBlocEvents {
void setPath(final List<String> path); void setPath(final PathUri uri);
void createFolder(final List<String> path); void createFolder(final PathUri uri);
} }
abstract interface class FilesBrowserBlocStates { abstract interface class FilesBrowserBlocStates {
BehaviorSubject<Result<List<WebDavFile>>> get files; BehaviorSubject<Result<List<WebDavFile>>> get files;
BehaviorSubject<List<String>> get path; BehaviorSubject<PathUri> get uri;
} }
class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents, FilesBrowserBlocStates { class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents, FilesBrowserBlocStates {
FilesBrowserBloc( FilesBrowserBloc(
this.options, this.options,
this.account, { this.account, {
final List<String>? initialPath, final PathUri? initialPath,
}) { }) {
if (initialPath != null) { if (initialPath != null) {
path.add(initialPath); uri.add(initialPath);
} }
unawaited(refresh()); unawaited(refresh());
@ -31,7 +31,7 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
@override @override
void dispose() { void dispose() {
unawaited(files.close()); unawaited(files.close());
unawaited(path.close()); unawaited(uri.close());
super.dispose(); super.dispose();
} }
@ -39,16 +39,16 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
BehaviorSubject<Result<List<WebDavFile>>> files = BehaviorSubject<Result<List<WebDavFile>>>(); BehaviorSubject<Result<List<WebDavFile>>> files = BehaviorSubject<Result<List<WebDavFile>>>();
@override @override
BehaviorSubject<List<String>> path = BehaviorSubject<List<String>>.seeded([]); BehaviorSubject<PathUri> uri = BehaviorSubject.seeded(PathUri.cwd());
@override @override
Future<void> refresh() async { Future<void> refresh() async {
await RequestManager.instance.wrapWebDav<List<WebDavFile>>( await RequestManager.instance.wrapWebDav<List<WebDavFile>>(
account.id, account.id,
'files-${path.value.join('/')}', 'files-${uri.value.path}',
files, files,
() => account.client.webdav.propfind( () => account.client.webdav.propfind(
Uri(pathSegments: path.value), uri.value,
prop: WebDavPropWithoutValues.fromBools( prop: WebDavPropWithoutValues.fromBools(
davgetcontenttype: true, davgetcontenttype: true,
davgetetag: true, davgetetag: true,
@ -65,13 +65,13 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
} }
@override @override
void setPath(final List<String> p) { void setPath(final PathUri uri) {
path.add(p); this.uri.add(uri);
unawaited(refresh()); unawaited(refresh());
} }
@override @override
void createFolder(final List<String> path) { void createFolder(final PathUri uri) {
wrapAction(() async => account.client.webdav.mkcol(Uri(pathSegments: path))); wrapAction(() async => account.client.webdav.mkcol(uri));
} }
} }

84
packages/neon/neon_files/lib/blocs/files.dart

@ -1,25 +1,25 @@
part of '../neon_files.dart'; part of '../neon_files.dart';
abstract interface class FilesBlocEvents { abstract interface class FilesBlocEvents {
void uploadFile(final List<String> path, final String localPath); void uploadFile(final PathUri uri, final String localPath);
void syncFile(final List<String> path); void syncFile(final PathUri uri);
void openFile(final List<String> path, final String etag, final String? mimeType); void openFile(final PathUri uri, final String etag, final String? mimeType);
void shareFileNative(final List<String> path, final String etag); void shareFileNative(final PathUri uri, final String etag);
void delete(final List<String> path); void delete(final PathUri uri);
void rename(final List<String> path, final String name); void rename(final PathUri uri, final String name);
void move(final List<String> path, final List<String> destination); void move(final PathUri uri, final PathUri destination);
void copy(final List<String> path, final List<String> destination); void copy(final PathUri uri, final PathUri destination);
void addFavorite(final List<String> path); void addFavorite(final PathUri uri);
void removeFavorite(final List<String> path); void removeFavorite(final PathUri uri);
} }
abstract interface class FilesBlocStates { abstract interface class FilesBlocStates {
@ -58,35 +58,35 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
BehaviorSubject<List<FilesTask>> tasks = BehaviorSubject<List<FilesTask>>.seeded([]); BehaviorSubject<List<FilesTask>> tasks = BehaviorSubject<List<FilesTask>>.seeded([]);
@override @override
void addFavorite(final List<String> path) { void addFavorite(final PathUri uri) {
wrapAction( wrapAction(
() async => account.client.webdav.proppatch( () async => account.client.webdav.proppatch(
Uri(pathSegments: path), uri,
set: WebDavProp(ocfavorite: 1), set: WebDavProp(ocfavorite: 1),
), ),
); );
} }
@override @override
void copy(final List<String> path, final List<String> destination) { void copy(final PathUri uri, final PathUri destination) {
wrapAction(() async => account.client.webdav.copy(Uri(pathSegments: path), Uri(pathSegments: destination))); wrapAction(() async => account.client.webdav.copy(uri, destination));
} }
@override @override
void delete(final List<String> path) { void delete(final PathUri uri) {
wrapAction(() async => account.client.webdav.delete(Uri(pathSegments: path))); wrapAction(() async => account.client.webdav.delete(uri));
} }
@override @override
void move(final List<String> path, final List<String> destination) { void move(final PathUri uri, final PathUri destination) {
wrapAction(() async => account.client.webdav.move(Uri(pathSegments: path), Uri(pathSegments: destination))); wrapAction(() async => account.client.webdav.move(uri, destination));
} }
@override @override
void openFile(final List<String> path, final String etag, final String? mimeType) { void openFile(final PathUri uri, final String etag, final String? mimeType) {
wrapAction( wrapAction(
() async { () async {
final file = await _cacheFile(path, etag); final file = await _cacheFile(uri, etag);
final result = await OpenFile.open(file.path, type: mimeType); final result = await OpenFile.open(file.path, type: mimeType);
if (result.type != ResultType.done) { if (result.type != ResultType.done) {
@ -98,10 +98,10 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
} }
@override @override
void shareFileNative(final List<String> path, final String etag) { void shareFileNative(final PathUri uri, final String etag) {
wrapAction( wrapAction(
() async { () async {
final file = await _cacheFile(path, etag); final file = await _cacheFile(uri, etag);
await Share.shareXFiles([XFile(file.path)]); await Share.shareXFiles([XFile(file.path)]);
}, },
@ -115,52 +115,52 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
} }
@override @override
void removeFavorite(final List<String> path) { void removeFavorite(final PathUri uri) {
wrapAction( wrapAction(
() async => account.client.webdav.proppatch( () async => account.client.webdav.proppatch(
Uri(pathSegments: path), uri,
set: WebDavProp(ocfavorite: 0), set: WebDavProp(ocfavorite: 0),
), ),
); );
} }
@override @override
void rename(final List<String> path, final String name) { void rename(final PathUri uri, final String name) {
wrapAction( wrapAction(
() async => account.client.webdav.move( () async => account.client.webdav.move(
Uri(pathSegments: path), uri,
Uri(pathSegments: List.from(path)..last = name), uri.rename(name),
), ),
); );
} }
@override @override
void syncFile(final List<String> path) { void syncFile(final PathUri uri) {
wrapAction( wrapAction(
() async { () async {
final file = File( final file = File(
p.join( p.joinAll([
await NeonPlatform.instance.userAccessibleAppDataPath, await NeonPlatform.instance.userAccessibleAppDataPath,
account.humanReadableID, account.humanReadableID,
'files', 'files',
path.join(Platform.pathSeparator), ...uri.pathSegments,
), ]),
); );
if (!file.parent.existsSync()) { if (!file.parent.existsSync()) {
file.parent.createSync(recursive: true); file.parent.createSync(recursive: true);
} }
await _downloadFile(path, file); await _downloadFile(uri, file);
}, },
disableTimeout: true, disableTimeout: true,
); );
} }
@override @override
void uploadFile(final List<String> path, final String localPath) { void uploadFile(final PathUri uri, final String localPath) {
wrapAction( wrapAction(
() async { () async {
final task = FilesUploadTask( final task = FilesUploadTask(
path: path, uri: uri,
file: File(localPath), file: File(localPath),
); );
tasks.add(tasks.value..add(task)); tasks.add(tasks.value..add(task));
@ -171,27 +171,27 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
); );
} }
Future<File> _cacheFile(final List<String> path, final String etag) async { Future<File> _cacheFile(final PathUri uri, final String etag) async {
final cacheDir = await getApplicationCacheDirectory(); 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('"', ''), uri.name));
if (!file.existsSync()) { if (!file.existsSync()) {
debugPrint('Downloading ${Uri(pathSegments: path)} since it does not exist'); debugPrint('Downloading $uri since it does not exist');
if (!file.parent.existsSync()) { if (!file.parent.existsSync()) {
await file.parent.create(recursive: true); await file.parent.create(recursive: true);
} }
await _downloadFile(path, file); await _downloadFile(uri, file);
} }
return file; return file;
} }
Future<void> _downloadFile( Future<void> _downloadFile(
final List<String> path, final PathUri uri,
final File file, final File file,
) async { ) async {
final task = FilesDownloadTask( final task = FilesDownloadTask(
path: path, uri: uri,
file: file, file: file,
); );
tasks.add(tasks.value..add(task)); tasks.add(tasks.value..add(task));
@ -200,12 +200,12 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
} }
FilesBrowserBloc getNewFilesBrowserBloc({ FilesBrowserBloc getNewFilesBrowserBloc({
final List<String>? initialPath, final PathUri? initialUri,
}) => }) =>
FilesBrowserBloc( FilesBrowserBloc(
options, options,
account, account,
initialPath: initialPath, initialPath: initialUri,
); );
void _downloadParallelismListener() { void _downloadParallelismListener() {

11
packages/neon/neon_files/lib/dialogs/choose_create.dart

@ -8,7 +8,7 @@ class FilesChooseCreateDialog extends StatefulWidget {
}); });
final FilesBloc bloc; final FilesBloc bloc;
final List<String> basePath; final PathUri basePath;
@override @override
State<FilesChooseCreateDialog> createState() => _FilesChooseCreateDialogState(); State<FilesChooseCreateDialog> createState() => _FilesChooseCreateDialogState();
@ -43,7 +43,10 @@ class _FilesChooseCreateDialogState extends State<FilesChooseCreateDialog> {
} }
} }
} }
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 @override
@ -104,12 +107,12 @@ class _FilesChooseCreateDialogState extends State<FilesChooseCreateDialog> {
onTap: () async { onTap: () async {
Navigator.of(context).pop(); Navigator.of(context).pop();
final result = await showDialog<List<String>>( final result = await showDialog<String>(
context: context, context: context,
builder: (final context) => const FilesCreateFolderDialog(), builder: (final context) => const FilesCreateFolderDialog(),
); );
if (result != null) { if (result != null) {
widget.bloc.browser.createFolder([...widget.basePath, ...result]); widget.bloc.browser.createFolder(widget.basePath.join(PathUri.parse(result)));
} }
}, },
), ),

16
packages/neon/neon_files/lib/dialogs/choose_folder.dart

@ -11,7 +11,7 @@ class FilesChooseFolderDialog extends StatelessWidget {
final FilesBrowserBloc bloc; final FilesBrowserBloc bloc;
final FilesBloc filesBloc; final FilesBloc filesBloc;
final List<String> originalPath; final PathUri originalPath;
@override @override
Widget build(final BuildContext context) => AlertDialog( Widget build(final BuildContext context) => AlertDialog(
@ -28,9 +28,9 @@ class FilesChooseFolderDialog extends StatelessWidget {
mode: FilesBrowserMode.selectDirectory, mode: FilesBrowserMode.selectDirectory,
), ),
), ),
StreamBuilder<List<String>>( StreamBuilder<PathUri>(
stream: bloc.path, stream: bloc.uri,
builder: (final context, final pathSnapshot) => pathSnapshot.hasData builder: (final context, final uriSnapshot) => uriSnapshot.hasData
? Container( ? Container(
margin: const EdgeInsets.all(10), margin: const EdgeInsets.all(10),
child: Row( child: Row(
@ -38,19 +38,19 @@ class FilesChooseFolderDialog extends StatelessWidget {
children: [ children: [
ElevatedButton( ElevatedButton(
onPressed: () async { onPressed: () async {
final result = await showDialog<List<String>>( final result = await showDialog<String>(
context: context, context: context,
builder: (final context) => const FilesCreateFolderDialog(), builder: (final context) => const FilesCreateFolderDialog(),
); );
if (result != null) { if (result != null) {
bloc.createFolder([...pathSnapshot.requireData, ...result]); bloc.createFolder(uriSnapshot.requireData.join(PathUri.parse(result)));
} }
}, },
child: Text(FilesLocalizations.of(context).folderCreate), child: Text(FilesLocalizations.of(context).folderCreate),
), ),
ElevatedButton( ElevatedButton(
onPressed: !(const ListEquality<String>().equals(originalPath, pathSnapshot.data)) onPressed: originalPath != uriSnapshot.requireData
? () => Navigator.of(context).pop(pathSnapshot.data) ? () => Navigator.of(context).pop(uriSnapshot.requireData)
: null, : null,
child: Text(FilesLocalizations.of(context).folderChoose), child: Text(FilesLocalizations.of(context).folderChoose),
), ),

2
packages/neon/neon_files/lib/dialogs/create_folder.dart

@ -22,7 +22,7 @@ class _FilesCreateFolderDialogState extends State<FilesCreateFolderDialog> {
void submit() { void submit() {
if (formKey.currentState!.validate()) { if (formKey.currentState!.validate()) {
Navigator.of(context).pop(controller.text.split('/')); Navigator.of(context).pop(controller.text);
} }
} }

19
packages/neon/neon_files/lib/models/file_details.dart

@ -3,8 +3,7 @@ part of '../neon_files.dart';
@immutable @immutable
class FileDetails { class FileDetails {
const FileDetails({ const FileDetails({
required this.path, required this.uri,
required this.isDirectory,
required this.size, required this.size,
required this.etag, required this.etag,
required this.mimeType, required this.mimeType,
@ -15,9 +14,7 @@ class FileDetails {
FileDetails.fromWebDav({ FileDetails.fromWebDav({
required final WebDavFile file, required final WebDavFile file,
required final List<String> path, }) : uri = file.path,
}) : path = List.from(path)..add(file.name),
isDirectory = file.isDirectory,
size = file.size, size = file.size,
etag = file.etag, etag = file.etag,
mimeType = file.mimeType, mimeType = file.mimeType,
@ -28,10 +25,9 @@ class FileDetails {
FileDetails.fromUploadTask({ FileDetails.fromUploadTask({
required FilesUploadTask this.task, required FilesUploadTask this.task,
}) : path = task.path, }) : uri = task.uri,
size = task.stat.size, size = task.stat.size,
lastModified = task.stat.modified, lastModified = task.stat.modified,
isDirectory = false,
etag = null, etag = null,
mimeType = null, mimeType = null,
hasPreview = null, hasPreview = null,
@ -40,8 +36,7 @@ class FileDetails {
FileDetails.fromDownloadTask({ FileDetails.fromDownloadTask({
required FilesDownloadTask this.task, required FilesDownloadTask this.task,
required final WebDavFile file, required final WebDavFile file,
}) : path = task.path, }) : uri = task.uri,
isDirectory = file.isDirectory,
size = file.size, size = file.size,
etag = file.etag, etag = file.etag,
mimeType = file.mimeType, mimeType = file.mimeType,
@ -64,11 +59,11 @@ class FileDetails {
} }
} }
String get name => path.last; String get name => uri.name;
final List<String> path; bool get isDirectory => uri.isDirectory;
final bool isDirectory; final PathUri uri;
final int? size; final int? size;

4
packages/neon/neon_files/lib/pages/details.dart

@ -43,8 +43,8 @@ class FilesDetailsPage extends StatelessWidget {
details.isDirectory details.isDirectory
? FilesLocalizations.of(context).detailsFolderName ? FilesLocalizations.of(context).detailsFolderName
: FilesLocalizations.of(context).detailsFileName: details.name, : FilesLocalizations.of(context).detailsFileName: details.name,
FilesLocalizations.of(context).detailsParentFolder: if (details.uri.parent != null)
details.path.length == 1 ? '/' : details.path.sublist(0, details.path.length - 1).join('/'), FilesLocalizations.of(context).detailsParentFolder: details.uri.parent!.path,
if (details.size != null) ...{ if (details.size != null) ...{
details.isDirectory details.isDirectory
? FilesLocalizations.of(context).detailsFolderSize ? FilesLocalizations.of(context).detailsFolderSize

2
packages/neon/neon_files/lib/pages/main.dart

@ -34,7 +34,7 @@ class _FilesMainPageState extends State<FilesMainPage> {
context: context, context: context,
builder: (final context) => FilesChooseCreateDialog( builder: (final context) => FilesChooseCreateDialog(
bloc: bloc, bloc: bloc,
basePath: bloc.browser.path.value, basePath: bloc.browser.uri.value,
), ),
); );
}, },

12
packages/neon/neon_files/lib/utils/task.dart

@ -2,11 +2,11 @@ part of '../neon_files.dart';
sealed class FilesTask { sealed class FilesTask {
FilesTask({ FilesTask({
required this.path, required this.uri,
required this.file, required this.file,
}); });
final List<String> path; final PathUri uri;
final File file; final File file;
@ -19,13 +19,13 @@ sealed class FilesTask {
class FilesDownloadTask extends FilesTask { class FilesDownloadTask extends FilesTask {
FilesDownloadTask({ FilesDownloadTask({
required super.path, required super.uri,
required super.file, required super.file,
}); });
Future<void> execute(final NextcloudClient client) async { Future<void> execute(final NextcloudClient client) async {
await client.webdav.getFile( await client.webdav.getFile(
Uri(pathSegments: path), uri,
file, file,
onProgress: streamController.add, onProgress: streamController.add,
); );
@ -35,7 +35,7 @@ class FilesDownloadTask extends FilesTask {
class FilesUploadTask extends FilesTask { class FilesUploadTask extends FilesTask {
FilesUploadTask({ FilesUploadTask({
required super.path, required super.uri,
required super.file, required super.file,
}); });
@ -46,7 +46,7 @@ class FilesUploadTask extends FilesTask {
await client.webdav.putFile( await client.webdav.putFile(
file, file,
stat, stat,
Uri(pathSegments: path), uri,
lastModified: stat.modified, lastModified: stat.modified,
onProgress: streamController.add, onProgress: streamController.add,
); );

29
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/utils.dart';
import 'package:neon_files/l10n/localizations.dart'; import 'package:neon_files/l10n/localizations.dart';
import 'package:neon_files/neon_files.dart'; import 'package:neon_files/neon_files.dart';
import 'package:nextcloud/webdav.dart';
class FileActions extends StatelessWidget { class FileActions extends StatelessWidget {
const FileActions({ const FileActions({
@ -18,12 +19,12 @@ class FileActions extends StatelessWidget {
final browserBloc = bloc.browser; final browserBloc = bloc.browser;
switch (action) { switch (action) {
case FilesFileAction.share: case FilesFileAction.share:
bloc.shareFileNative(details.path, details.etag!); bloc.shareFileNative(details.uri, details.etag!);
case FilesFileAction.toggleFavorite: case FilesFileAction.toggleFavorite:
if (details.isFavorite ?? false) { if (details.isFavorite ?? false) {
bloc.removeFavorite(details.path); bloc.removeFavorite(details.uri);
} else { } else {
bloc.addFavorite(details.path); bloc.addFavorite(details.uri);
} }
case FilesFileAction.details: case FilesFileAction.details:
await Navigator.of(context).push( await Navigator.of(context).push(
@ -46,15 +47,15 @@ class FileActions extends StatelessWidget {
value: details.name, value: details.name,
); );
if (result != null) { if (result != null) {
bloc.rename(details.path, result); bloc.rename(details.uri, result);
} }
case FilesFileAction.move: case FilesFileAction.move:
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
final originalPath = details.path.sublist(0, details.path.length - 1); final originalPath = details.uri.parent!;
final b = bloc.getNewFilesBrowserBloc(initialPath: originalPath); final b = bloc.getNewFilesBrowserBloc(initialUri: originalPath);
final result = await showDialog<List<String>?>( final result = await showDialog<PathUri>(
context: context, context: context,
builder: (final context) => FilesChooseFolderDialog( builder: (final context) => FilesChooseFolderDialog(
bloc: b, bloc: b,
@ -64,15 +65,15 @@ class FileActions extends StatelessWidget {
); );
b.dispose(); b.dispose();
if (result != null) { if (result != null) {
bloc.move(details.path, result..add(details.name)); bloc.move(details.uri, result.join(PathUri.parse(details.name)));
} }
case FilesFileAction.copy: case FilesFileAction.copy:
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
final originalPath = details.path.sublist(0, details.path.length - 1); final originalPath = details.uri.parent!;
final b = bloc.getNewFilesBrowserBloc(initialPath: originalPath); final b = bloc.getNewFilesBrowserBloc(initialUri: originalPath);
final result = await showDialog<List<String>?>( final result = await showDialog<PathUri>(
context: context, context: context,
builder: (final context) => FilesChooseFolderDialog( builder: (final context) => FilesChooseFolderDialog(
bloc: b, bloc: b,
@ -82,7 +83,7 @@ class FileActions extends StatelessWidget {
); );
b.dispose(); b.dispose();
if (result != null) { if (result != null) {
bloc.copy(details.path, result..add(details.name)); bloc.copy(details.uri, result.join(PathUri.parse(details.name)));
} }
case FilesFileAction.sync: case FilesFileAction.sync:
if (!context.mounted) { if (!context.mounted) {
@ -100,7 +101,7 @@ class FileActions extends StatelessWidget {
return; return;
} }
} }
bloc.syncFile(details.path); bloc.syncFile(details.uri);
case FilesFileAction.delete: case FilesFileAction.delete:
if (!context.mounted) { if (!context.mounted) {
return; return;
@ -111,7 +112,7 @@ class FileActions extends StatelessWidget {
? FilesLocalizations.of(context).folderDeleteConfirm(details.name) ? FilesLocalizations.of(context).folderDeleteConfirm(details.name)
: FilesLocalizations.of(context).fileDeleteConfirm(details.name), : FilesLocalizations.of(context).fileDeleteConfirm(details.name),
)) { )) {
bloc.delete(details.path); bloc.delete(details.uri);
} }
} }
} }

29
packages/neon/neon_files/lib/widgets/browser_view.dart

@ -43,12 +43,12 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
@override @override
Widget build(final BuildContext context) => ResultBuilder<List<WebDavFile>>.behaviorSubject( Widget build(final BuildContext context) => ResultBuilder<List<WebDavFile>>.behaviorSubject(
subject: widget.bloc.files, subject: widget.bloc.files,
builder: (final context, final filesSnapshot) => StreamBuilder<List<String>>( builder: (final context, final filesSnapshot) => StreamBuilder<PathUri>(
stream: widget.bloc.path, stream: widget.bloc.uri,
builder: (final context, final pathSnapshot) => StreamBuilder<List<FilesTask>>( builder: (final context, final uriSnapshot) => StreamBuilder<List<FilesTask>>(
stream: widget.filesBloc.tasks, stream: widget.filesBloc.tasks,
builder: (final context, final tasksSnapshot) { builder: (final context, final tasksSnapshot) {
if (!pathSnapshot.hasData || !tasksSnapshot.hasData) { if (!uriSnapshot.hasData || !tasksSnapshot.hasData) {
return const SizedBox(); return const SizedBox();
} }
return ValueListenableBuilder( return ValueListenableBuilder(
@ -68,9 +68,9 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
return BackButtonListener( return BackButtonListener(
onBackButtonPressed: () async { onBackButtonPressed: () async {
final path = pathSnapshot.requireData; final parent = uriSnapshot.requireData.parent;
if (path.isNotEmpty) { if (parent != null) {
widget.bloc.setPath(path.sublist(0, path.length - 1)); widget.bloc.setPath(parent);
return true; return true;
} }
return false; return false;
@ -87,12 +87,13 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
final uploadingTaskTiles = buildUploadTasks(tasksSnapshot.requireData, sorted); final uploadingTaskTiles = buildUploadTasks(tasksSnapshot.requireData, sorted);
return NeonListView( return NeonListView(
scrollKey: 'files-${pathSnapshot.requireData.join('/')}', scrollKey: 'files-${uriSnapshot.requireData.path}',
itemCount: sorted.length, itemCount: sorted.length,
itemBuilder: (final context, final index) { itemBuilder: (final context, final index) {
final file = sorted[index]; final file = sorted[index];
final matchingTask = tasksSnapshot.requireData final matchingTask = tasksSnapshot.requireData.firstWhereOrNull(
.firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name)); (final task) => file.name == task.uri.name && widget.bloc.uri.value == task.uri.parent,
);
final details = matchingTask != null final details = matchingTask != null
? FileDetails.fromTask( ? FileDetails.fromTask(
@ -101,7 +102,6 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
) )
: FileDetails.fromWebDav( : FileDetails.fromWebDav(
file: file, file: file,
path: widget.bloc.path.value,
); );
return FileListTile( return FileListTile(
@ -116,7 +116,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
onRefresh: widget.bloc.refresh, onRefresh: widget.bloc.refresh,
topScrollingChildren: [ topScrollingChildren: [
FilesBrowserNavigator( FilesBrowserNavigator(
path: pathSnapshot.requireData, uri: uriSnapshot.requireData,
bloc: widget.bloc, bloc: widget.bloc,
), ),
...uploadingTaskTiles, ...uploadingTaskTiles,
@ -147,9 +147,4 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
); );
} }
} }
bool _pathMatchesFile(final List<String> path, final String name) => const ListEquality<String>().equals(
[...widget.bloc.path.value, name],
path,
);
} }

4
packages/neon/neon_files/lib/widgets/file_list_tile.dart

@ -24,7 +24,7 @@ class FileListTile extends StatelessWidget {
Future<void> _onTap(final BuildContext context, final FileDetails details) async { Future<void> _onTap(final BuildContext context, final FileDetails details) async {
if (details.isDirectory) { if (details.isDirectory) {
browserBloc.setPath(details.path); browserBloc.setPath(details.uri);
} else if (mode == FilesBrowserMode.browser) { } else if (mode == FilesBrowserMode.browser) {
final sizeWarning = bloc.options.downloadSizeWarning.value; final sizeWarning = bloc.options.downloadSizeWarning.value;
if (sizeWarning != null && details.size != null && details.size! > sizeWarning) { if (sizeWarning != null && details.size != null && details.size! > sizeWarning) {
@ -38,7 +38,7 @@ class FileListTile extends StatelessWidget {
return; return;
} }
} }
bloc.openFile(details.path, details.etag!, details.mimeType); bloc.openFile(details.uri, details.etag!, details.mimeType);
} }
} }

7
packages/neon/neon_files/lib/widgets/file_preview.dart

@ -76,14 +76,12 @@ class FilePreviewImage extends NeonApiImage {
}) { }) {
final width = size.width.toInt(); final width = size.width.toInt();
final height = size.height.toInt(); final height = size.height.toInt();
final path = file.path.join('/'); final cacheKey = 'preview-${file.uri.path}-$width-$height';
final cacheKey = 'preview-$path-$width-$height';
return FilePreviewImage._( return FilePreviewImage._(
file: file, file: file,
size: size, size: size,
cacheKey: cacheKey, cacheKey: cacheKey,
path: path,
width: width, width: width,
height: height, height: height,
); );
@ -93,12 +91,11 @@ class FilePreviewImage extends NeonApiImage {
required final FileDetails file, required final FileDetails file,
required Size super.size, required Size super.size,
required super.cacheKey, required super.cacheKey,
required final String path,
required final int width, required final int width,
required final int height, required final int height,
}) : super( }) : super(
getImage: (final client) async => client.core.preview.getPreview( getImage: (final client) async => client.core.preview.getPreview(
file: path, file: file.uri.path,
x: width, x: width,
y: height, y: height,
), ),

22
packages/neon/neon_files/lib/widgets/navigator.dart

@ -2,12 +2,12 @@ part of '../neon_files.dart';
class FilesBrowserNavigator extends StatelessWidget { class FilesBrowserNavigator extends StatelessWidget {
const FilesBrowserNavigator({ const FilesBrowserNavigator({
required this.path, required this.uri,
required this.bloc, required this.bloc,
super.key, super.key,
}); });
final List<String> path; final PathUri uri;
final FilesBrowserBloc bloc; final FilesBrowserBloc bloc;
@override @override
@ -18,7 +18,7 @@ class FilesBrowserNavigator extends StatelessWidget {
horizontal: 10, horizontal: 10,
), ),
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
itemCount: path.length + 1, itemCount: uri.pathSegments.length + 1,
itemBuilder: (final context, final index) { itemBuilder: (final context, final index) {
if (index == 0) { if (index == 0) {
return IconButton( return IconButton(
@ -30,21 +30,23 @@ class FilesBrowserNavigator extends StatelessWidget {
tooltip: FilesLocalizations.of(context).goToPath(''), tooltip: FilesLocalizations.of(context).goToPath(''),
icon: const Icon(Icons.house), icon: const Icon(Icons.house),
onPressed: () { onPressed: () {
bloc.setPath([]); bloc.setPath(PathUri.cwd());
}, },
); );
} }
final path = this.path.sublist(0, index); final partialPath = PathUri(
final label = path.join('/'); isAbsolute: uri.isAbsolute,
isDirectory: uri.isDirectory,
pathSegments: uri.pathSegments.sublist(0, index),
);
return TextButton( return TextButton(
onPressed: () { onPressed: () {
bloc.setPath(path); bloc.setPath(partialPath);
}, },
child: Text( child: Text(
path.last, partialPath.name,
semanticsLabel: FilesLocalizations.of(context).goToPath(label), semanticsLabel: FilesLocalizations.of(context).goToPath(partialPath.name),
), ),
); );
}, },

37
packages/nextcloud/lib/src/webdav/client.dart

@ -4,13 +4,14 @@ import 'dart:typed_data';
import 'package:dynamite_runtime/http_client.dart'; import 'package:dynamite_runtime/http_client.dart';
import 'package:meta/meta.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/props.dart';
import 'package:nextcloud/src/webdav/webdav.dart'; import 'package:nextcloud/src/webdav/webdav.dart';
import 'package:universal_io/io.dart'; import 'package:universal_io/io.dart';
import 'package:xml/xml.dart' as xml; import 'package:xml/xml.dart' as xml;
/// Base path used on the server /// Base path used on the server
final webdavBase = Uri(path: '/remote.php/webdav'); final webdavBase = PathUri.parse('/remote.php/webdav');
/// WebDavClient class /// WebDavClient class
class WebDavClient { class WebDavClient {
@ -60,11 +61,11 @@ class WebDavClient {
return response; return response;
} }
Uri _constructUri([final Uri? path]) => constructUri(rootClient.baseURL, path); Uri _constructUri([final PathUri? path]) => constructUri(rootClient.baseURL, path);
@visibleForTesting @visibleForTesting
// ignore: public_member_api_docs // 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); final segments = baseURL.pathSegments.toList()..addAll(webdavBase.pathSegments);
if (path != null) { if (path != null) {
segments.addAll(path.pathSegments); segments.addAll(path.pathSegments);
@ -103,7 +104,7 @@ class WebDavClient {
/// Creates a collection at [path]. /// Creates a collection at [path].
/// ///
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL for more information. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL for more information.
Future<HttpClientResponse> mkcol(final Uri path) async => _send( Future<HttpClientResponse> mkcol(final PathUri path) async => _send(
'MKCOL', 'MKCOL',
_constructUri(path), _constructUri(path),
); );
@ -111,7 +112,7 @@ class WebDavClient {
/// Deletes the resource at [path]. /// Deletes the resource at [path].
/// ///
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_DELETE for more information. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_DELETE for more information.
Future<HttpClientResponse> delete(final Uri path) => _send( Future<HttpClientResponse> delete(final PathUri path) => _send(
'DELETE', 'DELETE',
_constructUri(path), _constructUri(path),
); );
@ -123,7 +124,7 @@ class WebDavClient {
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information.
Future<HttpClientResponse> put( Future<HttpClientResponse> put(
final Uint8List localData, final Uint8List localData,
final Uri path, { final PathUri path, {
final DateTime? lastModified, final DateTime? lastModified,
final DateTime? created, final DateTime? created,
}) => }) =>
@ -147,7 +148,7 @@ class WebDavClient {
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PUT for more information.
Future<HttpClientResponse> putStream( Future<HttpClientResponse> putStream(
final Stream<Uint8List> localData, final Stream<Uint8List> localData,
final Uri path, { final PathUri path, {
final DateTime? lastModified, final DateTime? lastModified,
final DateTime? created, final DateTime? created,
final int? contentLength, final int? contentLength,
@ -181,7 +182,7 @@ class WebDavClient {
Future<HttpClientResponse> putFile( Future<HttpClientResponse> putFile(
final File file, final File file,
final FileStat fileStat, final FileStat fileStat,
final Uri path, { final PathUri path, {
final DateTime? lastModified, final DateTime? lastModified,
final DateTime? created, final DateTime? created,
final void Function(double progress)? onProgress, final void Function(double progress)? onProgress,
@ -196,17 +197,17 @@ class WebDavClient {
); );
/// Gets the content of the file at [path]. /// Gets the content of the file at [path].
Future<Uint8List> get(final Uri path) async => (await getStream(path)).bytes; Future<Uint8List> get(final PathUri path) async => (await getStream(path)).bytes;
/// Gets the content of the file at [path]. /// Gets the content of the file at [path].
Future<HttpClientResponse> getStream(final Uri path) async => _send( Future<HttpClientResponse> getStream(final PathUri path) async => _send(
'GET', 'GET',
_constructUri(path), _constructUri(path),
); );
/// Gets the content of the file at [path]. /// Gets the content of the file at [path].
Future<void> getFile( Future<void> getFile(
final Uri path, final PathUri path,
final File file, { final File file, {
final void Function(double progress)? onProgress, final void Function(double progress)? onProgress,
}) async { }) async {
@ -236,7 +237,7 @@ class WebDavClient {
/// [depth] can be used to limit scope of the returned resources. /// [depth] can be used to limit scope of the returned resources.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPFIND for more information. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPFIND for more information.
Future<WebDavMultistatus> propfind( Future<WebDavMultistatus> propfind(
final Uri path, { final PathUri path, {
final WebDavPropWithoutValues? prop, final WebDavPropWithoutValues? prop,
final WebDavDepth? depth, final WebDavDepth? depth,
}) async => }) async =>
@ -256,7 +257,7 @@ class WebDavClient {
/// Optionally populates the [prop]s on the returned resources. /// Optionally populates the [prop]s on the returned resources.
/// See https://github.com/owncloud/docs/issues/359 for more information. /// See https://github.com/owncloud/docs/issues/359 for more information.
Future<WebDavMultistatus> report( Future<WebDavMultistatus> report(
final Uri path, final PathUri path,
final WebDavOcFilterRules filterRules, { final WebDavOcFilterRules filterRules, {
final WebDavPropWithoutValues? prop, final WebDavPropWithoutValues? prop,
}) async => }) async =>
@ -280,7 +281,7 @@ class WebDavClient {
/// Returns true if the update was successful. /// Returns true if the update was successful.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPPATCH for more information. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_PROPPATCH for more information.
Future<bool> proppatch( Future<bool> proppatch(
final Uri path, { final PathUri path, {
final WebDavProp? set, final WebDavProp? set,
final WebDavPropWithoutValues? remove, final WebDavPropWithoutValues? remove,
}) async { }) async {
@ -310,8 +311,8 @@ class WebDavClient {
/// If [overwrite] is set any existing resource will be replaced. /// If [overwrite] is set any existing resource will be replaced.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_MOVE for more information. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_MOVE for more information.
Future<HttpClientResponse> move( Future<HttpClientResponse> move(
final Uri sourcePath, final PathUri sourcePath,
final Uri destinationPath, { final PathUri destinationPath, {
final bool overwrite = false, final bool overwrite = false,
}) => }) =>
_send( _send(
@ -328,8 +329,8 @@ class WebDavClient {
/// If [overwrite] is set any existing resource will be replaced. /// If [overwrite] is set any existing resource will be replaced.
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_COPY for more information. /// See http://www.webdav.org/specs/rfc2518.html#METHOD_COPY for more information.
Future<HttpClientResponse> copy( Future<HttpClientResponse> copy(
final Uri sourcePath, final PathUri sourcePath,
final Uri destinationPath, { final PathUri destinationPath, {
final bool overwrite = false, final bool overwrite = false,
}) => }) =>
_send( _send(

15
packages/nextcloud/lib/src/webdav/file.dart

@ -1,4 +1,5 @@
import 'package:nextcloud/src/webdav/client.dart'; 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/props.dart';
import 'package:nextcloud/src/webdav/webdav.dart'; import 'package:nextcloud/src/webdav/webdav.dart';
@ -25,8 +26,14 @@ class WebDavFile {
_response.propstats.singleWhere((final propstat) => propstat.status.contains('200')).prop; _response.propstats.singleWhere((final propstat) => propstat.status.contains('200')).prop;
/// The path of file /// The path of file
late final Uri path = late final PathUri path = () {
Uri(pathSegments: Uri(path: _response.href).pathSegments.sublist(webdavBase.pathSegments.length)); final href = PathUri.parse(Uri.decodeFull(_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 /// The fileid namespaced by the instance id, globally unique
late final String? id = props.ocid; late final String? id = props.ocid;
@ -79,11 +86,11 @@ class WebDavFile {
late final bool? hasPreview = props.nchaspreview; late final bool? hasPreview = props.nchaspreview;
/// Returns the decoded name of the file / folder without the whole path /// 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. /// Whether the file is hidden.
late final bool isHidden = name.startsWith('.'); late final bool isHidden = name.startsWith('.');
/// Whether the file is a directory /// Whether the file is a directory
late final bool isDirectory = (isCollection ?? false) || path.pathSegments.last.isEmpty; late final bool isDirectory = (isCollection ?? false) || path.isDirectory;
} }

138
packages/nextcloud/lib/src/webdav/path_uri.dart

@ -0,0 +1,138 @@
import 'package:built_collection/built_collection.dart';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
/// A `Uri` like object that is specialized in file path handling.
@immutable
class PathUri {
/// Creates a new path URI.
const PathUri({
required this.isAbsolute,
required this.isDirectory,
required this.pathSegments,
});
/// Creates a new `PathUri` object by parsing a [path] string.
///
/// An empty [path] is considered to be the current working directory.
factory PathUri.parse(final String path) {
final parts = path.split('/');
if (parts.length == 1 && parts.single.isEmpty) {
return PathUri(
isAbsolute: false,
isDirectory: true,
pathSegments: BuiltList(),
);
}
return PathUri(
isAbsolute: parts.first.isEmpty,
isDirectory: parts.last.isEmpty,
pathSegments: BuiltList(parts.where((final element) => element.isNotEmpty)),
);
}
/// Creates a new empty path URI representing the current working directory.
factory PathUri.cwd() => PathUri(
isAbsolute: false,
isDirectory: true,
pathSegments: BuiltList(),
);
/// 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 BuiltList<String> pathSegments;
/// Returns the path as a string.
///
/// See [pathSegments] for getting the path as a list of its segments.
String get path {
final buffer = StringBuffer();
if (isAbsolute) {
buffer.write('/');
}
buffer.writeAll(pathSegments, '/');
if (isDirectory && pathSegments.isNotEmpty) {
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.rebuild((final b) => b.removeLast()),
);
}
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.rebuild((final b) => b.addAll(other.pathSegments)),
);
}
/// Renames the last path segment and returns a new path URI.
PathUri rename(final String name) {
if (name.contains('/')) {
throw Exception('Path names must not contain /');
}
return PathUri(
isAbsolute: isAbsolute,
isDirectory: isDirectory,
pathSegments: pathSegments.isNotEmpty
? pathSegments.rebuild(
(final b) => b
..removeLast()
..add(name),
)
: BuiltList(),
);
}
@override
bool operator ==(final Object other) =>
other is PathUri &&
isAbsolute == other.isAbsolute &&
isDirectory == other.isDirectory &&
pathSegments == other.pathSegments;
@override
int get hashCode => Object.hashAll([
isAbsolute,
isDirectory,
pathSegments,
]);
@override
String toString() => path;
}

1
packages/nextcloud/lib/webdav.dart

@ -4,6 +4,7 @@ import 'package:nextcloud/src/webdav/client.dart';
export 'src/webdav/client.dart'; export 'src/webdav/client.dart';
export 'src/webdav/file.dart'; export 'src/webdav/file.dart';
export 'src/webdav/path_uri.dart';
export 'src/webdav/props.dart'; export 'src/webdav/props.dart';
export 'src/webdav/webdav.dart'; export 'src/webdav/webdav.dart';

261
packages/nextcloud/test/webdav_test.dart

@ -19,17 +19,122 @@ void main() {
final baseURL = Uri.parse(values.$1); final baseURL = Uri.parse(values.$1);
final sanitizedBaseURL = Uri.parse(values.$2); final sanitizedBaseURL = Uri.parse(values.$2);
test('$baseURL', () { test(baseURL, () {
expect(WebDavClient.constructUri(baseURL).toString(), '$sanitizedBaseURL$webdavBase'); expect(
expect(WebDavClient.constructUri(baseURL, Uri(path: '/')).toString(), '$sanitizedBaseURL$webdavBase'); WebDavClient.constructUri(baseURL).toString(),
expect(WebDavClient.constructUri(baseURL, Uri(path: 'test')).toString(), '$sanitizedBaseURL$webdavBase/test'); '$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(
expect(WebDavClient.constructUri(baseURL, Uri(path: '/test/')).toString(), '$sanitizedBaseURL$webdavBase/test'); 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('/').join(PathUri.parse('test')), PathUri.parse('/test'));
expect(() => PathUri.parse('test').join(PathUri.parse('abc')), throwsA(isA<StateError>()));
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')), PathUri.parse('/test/abc'));
expect(PathUri.parse('/test/').join(PathUri.parse('/abc')), PathUri.parse('/test/abc'));
expect(PathUri.parse('/test/').join(PathUri.parse('/abc/')), PathUri.parse('/test/abc/'));
});
test('rename', () {
expect(PathUri.parse('').rename('test'), PathUri.parse(''));
expect(PathUri.parse('test').rename('abc'), PathUri.parse('abc'));
expect(PathUri.parse('test/').rename('abc'), PathUri.parse('abc/'));
expect(PathUri.parse('test/abc').rename('123'), PathUri.parse('test/123'));
expect(PathUri.parse('test/abc/').rename('123'), PathUri.parse('test/123/'));
expect(() => PathUri.parse('test').rename('abc/'), throwsA(isA<Exception>()));
expect(() => PathUri.parse('test/').rename('abc/'), throwsA(isA<Exception>()));
expect(() => PathUri.parse('test').rename('/abc'), throwsA(isA<Exception>()));
expect(() => PathUri.parse('test/').rename('/abc'), throwsA(isA<Exception>()));
expect(() => PathUri.parse('test').rename('abc/123'), throwsA(isA<Exception>()));
expect(() => PathUri.parse('test/').rename('abc/123'), throwsA(isA<Exception>()));
});
});
group( group(
'webdav', 'webdav',
() { () {
@ -44,7 +149,7 @@ void main() {
test('List directory', () async { test('List directory', () async {
final responses = (await client.webdav.propfind( final responses = (await client.webdav.propfind(
Uri(path: '/'), PathUri.parse('/'),
prop: WebDavPropWithoutValues.fromBools( prop: WebDavPropWithoutValues.fromBools(
nchaspreview: true, nchaspreview: true,
davgetcontenttype: true, davgetcontenttype: true,
@ -64,7 +169,7 @@ void main() {
test('List directory recursively', () async { test('List directory recursively', () async {
final responses = (await client.webdav.propfind( final responses = (await client.webdav.propfind(
Uri(path: '/'), PathUri.parse('/'),
depth: WebDavDepth.infinity, depth: WebDavDepth.infinity,
)) ))
.responses; .responses;
@ -73,7 +178,7 @@ void main() {
test('Get file props', () async { test('Get file props', () async {
final response = (await client.webdav.propfind( final response = (await client.webdav.propfind(
Uri(path: 'Nextcloud.png'), PathUri.parse('Nextcloud.png'),
prop: WebDavPropWithoutValues.fromBools( prop: WebDavPropWithoutValues.fromBools(
davgetlastmodified: true, davgetlastmodified: true,
davgetetag: true, davgetetag: true,
@ -107,7 +212,7 @@ void main() {
.toWebDavFiles() .toWebDavFiles()
.single; .single;
expect(response.path, Uri(path: 'Nextcloud.png')); expect(response.path, PathUri.parse('Nextcloud.png'));
expect(response.id, isNotEmpty); expect(response.id, isNotEmpty);
expect(response.fileId, isNotEmpty); expect(response.fileId, isNotEmpty);
expect(response.isCollection, isFalse); expect(response.isCollection, isFalse);
@ -156,11 +261,11 @@ void main() {
test('Get directory props', () async { test('Get directory props', () async {
final data = utf8.encode('test') as Uint8List; final data = utf8.encode('test') as Uint8List;
await client.webdav.mkcol(Uri(path: 'test')); await client.webdav.mkcol(PathUri.parse('test'));
await client.webdav.put(data, Uri(path: 'test/test.txt')); await client.webdav.put(data, PathUri.parse('test/test.txt'));
final response = (await client.webdav.propfind( final response = (await client.webdav.propfind(
Uri(path: 'test'), PathUri.parse('test'),
prop: WebDavPropWithoutValues.fromBools( prop: WebDavPropWithoutValues.fromBools(
davgetcontenttype: true, davgetcontenttype: true,
davgetlastmodified: true, davgetlastmodified: true,
@ -172,7 +277,7 @@ void main() {
.toWebDavFiles() .toWebDavFiles()
.single; .single;
expect(response.path, Uri(path: 'test/')); expect(response.path, PathUri.parse('test/'));
expect(response.isCollection, isTrue); expect(response.isCollection, isTrue);
expect(response.mimeType, isNull); expect(response.mimeType, isNull);
expect(response.size, data.lengthInBytes); expect(response.size, data.lengthInBytes);
@ -187,17 +292,17 @@ void main() {
}); });
test('Filter files', () async { 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; final id = response.headers['oc-fileid']!.first;
await client.webdav.proppatch( await client.webdav.proppatch(
Uri(path: 'test.txt'), PathUri.parse('test.txt'),
set: WebDavProp( set: WebDavProp(
ocfavorite: 1, ocfavorite: 1,
), ),
); );
final responses = (await client.webdav.report( final responses = (await client.webdav.report(
Uri(path: '/'), PathUri.parse('/'),
WebDavOcFilterRules( WebDavOcFilterRules(
ocfavorite: 1, ocfavorite: 1,
), ),
@ -221,13 +326,13 @@ void main() {
await client.webdav.put( await client.webdav.put(
utf8.encode('test') as Uint8List, utf8.encode('test') as Uint8List,
Uri(path: 'test.txt'), PathUri.parse('test.txt'),
lastModified: lastModifiedDate, lastModified: lastModifiedDate,
created: createdDate, created: createdDate,
); );
final updated = await client.webdav.proppatch( final updated = await client.webdav.proppatch(
Uri(path: 'test.txt'), PathUri.parse('test.txt'),
set: WebDavProp( set: WebDavProp(
ocfavorite: 1, ocfavorite: 1,
), ),
@ -235,7 +340,7 @@ void main() {
expect(updated, isTrue); expect(updated, isTrue);
final props = (await client.webdav.propfind( final props = (await client.webdav.propfind(
Uri(path: 'test.txt'), PathUri.parse('test.txt'),
prop: WebDavPropWithoutValues.fromBools( prop: WebDavPropWithoutValues.fromBools(
ocfavorite: true, ocfavorite: true,
davgetlastmodified: true, davgetlastmodified: true,
@ -255,10 +360,10 @@ void main() {
}); });
test('Remove properties', () async { 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( var updated = await client.webdav.proppatch(
Uri(path: 'test.txt'), PathUri.parse('test.txt'),
set: WebDavProp( set: WebDavProp(
ocfavorite: 1, ocfavorite: 1,
), ),
@ -266,7 +371,7 @@ void main() {
expect(updated, isTrue); expect(updated, isTrue);
var props = (await client.webdav.propfind( var props = (await client.webdav.propfind(
Uri(path: 'test.txt'), PathUri.parse('test.txt'),
prop: WebDavPropWithoutValues.fromBools( prop: WebDavPropWithoutValues.fromBools(
ocfavorite: true, ocfavorite: true,
nccreationtime: true, nccreationtime: true,
@ -281,7 +386,7 @@ void main() {
expect(props.ocfavorite, 1); expect(props.ocfavorite, 1);
updated = await client.webdav.proppatch( updated = await client.webdav.proppatch(
Uri(path: 'test.txt'), PathUri.parse('test.txt'),
remove: WebDavPropWithoutValues.fromBools( remove: WebDavPropWithoutValues.fromBools(
ocfavorite: true, ocfavorite: true,
), ),
@ -289,7 +394,7 @@ void main() {
expect(updated, isFalse); expect(updated, isFalse);
props = (await client.webdav.propfind( props = (await client.webdav.propfind(
Uri(path: 'test.txt'), PathUri.parse('test.txt'),
prop: WebDavPropWithoutValues.fromBools( prop: WebDavPropWithoutValues.fromBools(
ocfavorite: true, ocfavorite: true,
), ),
@ -311,11 +416,11 @@ void main() {
await client.webdav.putFile( await client.webdav.putFile(
source, source,
source.statSync(), source.statSync(),
Uri(path: 'test.png'), PathUri.parse('test.png'),
onProgress: progressValues.add, onProgress: progressValues.add,
); );
await client.webdav.getFile( await client.webdav.getFile(
Uri(path: 'test.png'), PathUri.parse('test.png'),
destination, destination,
onProgress: progressValues.add, onProgress: progressValues.add,
); );
@ -343,32 +448,32 @@ void main() {
test(name, () async { test(name, () async {
final content = utf8.encode('This is a test file') as Uint8List; 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); 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)); expect(downloadedContent, equals(content));
}); });
} }
test('put_no_parent', () async { test('put_no_parent', () async {
expect( 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 // https://github.com/nextcloud/server/issues/39625
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 409)), throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 409)),
); );
}); });
test('delete', () async { 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); expect(response.statusCode, 204);
}); });
test('delete_null', () async { test('delete_null', () async {
expect( expect(
() => client.webdav.delete(Uri(path: 'test.txt')), () => client.webdav.delete(PathUri.parse('test.txt')),
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 404)), throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 404)),
); );
}); });
@ -376,29 +481,29 @@ void main() {
// delete_fragment: This test is not applicable because the fragment is already removed on the client side // delete_fragment: This test is not applicable because the fragment is already removed on the client side
test('mkcol', () async { 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); expect(response.statusCode, 201);
}); });
test('mkcol_again', () async { test('mkcol_again', () async {
await client.webdav.mkcol(Uri(path: 'test')); await client.webdav.mkcol(PathUri.parse('test'));
expect( expect(
() => client.webdav.mkcol(Uri(path: 'test')), () => client.webdav.mkcol(PathUri.parse('test')),
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 405)), throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 405)),
); );
}); });
test('delete_coll', () async { 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); expect(response.statusCode, 204);
}); });
test('mkcol_no_parent', () async { test('mkcol_no_parent', () async {
expect( expect(
() => client.webdav.mkcol(Uri(path: '409me/noparent')), () => client.webdav.mkcol(PathUri.parse('409me/noparent')),
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 409)), throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 409)),
); );
}); });
@ -408,110 +513,110 @@ void main() {
group('copymove', () { group('copymove', () {
test('copy_simple', () async { 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); expect(response.statusCode, 201);
}); });
test('copy_overwrite', () async { test('copy_overwrite', () async {
await client.webdav.mkcol(Uri(path: 'src')); await client.webdav.mkcol(PathUri.parse('src'));
await client.webdav.mkcol(Uri(path: 'dst')); await client.webdav.mkcol(PathUri.parse('dst'));
expect( expect(
() => client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst')), () => client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst')),
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 412)), throwsA(predicate<DynamiteApiException>((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); expect(response.statusCode, 204);
}); });
test('copy_nodestcoll', () async { test('copy_nodestcoll', () async {
await client.webdav.mkcol(Uri(path: 'src')); await client.webdav.mkcol(PathUri.parse('src'));
expect( expect(
() => client.webdav.copy(Uri(path: 'src'), Uri(path: 'nonesuch/dst')), () => client.webdav.copy(PathUri.parse('src'), PathUri.parse('nonesuch/dst')),
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 409)), throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 409)),
); );
}); });
test('copy_coll', () async { test('copy_coll', () async {
await client.webdav.mkcol(Uri(path: 'src')); await client.webdav.mkcol(PathUri.parse('src'));
await client.webdav.mkcol(Uri(path: 'src/sub')); await client.webdav.mkcol(PathUri.parse('src/sub'));
for (var i = 0; i < 10; i++) { 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(PathUri.parse('src'), PathUri.parse('dst1'));
await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst2')); await client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst2'));
expect( expect(
() => client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst1')), () => client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst1')),
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 412)), throwsA(predicate<DynamiteApiException>((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); expect(response.statusCode, 204);
for (var i = 0; i < 10; i++) { 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); 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); expect(response.statusCode, 204);
response = await client.webdav.delete(Uri(path: 'dst2')); response = await client.webdav.delete(PathUri.parse('dst2'));
expect(response.statusCode, 204); expect(response.statusCode, 204);
}); });
// copy_shallow: Does not work on litmus, let's wait for https://github.com/nextcloud/server/issues/39627 // copy_shallow: Does not work on litmus, let's wait for https://github.com/nextcloud/server/issues/39627
test('move', () async { test('move', () async {
await client.webdav.put(Uint8List(0), Uri(path: 'src1.txt')); await client.webdav.put(Uint8List(0), PathUri.parse('src1.txt'));
await client.webdav.put(Uint8List(0), Uri(path: 'src2.txt')); await client.webdav.put(Uint8List(0), PathUri.parse('src2.txt'));
await client.webdav.mkcol(Uri(path: 'coll')); 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(response.statusCode, 201);
expect( 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<DynamiteApiException>((final e) => e.statusCode == 412)), throwsA(predicate<DynamiteApiException>((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); expect(response.statusCode, 204);
}); });
test('move_coll', () async { test('move_coll', () async {
await client.webdav.mkcol(Uri(path: 'src')); await client.webdav.mkcol(PathUri.parse('src'));
await client.webdav.mkcol(Uri(path: 'src/sub')); await client.webdav.mkcol(PathUri.parse('src/sub'));
for (var i = 0; i < 10; i++) { 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.put(Uint8List(0), PathUri.parse('noncoll'));
await client.webdav.copy(Uri(path: 'src'), Uri(path: 'dst2')); await client.webdav.copy(PathUri.parse('src'), PathUri.parse('dst2'));
await client.webdav.move(Uri(path: 'src'), Uri(path: 'dst1')); await client.webdav.move(PathUri.parse('src'), PathUri.parse('dst1'));
expect( expect(
() => client.webdav.move(Uri(path: 'dst1'), Uri(path: 'dst2')), () => client.webdav.move(PathUri.parse('dst1'), PathUri.parse('dst2')),
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 412)), throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 412)),
); );
await client.webdav.move(Uri(path: 'dst2'), Uri(path: 'dst1'), overwrite: true); await client.webdav.move(PathUri.parse('dst2'), PathUri.parse('dst1'), overwrite: true);
await client.webdav.copy(Uri(path: 'dst1'), Uri(path: 'dst2')); await client.webdav.copy(PathUri.parse('dst1'), PathUri.parse('dst2'));
for (var i = 0; i < 10; i++) { 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); 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(response.statusCode, 204);
expect( expect(
() => client.webdav.move(Uri(path: 'dst2'), Uri(path: 'noncoll')), () => client.webdav.move(PathUri.parse('dst2'), PathUri.parse('noncoll')),
throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 412)), throwsA(predicate<DynamiteApiException>((final e) => e.statusCode == 412)),
); );
}); });
@ -523,10 +628,10 @@ void main() {
// large_put: Already covered by large_get // large_put: Already covered by large_get
test('large_get', () async { 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); 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)); expect(downloadedContent, hasLength(largefileSize));
}); });
}); });

Loading…
Cancel
Save