diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0fb05f0bb8..888c4f28da 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -737,6 +737,9 @@ importers:
happy-dom:
specifier: ^18.0.1
version: 18.0.1
+ hash-wasm:
+ specifier: ^4.12.0
+ version: 4.12.0
intl-messageformat:
specifier: ^10.7.11
version: 10.7.16
@@ -6762,6 +6765,9 @@ packages:
resolution: {integrity: sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ hash-wasm@4.12.0:
+ resolution: {integrity: sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==}
+
hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
@@ -18623,6 +18629,8 @@ snapshots:
has-yarn@3.0.0: {}
+ hash-wasm@4.12.0: {}
+
hasown@2.0.2:
dependencies:
function-bind: 1.1.2
diff --git a/web/package.json b/web/package.json
index 86574fde4f..e5ab08ca30 100644
--- a/web/package.json
+++ b/web/package.json
@@ -46,6 +46,7 @@
"geojson": "^0.5.0",
"handlebars": "^4.7.8",
"happy-dom": "^18.0.1",
+ "hash-wasm": "^4.12.0",
"intl-messageformat": "^10.7.11",
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
diff --git a/web/src/lib/components/shared-components/upload-asset-preview.svelte b/web/src/lib/components/shared-components/upload-asset-preview.svelte
index 985400a033..1e186b3285 100644
--- a/web/src/lib/components/shared-components/upload-asset-preview.svelte
+++ b/web/src/lib/components/shared-components/upload-asset-preview.svelte
@@ -92,8 +92,8 @@
{#if uploadAsset.state === UploadState.STARTED}
-
-
+
+
{#if uploadAsset.message}
{uploadAsset.message}
{:else}
diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts
index c572ec1760..4e97ad6123 100644
--- a/web/src/lib/utils/file-uploader.ts
+++ b/web/src/lib/utils/file-uploader.ts
@@ -7,6 +7,7 @@ import { uploadRequest } from '$lib/utils';
import { addAssetsToAlbum } from '$lib/utils/asset-utils';
import { ExecutorQueue } from '$lib/utils/executor-queue';
import { asQueryString } from '$lib/utils/shared-links';
+import { hashFile } from '$lib/utils/sw-messaging';
import {
Action,
AssetMediaStatus,
@@ -154,16 +155,16 @@ async function fileUploader({
}
let responseData: { id: string; status: AssetMediaStatus; isTrashed?: boolean } | undefined;
- if (crypto?.subtle?.digest && !authManager.isSharedLink) {
+ if (!authManager.isSharedLink) {
uploadAssetsStore.updateItem(deviceAssetId, { message: $t('asset_hashing') });
await tick();
try {
- const bytes = await assetFile.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('');
-
+ const checksum = await hashFile(assetFile, {
+ id: deviceAssetId,
+ onProgress: (progress, total) => {
+ uploadAssetsStore.updateProgress(deviceAssetId, progress, total);
+ },
+ });
const {
results: [checkUploadResult],
} = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: [{ id: assetFile.name, checksum }] } });
diff --git a/web/src/lib/utils/sw-messaging.ts b/web/src/lib/utils/sw-messaging.ts
index 1a19d3c134..c8fde6efe7 100644
--- a/web/src/lib/utils/sw-messaging.ts
+++ b/web/src/lib/utils/sw-messaging.ts
@@ -1,8 +1,46 @@
+type OnProgress = (progress: number, total: number) => void;
+type Callback = { onChecksum: (checksum: string) => void; onProgress: OnProgress };
+
+const callbacks: Record = {};
const broadcast = new BroadcastChannel('immich');
-export function cancelImageUrl(url: string) {
+broadcast.addEventListener('message', (event) => {
+ const { type, id, checksum, progress, total } = event.data;
+
+ switch (type) {
+ case 'checksum': {
+ if (id && checksum) {
+ const callback = callbacks[id];
+ callback?.onChecksum(checksum);
+ delete callbacks[id];
+ }
+ break;
+ }
+
+ case 'hash.progress': {
+ if (id && progress && total) {
+ const callback = callbacks[id];
+ callback?.onProgress(progress, total);
+ }
+ break;
+ }
+ }
+});
+
+export const cancelImageUrl = (url: string) => {
broadcast.postMessage({ type: 'cancel', url });
-}
-export function preloadImageUrl(url: string) {
+};
+export const preloadImageUrl = (url: string) => {
broadcast.postMessage({ type: 'preload', url });
-}
+};
+
+export const hashFile = (file: File, { id, onProgress }: { id: string; onProgress: OnProgress }): Promise => {
+ return new Promise((onChecksum) => {
+ if (callbacks[id]) {
+ return;
+ }
+
+ callbacks[id] = { onChecksum, onProgress };
+ broadcast.postMessage({ type: 'hash', id, file });
+ });
+};
diff --git a/web/src/service-worker/broadcast-channel.ts b/web/src/service-worker/broadcast-channel.ts
index ae6f1e1be6..dfd10f0197 100644
--- a/web/src/service-worker/broadcast-channel.ts
+++ b/web/src/service-worker/broadcast-channel.ts
@@ -1,25 +1,68 @@
+import { createSHA1, sha1 } from 'hash-wasm';
import { handleCancel, handlePreload } from './request';
-export const installBroadcastChannelListener = () => {
- const broadcast = new BroadcastChannel('immich');
- // eslint-disable-next-line unicorn/prefer-add-event-listener
- broadcast.onmessage = (event) => {
- if (!event.data) {
- return;
+type HashRequest = { id: string; file: File };
+
+const MAX_HASH_FILE_SIZE = 100 * 1024 * 1024; // 100 MiB
+
+const broadcast = new BroadcastChannel('immich');
+
+broadcast.addEventListener('message', async (event) => {
+ if (!event.data) {
+ return;
+ }
+
+ const url = new URL(event.data.url, event.origin);
+
+ switch (event.data.type) {
+ case 'preload': {
+ handlePreload(url);
+ break;
}
- const url = new URL(event.data.url, event.origin);
-
- switch (event.data.type) {
- case 'preload': {
- handlePreload(url);
- break;
- }
-
- case 'cancel': {
- handleCancel(url);
- break;
- }
+ case 'cancel': {
+ handleCancel(url);
+ break;
}
- };
+
+ case 'hash': {
+ await handleHash(event.data);
+ }
+ }
+});
+
+const handleHash = async (request: HashRequest) => {
+ const { id, file } = request;
+ const checksum = file.size <= MAX_HASH_FILE_SIZE ? await hashSmallFile(request) : await hashLargeFile(request);
+ broadcast.postMessage({ type: 'checksum', id, checksum });
+};
+
+const hashSmallFile = async ({ file }: HashRequest): Promise => {
+ const buffer = await file.arrayBuffer();
+ return sha1(new Uint8Array(buffer));
+};
+
+const hashLargeFile = async ({ id, file }: HashRequest): Promise => {
+ const sha1 = await createSHA1();
+ const reader = file.stream().getReader();
+ let processedBytes = 0;
+ let lastUpdate = Date.now();
+
+ while (true) {
+ const { done, value } = await reader.read();
+ if (done) {
+ break;
+ }
+ sha1.update(value);
+ processedBytes += value.length;
+
+ broadcast.postMessage({
+ type: 'hash.progress',
+ id,
+ progress: processedBytes,
+ total: file.size,
+ });
+ }
+
+ return sha1.digest('hex');
};
diff --git a/web/src/service-worker/index.ts b/web/src/service-worker/index.ts
index 28336aca6a..a0c62deaa7 100644
--- a/web/src/service-worker/index.ts
+++ b/web/src/service-worker/index.ts
@@ -2,7 +2,7 @@
///
///
///
-import { installBroadcastChannelListener } from './broadcast-channel';
+import './broadcast-channel';
import { prune } from './cache';
import { handleRequest } from './request';
@@ -36,4 +36,3 @@ const handleFetch = (event: FetchEvent): void => {
sw.addEventListener('install', handleInstall, { passive: true });
sw.addEventListener('activate', handleActivate, { passive: true });
sw.addEventListener('fetch', handleFetch, { passive: true });
-installBroadcastChannelListener();