From adb265794cc557114639e801c4f18c6023f3fa24 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 | 24 ++++-- 4 files changed, 123 insertions(+), 89 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 00523df27a..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,6 +19,9 @@ export const openFileUploadDialog = ( fileSelector.type = 'file'; fileSelector.multiple = true; + + // 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) => { @@ -55,9 +58,10 @@ export const fileUploadHandler = async ( return; } - const acceptedFile = files.filter( - (e) => e.type.split('/')[0] === 'video' || e.type.split('/')[0] === 'image' - ); + 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); @@ -71,9 +75,9 @@ async function fileUploader( sharedKey: string | undefined = undefined, onDone?: (id: string) => void ) { - const assetType = asset.type.split('/')[0].toUpperCase(); - const temp = asset.name.split('.'); - const fileExtension = temp[temp.length - 1]; + const mimeType = getFileMimeType(asset); + const assetType = mimeType.split('/')[0].toUpperCase(); + const fileExtension = getFilenameExtension(asset.name); const formData = new FormData(); try { @@ -114,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(