mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	feat(web): allow uploading more file types (#1570)
* feat(web): allow uploading more file types * fix(web): make filename extension lowercase
This commit is contained in:
		
							parent
							
								
									8c20d8cb3d
								
							
						
					
					
						commit
						adb265794c
					
				@ -0,0 +1,69 @@
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
	import { fade } from 'svelte/transition';
 | 
			
		||||
	import { asByteUnitString } from '$lib/utils/byte-units';
 | 
			
		||||
	import { UploadAsset } from '$lib/models/upload-asset';
 | 
			
		||||
 | 
			
		||||
	export let uploadAsset: UploadAsset;
 | 
			
		||||
 | 
			
		||||
	let showFallbackImage = false;
 | 
			
		||||
	const previewURL = URL.createObjectURL(uploadAsset.file);
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<div
 | 
			
		||||
	in:fade={{ duration: 250 }}
 | 
			
		||||
	out:fade={{ duration: 100 }}
 | 
			
		||||
	class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]"
 | 
			
		||||
>
 | 
			
		||||
	<div class="relative">
 | 
			
		||||
		{#if showFallbackImage}
 | 
			
		||||
			<img
 | 
			
		||||
				in:fade={{ duration: 250 }}
 | 
			
		||||
				src="immich-logo.svg"
 | 
			
		||||
				alt="Immich Logo"
 | 
			
		||||
				class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg"
 | 
			
		||||
				draggable="false"
 | 
			
		||||
			/>
 | 
			
		||||
		{:else}
 | 
			
		||||
			<img
 | 
			
		||||
				in:fade={{ duration: 250 }}
 | 
			
		||||
				on:load={() => {
 | 
			
		||||
					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}
 | 
			
		||||
 | 
			
		||||
		<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30">
 | 
			
		||||
			<p
 | 
			
		||||
				class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase"
 | 
			
		||||
			>
 | 
			
		||||
				.{uploadAsset.fileExtension}
 | 
			
		||||
			</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<div class="p-2 pr-4 flex flex-col justify-between">
 | 
			
		||||
		<input
 | 
			
		||||
			disabled
 | 
			
		||||
			class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
 | 
			
		||||
			value={`[${asByteUnitString(uploadAsset.file.size)}] ${uploadAsset.file.name}`}
 | 
			
		||||
		/>
 | 
			
		||||
 | 
			
		||||
		<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
 | 
			
		||||
			<div
 | 
			
		||||
				class="bg-immich-primary h-[15px] rounded-md transition-all"
 | 
			
		||||
				style={`width: ${uploadAsset.progress}%`}
 | 
			
		||||
			/>
 | 
			
		||||
			<p class="absolute h-full w-full text-center top-0 text-[10px] ">
 | 
			
		||||
				{uploadAsset.progress}/100
 | 
			
		||||
			</p>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
@ -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 @@
 | 
			
		||||
				<div class="max-h-[400px] overflow-y-auto pr-2 rounded-lg immich-scrollbar">
 | 
			
		||||
					{#each $uploadAssetsStore as uploadAsset}
 | 
			
		||||
						{#key uploadAsset.id}
 | 
			
		||||
							<div
 | 
			
		||||
								in:fade={{ duration: 250 }}
 | 
			
		||||
								out:fade={{ duration: 100 }}
 | 
			
		||||
								class="text-xs mt-3 rounded-lg bg-immich-bg grid grid-cols-[70px_auto] gap-2 h-[70px]"
 | 
			
		||||
							>
 | 
			
		||||
								<div class="relative">
 | 
			
		||||
									<img
 | 
			
		||||
										in:fade={{ duration: 250 }}
 | 
			
		||||
										id={`${uploadAsset.id}`}
 | 
			
		||||
										src="/immich-logo.svg"
 | 
			
		||||
										alt=""
 | 
			
		||||
										class="h-[70px] w-[70px] object-cover rounded-tl-lg rounded-bl-lg "
 | 
			
		||||
										draggable="false"
 | 
			
		||||
									/>
 | 
			
		||||
 | 
			
		||||
									<div class="bottom-0 left-0 absolute w-full h-[25px] bg-immich-primary/30">
 | 
			
		||||
										<p
 | 
			
		||||
											class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase"
 | 
			
		||||
										>
 | 
			
		||||
											.{uploadAsset.fileExtension}
 | 
			
		||||
										</p>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
 | 
			
		||||
								<div class="p-2 pr-4 flex flex-col justify-between">
 | 
			
		||||
									<input
 | 
			
		||||
										disabled
 | 
			
		||||
										class="bg-gray-100 border w-full p-1 rounded-md text-[10px] px-2"
 | 
			
		||||
										value={`[${asByteUnitString(uploadAsset.file.size)}] ${uploadAsset.file.name}`}
 | 
			
		||||
									/>
 | 
			
		||||
 | 
			
		||||
									<div class="w-full bg-gray-300 h-[15px] rounded-md mt-[5px] text-white relative">
 | 
			
		||||
										<div
 | 
			
		||||
											class="bg-immich-primary h-[15px] rounded-md transition-all"
 | 
			
		||||
											style={`width: ${uploadAsset.progress}%`}
 | 
			
		||||
										/>
 | 
			
		||||
										<p class="absolute h-full w-full text-center top-0 text-[10px] ">
 | 
			
		||||
											{uploadAsset.progress}/100
 | 
			
		||||
										</p>
 | 
			
		||||
									</div>
 | 
			
		||||
								</div>
 | 
			
		||||
							</div>
 | 
			
		||||
							<UploadAssetPreview {uploadAsset} />
 | 
			
		||||
						{/key}
 | 
			
		||||
					{/each}
 | 
			
		||||
				</div>
 | 
			
		||||
 | 
			
		||||
@ -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 '';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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(
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user