-
+ {#each Object.keys($downloadAssets) as downloadKey (downloadKey)}
+ {@const download = $downloadAssets[downloadKey]}
+
+
+
+
■ {downloadKey}
+ {#if download.total}
+
{asByteUnitString(download.total, $locale)}
+ {/if}
+
+
+
+ {download.percentage}%
+
+
+
+
+ abort(downloadKey, download)} size="20" logo={Close} forceDark />
{/each}
diff --git a/web/src/lib/components/photos-page/actions/download-action.svelte b/web/src/lib/components/photos-page/actions/download-action.svelte
index 43b3a9ae42..e1fb24c085 100644
--- a/web/src/lib/components/photos-page/actions/download-action.svelte
+++ b/web/src/lib/components/photos-page/actions/download-action.svelte
@@ -14,12 +14,13 @@
const handleDownloadFiles = async () => {
const assets = Array.from(getAssets());
if (assets.length === 1) {
- await downloadFile(assets[0], sharedLinkKey);
clearSelect();
+ await downloadFile(assets[0], sharedLinkKey);
return;
}
- await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, clearSelect, sharedLinkKey);
+ clearSelect();
+ await downloadArchive(filename, { assetIds: assets.map((asset) => asset.id) }, sharedLinkKey);
};
diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte
index aa16f1b458..0f883a4a8f 100644
--- a/web/src/lib/components/share-page/individual-shared-viewer.svelte
+++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte
@@ -35,12 +35,7 @@
});
const downloadAssets = async () => {
- await downloadArchive(
- `immich-shared.zip`,
- { assetIds: assets.map((asset) => asset.id) },
- undefined,
- sharedLink.key,
- );
+ await downloadArchive(`immich-shared.zip`, { assetIds: assets.map((asset) => asset.id) }, sharedLink.key);
};
const handleUploadAssets = async (files: File[] = []) => {
diff --git a/web/src/lib/stores/download.ts b/web/src/lib/stores/download.ts
index a7a9f81c07..7dd13b18cf 100644
--- a/web/src/lib/stores/download.ts
+++ b/web/src/lib/stores/download.ts
@@ -1,6 +1,13 @@
import { derived, writable } from 'svelte/store';
-export const downloadAssets = writable
>({});
+export interface DownloadProgress {
+ progress: number;
+ total: number;
+ percentage: number;
+ abort: AbortController | null;
+}
+
+export const downloadAssets = writable>({});
export const isDownloading = derived(downloadAssets, ($downloadAssets) => {
if (Object.keys($downloadAssets).length == 0) {
@@ -10,17 +17,35 @@ export const isDownloading = derived(downloadAssets, ($downloadAssets) => {
return true;
});
-const update = (key: string, value: number | null) => {
+const update = (key: string, value: Partial | null) => {
downloadAssets.update((state) => {
const newState = { ...state };
+
if (value === null) {
delete newState[key];
- } else {
- newState[key] = value;
+ return newState;
}
+
+ if (!newState[key]) {
+ newState[key] = { progress: 0, total: 0, percentage: 0, abort: null };
+ }
+
+ const item = newState[key];
+ Object.assign(item, value);
+ item.percentage = Math.min(Math.floor((item.progress / item.total) * 100), 100);
+
return newState;
});
};
-export const clearDownload = (key: string) => update(key, null);
-export const updateDownload = (key: string, value: number) => update(key, value);
+export const downloadManager = {
+ add: (key: string, total: number, abort?: AbortController) => update(key, { total, abort }),
+ clear: (key: string) => update(key, null),
+ update: (key: string, progress: number, total?: number) => {
+ const download: Partial = { progress };
+ if (total !== undefined) {
+ download.total = total;
+ }
+ update(key, download);
+ },
+};
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts
index d420d9f57e..55169acf1f 100644
--- a/web/src/lib/utils/asset-utils.ts
+++ b/web/src/lib/utils/asset-utils.ts
@@ -1,5 +1,5 @@
import { notificationController, NotificationType } from '$lib/components/shared-components/notification/notification';
-import { clearDownload, updateDownload } from '$lib/stores/download';
+import { downloadManager } from '$lib/stores/download';
import { AddAssetsResponseDto, api, AssetApiGetDownloadInfoRequest, AssetResponseDto, DownloadResponseDto } from '@api';
import { handleError } from './handle-error';
@@ -37,7 +37,6 @@ const downloadBlob = (data: Blob, filename: string) => {
export const downloadArchive = async (
fileName: string,
options: Omit,
- onDone?: () => void,
key?: string,
) => {
let downloadInfo: DownloadResponseDto | null = null;
@@ -58,65 +57,77 @@ export const downloadArchive = async (
const suffix = downloadInfo.archives.length === 1 ? '' : `+${i + 1}`;
const archiveName = fileName.replace('.zip', `${suffix}.zip`);
- let downloadKey = `${archiveName}`;
+ let downloadKey = `${archiveName} `;
if (downloadInfo.archives.length > 1) {
downloadKey = `${archiveName} (${i + 1}/${downloadInfo.archives.length})`;
}
- updateDownload(downloadKey, 0);
+ const abort = new AbortController();
+ downloadManager.add(downloadKey, archive.size, abort);
try {
const { data } = await api.assetApi.downloadArchive(
{ assetIdsDto: { assetIds: archive.assetIds }, key },
{
responseType: 'blob',
- onDownloadProgress: (event) => updateDownload(downloadKey, Math.floor((event.loaded / archive.size) * 100)),
+ signal: abort.signal,
+ onDownloadProgress: (event) => downloadManager.update(downloadKey, event.loaded),
},
);
downloadBlob(data, archiveName);
} catch (e) {
handleError(e, 'Unable to download files');
- clearDownload(downloadKey);
+ downloadManager.clear(downloadKey);
return;
} finally {
- setTimeout(() => clearDownload(downloadKey), 3_000);
+ setTimeout(() => downloadManager.clear(downloadKey), 5_000);
}
}
-
- onDone?.();
};
export const downloadFile = async (asset: AssetResponseDto, key?: string) => {
- const assets = [{ filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`, id: asset.id }];
+ const assets = [
+ {
+ filename: `${asset.originalFileName}.${getFilenameExtension(asset.originalPath)}`,
+ id: asset.id,
+ size: asset.exifInfo?.fileSizeInByte || 0,
+ },
+ ];
if (asset.livePhotoVideoId) {
assets.push({
filename: `${asset.originalFileName}.mov`,
id: asset.livePhotoVideoId,
+ size: 0,
});
}
- for (const asset of assets) {
+ for (const { filename, id, size } of assets) {
+ const downloadKey = filename;
+
try {
- updateDownload(asset.filename, 0);
+ const abort = new AbortController();
+ downloadManager.add(downloadKey, size, abort);
const { data } = await api.assetApi.downloadFile(
- { id: asset.id, key },
+ { id, key },
{
responseType: 'blob',
onDownloadProgress: (event: ProgressEvent) => {
if (event.lengthComputable) {
- updateDownload(asset.filename, Math.floor((event.loaded / event.total) * 100));
+ downloadManager.update(downloadKey, event.loaded, event.total);
}
},
+ signal: abort.signal,
},
);
- downloadBlob(data, asset.filename);
+ downloadBlob(data, filename);
} catch (e) {
- handleError(e, `Error downloading ${asset.filename}`);
+ handleError(e, `Error downloading ${filename}`);
+ downloadManager.clear(downloadKey);
} finally {
- setTimeout(() => clearDownload(asset.filename), 3_000);
+ setTimeout(() => downloadManager.clear(downloadKey), 5_000);
}
}
};
diff --git a/web/src/lib/utils/handle-error.ts b/web/src/lib/utils/handle-error.ts
index 3c9c0e1c93..32a73f03d0 100644
--- a/web/src/lib/utils/handle-error.ts
+++ b/web/src/lib/utils/handle-error.ts
@@ -1,7 +1,12 @@
import type { ApiError } from '@api';
+import { CanceledError } from 'axios';
import { notificationController, NotificationType } from '../components/shared-components/notification/notification';
export async function handleError(error: unknown, message: string) {
+ if (error instanceof CanceledError) {
+ return;
+ }
+
console.error(`[handleError]: ${message}`, error);
let data = (error as ApiError)?.response?.data;