From 6b76021a06a8c2c83c53708e7c7037d363835393 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Mon, 22 Jul 2024 22:41:26 +0200 Subject: [PATCH 1/2] feat(nextcloud)!: Allow configurable WebDAV endpoint Signed-off-by: provokateurin --- .../neon_files/lib/src/blocs/browser.dart | 28 +- .../neon/neon_files/lib/src/blocs/files.dart | 30 +- .../neon/neon_files/lib/src/utils/task.dart | 46 +- .../src/interceptors/csrf_interceptor.dart | 5 +- packages/neon_http_client/pubspec.yaml | 5 +- .../src/api/webdav/models/webdav_file.dart | 18 +- .../lib/src/api/webdav/utils/webdav_uri.dart | 7 +- .../lib/src/api/webdav/webdav_client.dart | 12 +- packages/nextcloud/lib/webdav.dart | 15 +- packages/nextcloud/test/core_test.dart | 4 +- packages/nextcloud/test/webdav_test.dart | 583 ++++++++++-------- 11 files changed, 416 insertions(+), 337 deletions(-) diff --git a/packages/neon/neon_files/lib/src/blocs/browser.dart b/packages/neon/neon_files/lib/src/blocs/browser.dart index ce2ca5fdd94..3d0c73a0caa 100644 --- a/packages/neon/neon_files/lib/src/blocs/browser.dart +++ b/packages/neon/neon_files/lib/src/blocs/browser.dart @@ -91,19 +91,19 @@ class _FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBloc { await RequestManager.instance.wrap( account: account, subject: files, - getRequest: () => account.client.webdav.propfind_Request( - uri.value, - prop: const WebDavPropWithoutValues.fromBools( - davGetcontenttype: true, - davGetetag: true, - davGetlastmodified: true, - ncHasPreview: true, - ncMetadataBlurhash: true, - ocSize: true, - ocFavorite: true, - ), - depth: WebDavDepth.one, - ), + getRequest: () => account.client.webdav().propfind_Request( + uri.value, + prop: const WebDavPropWithoutValues.fromBools( + davGetcontenttype: true, + davGetetag: true, + davGetlastmodified: true, + ncHasPreview: true, + ncMetadataBlurhash: true, + ocSize: true, + ocFavorite: true, + ), + depth: WebDavDepth.one, + ), converter: const WebDavResponseConverter(), unwrap: (response) => BuiltList.build((b) { for (final file in response.toWebDavFiles()) { @@ -144,6 +144,6 @@ class _FilesBrowserBloc extends InteractiveBloc implements FilesBrowserBloc { @override Future createFolder(PathUri uri) async { - await wrapAction(() async => account.client.webdav.mkcol(uri)); + await wrapAction(() async => account.client.webdav().mkcol(uri)); } } diff --git a/packages/neon/neon_files/lib/src/blocs/files.dart b/packages/neon/neon_files/lib/src/blocs/files.dart index bc64c9071fb..3b2c772539f 100644 --- a/packages/neon/neon_files/lib/src/blocs/files.dart +++ b/packages/neon/neon_files/lib/src/blocs/files.dart @@ -164,46 +164,46 @@ class _FilesBloc extends InteractiveBloc implements FilesBloc { @override Future delete(PathUri uri) async { - await wrapAction(() async => account.client.webdav.delete(uri)); + await wrapAction(() async => account.client.webdav().delete(uri)); } @override Future rename(PathUri uri, String name) async { await wrapAction( - () async => account.client.webdav.move( - uri, - uri.rename(name), - ), + () async => account.client.webdav().move( + uri, + uri.rename(name), + ), ); } @override Future move(PathUri uri, PathUri destination) async { - await wrapAction(() async => account.client.webdav.move(uri, destination)); + await wrapAction(() async => account.client.webdav().move(uri, destination)); } @override Future copy(PathUri uri, PathUri destination) async { - await wrapAction(() async => account.client.webdav.copy(uri, destination)); + await wrapAction(() async => account.client.webdav().copy(uri, destination)); } @override Future addFavorite(PathUri uri) async { await wrapAction( - () async => account.client.webdav.proppatch( - uri, - set: const WebDavProp(ocFavorite: true), - ), + () async => account.client.webdav().proppatch( + uri, + set: const WebDavProp(ocFavorite: true), + ), ); } @override Future removeFavorite(PathUri uri) async { await wrapAction( - () async => account.client.webdav.proppatch( - uri, - set: const WebDavProp(ocFavorite: false), - ), + () async => account.client.webdav().proppatch( + uri, + set: const WebDavProp(ocFavorite: false), + ), ); } diff --git a/packages/neon/neon_files/lib/src/utils/task.dart b/packages/neon/neon_files/lib/src/utils/task.dart index e86200a1285..b21e9c19baa 100644 --- a/packages/neon/neon_files/lib/src/utils/task.dart +++ b/packages/neon/neon_files/lib/src/utils/task.dart @@ -67,11 +67,11 @@ class FilesDownloadTaskIO extends FilesTaskIO implements FilesDownloadTask { }); Future execute(NextcloudClient client) async { - await client.webdav.getFile( - uri, - file, - onProgress: progressController.add, - ); + await client.webdav().getFile( + uri, + file, + onProgress: progressController.add, + ); await progressController.close(); } } @@ -91,13 +91,13 @@ class FilesUploadTaskIO extends FilesTaskIO implements FilesUploadTask { late tz.TZDateTime lastModified = tz.TZDateTime.from(_stat.modified, tz.UTC); Future execute(NextcloudClient client) async { - await client.webdav.putFile( - file, - _stat, - uri, - lastModified: _stat.modified, - onProgress: progressController.add, - ); + await client.webdav().putFile( + file, + _stat, + uri, + lastModified: _stat.modified, + onProgress: progressController.add, + ); await progressController.close(); } } @@ -108,10 +108,10 @@ class FilesDownloadTaskMemory extends FilesTaskMemory implements FilesDownloadTa }); Future execute(NextcloudClient client) async { - final stream = client.webdav.getStream( - uri, - onProgress: progressController.add, - ); + final stream = client.webdav().getStream( + uri, + onProgress: progressController.add, + ); await stream.pipe(_stream); await progressController.close(); } @@ -134,13 +134,13 @@ class FilesUploadTaskMemory extends FilesTaskMemory implements FilesUploadTask { final tz.TZDateTime? lastModified; Future execute(NextcloudClient client) async { - await client.webdav.putStream( - _stream.stream, - uri, - lastModified: lastModified, - contentLength: size, - onProgress: progressController.add, - ); + await client.webdav().putStream( + _stream.stream, + uri, + lastModified: lastModified, + contentLength: size, + onProgress: progressController.add, + ); await progressController.close(); } } diff --git a/packages/neon_http_client/lib/src/interceptors/csrf_interceptor.dart b/packages/neon_http_client/lib/src/interceptors/csrf_interceptor.dart index c71788fe4bf..a07291ad5c8 100644 --- a/packages/neon_http_client/lib/src/interceptors/csrf_interceptor.dart +++ b/packages/neon_http_client/lib/src/interceptors/csrf_interceptor.dart @@ -1,11 +1,10 @@ import 'dart:convert'; +import 'package:dynamite_runtime/http_client.dart'; import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:neon_http_client/src/interceptors/http_interceptor.dart'; -import 'package:nextcloud/nextcloud.dart'; -import 'package:nextcloud/webdav.dart'; import 'package:universal_io/io.dart'; /// A HttpInterceptor that works around a Nextcloud CSRF bug when cookies are sent. @@ -44,7 +43,7 @@ final class CSRFInterceptor implements HttpInterceptor { @override bool shouldInterceptRequest(http.BaseRequest request) { - if (request.url.host != _baseURL.host || !request.url.path.startsWith('${_baseURL.path}$webdavBase')) { + if (request.url.host != _baseURL.host || !request.url.path.startsWith('${_baseURL.path}/remote.php')) { return false; } diff --git a/packages/neon_http_client/pubspec.yaml b/packages/neon_http_client/pubspec.yaml index 0711b52d148..5b0228d5882 100644 --- a/packages/neon_http_client/pubspec.yaml +++ b/packages/neon_http_client/pubspec.yaml @@ -12,10 +12,13 @@ dependencies: git: url: https://github.com/nextcloud/neon path: packages/cookie_store + dynamite_runtime: + git: + url: https://github.com/nextcloud/neon + path: packages/dynamite/dynamite_runtime http: ^1.0.0 logging: ^1.0.0 meta: ^1.0.0 - nextcloud: ^6.1.0 universal_io: ^2.0.0 dev_dependencies: diff --git a/packages/nextcloud/lib/src/api/webdav/models/webdav_file.dart b/packages/nextcloud/lib/src/api/webdav/models/webdav_file.dart index 7b765c30fc5..7e2460e81ef 100644 --- a/packages/nextcloud/lib/src/api/webdav/models/webdav_file.dart +++ b/packages/nextcloud/lib/src/api/webdav/models/webdav_file.dart @@ -4,8 +4,15 @@ import 'package:timezone/timezone.dart' as tz; // ignore: public_member_api_docs extension WebDavMultistatusFile on WebDavMultistatus { /// Convert the [WebDavMultistatus] into a [WebDavFile] for easier handling - List toWebDavFiles() => - responses.where((response) => response.href != null).map((response) => WebDavFile(response: response)).toList(); + List toWebDavFiles({String endpoint = 'remote.php/webdav'}) => responses + .where((response) => response.href != null) + .map( + (response) => WebDavFile( + response: response, + endpoint: endpoint, + ), + ) + .toList(); } /// WebDavFile class @@ -13,9 +20,12 @@ class WebDavFile { /// Creates a new WebDavFile object with the given path WebDavFile({ required WebDavResponse response, - }) : _response = response; + required String endpoint, + }) : _response = response, + _endpoint = PathUri.parse(endpoint); final WebDavResponse _response; + final PathUri _endpoint; /// Get the props of the file late final WebDavProp props = _response.propstats.singleWhere((propstat) => propstat.status.contains('200')).prop; @@ -26,7 +36,7 @@ class WebDavFile { return PathUri( isAbsolute: false, isDirectory: href.isDirectory, - pathSegments: href.pathSegments.sublist(webdavBase.pathSegments.length), + pathSegments: href.pathSegments.sublist(_endpoint.pathSegments.length), ); }(); diff --git a/packages/nextcloud/lib/src/api/webdav/utils/webdav_uri.dart b/packages/nextcloud/lib/src/api/webdav/utils/webdav_uri.dart index d9020ec217f..52b20cba919 100644 --- a/packages/nextcloud/lib/src/api/webdav/utils/webdav_uri.dart +++ b/packages/nextcloud/lib/src/api/webdav/utils/webdav_uri.dart @@ -1,13 +1,10 @@ import 'package:meta/meta.dart'; import 'package:nextcloud/src/api/webdav/webdav.dart'; -/// Base path used on the server -final webdavBase = PathUri.parse('/remote.php/webdav'); - /// Constructs the uri for a webdav request for a given server [baseURL] and file [path]. @internal -Uri constructUri(Uri baseURL, [PathUri? path]) { - final segments = baseURL.pathSegments.toList()..addAll(webdavBase.pathSegments); +Uri constructUri(Uri baseURL, String endpoint, [PathUri? path]) { + final segments = baseURL.pathSegments.toList()..addAll(PathUri.parse(endpoint).pathSegments); if (path != null) { segments.addAll(path.pathSegments); } diff --git a/packages/nextcloud/lib/src/api/webdav/webdav_client.dart b/packages/nextcloud/lib/src/api/webdav/webdav_client.dart index 38fabd335fc..1089ca6a59a 100644 --- a/packages/nextcloud/lib/src/api/webdav/webdav_client.dart +++ b/packages/nextcloud/lib/src/api/webdav/webdav_client.dart @@ -13,7 +13,10 @@ import 'package:universal_io/io.dart' show File, FileStat; /// WebDavClient class class WebDavClient { // ignore: public_member_api_docs - WebDavClient(this.rootClient) : csrfClient = WebDavCSRFClient(rootClient); + WebDavClient( + this.rootClient, { + this.endpoint = 'remote.php/webdav', + }) : csrfClient = WebDavCSRFClient(rootClient); // ignore: public_member_api_docs final NextcloudClient rootClient; @@ -22,7 +25,12 @@ class WebDavClient { // TODO: Fix this bug in server. final WebDavCSRFClient csrfClient; - Uri _constructUri([PathUri? path]) => constructUri(rootClient.baseURL, path); + /// WebDAV endpoint used for all operations. + /// + /// Defaults to `remote.php/webdav` for accessing "Files". + final String endpoint; + + Uri _constructUri([PathUri? path]) => constructUri(rootClient.baseURL, endpoint, path); /// Request to get the WebDAV capabilities of the server. /// diff --git a/packages/nextcloud/lib/webdav.dart b/packages/nextcloud/lib/webdav.dart index ca884507855..e1145752573 100644 --- a/packages/nextcloud/lib/webdav.dart +++ b/packages/nextcloud/lib/webdav.dart @@ -6,8 +6,17 @@ export 'src/api/webdav/webdav.dart' hide DurationXMLConverter, WebDavCSRFClient, // ignore: public_member_api_docs extension WebDAVExtension on NextcloudClient { - static final _webdav = Expando(); + static final _webdav = Expando>(); - /// Client for WebDAV - WebDavClient get webdav => _webdav[this] ??= WebDavClient(this); + /// Client for WebDAV. + /// + /// Defaults to `remote.php/webdav` for accessing "Files". + WebDavClient webdav({String endpoint = 'remote.php/webdav'}) { + _webdav[this] ??= {}; + + return _webdav[this]![endpoint] ??= WebDavClient( + this, + endpoint: endpoint, + ); + } } diff --git a/packages/nextcloud/test/core_test.dart b/packages/nextcloud/test/core_test.dart index 000db284c96..53b8a1a6f7d 100644 --- a/packages/nextcloud/test/core_test.dart +++ b/packages/nextcloud/test/core_test.dart @@ -133,10 +133,10 @@ void main() { group('Preview', () { test('Get', () async { final file = File('test/files/test.png'); - await client.webdav.putFile(file, file.statSync(), PathUri.parse('preview.png')); + await client.webdav().putFile(file, file.statSync(), PathUri.parse('preview.png')); addTearDown(() async { closeFixture(); - await client.webdav.delete(PathUri.parse('preview.png')); + await client.webdav().delete(PathUri.parse('preview.png')); }); final response = await client.core.preview.getPreview( diff --git a/packages/nextcloud/test/webdav_test.dart b/packages/nextcloud/test/webdav_test.dart index 86df0eb5d52..ff9181ae13c 100644 --- a/packages/nextcloud/test/webdav_test.dart +++ b/packages/nextcloud/test/webdav_test.dart @@ -17,6 +17,8 @@ class MockCallbackFunction extends Mock { void main() { group('constructUri', () { + const endpoint = 'remote.php/webdav'; + for (final values in [ ('http://cloud.example.com', 'http://cloud.example.com'), ('http://cloud.example.com/', 'http://cloud.example.com'), @@ -28,28 +30,28 @@ void main() { test(baseURL, () { expect( - constructUri(baseURL).toString(), - '$sanitizedBaseURL$webdavBase', + constructUri(baseURL, endpoint).toString(), + '$sanitizedBaseURL/$endpoint', ); expect( - constructUri(baseURL, PathUri.parse('/')).toString(), - '$sanitizedBaseURL$webdavBase', + constructUri(baseURL, endpoint, PathUri.parse('/')).toString(), + '$sanitizedBaseURL/$endpoint', ); expect( - constructUri(baseURL, PathUri.parse('test')).toString(), - '$sanitizedBaseURL$webdavBase/test', + constructUri(baseURL, endpoint, PathUri.parse('test')).toString(), + '$sanitizedBaseURL/$endpoint/test', ); expect( - constructUri(baseURL, PathUri.parse('test/')).toString(), - '$sanitizedBaseURL$webdavBase/test', + constructUri(baseURL, endpoint, PathUri.parse('test/')).toString(), + '$sanitizedBaseURL/$endpoint/test', ); expect( - constructUri(baseURL, PathUri.parse('/test')).toString(), - '$sanitizedBaseURL$webdavBase/test', + constructUri(baseURL, endpoint, PathUri.parse('/test')).toString(), + '$sanitizedBaseURL/$endpoint/test', ); expect( - constructUri(baseURL, PathUri.parse('/test/')).toString(), - '$sanitizedBaseURL$webdavBase/test', + constructUri(baseURL, endpoint, PathUri.parse('/test/')).toString(), + '$sanitizedBaseURL/$endpoint/test', ); }); } @@ -179,7 +181,8 @@ void main() { final progress = []; final buffer = BytesBuilder(copy: false); - await client.webdav + await client + .webdav() .getStream( PathUri.cwd(), onProgress: progress.add, @@ -202,106 +205,106 @@ void main() { container = await DockerContainer.create(preset); client = await TestNextcloudClient.create(container); - await client.webdav.mkcol(PathUri.parse('test')); + await client.webdav().mkcol(PathUri.parse('test')); resetFixture(); }); tearDownAll(() async { closeFixture(); - await client.webdav.delete(PathUri.parse('test')); + await client.webdav().delete(PathUri.parse('test')); await container.destroy(); }); test('List directory', () async { - final responses = (await client.webdav.propfind( - PathUri.parse('test'), - prop: const WebDavPropWithoutValues.fromBools( - ncHasPreview: true, - davGetcontenttype: true, - davGetlastmodified: true, - ocSize: true, - ), - )) + final responses = (await client.webdav().propfind( + PathUri.parse('test'), + prop: const WebDavPropWithoutValues.fromBools( + ncHasPreview: true, + davGetcontenttype: true, + davGetlastmodified: true, + ocSize: true, + ), + )) .responses; expect(responses, isNotEmpty); }); test('List directory recursively', () async { - final responses = (await client.webdav.propfind( - PathUri.parse('test'), - depth: WebDavDepth.infinity, - )) + final responses = (await client.webdav().propfind( + PathUri.parse('test'), + depth: WebDavDepth.infinity, + )) .responses; expect(responses, isNotEmpty); }); test('Get file props', () async { final file = File('test/files/test.png'); - await client.webdav.putFile(file, file.statSync(), PathUri.parse('test/test.png')); - - final result = await client.webdav.propfind( - PathUri.parse('test/test.png'), - prop: const WebDavPropWithoutValues.fromBools( - davCreationdate: true, - davDisplayname: true, - davGetcontentlanguage: true, - davGetcontentlength: true, - davGetcontenttype: true, - davGetetag: true, - davGetlastmodified: true, - davQuotaAvailableBytes: true, - davQuotaUsedBytes: true, - davResourcetype: true, - ncAclCanManage: true, - ncAclEnabled: true, - ncAclList: true, - ncContainedFileCount: true, - ncContainedFolderCount: true, - ncCreationTime: true, - ncDataFingerprint: true, - ncGroupFolderId: true, - ncHasPreview: true, - ncHidden: true, - ncInheritedAclList: true, - ncIsEncrypted: true, - ncIsMountRoot: true, - ncLock: true, - ncLockOwner: true, - ncLockOwnerDisplayname: true, - ncLockOwnerEditor: true, - ncLockOwnerType: true, - ncLockTime: true, - ncLockTimeout: true, - ncLockToken: true, - ncMountType: true, - ncNote: true, - ncReminderDueDate: true, - ncRichWorkspace: true, - ncRichWorkspaceFile: true, - ncShareAttributes: true, - ncSharees: true, - ncUploadTime: true, - ncVersionAuthor: true, - ncVersionLabel: true, - ncMetadataBlurhash: true, - ocChecksums: true, - ocCommentsCount: true, - ocCommentsHref: true, - ocCommentsUnread: true, - ocDownloadURL: true, - ocFavorite: true, - ocFileid: true, - ocId: true, - ocOwnerDisplayName: true, - ocOwnerId: true, - ocPermissions: true, - ocShareTypes: true, - ocSize: true, - ocTags: true, - ocmSharePermissions: true, - ocsSharePermissions: true, - ), - ); + await client.webdav().putFile(file, file.statSync(), PathUri.parse('test/test.png')); + + final result = await client.webdav().propfind( + PathUri.parse('test/test.png'), + prop: const WebDavPropWithoutValues.fromBools( + davCreationdate: true, + davDisplayname: true, + davGetcontentlanguage: true, + davGetcontentlength: true, + davGetcontenttype: true, + davGetetag: true, + davGetlastmodified: true, + davQuotaAvailableBytes: true, + davQuotaUsedBytes: true, + davResourcetype: true, + ncAclCanManage: true, + ncAclEnabled: true, + ncAclList: true, + ncContainedFileCount: true, + ncContainedFolderCount: true, + ncCreationTime: true, + ncDataFingerprint: true, + ncGroupFolderId: true, + ncHasPreview: true, + ncHidden: true, + ncInheritedAclList: true, + ncIsEncrypted: true, + ncIsMountRoot: true, + ncLock: true, + ncLockOwner: true, + ncLockOwnerDisplayname: true, + ncLockOwnerEditor: true, + ncLockOwnerType: true, + ncLockTime: true, + ncLockTimeout: true, + ncLockToken: true, + ncMountType: true, + ncNote: true, + ncReminderDueDate: true, + ncRichWorkspace: true, + ncRichWorkspaceFile: true, + ncShareAttributes: true, + ncSharees: true, + ncUploadTime: true, + ncVersionAuthor: true, + ncVersionLabel: true, + ncMetadataBlurhash: true, + ocChecksums: true, + ocCommentsCount: true, + ocCommentsHref: true, + ocCommentsUnread: true, + ocDownloadURL: true, + ocFavorite: true, + ocFileid: true, + ocId: true, + ocOwnerDisplayName: true, + ocOwnerId: true, + ocPermissions: true, + ocShareTypes: true, + ocSize: true, + ocTags: true, + ocmSharePermissions: true, + ocsSharePermissions: true, + ), + ); final response = result.toWebDavFiles().single; expect(response.path, PathUri.parse('test/test.png')); @@ -383,19 +386,19 @@ void main() { test('Get directory props', () async { final data = utf8.encode('test'); - await client.webdav.mkcol(PathUri.parse('test/dir-props')); - await client.webdav.put(data, PathUri.parse('test/dir-props/test.txt')); - - final response = (await client.webdav.propfind( - PathUri.parse('test/dir-props'), - prop: const WebDavPropWithoutValues.fromBools( - davGetcontenttype: true, - davGetlastmodified: true, - davResourcetype: true, - ocSize: true, - ), - depth: WebDavDepth.zero, - )) + await client.webdav().mkcol(PathUri.parse('test/dir-props')); + await client.webdav().put(data, PathUri.parse('test/dir-props/test.txt')); + + final response = (await client.webdav().propfind( + PathUri.parse('test/dir-props'), + prop: const WebDavPropWithoutValues.fromBools( + davGetcontenttype: true, + davGetlastmodified: true, + davResourcetype: true, + ocSize: true, + ), + depth: WebDavDepth.zero, + )) .toWebDavFiles() .single; @@ -420,25 +423,25 @@ void main() { }); test('Filter files', () async { - final response = await client.webdav.put(utf8.encode('test'), PathUri.parse('test/filter.txt')); + final response = await client.webdav().put(utf8.encode('test'), PathUri.parse('test/filter.txt')); final id = response.headers['oc-fileid']; - await client.webdav.proppatch( - PathUri.parse('test/filter.txt'), - set: const WebDavProp( - ocFavorite: true, - ), - ); + await client.webdav().proppatch( + PathUri.parse('test/filter.txt'), + set: const WebDavProp( + ocFavorite: true, + ), + ); - final responses = (await client.webdav.report( - PathUri.parse('test'), - const WebDavOcFilterRules( - ocFavorite: true, - ), - prop: const WebDavPropWithoutValues.fromBools( - ocId: true, - ocFavorite: true, - ), - )) + final responses = (await client.webdav().report( + PathUri.parse('test'), + const WebDavOcFilterRules( + ocFavorite: true, + ), + prop: const WebDavPropWithoutValues.fromBools( + ocId: true, + ocFavorite: true, + ), + )) .responses; expect(responses, isNotEmpty); final props = responses.singleWhere((response) => response.href!.endsWith('/filter.txt')).propstats.first.prop; @@ -451,30 +454,30 @@ void main() { final createdDate = DateTime.utc(1971, 2); final uploadTime = DateTime.timestamp(); - await client.webdav.put( - utf8.encode('test'), - PathUri.parse('test/set-props.txt'), - lastModified: lastModifiedDate, - created: createdDate, - ); + await client.webdav().put( + utf8.encode('test'), + PathUri.parse('test/set-props.txt'), + lastModified: lastModifiedDate, + created: createdDate, + ); - final updated = await client.webdav.proppatch( - PathUri.parse('test/set-props.txt'), - set: const WebDavProp( - ocFavorite: true, - ), - ); + final updated = await client.webdav().proppatch( + PathUri.parse('test/set-props.txt'), + set: const WebDavProp( + ocFavorite: true, + ), + ); expect(updated, isTrue); - final props = (await client.webdav.propfind( - PathUri.parse('test/set-props.txt'), - prop: const WebDavPropWithoutValues.fromBools( - ocFavorite: true, - davGetlastmodified: true, - ncCreationTime: true, - ncUploadTime: true, - ), - )) + final props = (await client.webdav().propfind( + PathUri.parse('test/set-props.txt'), + prop: const WebDavPropWithoutValues.fromBools( + ocFavorite: true, + davGetlastmodified: true, + ncCreationTime: true, + ncUploadTime: true, + ), + )) .responses .single .propstats @@ -487,24 +490,24 @@ void main() { }); test('Remove properties', () async { - await client.webdav.put(utf8.encode('test'), PathUri.parse('test/remove-props.txt')); + await client.webdav().put(utf8.encode('test'), PathUri.parse('test/remove-props.txt')); - var updated = await client.webdav.proppatch( - PathUri.parse('test/remove-props.txt'), - set: const WebDavProp( - ocFavorite: true, - ), - ); + var updated = await client.webdav().proppatch( + PathUri.parse('test/remove-props.txt'), + set: const WebDavProp( + ocFavorite: true, + ), + ); expect(updated, isTrue); - var props = (await client.webdav.propfind( - PathUri.parse('test/remove-props.txt'), - prop: const WebDavPropWithoutValues.fromBools( - ocFavorite: true, - ncCreationTime: true, - ncUploadTime: true, - ), - )) + var props = (await client.webdav().propfind( + PathUri.parse('test/remove-props.txt'), + prop: const WebDavPropWithoutValues.fromBools( + ocFavorite: true, + ncCreationTime: true, + ncUploadTime: true, + ), + )) .responses .single .propstats @@ -512,20 +515,20 @@ void main() { .prop; expect(props.ocFavorite, true); - updated = await client.webdav.proppatch( - PathUri.parse('test/remove-props.txt'), - remove: const WebDavPropWithoutValues.fromBools( - ocFavorite: true, - ), - ); + updated = await client.webdav().proppatch( + PathUri.parse('test/remove-props.txt'), + remove: const WebDavPropWithoutValues.fromBools( + ocFavorite: true, + ), + ); expect(updated, isFalse); - props = (await client.webdav.propfind( - PathUri.parse('test/remove-props.txt'), - prop: const WebDavPropWithoutValues.fromBools( - ocFavorite: true, - ), - )) + props = (await client.webdav().propfind( + PathUri.parse('test/remove-props.txt'), + prop: const WebDavPropWithoutValues.fromBools( + ocFavorite: true, + ), + )) .responses .single .propstats @@ -540,17 +543,17 @@ void main() { final source = File('test/files/test.png'); final progressValues = []; - await client.webdav.putFile( - source, - source.statSync(), - PathUri.parse('test/upload_file.png'), - onProgress: progressValues.add, - ); - await client.webdav.getFile( - PathUri.parse('test/upload_file.png'), - destination, - onProgress: progressValues.add, - ); + await client.webdav().putFile( + source, + source.statSync(), + PathUri.parse('test/upload_file.png'), + onProgress: progressValues.add, + ); + await client.webdav().getFile( + PathUri.parse('test/upload_file.png'), + destination, + onProgress: progressValues.add, + ); expect(progressValues, containsAll([1.0, 1.0])); expect(destination.readAsBytesSync(), source.readAsBytesSync()); @@ -563,16 +566,16 @@ void main() { final source = File('test/files/test.png'); final progressValues = []; - await client.webdav.putStream( - source.openRead(), - PathUri.parse('test/upload_stream.png'), - contentLength: source.lengthSync(), - onProgress: progressValues.add, - ); - final stream = client.webdav.getStream( - PathUri.parse('test/upload_stream.png'), - onProgress: progressValues.add, - ); + await client.webdav().putStream( + source.openRead(), + PathUri.parse('test/upload_stream.png'), + contentLength: source.lengthSync(), + onProgress: progressValues.add, + ); + final stream = client.webdav().getStream( + PathUri.parse('test/upload_stream.png'), + onProgress: progressValues.add, + ); await stream.pipe(destination.openWrite()); expect(progressValues, containsAll([1.0, 1.0])); expect(destination.readAsBytesSync(), source.readAsBytesSync()); @@ -582,7 +585,7 @@ void main() { test('getStream error handling', () async { expect( - client.webdav.getStream(PathUri.parse('test/404.txt')), + client.webdav().getStream(PathUri.parse('test/404.txt')), emitsError(predicate((e) => e.statusCode == 404)), ); }); @@ -590,7 +593,7 @@ void main() { group('litmus', () { group('basic', () { test('options', () async { - final options = await client.webdav.options(); + final options = await client.webdav().options(); expect(options.capabilities, contains('1')); expect(options.capabilities, contains('3')); // Nextcloud only contains a fake plugin for Class 2 support: https://github.com/nextcloud/server/blob/master/apps/dav/lib/Connector/Sabre/FakeLockerPlugin.php @@ -605,32 +608,32 @@ void main() { test(name, () async { final content = utf8.encode('This is a test file'); - final response = await client.webdav.put(content, PathUri.parse('test/$path')); + final response = await client.webdav().put(content, PathUri.parse('test/$path')); expect(response.statusCode, 201); - final downloadedContent = await client.webdav.get(PathUri.parse('test/$path')); + final downloadedContent = await client.webdav().get(PathUri.parse('test/$path')); expect(downloadedContent, equals(content)); }); } test('put_no_parent', () async { await expectLater( - () => client.webdav.put(Uint8List(0), PathUri.parse('test/409me/noparent.txt')), + () => client.webdav().put(Uint8List(0), PathUri.parse('test/409me/noparent.txt')), // https://github.com/nextcloud/server/issues/39625 throwsA(predicate((e) => e.statusCode == 409)), ); }); test('delete', () async { - await client.webdav.put(Uint8List(0), PathUri.parse('test/delete.txt')); + await client.webdav().put(Uint8List(0), PathUri.parse('test/delete.txt')); - final response = await client.webdav.delete(PathUri.parse('test/delete.txt')); + final response = await client.webdav().delete(PathUri.parse('test/delete.txt')); expect(response.statusCode, 204); }); test('delete_null', () async { await expectLater( - () => client.webdav.delete(PathUri.parse('test/delete-null.txt')), + () => client.webdav().delete(PathUri.parse('test/delete-null.txt')), throwsA(predicate((e) => e.statusCode == 404)), ); }); @@ -638,29 +641,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(PathUri.parse('test/mkcol')); + final response = await client.webdav().mkcol(PathUri.parse('test/mkcol')); expect(response.statusCode, 201); }); test('mkcol_again', () async { - await client.webdav.mkcol(PathUri.parse('test/mkcol-again')); + await client.webdav().mkcol(PathUri.parse('test/mkcol-again')); await expectLater( - () => client.webdav.mkcol(PathUri.parse('test/mkcol-again')), + () => client.webdav().mkcol(PathUri.parse('test/mkcol-again')), throwsA(predicate((e) => e.statusCode == 405)), ); }); test('delete_coll', () async { - var response = await client.webdav.mkcol(PathUri.parse('test/delete-coll')); + var response = await client.webdav().mkcol(PathUri.parse('test/delete-coll')); - response = await client.webdav.delete(PathUri.parse('test/delete-coll')); + response = await client.webdav().delete(PathUri.parse('test/delete-coll')); expect(response.statusCode, 204); }); test('mkcol_no_parent', () async { await expectLater( - () => client.webdav.mkcol(PathUri.parse('test/409me/noparent')), + () => client.webdav().mkcol(PathUri.parse('test/409me/noparent')), throwsA(predicate((e) => e.statusCode == 409)), ); }); @@ -670,120 +673,167 @@ void main() { group('copymove', () { test('copy_simple', () async { - await client.webdav.mkcol(PathUri.parse('test/copy-simple-src')); + await client.webdav().mkcol(PathUri.parse('test/copy-simple-src')); - final response = - await client.webdav.copy(PathUri.parse('test/copy-simple-src'), PathUri.parse('test/copy-simple-dst')); + final response = await client.webdav().copy( + PathUri.parse('test/copy-simple-src'), + PathUri.parse('test/copy-simple-dst'), + ); expect(response.statusCode, 201); }); test('copy_overwrite', () async { - await client.webdav.mkcol(PathUri.parse('test/copy-overwrite-src')); - await client.webdav.mkcol(PathUri.parse('test/copy-overwrite-dst')); + await client.webdav().mkcol(PathUri.parse('test/copy-overwrite-src')); + await client.webdav().mkcol(PathUri.parse('test/copy-overwrite-dst')); await expectLater( - () => client.webdav - .copy(PathUri.parse('test/copy-overwrite-src'), PathUri.parse('test/copy-overwrite-dst')), + () => client.webdav().copy( + PathUri.parse('test/copy-overwrite-src'), + PathUri.parse('test/copy-overwrite-dst'), + ), throwsA(predicate((e) => e.statusCode == 412)), ); - final response = await client.webdav.copy( - PathUri.parse('test/copy-overwrite-src'), - PathUri.parse('test/copy-overwrite-dst'), - overwrite: true, - ); + final response = await client.webdav().copy( + PathUri.parse('test/copy-overwrite-src'), + PathUri.parse('test/copy-overwrite-dst'), + overwrite: true, + ); expect(response.statusCode, 204); }); test('copy_nodestcoll', () async { - await client.webdav.mkcol(PathUri.parse('test/copy-nodestcoll-src')); + await client.webdav().mkcol(PathUri.parse('test/copy-nodestcoll-src')); await expectLater( - () => client.webdav.copy(PathUri.parse('test/copy-nodestcoll-src'), PathUri.parse('test/nonesuch/dst')), + () => client.webdav().copy( + PathUri.parse('test/copy-nodestcoll-src'), + PathUri.parse('test/nonesuch/dst'), + ), throwsA(predicate((e) => e.statusCode == 409)), ); }); test('copy_coll', () async { - await client.webdav.mkcol(PathUri.parse('test/copy-coll-src')); - await client.webdav.mkcol(PathUri.parse('test/copy-coll-src/sub')); + await client.webdav().mkcol(PathUri.parse('test/copy-coll-src')); + await client.webdav().mkcol(PathUri.parse('test/copy-coll-src/sub')); for (var i = 0; i < 10; i++) { - await client.webdav.put(Uint8List(0), PathUri.parse('test/copy-coll-src/$i.txt')); + await client.webdav().put(Uint8List(0), PathUri.parse('test/copy-coll-src/$i.txt')); } - await client.webdav.copy(PathUri.parse('test/copy-coll-src'), PathUri.parse('test/copy-coll-dst1')); - await client.webdav.copy(PathUri.parse('test/copy-coll-src'), PathUri.parse('test/copy-coll-dst2')); + await client.webdav().copy( + PathUri.parse('test/copy-coll-src'), + PathUri.parse('test/copy-coll-dst1'), + ); + await client.webdav().copy( + PathUri.parse('test/copy-coll-src'), + PathUri.parse('test/copy-coll-dst2'), + ); await expectLater( - () => client.webdav.copy(PathUri.parse('test/copy-coll-src'), PathUri.parse('test/copy-coll-dst1')), + () => client.webdav().copy( + PathUri.parse('test/copy-coll-src'), + PathUri.parse('test/copy-coll-dst1'), + ), throwsA(predicate((e) => e.statusCode == 412)), ); - var response = await client.webdav - .copy(PathUri.parse('test/copy-coll-src'), PathUri.parse('test/copy-coll-dst2'), overwrite: true); + var response = await client.webdav().copy( + PathUri.parse('test/copy-coll-src'), + PathUri.parse('test/copy-coll-dst2'), + overwrite: true, + ); expect(response.statusCode, 204); for (var i = 0; i < 10; i++) { - response = await client.webdav.delete(PathUri.parse('test/copy-coll-dst1/$i.txt')); + response = await client.webdav().delete(PathUri.parse('test/copy-coll-dst1/$i.txt')); expect(response.statusCode, 204); } - response = await client.webdav.delete(PathUri.parse('test/copy-coll-dst1/sub')); + response = await client.webdav().delete(PathUri.parse('test/copy-coll-dst1/sub')); expect(response.statusCode, 204); - response = await client.webdav.delete(PathUri.parse('test/copy-coll-dst2')); + response = await client.webdav().delete(PathUri.parse('test/copy-coll-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), PathUri.parse('test/move-src1.txt')); - await client.webdav.put(Uint8List(0), PathUri.parse('test/move-src2.txt')); - await client.webdav.mkcol(PathUri.parse('test/move-coll')); - - var response = - await client.webdav.move(PathUri.parse('test/move-src1.txt'), PathUri.parse('test/move-dst.txt')); + await client.webdav().put(Uint8List(0), PathUri.parse('test/move-src1.txt')); + await client.webdav().put(Uint8List(0), PathUri.parse('test/move-src2.txt')); + await client.webdav().mkcol(PathUri.parse('test/move-coll')); + + var response = await client.webdav().move( + PathUri.parse('test/move-src1.txt'), + PathUri.parse('test/move-dst.txt'), + ); expect(response.statusCode, 201); await expectLater( - () => client.webdav.move(PathUri.parse('test/move-src2.txt'), PathUri.parse('test/move-dst.txt')), + () => client.webdav().move( + PathUri.parse('test/move-src2.txt'), + PathUri.parse('test/move-dst.txt'), + ), throwsA(predicate((e) => e.statusCode == 412)), ); - response = await client.webdav - .move(PathUri.parse('test/move-src2.txt'), PathUri.parse('test/move-dst.txt'), overwrite: true); + response = await client.webdav().move( + PathUri.parse('test/move-src2.txt'), + PathUri.parse('test/move-dst.txt'), + overwrite: true, + ); expect(response.statusCode, 204); }); test('move_coll', () async { - await client.webdav.mkcol(PathUri.parse('test/move-coll-src')); - await client.webdav.mkcol(PathUri.parse('test/move-coll-src/sub')); + await client.webdav().mkcol(PathUri.parse('test/move-coll-src')); + await client.webdav().mkcol(PathUri.parse('test/move-coll-src/sub')); for (var i = 0; i < 10; i++) { - await client.webdav.put(Uint8List(0), PathUri.parse('test/move-coll-src/$i.txt')); + await client.webdav().put(Uint8List(0), PathUri.parse('test/move-coll-src/$i.txt')); } - await client.webdav.put(Uint8List(0), PathUri.parse('test/move-coll-noncoll')); - await client.webdav.copy(PathUri.parse('test/move-coll-src'), PathUri.parse('test/move-coll-dst2')); - await client.webdav.move(PathUri.parse('test/move-coll-src'), PathUri.parse('test/move-coll-dst1')); + await client.webdav().put(Uint8List(0), PathUri.parse('test/move-coll-noncoll')); + await client.webdav().copy( + PathUri.parse('test/move-coll-src'), + PathUri.parse('test/move-coll-dst2'), + ); + await client.webdav().move( + PathUri.parse('test/move-coll-src'), + PathUri.parse('test/move-coll-dst1'), + ); await expectLater( - () => client.webdav.move(PathUri.parse('test/move-coll-dst1'), PathUri.parse('test/move-coll-dst2')), + () => client.webdav().move( + PathUri.parse('test/move-coll-dst1'), + PathUri.parse('test/move-coll-dst2'), + ), throwsA(predicate((e) => e.statusCode == 412)), ); - await client.webdav - .move(PathUri.parse('test/move-coll-dst2'), PathUri.parse('test/move-coll-dst1'), overwrite: true); - await client.webdav.copy(PathUri.parse('test/move-coll-dst1'), PathUri.parse('test/move-coll-dst2')); + await client.webdav().move( + PathUri.parse('test/move-coll-dst2'), + PathUri.parse('test/move-coll-dst1'), + overwrite: true, + ); + await client.webdav().copy( + PathUri.parse('test/move-coll-dst1'), + PathUri.parse('test/move-coll-dst2'), + ); for (var i = 0; i < 10; i++) { - final response = await client.webdav.delete(PathUri.parse('test/move-coll-dst1/$i.txt')); + final response = await client.webdav().delete( + PathUri.parse('test/move-coll-dst1/$i.txt'), + ); expect(response.statusCode, 204); } - final response = await client.webdav.delete(PathUri.parse('test/move-coll-dst1/sub')); + final response = await client.webdav().delete(PathUri.parse('test/move-coll-dst1/sub')); expect(response.statusCode, 204); await expectLater( - () => client.webdav.move(PathUri.parse('test/move-coll-dst2'), PathUri.parse('test/move-coll-noncoll')), + () => client.webdav().move( + PathUri.parse('test/move-coll-dst2'), + PathUri.parse('test/move-coll-noncoll'), + ), throwsA(predicate((e) => e.statusCode == 412)), ); }); @@ -795,10 +845,13 @@ void main() { // large_put: Already covered by large_get test('large_get', () async { - final response = await client.webdav.put(Uint8List(largefileSize), PathUri.parse('test/largefile.txt')); + final response = await client.webdav().put( + Uint8List(largefileSize), + PathUri.parse('test/largefile.txt'), + ); expect(response.statusCode, 201); - final downloadedContent = await client.webdav.get(PathUri.parse('test/largefile.txt')); + final downloadedContent = await client.webdav().get(PathUri.parse('test/largefile.txt')); expect(downloadedContent, hasLength(largefileSize)); }); }); @@ -813,16 +866,16 @@ void main() { final destination = File('${destinationDir.path}/empty-file'); final source = File('${destinationDir.path}/empty-file-source')..createSync(); - await client.webdav.putFile( - source, - source.statSync(), - PathUri.parse('test/empty-file'), - ); - await client.webdav.getFile( - PathUri.parse('test/empty-file'), - destination, - onProgress: callback.progressCallback, - ); + await client.webdav().putFile( + source, + source.statSync(), + PathUri.parse('test/empty-file'), + ); + await client.webdav().getFile( + PathUri.parse('test/empty-file'), + destination, + onProgress: callback.progressCallback, + ); verify(() => callback.progressCallback(1)).called(1); verifyNever(() => callback.progressCallback(any(that: isNot(1)))); From 8e503afd2bebe7a528ad3d82ffe2c7145af62ea9 Mon Sep 17 00:00:00 2001 From: provokateurin Date: Mon, 22 Jul 2024 23:42:21 +0200 Subject: [PATCH 2/2] feat(nextcloud): Implement chunked upload Signed-off-by: provokateurin --- .../src/api/webdav/chunked_upload_client.dart | 107 ++++++++++++++++++ .../lib/src/api/webdav/webdav_client.dart | 16 ++- packages/nextcloud/pubspec.yaml | 1 + .../fixtures/webdav/chunked_upload.regexp | 37 ++++++ packages/nextcloud/test/webdav_test.dart | 40 +++++++ 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 packages/nextcloud/lib/src/api/webdav/chunked_upload_client.dart create mode 100644 packages/nextcloud/test/fixtures/webdav/chunked_upload.regexp diff --git a/packages/nextcloud/lib/src/api/webdav/chunked_upload_client.dart b/packages/nextcloud/lib/src/api/webdav/chunked_upload_client.dart new file mode 100644 index 00000000000..c005f80e812 --- /dev/null +++ b/packages/nextcloud/lib/src/api/webdav/chunked_upload_client.dart @@ -0,0 +1,107 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http; +import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud/src/api/webdav/webdav.dart'; +import 'package:nextcloud/src/utils/utils.dart'; +import 'package:uuid/uuid.dart'; + +const _uuid = Uuid(); + +typedef ChunkedUpload = ({String token, PathUri destination}); + +class ChunkedUploadClient { + ChunkedUploadClient( + NextcloudClient rootClient, + String username, + ) : _username = username, + _webDavClient = WebDavClient( + rootClient, + endpoint: 'remote.php/dav/uploads/$username', + ); + + final WebDavClient _webDavClient; + final String _username; + + Future start(PathUri path) async { + final destination = PathUri.parse('/remote.php/dav/files/$_username/').join(path); + final token = _uuid.v7(); + + final streamedResponse = await _webDavClient.mkcol( + PathUri.parse(token), + headers: { + 'Destination': destination.toString(), + }, + ); + if (streamedResponse.statusCode != 201) { + final response = await http.Response.fromStream(streamedResponse); + + throw DynamiteStatusCodeException(response); + } + + return ( + token: token, + destination: destination, + ); + } + + Future uploadChunk( + ChunkedUpload chunkedUpload, + int index, + Uint8List chunk, + int totalLength, + ) async { + assert( + index >= 1 && index <= 10000, + 'Index must be a number between 1 and 10000', + ); + assert( + chunk.lengthInBytes >= 5 * pow(1024, 2) && chunk.lengthInBytes <= 5 * pow(1024, 3), + 'Chunk size must be between 5MB and 5GB', + ); + + final streamedResponse = await _webDavClient.put( + chunk, + PathUri.parse('${chunkedUpload.token}/$index'), + headers: { + 'Destination': chunkedUpload.destination.toString(), + 'OC-Total-Length': totalLength.toString(), + }, + ); + if (streamedResponse.statusCode != 201) { + final response = await http.Response.fromStream(streamedResponse); + + throw DynamiteStatusCodeException(response); + } + } + + Future assembleChunks( + ChunkedUpload chunkedUpload, + int totalLength, { + DateTime? lastModified, + DateTime? created, + }) async { + final request = http.Request( + 'MOVE', + constructUri( + _webDavClient.rootClient.baseURL, + 'remote.php/dav/uploads/$_username', + PathUri.parse('${chunkedUpload.token}/.file'), + ), + ); + request.headers.addAll({ + 'Destination': chunkedUpload.destination.toString(), + 'OC-Total-Length': totalLength.toString(), + if (lastModified != null) 'X-OC-Mtime': lastModified.secondsSinceEpoch.toString(), + if (created != null) 'X-OC-CTime': created.secondsSinceEpoch.toString(), + }); + + final streamedResponse = await _webDavClient.csrfClient.send(request); + if (streamedResponse.statusCode != 201) { + final response = await http.Response.fromStream(streamedResponse); + + throw DynamiteStatusCodeException(response); + } + } +} diff --git a/packages/nextcloud/lib/src/api/webdav/webdav_client.dart b/packages/nextcloud/lib/src/api/webdav/webdav_client.dart index 1089ca6a59a..b89eae8b584 100644 --- a/packages/nextcloud/lib/src/api/webdav/webdav_client.dart +++ b/packages/nextcloud/lib/src/api/webdav/webdav_client.dart @@ -70,8 +70,14 @@ class WebDavClient { /// See: /// * http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL for more information. /// * [mkcol_Request] for the request sent by this method. - Future mkcol(PathUri path) { + Future mkcol( + PathUri path, { + Map? headers, + }) { final request = mkcol_Request(path); + if (headers != null) { + request.headers.addAll(headers); + } return csrfClient.send(request); } @@ -132,6 +138,7 @@ class WebDavClient { PathUri path, { DateTime? lastModified, DateTime? created, + Map? headers, }) { final request = put_Request( localData, @@ -139,6 +146,9 @@ class WebDavClient { lastModified: lastModified, created: created, ); + if (headers != null) { + request.headers.addAll(headers); + } return csrfClient.send(request); } @@ -513,12 +523,16 @@ class WebDavClient { PathUri sourcePath, PathUri destinationPath, { bool overwrite = false, + Map? headers, }) { final request = move_Request( sourcePath, destinationPath, overwrite: overwrite, ); + if (headers != null) { + request.headers.addAll(headers); + } return csrfClient.send(request); } diff --git a/packages/nextcloud/pubspec.yaml b/packages/nextcloud/pubspec.yaml index 3b77771eaa3..fd2791d166e 100644 --- a/packages/nextcloud/pubspec.yaml +++ b/packages/nextcloud/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: timezone: ^0.9.4 universal_io: ^2.0.0 uri: ^1.0.0 + uuid: ^4.0.0 version: ^3.0.0 xml: ^6.0.0 xml_annotation: ^2.1.0 diff --git a/packages/nextcloud/test/fixtures/webdav/chunked_upload.regexp b/packages/nextcloud/test/fixtures/webdav/chunked_upload.regexp new file mode 100644 index 00000000000..0181b1eb111 --- /dev/null +++ b/packages/nextcloud/test/fixtures/webdav/chunked_upload.regexp @@ -0,0 +1,37 @@ +MKCOL http://localhost/remote\.php/dav/uploads/user1/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} +authorization: Bearer mock +content-type: application/xml +destination: //:80/remote\.php/dav/files/user1/test/test\.bin +ocs-apirequest: true +requesttoken: token +PUT http://localhost/remote\.php/dav/uploads/user1/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/1 +authorization: Bearer mock +content-length: 10485760 +content-type: application/xml +destination: //:80/remote\.php/dav/files/user1/test/test\.bin +oc-total-length: 15728640 +ocs-apirequest: true +requesttoken: token +.+ +PUT http://localhost/remote\.php/dav/uploads/user1/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/2 +authorization: Bearer mock +content-length: 5242880 +content-type: application/xml +destination: //:80/remote\.php/dav/files/user1/test/test\.bin +oc-total-length: 15728640 +ocs-apirequest: true +requesttoken: token +.+ +MOVE http://localhost/remote\.php/dav/uploads/user1/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/\.file +destination: //:80/remote\.php/dav/files/user1/test/test\.bin +oc-total-length: 15728640 +ocs-apirequest: true +requesttoken: token +x-oc-ctime: 0 +x-oc-mtime: 31536000 +PROPFIND http://localhost/remote\.php/webdav/test/test\.bin +authorization: Bearer mock +content-type: application/xml +ocs-apirequest: true +requesttoken: token + \ No newline at end of file diff --git a/packages/nextcloud/test/webdav_test.dart b/packages/nextcloud/test/webdav_test.dart index ff9181ae13c..5cdb789bf81 100644 --- a/packages/nextcloud/test/webdav_test.dart +++ b/packages/nextcloud/test/webdav_test.dart @@ -4,11 +4,13 @@ import 'dart:typed_data'; import 'package:mocktail/mocktail.dart'; import 'package:nextcloud/nextcloud.dart'; +import 'package:nextcloud/src/api/webdav/chunked_upload_client.dart'; import 'package:nextcloud/src/api/webdav/webdav.dart'; import 'package:nextcloud/src/utils/date_time.dart'; import 'package:nextcloud/webdav.dart'; import 'package:nextcloud_test/nextcloud_test.dart'; import 'package:test/test.dart'; +import 'package:timezone/timezone.dart' as tz; import 'package:universal_io/io.dart'; class MockCallbackFunction extends Mock { @@ -215,6 +217,44 @@ void main() { await container.destroy(); }); + test('Chunked upload', () async { + final chunkedUploadClient = ChunkedUploadClient(client, 'user1'); + + final chunkedUpload = await chunkedUploadClient.start(PathUri.parse('test/test.bin')); + + final chunk1 = Uint8List.fromList(List.generate(10 * pow(1024, 2).toInt(), (i) => 1)); + final chunk2 = Uint8List.fromList(List.generate(5 * pow(1024, 2).toInt(), (i) => 2)); + final combined = Uint8List.fromList(chunk1 + chunk2); + + await chunkedUploadClient.uploadChunk(chunkedUpload, 1, chunk1, combined.lengthInBytes); + await chunkedUploadClient.uploadChunk(chunkedUpload, 2, chunk2, combined.lengthInBytes); + + final lastModified = tz.TZDateTime(tz.UTC, 1971); + final created = tz.TZDateTime(tz.UTC, 1970); + + await chunkedUploadClient.assembleChunks( + chunkedUpload, + combined.lengthInBytes, + lastModified: lastModified, + created: created, + ); + + final result = await client.webdav().propfind( + PathUri.parse('test/test.bin'), + prop: const WebDavPropWithoutValues.fromBools( + davGetlastmodified: true, + davGetcontentlength: true, + ncCreationTime: true, + ), + ); + final response = result.toWebDavFiles().single; + + expect(response.path, PathUri.parse('test/test.bin')); + expect(response.lastModified, lastModified); + expect(response.createdDate, created); + expect(response.size, combined.lengthInBytes); + }); + test('List directory', () async { final responses = (await client.webdav().propfind( PathUri.parse('test'),