1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
5 import { CollectionResource, defaultCollectionSelectedFields } from "models/collection";
6 import { AxiosInstance } from "axios";
7 import { CollectionFile, CollectionDirectory } from "models/collection-file";
8 import { WebDAV } from "common/webdav";
9 import { AuthService } from "../auth-service/auth-service";
10 import { extractFilesData } from "./collection-service-files-response";
11 import { TrashableResourceService } from "services/common-service/trashable-resource-service";
12 import { ApiActions } from "services/api/api-actions";
13 import { Session } from "models/session";
14 import { CommonService } from "services/common-service/common-service";
15 import { snakeCase } from "lodash";
17 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
18 type CollectionPartialUpdateOrCreate = Partial<CollectionResource> & Pick<CollectionResource, "uuid"> |
19 Partial<CollectionResource> & Pick<CollectionResource, "ownerUuid">;
21 export const emptyCollectionPdh = 'd41d8cd98f00b204e9800998ecf8427e+0';
23 export class CollectionService extends TrashableResourceService<CollectionResource> {
24 constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
25 super(serverApi, "collections", actions, [
28 'replicationConfirmed',
29 'replicationConfirmedAt',
30 'storageClassesConfirmed',
31 'storageClassesConfirmedAt',
32 'unsignedManifestText',
37 async get(uuid: string, showErrors?: boolean, select?: string[], session?: Session) {
38 super.validateUuid(uuid);
39 const selectParam = select || defaultCollectionSelectedFields;
40 return super.get(uuid, showErrors, selectParam, session);
43 create(data?: Partial<CollectionResource>, showErrors?: boolean) {
44 return super.create({ ...data, preserveVersion: true }, showErrors);
47 update(uuid: string, data: Partial<CollectionResource>, showErrors?: boolean) {
48 const select = [...Object.keys(data), 'version', 'modifiedAt'];
49 return super.update(uuid, { ...data, preserveVersion: true }, showErrors, select);
52 async files(uuid: string) {
53 const request = await this.webdavClient.propfind(`c=${uuid}`);
54 if (request.responseXML != null) {
55 return extractFilesData(request.responseXML);
57 return Promise.reject();
60 private combineFilePath(parts: string[]) {
61 return parts.reduce((path, part) => {
62 // Trim leading and trailing slashes
63 const trimmedPart = part.split('/').filter(Boolean).join('/');
64 if (trimmedPart.length) {
65 const separator = path.endsWith('/') ? '' : '/';
66 return `${path}${separator}${trimmedPart}`;
73 private replaceFiles(data: CollectionPartialUpdateOrCreate, fileMap: {}, showErrors?: boolean) {
76 preserve_version: true,
77 ...CommonService.mapKeys(snakeCase)(data),
78 // Don't send uuid in payload when creating
81 replace_files: fileMap
84 return CommonService.defaultResponse(
86 .put<CollectionResource>(`/${this.resourceType}/${data.uuid}`, payload),
92 return CommonService.defaultResponse(
94 .post<CollectionResource>(`/${this.resourceType}`, payload),
102 async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress, targetLocation: string = '') {
103 if (collectionUuid === "" || files.length === 0) { return; }
104 // files have to be uploaded sequentially
105 for (let idx = 0; idx < files.length; idx++) {
106 await this.uploadFile(collectionUuid, files[idx], idx, onProgress, targetLocation);
108 await this.update(collectionUuid, { preserveVersion: true });
111 async renameFile(collectionUuid: string, collectionPdh: string, oldPath: string, newPath: string) {
112 return this.replaceFiles({uuid: collectionUuid}, {
113 [this.combineFilePath([newPath])]: `${collectionPdh}${this.combineFilePath([oldPath])}`,
114 [this.combineFilePath([oldPath])]: '',
118 extendFileURL = (file: CollectionDirectory | CollectionFile) => {
119 const baseUrl = this.webdavClient.getBaseUrl().endsWith('/')
120 ? this.webdavClient.getBaseUrl().slice(0, -1)
121 : this.webdavClient.getBaseUrl();
122 const apiToken = this.authService.getApiToken();
123 const encodedApiToken = apiToken ? encodeURI(apiToken) : '';
124 const userApiToken = `/t=${encodedApiToken}/`;
125 const splittedPrevFileUrl = file.url.split('/');
126 const url = `${baseUrl}/${splittedPrevFileUrl[1]}${userApiToken}${splittedPrevFileUrl.slice(2).join('/')}`;
133 async getFileContents(file: CollectionFile) {
134 return (await this.webdavClient.get(`c=${file.id}`)).response;
137 private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }, targetLocation: string = '') {
138 const fileURL = `c=${targetLocation !== '' ? targetLocation : collectionUuid}/${file.name}`.replace('//', '/');
139 const requestConfig = {
141 'Content-Type': 'text/octet-stream'
143 onUploadProgress: (e: ProgressEvent) => {
144 onProgress(fileId, e.loaded, e.total, Date.now());
147 return this.webdavClient.upload(fileURL, [file], requestConfig);
150 deleteFiles(collectionUuid: string, files: string[], showErrors?: boolean) {
151 const optimizedFiles = files
152 .sort((a, b) => a.length - b.length)
153 .reduce((acc, currentPath) => {
154 const parentPathFound = acc.find((parentPath) => currentPath.indexOf(`${parentPath}/`) > -1);
156 if (!parentPathFound) {
157 return [...acc, currentPath];
163 const fileMap = optimizedFiles.reduce((obj, filePath) => {
166 [this.combineFilePath([filePath])]: ''
170 return this.replaceFiles({uuid: collectionUuid}, fileMap, showErrors);
173 copyFiles(sourcePdh: string, files: string[], destinationCollection: CollectionPartialUpdateOrCreate, destinationPath: string, showErrors?: boolean) {
174 const fileMap = files.reduce((obj, sourceFile) => {
175 const sourceFileName = sourceFile.split('/').filter(Boolean).slice(-1).join("");
178 [this.combineFilePath([destinationPath, sourceFileName])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`
182 return this.replaceFiles(destinationCollection, fileMap, showErrors);
185 moveFiles(sourceUuid: string, sourcePdh: string, files: string[], destinationCollection: CollectionPartialUpdateOrCreate, destinationPath: string, showErrors?: boolean) {
186 if (sourceUuid === destinationCollection.uuid) {
187 const fileMap = files.reduce((obj, sourceFile) => {
188 const sourceFileName = sourceFile.split('/').filter(Boolean).slice(-1).join("");
191 [this.combineFilePath([destinationPath, sourceFileName])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`,
192 [this.combineFilePath([sourceFile])]: '',
196 return this.replaceFiles({uuid: sourceUuid}, fileMap, showErrors)
198 return this.copyFiles(sourcePdh, files, destinationCollection, destinationPath, showErrors)
200 return this.deleteFiles(sourceUuid, files, showErrors);
205 createDirectory(collectionUuid: string, path: string, showErrors?: boolean) {
206 const fileMap = {[this.combineFilePath([path])]: emptyCollectionPdh};
208 return this.replaceFiles({uuid: collectionUuid}, fileMap, showErrors);