mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(web): determine duplication of upload on client (#8825)
* web upload duplicate verification on client * _ * fix formating * chore: clean up --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							parent
							
								
									7961d00e56
								
							
						
					
					
						commit
						ec4e6a143e
					
				@ -1,4 +1,4 @@
 | 
				
			|||||||
import { derived, writable } from 'svelte/store';
 | 
					import { derived, get, writable } from 'svelte/store';
 | 
				
			||||||
import { UploadState, type UploadAsset } from '../models/upload-asset';
 | 
					import { UploadState, type UploadAsset } from '../models/upload-asset';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function createUploadStore() {
 | 
					function createUploadStore() {
 | 
				
			||||||
@ -22,6 +22,11 @@ function createUploadStore() {
 | 
				
			|||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const addNewUploadAsset = (newAsset: UploadAsset) => {
 | 
					  const addNewUploadAsset = (newAsset: UploadAsset) => {
 | 
				
			||||||
 | 
					    const assets = get(uploadAssets);
 | 
				
			||||||
 | 
					    const duplicate = assets.find((asset) => asset.id === newAsset.id);
 | 
				
			||||||
 | 
					    if (duplicate) {
 | 
				
			||||||
 | 
					      uploadAssets.update((assets) => assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset)));
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
      totalUploadCounter.update((c) => c + 1);
 | 
					      totalUploadCounter.update((c) => c + 1);
 | 
				
			||||||
      uploadAssets.update((assets) => [
 | 
					      uploadAssets.update((assets) => [
 | 
				
			||||||
        ...assets,
 | 
					        ...assets,
 | 
				
			||||||
@ -33,6 +38,7 @@ function createUploadStore() {
 | 
				
			|||||||
          eta: 0,
 | 
					          eta: 0,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const updateProgress = (id: string, loaded: number, total: number) => {
 | 
					  const updateProgress = (id: string, loaded: number, total: number) => {
 | 
				
			||||||
 | 
				
			|||||||
@ -3,7 +3,14 @@ import { uploadAssetsStore } from '$lib/stores/upload';
 | 
				
			|||||||
import { getKey, uploadRequest } from '$lib/utils';
 | 
					import { getKey, uploadRequest } from '$lib/utils';
 | 
				
			||||||
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
 | 
					import { addAssetsToAlbum } from '$lib/utils/asset-utils';
 | 
				
			||||||
import { ExecutorQueue } from '$lib/utils/executor-queue';
 | 
					import { ExecutorQueue } from '$lib/utils/executor-queue';
 | 
				
			||||||
import { defaults, getSupportedMediaTypes, type AssetFileUploadResponseDto } from '@immich/sdk';
 | 
					import {
 | 
				
			||||||
 | 
					  Action,
 | 
				
			||||||
 | 
					  checkBulkUpload,
 | 
				
			||||||
 | 
					  defaults,
 | 
				
			||||||
 | 
					  getSupportedMediaTypes,
 | 
				
			||||||
 | 
					  type AssetFileUploadResponseDto,
 | 
				
			||||||
 | 
					} from '@immich/sdk';
 | 
				
			||||||
 | 
					import { tick } from 'svelte';
 | 
				
			||||||
import { getServerErrorMessage, handleError } from './handle-error';
 | 
					import { getServerErrorMessage, handleError } from './handle-error';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let _extensions: string[];
 | 
					let _extensions: string[];
 | 
				
			||||||
@ -70,8 +77,9 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
 | 
				
			|||||||
  const fileCreatedAt = new Date(asset.lastModified).toISOString();
 | 
					  const fileCreatedAt = new Date(asset.lastModified).toISOString();
 | 
				
			||||||
  const deviceAssetId = getDeviceAssetId(asset);
 | 
					  const deviceAssetId = getDeviceAssetId(asset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return new Promise((resolve) => resolve(uploadAssetsStore.markStarted(deviceAssetId)))
 | 
					  uploadAssetsStore.markStarted(deviceAssetId);
 | 
				
			||||||
    .then(() => {
 | 
					
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
    const formData = new FormData();
 | 
					    const formData = new FormData();
 | 
				
			||||||
    for (const [key, value] of Object.entries({
 | 
					    for (const [key, value] of Object.entries({
 | 
				
			||||||
      deviceAssetId,
 | 
					      deviceAssetId,
 | 
				
			||||||
@ -85,45 +93,66 @@ async function fileUploader(asset: File, albumId: string | undefined = undefined
 | 
				
			|||||||
      formData.append(key, value);
 | 
					      formData.append(key, value);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let responseData: AssetFileUploadResponseDto | undefined;
 | 
				
			||||||
    const key = getKey();
 | 
					    const key = getKey();
 | 
				
			||||||
 | 
					    if (crypto?.subtle?.digest && !key) {
 | 
				
			||||||
 | 
					      uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Hashing...' });
 | 
				
			||||||
 | 
					      await tick();
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        const bytes = await asset.arrayBuffer();
 | 
				
			||||||
 | 
					        const hash = await crypto.subtle.digest('SHA-1', bytes);
 | 
				
			||||||
 | 
					        const checksum = Array.from(new Uint8Array(hash))
 | 
				
			||||||
 | 
					          .map((b) => b.toString(16).padStart(2, '0'))
 | 
				
			||||||
 | 
					          .join('');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return uploadRequest<AssetFileUploadResponseDto>({
 | 
					        const {
 | 
				
			||||||
 | 
					          results: [checkUploadResult],
 | 
				
			||||||
 | 
					        } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: [{ id: asset.name, checksum }] } });
 | 
				
			||||||
 | 
					        if (checkUploadResult.action === Action.Reject && checkUploadResult.assetId) {
 | 
				
			||||||
 | 
					          responseData = { duplicate: true, id: checkUploadResult.assetId };
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      } catch (error) {
 | 
				
			||||||
 | 
					        console.error(`Error calculating sha1 file=${asset.name})`, error);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!responseData) {
 | 
				
			||||||
 | 
					      uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Uploading...' });
 | 
				
			||||||
 | 
					      const response = await uploadRequest<AssetFileUploadResponseDto>({
 | 
				
			||||||
        url: defaults.baseUrl + '/asset/upload' + (key ? `?key=${key}` : ''),
 | 
					        url: defaults.baseUrl + '/asset/upload' + (key ? `?key=${key}` : ''),
 | 
				
			||||||
        data: formData,
 | 
					        data: formData,
 | 
				
			||||||
        onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
 | 
					        onUploadProgress: (event) => uploadAssetsStore.updateProgress(deviceAssetId, event.loaded, event.total),
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    })
 | 
					      if (![200, 201].includes(response.status)) {
 | 
				
			||||||
    .then(async (response) => {
 | 
					        throw new Error('Failed to upload file');
 | 
				
			||||||
      if (response.status == 200 || response.status == 201) {
 | 
					      }
 | 
				
			||||||
        const res: AssetFileUploadResponseDto = response.data;
 | 
					      responseData = response.data;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    const { duplicate, id: assetId } = responseData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (res.duplicate) {
 | 
					    if (duplicate) {
 | 
				
			||||||
      uploadAssetsStore.duplicateCounter.update((count) => count + 1);
 | 
					      uploadAssetsStore.duplicateCounter.update((count) => count + 1);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      uploadAssetsStore.successCounter.update((c) => c + 1);
 | 
					      uploadAssetsStore.successCounter.update((c) => c + 1);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (albumId && res.id) {
 | 
					    if (albumId && assetId) {
 | 
				
			||||||
      uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Adding to album...' });
 | 
					      uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Adding to album...' });
 | 
				
			||||||
          await addAssetsToAlbum(albumId, [res.id]);
 | 
					      await addAssetsToAlbum(albumId, [assetId]);
 | 
				
			||||||
      uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Added to album' });
 | 
					      uploadAssetsStore.updateAsset(deviceAssetId, { message: 'Added to album' });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        uploadAssetsStore.updateAsset(deviceAssetId, {
 | 
					    uploadAssetsStore.updateAsset(deviceAssetId, { state: duplicate ? UploadState.DUPLICATED : UploadState.DONE });
 | 
				
			||||||
          state: res.duplicate ? UploadState.DUPLICATED : UploadState.DONE,
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    setTimeout(() => {
 | 
					    setTimeout(() => {
 | 
				
			||||||
      uploadAssetsStore.removeUploadAsset(deviceAssetId);
 | 
					      uploadAssetsStore.removeUploadAsset(deviceAssetId);
 | 
				
			||||||
    }, 1000);
 | 
					    }, 1000);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return res.id;
 | 
					    return assetId;
 | 
				
			||||||
      }
 | 
					  } catch (error) {
 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .catch((error) => {
 | 
					 | 
				
			||||||
    handleError(error, 'Unable to upload file');
 | 
					    handleError(error, 'Unable to upload file');
 | 
				
			||||||
    const reason = getServerErrorMessage(error) || error;
 | 
					    const reason = getServerErrorMessage(error) || error;
 | 
				
			||||||
    uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason });
 | 
					    uploadAssetsStore.updateAsset(deviceAssetId, { state: UploadState.ERROR, error: reason });
 | 
				
			||||||
      return undefined;
 | 
					    return;
 | 
				
			||||||
    });
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user