Merge branch '19421-restore-old-redirect-key' into main. Closes #19421
[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 { customEncodeURI } from "common/url";
14 import { Session } from "models/session";
15
16 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
17
18 export class CollectionService extends TrashableResourceService<CollectionResource> {
19     constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
20         super(serverApi, "collections", actions, [
21             'fileCount',
22             'fileSizeTotal',
23             'replicationConfirmed',
24             'replicationConfirmedAt',
25             'storageClassesConfirmed',
26             'storageClassesConfirmedAt',
27             'unsignedManifestText',
28             'version',
29         ]);
30     }
31
32     async get(uuid: string, showErrors?: boolean, select?: string[], session?: Session) {
33         super.validateUuid(uuid);
34         const selectParam = select || defaultCollectionSelectedFields;
35         return super.get(uuid, showErrors, selectParam, session);
36     }
37
38     create(data?: Partial<CollectionResource>) {
39         return super.create({ ...data, preserveVersion: true });
40     }
41
42     update(uuid: string, data: Partial<CollectionResource>) {
43         const select = [...Object.keys(data), 'version', 'modifiedAt'];
44         return super.update(uuid, { ...data, preserveVersion: true }, select);
45     }
46
47     async files(uuid: string) {
48         const request = await this.webdavClient.propfind(`c=${uuid}`);
49         if (request.responseXML != null) {
50             return extractFilesData(request.responseXML);
51         }
52         return Promise.reject();
53     }
54
55     async deleteFiles(collectionUuid: string, filePaths: string[]) {
56         const sortedUniquePaths = Array.from(new Set(filePaths))
57             .sort((a, b) => a.length - b.length)
58             .reduce((acc, currentPath) => {
59                 const parentPathFound = acc.find((parentPath) => currentPath.indexOf(`${parentPath}/`) > -1);
60
61                 if (!parentPathFound) {
62                     return [...acc, currentPath];
63                 }
64
65                 return acc;
66             }, []);
67
68         for (const path of sortedUniquePaths) {
69             if (path.indexOf(collectionUuid) === -1) {
70                 await this.webdavClient.delete(`c=${collectionUuid}${path}`);
71             } else {
72                 await this.webdavClient.delete(`c=${path}`);
73             }
74         }
75         await this.update(collectionUuid, { preserveVersion: true });
76     }
77
78     async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress, targetLocation: string = '') {
79         if (collectionUuid === "" || files.length === 0) { return; }
80         // files have to be uploaded sequentially
81         for (let idx = 0; idx < files.length; idx++) {
82             await this.uploadFile(collectionUuid, files[idx], idx, onProgress, targetLocation);
83         }
84         await this.update(collectionUuid, { preserveVersion: true });
85     }
86
87     async moveFile(collectionUuid: string, oldPath: string, newPath: string) {
88         await this.webdavClient.move(
89             `c=${collectionUuid}${oldPath}`,
90             `c=${collectionUuid}/${customEncodeURI(newPath)}`
91         );
92         await this.update(collectionUuid, { preserveVersion: true });
93     }
94
95     extendFileURL = (file: CollectionDirectory | CollectionFile) => {
96         const baseUrl = this.webdavClient.defaults.baseURL.endsWith('/')
97             ? this.webdavClient.defaults.baseURL.slice(0, -1)
98             : this.webdavClient.defaults.baseURL;
99         const apiToken = this.authService.getApiToken();
100         const encodedApiToken = apiToken ? encodeURI(apiToken) : '';
101         const userApiToken = `/t=${encodedApiToken}/`;
102         const splittedPrevFileUrl = file.url.split('/');
103         const url = `${baseUrl}/${splittedPrevFileUrl[1]}${userApiToken}${splittedPrevFileUrl.slice(2).join('/')}`;
104         return {
105             ...file,
106             url
107         };
108     }
109
110     private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }, targetLocation: string = '') {
111         const fileURL = `c=${targetLocation !== '' ? targetLocation : collectionUuid}/${file.name}`.replace('//', '/');
112         const requestConfig = {
113             headers: {
114                 'Content-Type': 'text/octet-stream'
115             },
116             onUploadProgress: (e: ProgressEvent) => {
117                 onProgress(fileId, e.loaded, e.total, Date.now());
118             },
119         };
120         return this.webdavClient.upload(fileURL, [file], requestConfig);
121     }
122 }