From 8b9e4e4245baa25955f3c1aa94e968ba20d3409a Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 9 Feb 2023 17:08:19 +0100 Subject: [PATCH] feat(web): allow uploading more file types (#1570) * feat(web): allow uploading more file types * fix(web): make filename extension lowercase --- .../upload-asset-preview.svelte | 69 +++++++++++++++ .../shared-components/upload-panel.svelte | 84 +------------------ web/src/lib/utils/asset-utils.ts | 35 ++++++++ web/src/lib/utils/file-uploader.ts | 39 ++++----- 4 files changed, 126 insertions(+), 101 deletions(-) create mode 100644 web/src/lib/components/shared-components/upload-asset-preview.svelte diff --git a/web/src/lib/components/shared-components/upload-asset-preview.svelte b/web/src/lib/components/shared-components/upload-asset-preview.svelte new file mode 100644 index 0000000000..21dfd304bf --- /dev/null +++ b/web/src/lib/components/shared-components/upload-asset-preview.svelte @@ -0,0 +1,69 @@ + + +
+
+ {#if showFallbackImage} + Immich Logo + {:else} + { + URL.revokeObjectURL(previewURL); + }} + on:error={() => { + URL.revokeObjectURL(previewURL); + showFallbackImage = true; + }} + src={previewURL} + alt="Preview of asset" + class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg" + draggable="false" + /> + {/if} + +
+

+ .{uploadAsset.fileExtension} +

+
+
+ +
+ + +
+
+

+ {uploadAsset.progress}/100 +

+
+
+
diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index 7e24627d8d..74753437bd 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -4,55 +4,20 @@ import { uploadAssetsStore } from '$lib/stores/upload'; import CloudUploadOutline from 'svelte-material-icons/CloudUploadOutline.svelte'; import WindowMinimize from 'svelte-material-icons/WindowMinimize.svelte'; - import type { UploadAsset } from '$lib/models/upload-asset'; import { notificationController, NotificationType } from './notification/notification'; - import { asByteUnitString } from '$lib/utils/byte-units'; + import UploadAssetPreview from './upload-asset-preview.svelte'; let showDetail = true; - let uploadLength = 0; + let isUploading = false; - const showUploadImageThumbnail = async (a: UploadAsset) => { - const extension = a.fileExtension.toLowerCase(); - - if (extension == 'jpeg' || extension == 'jpg' || extension == 'png') { - try { - const imgData = await a.file.arrayBuffer(); - const arrayBufferView = new Uint8Array(imgData); - const blob = new Blob([arrayBufferView], { type: 'image/jpeg' }); - const urlCreator = window.URL || window.webkitURL; - const imageUrl = urlCreator.createObjectURL(blob); - // TODO: There is probably a cleaner way of doing this - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const img: any = document.getElementById(`${a.id}`); - img.src = imageUrl; - } catch { - // Do nothing? - } - } - }; - - // Reactive action to get thumbnail image of upload asset whenever there is a new one added to the list + // Reactive action to update asset uploadLength whenever there is a new one added to the list $: { if ($uploadAssetsStore.length != uploadLength) { - $uploadAssetsStore.map((asset) => { - showUploadImageThumbnail(asset); - }); - uploadLength = $uploadAssetsStore.length; } } - $: { - if (showDetail) { - $uploadAssetsStore.map((asset) => { - showUploadImageThumbnail(asset); - }); - } - } - - let isUploading = false; - uploadAssetsStore.isUploading.subscribe((value) => { isUploading = value; }); @@ -88,48 +53,7 @@
{#each $uploadAssetsStore as uploadAsset} {#key uploadAsset.id} -
-
- - -
-

- .{uploadAsset.fileExtension} -

-
-
- -
- - -
-
-

- {uploadAsset.progress}/100 -

-
-
-
+ {/key} {/each}
diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index a245ea03cd..06642b0a0b 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -111,3 +111,38 @@ export async function bulkDownload( }); } } + +/** + * Returns the lowercase filename extension without a dot (.) and + * an empty string when not found. + */ +export function getFilenameExtension(filename: string): string { + const lastIndex = filename.lastIndexOf('.'); + return filename.slice(lastIndex + 1).toLowerCase(); +} + +/** + * Returns the MIME type of the file and an empty string when not found. + */ +export function getFileMimeType(file: File): string { + if (file.type !== '') { + // Return the MIME type determined by the browser. + return file.type; + } + + // Return MIME type based on the file extension. + switch (getFilenameExtension(file.name)) { + case 'heic': + return 'image/heic'; + case 'heif': + return 'image/heif'; + case 'dng': + return 'image/dng'; + case '3gp': + return 'video/3gpp'; + case 'nef': + return 'image/nef'; + default: + return ''; + } +} diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 5b93da91d5..cffcfa55b3 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -7,7 +7,7 @@ import * as exifr from 'exifr'; import { uploadAssetsStore } from '$lib/stores/upload'; import type { UploadAsset } from '../models/upload-asset'; import { api, AssetFileUploadResponseDto } from '@api'; -import { addAssetsToAlbum } from '$lib/utils/asset-utils'; +import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils'; export const openFileUploadDialog = ( albumId: string | undefined = undefined, @@ -19,7 +19,10 @@ export const openFileUploadDialog = ( fileSelector.type = 'file'; fileSelector.multiple = true; - fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef,.srw,.raf'; + + // When adding a content type that is unsupported by browsers, make sure + // to also add it to getFileMimeType() otherwise the upload will fail. + fileSelector.accept = 'image/*,video/*,.heic,.heif,.dng,.3gp,.nef'; fileSelector.onchange = async (e: Event) => { const target = e.target as HTMLInputElement; @@ -46,7 +49,7 @@ export const fileUploadHandler = async ( if (files.length > 50) { notificationController.show({ type: NotificationType.Error, - message: `Cannot upload more than 50 files at a time - you are uploading ${files.length} files. + message: `Cannot upload more than 50 files at a time - you are uploading ${files.length} files. Please check out the bulk upload documentation if you need to upload more than 50 files.`, timeout: 10000, action: { type: 'link', target: 'https://immich.app/docs/features/bulk-upload' } @@ -55,23 +58,16 @@ export const fileUploadHandler = async ( return; } - for (const asset of files) { + const acceptedFile = files.filter((file) => { + const assetType = getFileMimeType(file).split('/')[0]; + return assetType === 'video' || assetType === 'image'; + }); + + for (const asset of acceptedFile) { await fileUploader(asset, albumId, sharedKey, onDone); } }; -function getMimeType(file: File) { - const extension = file.name.split('.').pop() as string; - switch (extension.toLowerCase()) { - case 'raf': - return 'image/x-fuji-raf'; - case 'srw': - return 'image/x-samsung-srw'; - default: - return file.type; - } -} - //TODO: should probably use the @api SDK async function fileUploader( asset: File, @@ -79,10 +75,9 @@ async function fileUploader( sharedKey: string | undefined = undefined, onDone?: (id: string) => void ) { - const mimeType = getMimeType(asset); + const mimeType = getFileMimeType(asset); const assetType = mimeType.split('/')[0].toUpperCase(); - const temp = asset.name.split('.'); - const fileExtension = temp[temp.length - 1]; + const fileExtension = getFilenameExtension(asset.name); const formData = new FormData(); try { @@ -123,8 +118,10 @@ async function fileUploader( // Get asset file extension formData.append('fileExtension', '.' + fileExtension); - // Get asset binary data. - formData.append('assetData', asset); + // Get asset binary data with a custom MIME type, because browsers will + // use application/octet-stream for unsupported MIME types, leading to + // failed uploads. + formData.append('assetData', new File([asset], asset.name, { type: mimeType })); // Check if asset upload on server before performing upload const { data, status } = await api.assetApi.checkDuplicateAsset(