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,
31 suppressErrors = false,
32 }: { shareId: string; parentLinkId: string; filename: string; suppressErrors?: boolean }
34 const parentHashKey = await getLinkHashKey(abortSignal, shareId, parentLinkId);
36 throw Error('Missing hash key on folder link');
39 const [namePart, extension] = splitLinkName(filename);
40 const hash = await generateLookupHash(filename, parentHashKey);
42 const findAdjustedName = async (
50 const hashesToCheck = await Promise.all(
51 range(start, start + HASH_CHECK_AMOUNT).map(async (i) => {
58 const adjustedFileName = adjustName(i, namePart, extension);
60 filename: adjustedFileName,
61 hash: await generateLookupHash(adjustedFileName, parentHashKey),
66 const Hashes = hashesToCheck.map(({ hash }) => hash);
67 const { AvailableHashes, PendingHashes } = await debouncedRequest<HashCheckResult>(
68 queryCheckAvailableHashes(shareId, parentLinkId, { Hashes }, suppressErrors),
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);
80 draftLinkId: pendingAvailableHashes[0].LinkID,
81 clientUid: pendingAvailableHashes[0].ClientUID,
86 if (!AvailableHashes.length) {
87 return findAdjustedName(start + HASH_CHECK_AMOUNT);
89 const availableName = hashesToCheck.find(({ hash }) => hash === AvailableHashes[0]);
92 throw new Error('Backend returned unexpected hash');
95 const draftHashes = PendingHashes.filter(({ ClientUID }) => !isClientUidAvailable(ClientUID));
96 const draftLinkId = draftHashes.find(({ Hash }) => Hash === hash)?.LinkID;
103 return findAdjustedName();
107 * Checks if there is a Photos file with the same Hash and ContentHash
109 const findDuplicateContentHash = async (
110 abortSignal: AbortSignal,
116 }: { file: File; volumeId: string; shareId: string; parentLinkId: string }
120 draftLinkId?: string;
122 isDuplicatePhotos?: boolean;
124 const parentHashKey = await getLinkHashKey(abortSignal, shareId, parentLinkId);
125 if (!parentHashKey) {
126 throw Error('Missing hash key on folder link');
128 const hash = await generateLookupHash(file.name, parentHashKey);
130 const { DuplicateHashes } = await debouncedRequest<{ DuplicateHashes: DuplicatePhotosHash[] }>(
131 queryPhotosDuplicates(volumeId, {
137 // If no name duplicates hash we don't check ContentHash duplicity
138 if (!DuplicateHashes.length) {
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) => {
150 sha1Instance.update(new Uint8Array(chunk.buffer));
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
162 if (duplicatePhotoHashActive) {
166 isDuplicatePhotos: true,
170 const duplicatePhotoHashDraft = DuplicateHashes.find(
171 (duplicatePhotosHash) => duplicatePhotosHash.LinkState === LinkState.DRAFT
173 if (duplicatePhotoHashDraft) {
177 draftLinkId: duplicatePhotoHashDraft.LinkID,
178 clientUid: isClientUidAvailable(duplicatePhotoHashDraft.ClientUID)
179 ? duplicatePhotoHashDraft.ClientUID
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);
196 const findHash = async (
197 abortSignal: AbortSignal,
198 { shareId, parentLinkId, filename }: { shareId: string; parentLinkId: string; filename: string }
200 const parentHashKey = await getLinkHashKey(abortSignal, shareId, parentLinkId);
201 if (!parentHashKey) {
202 throw Error('Missing hash key on folder link');
205 const hash = await generateLookupHash(filename, parentHashKey);
212 findDuplicateContentHash,