15768: fixed rerun bug Arvados-DCO-1.1-Signed-off-by: Lisa Knox <lisa.knox@curii...
[arvados.git] / src / services / collection-service / collection-service.ts
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
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
16 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
17
18 export const emptyCollectionPdh = "d41d8cd98f00b204e9800998ecf8427e+0";
19
20 export class CollectionService extends TrashableResourceService<CollectionResource> {
21     constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
22         super(serverApi, "collections", actions, [
23             "fileCount",
24             "fileSizeTotal",
25             "replicationConfirmed",
26             "replicationConfirmedAt",
27             "storageClassesConfirmed",
28             "storageClassesConfirmedAt",
29             "unsignedManifestText",
30             "version",
31         ]);
32     }
33
34     async get(uuid: string, showErrors?: boolean, select?: string[], session?: Session) {
35         super.validateUuid(uuid);
36         const selectParam = select || defaultCollectionSelectedFields;
37         return super.get(uuid, showErrors, selectParam, session);
38     }
39
40     create(data?: Partial<CollectionResource>, showErrors?: boolean) {
41         return super.create({ ...data, preserveVersion: true }, showErrors);
42     }
43
44     update(uuid: string, data: Partial<CollectionResource>, showErrors?: boolean) {
45         const select = [...Object.keys(data), "version", "modifiedAt"];
46         return super.update(uuid, { ...data, preserveVersion: true }, showErrors, select);
47     }
48
49     async files(uuid: string) {
50         const request = await this.webdavClient.propfind(`c=${uuid}`);
51         if (request.responseXML != null) {
52             return extractFilesData(request.responseXML);
53         }
54
55         return Promise.reject();
56     }
57
58     private combineFilePath(parts: string[]) {
59         return parts.reduce((path, part) => {
60             // Trim leading and trailing slashes
61             const trimmedPart = part.split("/").filter(Boolean).join("/");
62             if (trimmedPart.length) {
63                 const separator = path.endsWith("/") ? "" : "/";
64                 return `${path}${separator}${trimmedPart}`;
65             } else {
66                 return path;
67             }
68         }, "/");
69     }
70
71     private replaceFiles(collectionUuid: string, fileMap: {}, showErrors?: boolean) {
72         const payload = {
73             collection: {
74                 preserve_version: true,
75             },
76             replace_files: fileMap,
77         };
78
79         return CommonService.defaultResponse(
80             this.serverApi.put<CollectionResource>(`/${this.resourceType}/${collectionUuid}`, payload),
81             this.actions,
82             true, // mapKeys
83             showErrors
84         );
85     }
86
87     async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress, targetLocation: string = "") {
88         if (collectionUuid === "" || files.length === 0) {
89             return;
90         }
91         // files have to be uploaded sequentially
92         for (let idx = 0; idx < files.length; idx++) {
93             await this.uploadFile(collectionUuid, files[idx], idx, onProgress, targetLocation);
94         }
95         await this.update(collectionUuid, { preserveVersion: true });
96     }
97
98     async renameFile(collectionUuid: string, collectionPdh: string, oldPath: string, newPath: string) {
99         return this.replaceFiles(collectionUuid, {
100             [this.combineFilePath([newPath])]: `${collectionPdh}${this.combineFilePath([oldPath])}`,
101             [this.combineFilePath([oldPath])]: "",
102         });
103     }
104
105     extendFileURL = (file: CollectionDirectory | CollectionFile) => {
106         const baseUrl = this.webdavClient.getBaseUrl().endsWith("/") ? this.webdavClient.getBaseUrl().slice(0, -1) : this.webdavClient.getBaseUrl();
107         const apiToken = this.authService.getApiToken();
108         const encodedApiToken = apiToken ? encodeURI(apiToken) : "";
109         const userApiToken = `/t=${encodedApiToken}/`;
110         const splittedPrevFileUrl = file.url.split("/");
111         const url = `${baseUrl}/${splittedPrevFileUrl[1]}${userApiToken}${splittedPrevFileUrl.slice(2).join("/")}`;
112         return {
113             ...file,
114             url,
115         };
116     };
117
118     async getFileContents(file: CollectionFile) {
119         return (await this.webdavClient.get(`c=${file.id}`)).response;
120     }
121
122     private async uploadFile(
123         collectionUuid: string,
124         file: File,
125         fileId: number,
126         onProgress: UploadProgress = () => {
127             return;
128         },
129         targetLocation: string = ""
130     ) {
131         const fileURL = `c=${targetLocation !== "" ? targetLocation : collectionUuid}/${file.name}`.replace("//", "/");
132         const requestConfig = {
133             headers: {
134                 "Content-Type": "text/octet-stream",
135             },
136             onUploadProgress: (e: ProgressEvent) => {
137                 onProgress(fileId, e.loaded, e.total, Date.now());
138             },
139         };
140         return this.webdavClient.upload(fileURL, [file], requestConfig);
141     }
142
143     deleteFiles(collectionUuid: string, files: string[], showErrors?: boolean) {
144         const optimizedFiles = files
145             .sort((a, b) => a.length - b.length)
146             .reduce((acc, currentPath) => {
147                 const parentPathFound = acc.find(parentPath => currentPath.indexOf(`${parentPath}/`) > -1);
148
149                 if (!parentPathFound) {
150                     return [...acc, currentPath];
151                 }
152
153                 return acc;
154             }, []);
155
156         const fileMap = optimizedFiles.reduce((obj, filePath) => {
157             return {
158                 ...obj,
159                 [this.combineFilePath([filePath])]: "",
160             };
161         }, {});
162
163         return this.replaceFiles(collectionUuid, fileMap, showErrors);
164     }
165
166     copyFiles(sourcePdh: string, files: string[], destinationCollectionUuid: string, destinationPath: string, showErrors?: boolean) {
167         const fileMap = files.reduce((obj, sourceFile) => {
168             const sourceFileName = sourceFile.split("/").filter(Boolean).slice(-1).join("");
169             return {
170                 ...obj,
171                 [this.combineFilePath([destinationPath, sourceFileName])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`,
172             };
173         }, {});
174
175         return this.replaceFiles(destinationCollectionUuid, fileMap, showErrors);
176     }
177
178     moveFiles(
179         sourceUuid: string,
180         sourcePdh: string,
181         files: string[],
182         destinationCollectionUuid: string,
183         destinationPath: string,
184         showErrors?: boolean
185     ) {
186         if (sourceUuid === destinationCollectionUuid) {
187             const fileMap = files.reduce((obj, sourceFile) => {
188                 const sourceFileName = sourceFile.split("/").filter(Boolean).slice(-1).join("");
189                 return {
190                     ...obj,
191                     [this.combineFilePath([destinationPath, sourceFileName])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`,
192                     [this.combineFilePath([sourceFile])]: "",
193                 };
194             }, {});
195
196             return this.replaceFiles(sourceUuid, fileMap, showErrors);
197         } else {
198             return this.copyFiles(sourcePdh, files, destinationCollectionUuid, destinationPath, showErrors).then(() => {
199                 return this.deleteFiles(sourceUuid, files, showErrors);
200             });
201         }
202     }
203
204     createDirectory(collectionUuid: string, path: string, showErrors?: boolean) {
205         const fileMap = { [this.combineFilePath([path])]: emptyCollectionPdh };
206
207         return this.replaceFiles(collectionUuid, fileMap, showErrors);
208     }
209 }