Browse Source

refactor(nextcloud,neon_files): Introduce PathUri for WebDAV path handling

Signed-off-by: jld3103 <jld3103yt@gmail.com>
pull/1103/head
jld3103 1 year ago
parent
commit
eade429f45
No known key found for this signature in database
GPG Key ID: 9062417B9E8EB7B3
  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';
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 {
BehaviorSubject<Result<List<WebDavFile>>> get files;
BehaviorSubject<List<String>> get path;
BehaviorSubject<PathUri> get uri;
}
class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents, FilesBrowserBlocStates {
FilesBrowserBloc(
this.options,
this.account, {
final List<String>? initialPath,
final PathUri? initialPath,
}) {
if (initialPath != null) {
path.add(initialPath);
uri.add(initialPath);
}
unawaited(refresh());
@ -31,7 +31,7 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
@override
void dispose() {
unawaited(files.close());
unawaited(path.close());
unawaited(uri.close());
super.dispose();
}
@ -39,16 +39,16 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
BehaviorSubject<Result<List<WebDavFile>>> files = BehaviorSubject<Result<List<WebDavFile>>>();
@override
BehaviorSubject<List<String>> path = BehaviorSubject<List<String>>.seeded([]);
BehaviorSubject<PathUri> uri = BehaviorSubject.seeded(PathUri.cwd());
@override
Future<void> refresh() async {
await RequestManager.instance.wrapWebDav<List<WebDavFile>>(
account.id,
'files-${path.value.join('/')}',
'files-${uri.value.path}',
files,
() => account.client.webdav.propfind(
Uri(pathSegments: path.value),
uri.value,
prop: WebDavPropWithoutValues.fromBools(
davgetcontenttype: true,
davgetetag: true,
@ -65,13 +65,13 @@ class FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBlocEvents
}
@override
void setPath(final List<String> p) {
path.add(p);
void setPath(final PathUri uri) {
this.uri.add(uri);
unawaited(refresh());
}
@override
void createFolder(final List<String> path) {
wrapAction(() async => account.client.webdav.mkcol(Uri(pathSegments: path)));
void createFolder(final PathUri uri) {
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';
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 {
@ -58,35 +58,35 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
BehaviorSubject<List<FilesTask>> tasks = BehaviorSubject<List<FilesTask>>.seeded([]);
@override
void addFavorite(final List<String> path) {
void addFavorite(final PathUri uri) {
wrapAction(
() async => account.client.webdav.proppatch(
Uri(pathSegments: path),
uri,
set: WebDavProp(ocfavorite: 1),
),
);
}
@override
void copy(final List<String> path, final List<String> destination) {
wrapAction(() async => account.client.webdav.copy(Uri(pathSegments: path), Uri(pathSegments: destination)));
void copy(final PathUri uri, final PathUri destination) {
wrapAction(() async => account.client.webdav.copy(uri, destination));
}
@override
void delete(final List<String> path) {
wrapAction(() async => account.client.webdav.delete(Uri(pathSegments: path)));
void delete(final PathUri uri) {
wrapAction(() async => account.client.webdav.delete(uri));
}
@override
void move(final List<String> path, final List<String> destination) {
wrapAction(() async => account.client.webdav.move(Uri(pathSegments: path), Uri(pathSegments: destination)));
void move(final PathUri uri, final PathUri destination) {
wrapAction(() async => account.client.webdav.move(uri, destination));
}
@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(
() async {
final file = await _cacheFile(path, etag);
final file = await _cacheFile(uri, etag);
final result = await OpenFile.open(file.path, type: mimeType);
if (result.type != ResultType.done) {
@ -98,10 +98,10 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
}
@override
void shareFileNative(final List<String> path, final String etag) {
void shareFileNative(final PathUri uri, final String etag) {
wrapAction(
() async {
final file = await _cacheFile(path, etag);
final file = await _cacheFile(uri, etag);
await Share.shareXFiles([XFile(file.path)]);
},
@ -115,52 +115,52 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
}
@override
void removeFavorite(final List<String> path) {
void removeFavorite(final PathUri uri) {
wrapAction(
() async => account.client.webdav.proppatch(
Uri(pathSegments: path),
uri,
set: WebDavProp(ocfavorite: 0),
),
);
}
@override
void rename(final List<String> path, final String name) {
void rename(final PathUri uri, final String name) {
wrapAction(
() async => account.client.webdav.move(
Uri(pathSegments: path),
Uri(pathSegments: List.from(path)..last = name),
uri,
uri.rename(name),
),
);
}
@override
void syncFile(final List<String> path) {
void syncFile(final PathUri uri) {
wrapAction(
() async {
final file = File(
p.join(
p.joinAll([
await NeonPlatform.instance.userAccessibleAppDataPath,
account.humanReadableID,
'files',
path.join(Platform.pathSeparator),
),
...uri.pathSegments,
]),
);
if (!file.parent.existsSync()) {
file.parent.createSync(recursive: true);
}
await _downloadFile(path, file);
await _downloadFile(uri, file);
},
disableTimeout: true,
);
}
@override
void uploadFile(final List<String> path, final String localPath) {
void uploadFile(final PathUri uri, final String localPath) {
wrapAction(
() async {
final task = FilesUploadTask(
path: path,
uri: uri,
file: File(localPath),
);
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 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()) {
debugPrint('Downloading ${Uri(pathSegments: path)} since it does not exist');
debugPrint('Downloading $uri since it does not exist');
if (!file.parent.existsSync()) {
await file.parent.create(recursive: true);
}
await _downloadFile(path, file);
await _downloadFile(uri, file);
}
return file;
}
Future<void> _downloadFile(
final List<String> path,
final PathUri uri,
final File file,
) async {
final task = FilesDownloadTask(
path: path,
uri: uri,
file: file,
);
tasks.add(tasks.value..add(task));
@ -200,12 +200,12 @@ class FilesBloc extends InteractiveBloc implements FilesBlocEvents, FilesBlocSta
}
FilesBrowserBloc getNewFilesBrowserBloc({
final List<String>? initialPath,
final PathUri? initialUri,
}) =>
FilesBrowserBloc(
options,
account,
initialPath: initialPath,
initialPath: initialUri,
);
void _downloadParallelismListener() {

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

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

16
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<String> originalPath;
final PathUri originalPath;
@override
Widget build(final BuildContext context) => AlertDialog(
@ -28,9 +28,9 @@ class FilesChooseFolderDialog extends StatelessWidget {
mode: FilesBrowserMode.selectDirectory,
),
),
StreamBuilder<List<String>>(
stream: bloc.path,
builder: (final context, final pathSnapshot) => pathSnapshot.hasData
StreamBuilder<PathUri>(
stream: bloc.uri,
builder: (final context, final uriSnapshot) => uriSnapshot.hasData
? Container(
margin: const EdgeInsets.all(10),
child: Row(
@ -38,19 +38,19 @@ class FilesChooseFolderDialog extends StatelessWidget {
children: [
ElevatedButton(
onPressed: () async {
final result = await showDialog<List<String>>(
final result = await showDialog<String>(
context: context,
builder: (final context) => const FilesCreateFolderDialog(),
);
if (result != null) {
bloc.createFolder([...pathSnapshot.requireData, ...result]);
bloc.createFolder(uriSnapshot.requireData.join(PathUri.parse(result)));
}
},
child: Text(FilesLocalizations.of(context).folderCreate),
),
ElevatedButton(
onPressed: !(const ListEquality<String>().equals(originalPath, pathSnapshot.data))
? () => Navigator.of(context).pop(pathSnapshot.data)
onPressed: originalPath != uriSnapshot.requireData
? () => Navigator.of(context).pop(uriSnapshot.requireData)
: null,
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() {
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
class FileDetails {
const FileDetails({
required this.path,
required this.isDirectory,
required this.uri,
required this.size,
required this.etag,
required this.mimeType,
@ -15,9 +14,7 @@ class FileDetails {
FileDetails.fromWebDav({
required final WebDavFile file,
required final List<String> path,
}) : path = List.from(path)..add(file.name),
isDirectory = file.isDirectory,
}) : uri = file.path,
size = file.size,
etag = file.etag,
mimeType = file.mimeType,
@ -28,10 +25,9 @@ class FileDetails {
FileDetails.fromUploadTask({
required FilesUploadTask this.task,
}) : path = task.path,
}) : uri = task.uri,
size = task.stat.size,
lastModified = task.stat.modified,
isDirectory = false,
etag = null,
mimeType = null,
hasPreview = null,
@ -40,8 +36,7 @@ class FileDetails {
FileDetails.fromDownloadTask({
required FilesDownloadTask this.task,
required final WebDavFile file,
}) : path = task.path,
isDirectory = file.isDirectory,
}) : uri = task.uri,
size = file.size,
etag = file.etag,
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;

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

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

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

@ -34,7 +34,7 @@ class _FilesMainPageState extends State<FilesMainPage> {
context: context,
builder: (final context) => FilesChooseCreateDialog(
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 {
FilesTask({
required this.path,
required this.uri,
required this.file,
});
final List<String> path;
final PathUri uri;
final File file;
@ -19,13 +19,13 @@ sealed class FilesTask {
class FilesDownloadTask extends FilesTask {
FilesDownloadTask({
required super.path,
required super.uri,
required super.file,
});
Future<void> execute(final NextcloudClient client) async {
await client.webdav.getFile(
Uri(pathSegments: path),
uri,
file,
onProgress: streamController.add,
);
@ -35,7 +35,7 @@ class FilesDownloadTask extends FilesTask {
class FilesUploadTask extends FilesTask {
FilesUploadTask({
required super.path,
required super.uri,
required super.file,
});
@ -46,7 +46,7 @@ class FilesUploadTask extends FilesTask {
await client.webdav.putFile(
file,
stat,
Uri(pathSegments: path),
uri,
lastModified: stat.modified,
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_files/l10n/localizations.dart';
import 'package:neon_files/neon_files.dart';
import 'package:nextcloud/webdav.dart';
class FileActions extends StatelessWidget {
const FileActions({
@ -18,12 +19,12 @@ class FileActions extends StatelessWidget {
final browserBloc = bloc.browser;
switch (action) {
case FilesFileAction.share:
bloc.shareFileNative(details.path, details.etag!);
bloc.shareFileNative(details.uri, details.etag!);
case FilesFileAction.toggleFavorite:
if (details.isFavorite ?? false) {
bloc.removeFavorite(details.path);
bloc.removeFavorite(details.uri);
} else {
bloc.addFavorite(details.path);
bloc.addFavorite(details.uri);
}
case FilesFileAction.details:
await Navigator.of(context).push(
@ -46,15 +47,15 @@ class FileActions extends StatelessWidget {
value: details.name,
);
if (result != null) {
bloc.rename(details.path, result);
bloc.rename(details.uri, result);
}
case FilesFileAction.move:
if (!context.mounted) {
return;
}
final originalPath = details.path.sublist(0, details.path.length - 1);
final b = bloc.getNewFilesBrowserBloc(initialPath: originalPath);
final result = await showDialog<List<String>?>(
final originalPath = details.uri.parent!;
final b = bloc.getNewFilesBrowserBloc(initialUri: originalPath);
final result = await showDialog<PathUri>(
context: context,
builder: (final context) => FilesChooseFolderDialog(
bloc: b,
@ -64,15 +65,15 @@ class FileActions extends StatelessWidget {
);
b.dispose();
if (result != null) {
bloc.move(details.path, result..add(details.name));
bloc.move(details.uri, result.join(PathUri.parse(details.name)));
}
case FilesFileAction.copy:
if (!context.mounted) {
return;
}
final originalPath = details.path.sublist(0, details.path.length - 1);
final b = bloc.getNewFilesBrowserBloc(initialPath: originalPath);
final result = await showDialog<List<String>?>(
final originalPath = details.uri.parent!;
final b = bloc.getNewFilesBrowserBloc(initialUri: originalPath);
final result = await showDialog<PathUri>(
context: context,
builder: (final context) => FilesChooseFolderDialog(
bloc: b,
@ -82,7 +83,7 @@ class FileActions extends StatelessWidget {
);
b.dispose();
if (result != null) {
bloc.copy(details.path, result..add(details.name));
bloc.copy(details.uri, result.join(PathUri.parse(details.name)));
}
case FilesFileAction.sync:
if (!context.mounted) {
@ -100,7 +101,7 @@ class FileActions extends StatelessWidget {
return;
}
}
bloc.syncFile(details.path);
bloc.syncFile(details.uri);
case FilesFileAction.delete:
if (!context.mounted) {
return;
@ -111,7 +112,7 @@ class FileActions extends StatelessWidget {
? FilesLocalizations.of(context).folderDeleteConfirm(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
Widget build(final BuildContext context) => ResultBuilder<List<WebDavFile>>.behaviorSubject(
subject: widget.bloc.files,
builder: (final context, final filesSnapshot) => StreamBuilder<List<String>>(
stream: widget.bloc.path,
builder: (final context, final pathSnapshot) => StreamBuilder<List<FilesTask>>(
builder: (final context, final filesSnapshot) => StreamBuilder<PathUri>(
stream: widget.bloc.uri,
builder: (final context, final uriSnapshot) => StreamBuilder<List<FilesTask>>(
stream: widget.filesBloc.tasks,
builder: (final context, final tasksSnapshot) {
if (!pathSnapshot.hasData || !tasksSnapshot.hasData) {
if (!uriSnapshot.hasData || !tasksSnapshot.hasData) {
return const SizedBox();
}
return ValueListenableBuilder(
@ -68,9 +68,9 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
return BackButtonListener(
onBackButtonPressed: () async {
final path = pathSnapshot.requireData;
if (path.isNotEmpty) {
widget.bloc.setPath(path.sublist(0, path.length - 1));
final parent = uriSnapshot.requireData.parent;
if (parent != null) {
widget.bloc.setPath(parent);
return true;
}
return false;
@ -87,12 +87,13 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
final uploadingTaskTiles = buildUploadTasks(tasksSnapshot.requireData, sorted);
return NeonListView(
scrollKey: 'files-${pathSnapshot.requireData.join('/')}',
scrollKey: 'files-${uriSnapshot.requireData.path}',
itemCount: sorted.length,
itemBuilder: (final context, final index) {
final file = sorted[index];
final matchingTask = tasksSnapshot.requireData
.firstWhereOrNull((final task) => _pathMatchesFile(task.path, file.name));
final matchingTask = tasksSnapshot.requireData.firstWhereOrNull(
(final task) => file.name == task.uri.name && widget.bloc.uri.value == task.uri.parent,
);
final details = matchingTask != null
? FileDetails.fromTask(
@ -101,7 +102,6 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
)
: FileDetails.fromWebDav(
file: file,
path: widget.bloc.path.value,
);
return FileListTile(
@ -116,7 +116,7 @@ class _FilesBrowserViewState extends State<FilesBrowserView> {
onRefresh: widget.bloc.refresh,
topScrollingChildren: [
FilesBrowserNavigator(
path: pathSnapshot.requireData,
uri: uriSnapshot.requireData,
bloc: widget.bloc,
),
...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 {
if (details.isDirectory) {
browserBloc.setPath(details.path);
browserBloc.setPath(details.uri);
} else if (mode == FilesBrowserMode.browser) {
final sizeWarning = bloc.options.downloadSizeWarning.value;
if (sizeWarning != null && details.size != null && details.size! > sizeWarning) {
@ -38,7 +38,7 @@ class FileListTile extends StatelessWidget {
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 height = size.height.toInt();
final path = file.path.join('/');
final cacheKey = 'preview-$path-$width-$height';
final cacheKey = 'preview-${file.uri.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.uri.path,
x: width,
y: height,
),

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

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

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:meta/meta.dart';
import 'package:nextcloud/src/webdav/path_uri.dart';
import 'package:nextcloud/src/webdav/props.dart';
import 'package:nextcloud/src/webdav/webdav.dart';
import 'package:universal_io/io.dart';
import 'package:xml/xml.dart' as xml;
/// Base path used on the server
final webdavBase = Uri(path: '/remote.php/webdav');
final webdavBase = PathUri.parse('/remote.php/webdav');
/// WebDavClient class
class WebDavClient {
@ -60,11 +61,11 @@ class WebDavClient {
return response;
}
Uri _constructUri([final Uri? path]) => constructUri(rootClient.baseURL, path);
Uri _constructUri([final PathUri? path]) => constructUri(rootClient.baseURL, path);
@visibleForTesting
// ignore: public_member_api_docs
static Uri constructUri(final Uri baseURL, [final Uri? path]) {
static Uri constructUri(final Uri baseURL, [final PathUri? path]) {
final segments = baseURL.pathSegments.toList()..addAll(webdavBase.pathSegments);
if (path != null) {
segments.addAll(path.pathSegments);
@ -103,7 +104,7 @@ class WebDavClient {
/// Creates a collection at [path].
///
/// See http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL for more information.
Future<HttpClientResponse> mkcol(final Uri path) async => _send(
Future<HttpClientResponse> 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<HttpClientResponse> delete(final Uri path) => _send(
Future<HttpClientResponse> 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<HttpClientResponse> 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<HttpClientResponse> putStream(
final Stream<Uint8List> localData,
final Uri path, {
final PathUri path, {
final DateTime? lastModified,
final DateTime? created,
final int? contentLength,
@ -181,7 +182,7 @@ class WebDavClient {
Future<HttpClientResponse> 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<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].
Future<HttpClientResponse> getStream(final Uri path) async => _send(
Future<HttpClientResponse> getStream(final PathUri path) async => _send(
'GET',
_constructUri(path),
);
/// Gets the content of the file at [path].
Future<void> 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<WebDavMultistatus> 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<WebDavMultistatus> 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<bool> 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<HttpClientResponse> 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<HttpClientResponse> copy(
final Uri sourcePath,
final Uri destinationPath, {
final PathUri sourcePath,
final PathUri destinationPath, {
final bool overwrite = false,
}) =>
_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/path_uri.dart';
import 'package:nextcloud/src/webdav/props.dart';
import 'package:nextcloud/src/webdav/webdav.dart';
@ -25,8 +26,14 @@ class WebDavFile {
_response.propstats.singleWhere((final propstat) => propstat.status.contains('200')).prop;
/// The path of file
late final Uri path =
Uri(pathSegments: Uri(path: _response.href).pathSegments.sublist(webdavBase.pathSegments.length));
late final PathUri path = () {
final href = PathUri.parse(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
late final String? id = props.ocid;
@ -79,11 +86,11 @@ class WebDavFile {
late final bool? hasPreview = props.nchaspreview;
/// Returns the decoded name of the file / folder without the whole path
late final String name = path.pathSegments.where((final s) => s.isNotEmpty).lastOrNull ?? '';
late final String name = path.name;
/// Whether the file is hidden.
late final bool isHidden = name.startsWith('.');
/// Whether the file is a directory
late final bool isDirectory = (isCollection ?? false) || path.pathSegments.last.isEmpty;
late final bool isDirectory = (isCollection ?? false) || path.isDirectory;
}

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

Loading…
Cancel
Save