Merge branch 'feat/rbf-wording' into 'main'
[ProtonMail-WebClient.git] / packages / drive-store / store / _uploads / UploadProvider / useUploadHelper.ts
blobc1945865ae8719707bbf1133fad5bae9d4cf3959
1 import { sha1 } from '@noble/hashes/sha1';
2 import type { ReadableStream } from 'web-streams-polyfill';
4 import { arrayToHexString } from '@proton/crypto/lib/utils';
5 import { queryCheckAvailableHashes } from '@proton/shared/lib/api/drive/link';
6 import { queryPhotosDuplicates } from '@proton/shared/lib/api/drive/photos';
7 import type { HashCheckResult } from '@proton/shared/lib/interfaces/drive/link';
8 import { LinkState } from '@proton/shared/lib/interfaces/drive/link';
9 import type { DuplicatePhotosHash } from '@proton/shared/lib/interfaces/drive/photos';
10 import { generateLookupHash } from '@proton/shared/lib/keys/driveKeys';
11 import range from '@proton/utils/range';
13 import { untilStreamEnd } from '../../../utils/stream';
14 import { useDebouncedRequest } from '../../_api';
15 import { adjustName, splitLinkName, useLink, useLinksListing } from '../../_links';
16 import { isClientUidAvailable } from './uploadClientUid';
18 const HASH_CHECK_AMOUNT = 10;
20 export default function useUploadHelper() {
21     const debouncedRequest = useDebouncedRequest();
22     const { getLinkHashKey } = useLink();
23     const { loadChildren, getCachedChildren } = useLinksListing();
25     const findAvailableName = async (
26         abortSignal: AbortSignal,
27         {
28             shareId,
29             parentLinkId,
30             filename,
31             suppressErrors = false,
32         }: { shareId: string; parentLinkId: string; filename: string; suppressErrors?: boolean }
33     ) => {
34         const parentHashKey = await getLinkHashKey(abortSignal, shareId, parentLinkId);
35         if (!parentHashKey) {
36             throw Error('Missing hash key on folder link');
37         }
39         const [namePart, extension] = splitLinkName(filename);
40         const hash = await generateLookupHash(filename, parentHashKey);
42         const findAdjustedName = async (
43             start = 0
44         ): Promise<{
45             filename: string;
46             hash: string;
47             draftLinkId?: string;
48             clientUid?: string;
49         }> => {
50             const hashesToCheck = await Promise.all(
51                 range(start, start + HASH_CHECK_AMOUNT).map(async (i) => {
52                     if (i === 0) {
53                         return {
54                             filename,
55                             hash,
56                         };
57                     }
58                     const adjustedFileName = adjustName(i, namePart, extension);
59                     return {
60                         filename: adjustedFileName,
61                         hash: await generateLookupHash(adjustedFileName, parentHashKey),
62                     };
63                 })
64             );
66             const Hashes = hashesToCheck.map(({ hash }) => hash);
67             const { AvailableHashes, PendingHashes } = await debouncedRequest<HashCheckResult>(
68                 queryCheckAvailableHashes(shareId, parentLinkId, { Hashes }, suppressErrors),
69                 abortSignal
70             );
72             // Check if pending drafts are created by this client and is safe
73             // to automatically replace the draft without user interaction.
74             const pendingAvailableHashes = PendingHashes.filter(({ ClientUID }) => isClientUidAvailable(ClientUID));
75             if (pendingAvailableHashes.length) {
76                 const availableName = hashesToCheck.find(({ hash }) => hash === pendingAvailableHashes[0].Hash);
77                 if (availableName) {
78                     return {
79                         ...availableName,
80                         draftLinkId: pendingAvailableHashes[0].LinkID,
81                         clientUid: pendingAvailableHashes[0].ClientUID,
82                     };
83                 }
84             }
86             if (!AvailableHashes.length) {
87                 return findAdjustedName(start + HASH_CHECK_AMOUNT);
88             }
89             const availableName = hashesToCheck.find(({ hash }) => hash === AvailableHashes[0]);
91             if (!availableName) {
92                 throw new Error('Backend returned unexpected hash');
93             }
95             const draftHashes = PendingHashes.filter(({ ClientUID }) => !isClientUidAvailable(ClientUID));
96             const draftLinkId = draftHashes.find(({ Hash }) => Hash === hash)?.LinkID;
98             return {
99                 ...availableName,
100                 draftLinkId,
101             };
102         };
103         return findAdjustedName();
104     };
106     /**
107      * Checks if there is a Photos file with the same Hash and ContentHash
108      */
109     const findDuplicateContentHash = async (
110         abortSignal: AbortSignal,
111         {
112             file,
113             volumeId,
114             shareId,
115             parentLinkId,
116         }: { file: File; volumeId: string; shareId: string; parentLinkId: string }
117     ): Promise<{
118         filename: string;
119         hash: string;
120         draftLinkId?: string;
121         clientUid?: string;
122         isDuplicatePhotos?: boolean;
123     }> => {
124         const parentHashKey = await getLinkHashKey(abortSignal, shareId, parentLinkId);
125         if (!parentHashKey) {
126             throw Error('Missing hash key on folder link');
127         }
128         const hash = await generateLookupHash(file.name, parentHashKey);
130         const { DuplicateHashes } = await debouncedRequest<{ DuplicateHashes: DuplicatePhotosHash[] }>(
131             queryPhotosDuplicates(volumeId, {
132                 nameHashes: [hash],
133             }),
134             abortSignal
135         );
137         // If no name duplicates hash we don't check ContentHash duplicity
138         if (!DuplicateHashes.length) {
139             return {
140                 filename: file.name,
141                 hash,
142             };
143         }
145         // Force polyfill type for ReadableStream
146         const fileStream = file.stream() as ReadableStream<Uint8Array>;
147         const sha1Instance = sha1.create();
148         await untilStreamEnd<Uint8Array>(fileStream, async (chunk) => {
149             if (chunk?.buffer) {
150                 sha1Instance.update(new Uint8Array(chunk.buffer));
151             }
152         });
154         const sha1Hash = sha1Instance.digest();
156         const contentHash = await generateLookupHash(arrayToHexString(sha1Hash), parentHashKey);
158         const duplicatePhotoHashActive = DuplicateHashes.find(
159             (duplicatePhotosHash) =>
160                 duplicatePhotosHash.ContentHash === contentHash && duplicatePhotosHash.LinkState === LinkState.ACTIVE
161         );
162         if (duplicatePhotoHashActive) {
163             return {
164                 filename: file.name,
165                 hash,
166                 isDuplicatePhotos: true,
167             };
168         }
170         const duplicatePhotoHashDraft = DuplicateHashes.find(
171             (duplicatePhotosHash) => duplicatePhotosHash.LinkState === LinkState.DRAFT
172         );
173         if (duplicatePhotoHashDraft) {
174             return {
175                 filename: file.name,
176                 hash,
177                 draftLinkId: duplicatePhotoHashDraft.LinkID,
178                 clientUid: isClientUidAvailable(duplicatePhotoHashDraft.ClientUID)
179                     ? duplicatePhotoHashDraft.ClientUID
180                     : undefined,
181             };
182         }
184         return {
185             filename: file.name,
186             hash,
187         };
188     };
190     const getLinkByName = async (abortSignal: AbortSignal, shareId: string, parentLinkID: string, name: string) => {
191         await loadChildren(abortSignal, shareId, parentLinkID);
192         const { links } = getCachedChildren(abortSignal, shareId, parentLinkID);
193         return links?.find((link) => link.name === name);
194     };
196     const findHash = async (
197         abortSignal: AbortSignal,
198         { shareId, parentLinkId, filename }: { shareId: string; parentLinkId: string; filename: string }
199     ) => {
200         const parentHashKey = await getLinkHashKey(abortSignal, shareId, parentLinkId);
201         if (!parentHashKey) {
202             throw Error('Missing hash key on folder link');
203         }
205         const hash = await generateLookupHash(filename, parentHashKey);
207         return hash;
208     };
210     return {
211         findAvailableName,
212         findDuplicateContentHash,
213         findHash,
214         getLinkByName,
215     };