[arvados-workbench2] created: 2.5.0-79-g4d78e6e8
git repository hosting
git at public.arvados.org
Wed Mar 15 20:21:03 UTC 2023
at 4d78e6e875392cdcadad164cc3863a552343833f (commit)
commit 4d78e6e875392cdcadad164cc3863a552343833f
Author: Stephen Smith <stephen at curii.com>
Date: Wed Mar 15 16:19:49 2023 -0400
20029: Add collection batch file delete/copy/move and unit tests
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/common/webdav.ts b/src/common/webdav.ts
index bb8a68bd..c95d8747 100644
--- a/src/common/webdav.ts
+++ b/src/common/webdav.ts
@@ -88,6 +88,15 @@ export class WebDAV {
method: 'DELETE'
})
+ mkdir = (url: string, config: WebDAVRequestConfig = {}) =>
+ this.request({
+ ...config, url,
+ method: 'MKCOL',
+ headers: {
+ ...config.headers,
+ }
+ })
+
private request = (config: RequestConfig) => {
return new Promise<XMLHttpRequest>((resolve, reject) => {
const r = this.createRequest();
diff --git a/src/services/collection-service/collection-service.test.ts b/src/services/collection-service/collection-service.test.ts
index 4be17213..4649437a 100644
--- a/src/services/collection-service/collection-service.test.ts
+++ b/src/services/collection-service/collection-service.test.ts
@@ -23,10 +23,12 @@ describe('collection-service', () => {
webdavClient = {
delete: jest.fn(),
upload: jest.fn(),
+ mkdir: jest.fn(),
} as any;
authService = {} as AuthService;
actions = {
progressFn: jest.fn(),
+ errorFn: jest.fn(),
} as any;
collectionService = new CollectionService(serverApi, webdavClient, authService, actions);
collectionService.update = jest.fn();
@@ -165,4 +167,165 @@ describe('collection-service', () => {
expect(webdavClient.delete).toHaveBeenCalledWith("c=zzzzz-tpzed-5o5tg0l9a57gxxx/root/1");
});
});
+
+ describe('batch file operations', () => {
+ it('should batch remove files', async () => {
+ serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+ // given
+ const filePaths: string[] = ['/root/1', '/secondFile', 'barefile.txt'];
+ const collectionUUID = 'zzzzz-4zz18-5o5tg0l9a57gxxx';
+
+ // when
+ await collectionService.batchFileDelete(collectionUUID, filePaths);
+
+ // then
+ expect(serverApi.put).toHaveBeenCalledTimes(1);
+ expect(serverApi.put).toHaveBeenCalledWith(
+ `/collections/${collectionUUID}`, {
+ collection: {
+ preserve_version: true
+ },
+ replace_files: {
+ '/root/1': '',
+ '/secondFile': '',
+ '/barefile.txt': '',
+ },
+ }
+ );
+ });
+
+ it('should batch copy files', async () => {
+ serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+ // given
+ const filePaths: string[] = ['/root/1', '/secondFile', 'barefile.txt'];
+ // const collectionUUID = 'zzzzz-4zz18-5o5tg0l9a57gxxx';
+ const collectionPDH = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+ const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+ const destinationPath = '/destinationPath';
+
+ // when
+ await collectionService.batchFileCopy(collectionPDH, filePaths, destinationUuid, destinationPath);
+
+ // then
+ expect(serverApi.put).toHaveBeenCalledTimes(1);
+ expect(serverApi.put).toHaveBeenCalledWith(
+ `/collections/${destinationUuid}`, {
+ collection: {
+ preserve_version: true
+ },
+ replace_files: {
+ [`${destinationPath}/1`]: `${collectionPDH}/root/1`,
+ [`${destinationPath}/secondFile`]: `${collectionPDH}/secondFile`,
+ [`${destinationPath}/barefile.txt`]: `${collectionPDH}/barefile.txt`,
+ },
+ }
+ );
+ });
+
+ it('should batch move files', async () => {
+ serverApi.put = jest.fn(() => Promise.resolve({ data: {} }));
+ // given
+ const filePaths: string[] = ['/rootFile', '/secondFile', '/subpath/subfile', 'barefile.txt'];
+ const srcCollectionUUID = 'zzzzz-4zz18-5o5tg0l9a57gxxx';
+ const srcCollectionPDH = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+ const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+ const destinationPath = '/destinationPath';
+
+ // when
+ await collectionService.batchFileMove(srcCollectionUUID, srcCollectionPDH, filePaths, destinationUuid, destinationPath);
+
+ // then
+ expect(serverApi.put).toHaveBeenCalledTimes(2);
+ // Verify copy
+ expect(serverApi.put).toHaveBeenCalledWith(
+ `/collections/${destinationUuid}`, {
+ collection: {
+ preserve_version: true
+ },
+ replace_files: {
+ [`${destinationPath}/rootFile`]: `${srcCollectionPDH}/rootFile`,
+ [`${destinationPath}/secondFile`]: `${srcCollectionPDH}/secondFile`,
+ [`${destinationPath}/subfile`]: `${srcCollectionPDH}/subpath/subfile`,
+ [`${destinationPath}/barefile.txt`]: `${srcCollectionPDH}/barefile.txt`,
+ },
+ }
+ );
+ // Verify delete
+ expect(serverApi.put).toHaveBeenCalledWith(
+ `/collections/${srcCollectionUUID}`, {
+ collection: {
+ preserve_version: true
+ },
+ replace_files: {
+ "/rootFile": "",
+ "/secondFile": "",
+ "/subpath/subfile": "",
+ "/barefile.txt": "",
+ },
+ }
+ );
+ });
+
+ it('should abort batch move when copy fails', async () => {
+ // Simulate failure to copy
+ serverApi.put = jest.fn(() => Promise.reject({
+ data: {},
+ response: {
+ "errors": ["error getting snapshot of \"rootFile\" from \"8cd9ce1dfa21c635b620b1bfee7aaa08+180\": file does not exist"]
+ }
+ }));
+ // given
+ const filePaths: string[] = ['/rootFile', '/secondFile', '/subpath/subfile', 'barefile.txt'];
+ const srcCollectionUUID = 'zzzzz-4zz18-5o5tg0l9a57gxxx';
+ const srcCollectionPDH = '8cd9ce1dfa21c635b620b1bfee7aaa08+180';
+
+ const destinationUuid = 'zzzzz-4zz18-ywq0rvhwwhkjnfq';
+ const destinationPath = '/destinationPath';
+
+ // when
+ try {
+ await collectionService.batchFileMove(srcCollectionUUID, srcCollectionPDH, filePaths, destinationUuid, destinationPath);
+ } catch {}
+
+ // then
+ expect(serverApi.put).toHaveBeenCalledTimes(1);
+ // Verify copy
+ expect(serverApi.put).toHaveBeenCalledWith(
+ `/collections/${destinationUuid}`, {
+ collection: {
+ preserve_version: true
+ },
+ replace_files: {
+ [`${destinationPath}/rootFile`]: `${srcCollectionPDH}/rootFile`,
+ [`${destinationPath}/secondFile`]: `${srcCollectionPDH}/secondFile`,
+ [`${destinationPath}/subfile`]: `${srcCollectionPDH}/subpath/subfile`,
+ [`${destinationPath}/barefile.txt`]: `${srcCollectionPDH}/barefile.txt`,
+ },
+ }
+ );
+ });
+ });
+
+ describe('createDirectory', () => {
+ it('creates empty directory', async () => {
+ // given
+ const directoryNames = {
+ 'newDir': 'newDir',
+ '/fooDir': 'fooDir',
+ '/anotherPath/': 'anotherPath',
+ 'trailingSlash/': 'trailingSlash',
+ };
+ const collectionUUID = 'zzzzz-tpzed-5o5tg0l9a57gxxx';
+
+ Object.keys(directoryNames).map(async (path) => {
+ // when
+ await collectionService.createDirectory(collectionUUID, path);
+ // then
+ expect(webdavClient.mkdir).toHaveBeenCalledWith(`c=${collectionUUID}/${directoryNames[path]}`);
+ });
+ });
+ });
+
});
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index 1a03d8da..77ad5d38 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -12,6 +12,7 @@ import { TrashableResourceService } from "services/common-service/trashable-reso
import { ApiActions } from "services/api/api-actions";
import { customEncodeURI } from "common/url";
import { Session } from "models/session";
+import { CommonService } from "services/common-service/common-service";
export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
@@ -123,4 +124,65 @@ export class CollectionService extends TrashableResourceService<CollectionResour
};
return this.webdavClient.upload(fileURL, [file], requestConfig);
}
+
+ batchFileDelete(collectionUuid: string, files: string[], showErrors?: boolean) {
+ const payload = {
+ collection: {
+ preserve_version: true
+ },
+ replace_files: files.reduce((obj, filePath) => {
+ const pathStart = filePath.startsWith('/') ? '' : '/';
+ return {
+ ...obj,
+ [`${pathStart}${filePath}`]: ''
+ }
+ }, {})
+ };
+
+ return CommonService.defaultResponse(
+ this.serverApi
+ .put<CollectionResource>(`/${this.resourceType}/${collectionUuid}`, payload),
+ this.actions,
+ true, // mapKeys
+ showErrors
+ );
+ }
+
+ batchFileCopy(sourcePdh: string, files: string[], destinationCollectionUuid: string, destinationCollectionPath: string, showErrors?: boolean) {
+ const pathStart = destinationCollectionPath.startsWith('/') ? '' : '/';
+ const separator = destinationCollectionPath.endsWith('/') ? '' : '/';
+ const destinationPath = `${pathStart}${destinationCollectionPath}${separator}`;
+ const payload = {
+ collection: {
+ preserve_version: true
+ },
+ replace_files: files.reduce((obj, sourceFile) => {
+ const sourcePath = sourceFile.startsWith('/') ? sourceFile : `/${sourceFile}`;
+ return {
+ ...obj,
+ [`${destinationPath}${sourceFile.split('/').slice(-1)}`]: `${sourcePdh}${sourcePath}`
+ };
+ }, {})
+ };
+
+ return CommonService.defaultResponse(
+ this.serverApi
+ .put<CollectionResource>(`/${this.resourceType}/${destinationCollectionUuid}`, payload),
+ this.actions,
+ true, // mapKeys
+ showErrors
+ );
+ }
+
+ batchFileMove(sourceUuid: string, sourcePdh: string, files: string[], destinationCollectionUuid: string, destinationPath: string, showErrors?: boolean) {
+ return this.batchFileCopy(sourcePdh, files, destinationCollectionUuid, destinationPath, showErrors)
+ .then(() => {
+ return this.batchFileDelete(sourceUuid, files, showErrors);
+ });
+ }
+
+ createDirectory(collectionUuid: string, path: string) {
+ return this.webdavClient.mkdir(`c=${collectionUuid}/${customEncodeURI(path)}`);
+ }
+
}
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list