refactor(server): calculate asset type server side (#3200)

* refactor(server): calculate asset type server-side

* chore: open api

* chore: remove comments

* fix: linting

* update

* Revert "update"

This reverts commit dc58702923250b9385d22468a7afe77dc9972a03.

* fix: upload LivePhotos

* chore: remove unused request fields for upload

* remove unused method

* mobile-fix: livePhoto filename

* fix: revert check for livephotos filename and extension

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Jason Rasmussen
2023-07-11 23:56:30 -04:00
committed by GitHub
parent 93462aafbc
commit b71d7e33bb
24 changed files with 97 additions and 580 deletions

View File

@@ -1375,12 +1375,6 @@ export interface GetAssetCountByTimeBucketDto {
* @interface ImportAssetDto
*/
export interface ImportAssetDto {
/**
*
* @type {AssetTypeEnum}
* @memberof ImportAssetDto
*/
'assetType': AssetTypeEnum;
/**
*
* @type {boolean}
@@ -1448,8 +1442,6 @@ export interface ImportAssetDto {
*/
'duration'?: string;
}
/**
*
* @export
@@ -5699,9 +5691,7 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
},
/**
*
* @param {AssetTypeEnum} assetType
* @param {File} assetData
* @param {string} fileExtension
* @param {string} deviceAssetId
* @param {string} deviceId
* @param {string} fileCreatedAt
@@ -5717,13 +5707,9 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
uploadFile: async (assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetType' is not null or undefined
assertParamExists('uploadFile', 'assetType', assetType)
uploadFile: async (assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
// verify required parameter 'assetData' is not null or undefined
assertParamExists('uploadFile', 'assetData', assetData)
// verify required parameter 'fileExtension' is not null or undefined
assertParamExists('uploadFile', 'fileExtension', fileExtension)
// verify required parameter 'deviceAssetId' is not null or undefined
assertParamExists('uploadFile', 'deviceAssetId', deviceAssetId)
// verify required parameter 'deviceId' is not null or undefined
@@ -5761,10 +5747,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
}
if (assetType !== undefined) {
localVarFormParams.append('assetType', new Blob([JSON.stringify(assetType)], { type: "application/json", }));
}
if (assetData !== undefined) {
localVarFormParams.append('assetData', assetData as any);
}
@@ -5781,10 +5763,6 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
localVarFormParams.append('isReadOnly', isReadOnly as any);
}
if (fileExtension !== undefined) {
localVarFormParams.append('fileExtension', fileExtension as any);
}
if (deviceAssetId !== undefined) {
localVarFormParams.append('deviceAssetId', deviceAssetId as any);
}
@@ -6098,9 +6076,7 @@ export const AssetApiFp = function(configuration?: Configuration) {
},
/**
*
* @param {AssetTypeEnum} assetType
* @param {File} assetData
* @param {string} fileExtension
* @param {string} deviceAssetId
* @param {string} deviceId
* @param {string} fileCreatedAt
@@ -6116,8 +6092,8 @@ export const AssetApiFp = function(configuration?: Configuration) {
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options);
async uploadFile(assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
}
@@ -6364,9 +6340,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
},
/**
*
* @param {AssetTypeEnum} assetType
* @param {File} assetData
* @param {string} fileExtension
* @param {string} deviceAssetId
* @param {string} deviceId
* @param {string} fileCreatedAt
@@ -6382,8 +6356,8 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
uploadFile(assetType: AssetTypeEnum, assetData: File, fileExtension: string, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
return localVarFp.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options).then((request) => request(axios, basePath));
uploadFile(assetData: File, deviceAssetId: string, deviceId: string, fileCreatedAt: string, fileModifiedAt: string, isFavorite: boolean, key?: string, livePhotoData?: File, sidecarData?: File, isReadOnly?: boolean, isArchived?: boolean, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
return localVarFp.uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration, options).then((request) => request(axios, basePath));
},
};
};
@@ -6807,13 +6781,6 @@ export interface AssetApiUpdateAssetRequest {
* @interface AssetApiUploadFileRequest
*/
export interface AssetApiUploadFileRequest {
/**
*
* @type {AssetTypeEnum}
* @memberof AssetApiUploadFile
*/
readonly assetType: AssetTypeEnum
/**
*
* @type {File}
@@ -6821,13 +6788,6 @@ export interface AssetApiUploadFileRequest {
*/
readonly assetData: File
/**
*
* @type {string}
* @memberof AssetApiUploadFile
*/
readonly fileExtension: string
/**
*
* @type {string}
@@ -7187,7 +7147,7 @@ export class AssetApi extends BaseAPI {
* @memberof AssetApi
*/
public uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig) {
return AssetApiFp(this.configuration).uploadFile(requestParameters.assetType, requestParameters.assetData, requestParameters.fileExtension, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isReadOnly, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath));
return AssetApiFp(this.configuration).uploadFile(requestParameters.assetData, requestParameters.deviceAssetId, requestParameters.deviceId, requestParameters.fileCreatedAt, requestParameters.fileModifiedAt, requestParameters.isFavorite, requestParameters.key, requestParameters.livePhotoData, requestParameters.sidecarData, requestParameters.isReadOnly, requestParameters.isArchived, requestParameters.isVisible, requestParameters.duration, options).then((request) => request(this.axios, this.basePath));
}
}

View File

@@ -4,6 +4,7 @@
import { asByteUnitString } from '$lib/utils/byte-units';
import { fade } from 'svelte/transition';
import ImmichLogo from './immich-logo.svelte';
import { getFilenameExtension } from '../../utils/asset-utils';
export let uploadAsset: UploadAsset;
@@ -42,7 +43,7 @@
<p
class="absolute bottom-1 right-1 object-right-bottom text-gray-50/95 font-semibold stroke-immich-primary uppercase"
>
.{uploadAsset.fileExtension}
.{getFilenameExtension(uploadAsset.file.name)}
</p>
</div>
</div>

View File

@@ -2,5 +2,4 @@ export type UploadAsset = {
id: string;
file: File;
progress: number;
fileExtension: string;
};

View File

@@ -1,6 +1,6 @@
import type { AssetResponseDto } from '@api';
import { describe, expect, it } from '@jest/globals';
import { getAssetFilename, getFileMimeType, getFilenameExtension } from './asset-utils';
import { getAssetFilename, getFilenameExtension } from './asset-utils';
describe('get file extension from filename', () => {
it('returns the extension without including the dot', () => {
@@ -57,88 +57,3 @@ describe('get asset filename', () => {
});
});
});
describe('get file mime type', () => {
for (const { mimetype, extension } of [
{ mimetype: 'image/avif', extension: 'avif' },
{ mimetype: 'image/gif', extension: 'gif' },
{ mimetype: 'image/heic', extension: 'heic' },
{ mimetype: 'image/heif', extension: 'heif' },
{ mimetype: 'image/jpeg', extension: 'jpeg' },
{ mimetype: 'image/jpeg', extension: 'jpg' },
{ mimetype: 'image/jxl', extension: 'jxl' },
{ mimetype: 'image/png', extension: 'png' },
{ mimetype: 'image/tiff', extension: 'tiff' },
{ mimetype: 'image/webp', extension: 'webp' },
{ mimetype: 'image/x-adobe-dng', extension: 'dng' },
{ mimetype: 'image/x-arriflex-ari', extension: 'ari' },
{ mimetype: 'image/x-canon-cr2', extension: 'cr2' },
{ mimetype: 'image/x-canon-cr3', extension: 'cr3' },
{ mimetype: 'image/x-canon-crw', extension: 'crw' },
{ mimetype: 'image/x-epson-erf', extension: 'erf' },
{ mimetype: 'image/x-fuji-raf', extension: 'raf' },
{ mimetype: 'image/x-hasselblad-3fr', extension: '3fr' },
{ mimetype: 'image/x-hasselblad-fff', extension: 'fff' },
{ mimetype: 'image/x-kodak-dcr', extension: 'dcr' },
{ mimetype: 'image/x-kodak-k25', extension: 'k25' },
{ mimetype: 'image/x-kodak-kdc', extension: 'kdc' },
{ mimetype: 'image/x-leica-rwl', extension: 'rwl' },
{ mimetype: 'image/x-minolta-mrw', extension: 'mrw' },
{ mimetype: 'image/x-nikon-nef', extension: 'nef' },
{ mimetype: 'image/x-olympus-orf', extension: 'orf' },
{ mimetype: 'image/x-olympus-ori', extension: 'ori' },
{ mimetype: 'image/x-panasonic-raw', extension: 'raw' },
{ mimetype: 'image/x-pentax-pef', extension: 'pef' },
{ mimetype: 'image/x-phantom-cin', extension: 'cin' },
{ mimetype: 'image/x-phaseone-cap', extension: 'cap' },
{ mimetype: 'image/x-phaseone-iiq', extension: 'iiq' },
{ mimetype: 'image/x-samsung-srw', extension: 'srw' },
{ mimetype: 'image/x-sigma-x3f', extension: 'x3f' },
{ mimetype: 'image/x-sony-arw', extension: 'arw' },
{ mimetype: 'image/x-sony-sr2', extension: 'sr2' },
{ mimetype: 'image/x-sony-srf', extension: 'srf' },
{ mimetype: 'video/3gpp', extension: '3gp' },
{ mimetype: 'video/avi', extension: 'avi' },
{ mimetype: 'video/mp2t', extension: 'm2ts' },
{ mimetype: 'video/mp2t', extension: 'mts' },
{ mimetype: 'video/mp4', extension: 'mp4' },
{ mimetype: 'video/mpeg', extension: 'mpg' },
{ mimetype: 'video/quicktime', extension: 'mov' },
{ mimetype: 'video/webm', extension: 'webm' },
{ mimetype: 'video/x-flv', extension: 'flv' },
{ mimetype: 'video/x-matroska', extension: 'mkv' },
{ mimetype: 'video/x-ms-wmv', extension: 'wmv' },
]) {
it(`returns the mime type for ${extension}`, () => {
expect(getFileMimeType({ name: `filename.${extension}` } as File)).toEqual(mimetype);
});
}
it('returns the mime type from the file', () => {
[
{
file: {
name: 'filename.jpg',
type: 'image/jpeg',
},
result: 'image/jpeg',
},
{
file: {
name: 'filename.txt',
type: 'text/plain',
},
result: 'text/plain',
},
{
file: {
name: 'filename.txt',
type: '',
},
result: '',
},
].forEach(({ file, result }) => {
expect(getFileMimeType(file as File)).toEqual(result);
});
});
});

View File

@@ -136,66 +136,6 @@ export function getAssetFilename(asset: AssetResponseDto): string {
return `${asset.originalFileName}.${fileExtension}`;
}
/**
* Returns the MIME type of the file and an empty string when not found.
*/
export function getFileMimeType(file: File): string {
const mimeTypes: Record<string, string> = {
'3fr': 'image/x-hasselblad-3fr',
'3gp': 'video/3gpp',
ari: 'image/x-arriflex-ari',
arw: 'image/x-sony-arw',
avi: 'video/avi',
avif: 'image/avif',
cap: 'image/x-phaseone-cap',
cin: 'image/x-phantom-cin',
cr2: 'image/x-canon-cr2',
cr3: 'image/x-canon-cr3',
crw: 'image/x-canon-crw',
dcr: 'image/x-kodak-dcr',
dng: 'image/x-adobe-dng',
erf: 'image/x-epson-erf',
fff: 'image/x-hasselblad-fff',
flv: 'video/x-flv',
gif: 'image/gif',
heic: 'image/heic',
heif: 'image/heif',
iiq: 'image/x-phaseone-iiq',
insp: 'image/jpeg',
insv: 'video/mp4',
jpeg: 'image/jpeg',
jpg: 'image/jpeg',
jxl: 'image/jxl',
k25: 'image/x-kodak-k25',
kdc: 'image/x-kodak-kdc',
m2ts: 'video/mp2t',
mkv: 'video/x-matroska',
mov: 'video/quicktime',
mp4: 'video/mp4',
mpg: 'video/mpeg',
mrw: 'image/x-minolta-mrw',
mts: 'video/mp2t',
nef: 'image/x-nikon-nef',
orf: 'image/x-olympus-orf',
ori: 'image/x-olympus-ori',
pef: 'image/x-pentax-pef',
png: 'image/png',
raf: 'image/x-fuji-raf',
raw: 'image/x-panasonic-raw',
rwl: 'image/x-leica-rwl',
sr2: 'image/x-sony-sr2',
srf: 'image/x-sony-srf',
srw: 'image/x-samsung-srw',
tiff: 'image/tiff',
webm: 'video/webm',
webp: 'image/webp',
wmv: 'video/x-ms-wmv',
x3f: 'image/x-sigma-x3f',
};
// Return the MIME type determined by the browser or the MIME type based on the file extension.
return file.type || (mimeTypes[getFilenameExtension(file.name)] ?? '');
}
function isRotated90CW(orientation: number) {
return orientation == 6 || orientation == 90;
}

View File

@@ -1,11 +1,59 @@
import { uploadAssetsStore } from '$lib/stores/upload';
import { addAssetsToAlbum, getFileMimeType, getFilenameExtension } from '$lib/utils/asset-utils';
import { addAssetsToAlbum, getFilenameExtension } from '$lib/utils/asset-utils';
import type { AssetFileUploadResponseDto } from '@api';
import axios from 'axios';
import { combineLatestAll, filter, firstValueFrom, from, mergeMap, of } from 'rxjs';
import type { UploadAsset } from '../models/upload-asset';
import { notificationController, NotificationType } from './../components/shared-components/notification/notification';
const extensions = [
'.3fr',
'.3gp',
'.ari',
'.arw',
'.avi',
'.avif',
'.cap',
'.cin',
'.cr2',
'.cr3',
'.crw',
'.dcr',
'.dng',
'.erf',
'.fff',
'.flv',
'.gif',
'.heic',
'.heif',
'.iiq',
'.jpeg',
'.jpg',
'.k25',
'.kdc',
'.mkv',
'.mov',
'.mp2t',
'.mp4',
'.mpeg',
'.mrw',
'.nef',
'.orf',
'.ori',
'.pef',
'.png',
'.raf',
'.raw',
'.rwl',
'.sr2',
'.srf',
'.srw',
'.tiff',
'.webm',
'.webp',
'.wmv',
'.x3f',
];
export const openFileUploadDialog = async (
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined,
@@ -16,52 +64,7 @@ export const openFileUploadDialog = async (
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/*',
'.3fr',
'.3gp',
'.ari',
'.arw',
'.avif',
'.cap',
'.cin',
'.cr2',
'.cr3',
'.crw',
'.dcr',
'.dng',
'.erf',
'.fff',
'.heic',
'.heif',
'.iiq',
'.insp',
'.insv',
'.jxl',
'.k25',
'.kdc',
'.m2ts',
'.mov',
'.mrw',
'.mts',
'.nef',
'.orf',
'.ori',
'.pef',
'.raf',
'.raf',
'.raw',
'.rwl',
'.sr2',
'.srf',
'.srw',
'.x3f',
].join(',');
fileSelector.accept = extensions.join(',');
fileSelector.onchange = async (e: Event) => {
const target = e.target as HTMLInputElement;
if (!target.files) {
@@ -87,10 +90,7 @@ export const fileUploadHandler = async (
) => {
return firstValueFrom(
from(files).pipe(
filter((file) => {
const assetType = getFileMimeType(file).split('/')[0];
return assetType === 'video' || assetType === 'image';
}),
filter((file) => extensions.includes('.' + getFilenameExtension(file.name))),
mergeMap(async (file) => of(await fileUploader(file, albumId, sharedKey)), 2),
combineLatestAll(),
),
@@ -103,51 +103,24 @@ async function fileUploader(
albumId: string | undefined = undefined,
sharedKey: string | undefined = undefined,
): Promise<string | undefined> {
const mimeType = getFileMimeType(asset);
const assetType = mimeType.split('/')[0].toUpperCase();
const fileExtension = getFilenameExtension(asset.name);
const formData = new FormData();
const fileCreatedAt = new Date(asset.lastModified).toISOString();
const deviceAssetId = 'web' + '-' + asset.name + '-' + asset.lastModified;
try {
// Create and add pseudo-unique ID of asset on the device
formData.append('deviceAssetId', deviceAssetId);
// Get device id - for web -> use WEB
formData.append('deviceId', 'WEB');
// Get asset type
formData.append('assetType', assetType);
// Get Asset Created Date
formData.append('fileCreatedAt', fileCreatedAt);
// Get Asset Modified At
formData.append('fileModifiedAt', new Date(asset.lastModified).toISOString());
// Set Asset is Favorite to false
formData.append('isFavorite', 'false');
// Get asset duration
formData.append('duration', '0:00:00.000000');
formData.append('assetData', new File([asset], asset.name));
// Get asset file extension
formData.append('fileExtension', '.' + fileExtension);
// 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 }));
const newUploadAsset: UploadAsset = {
uploadAssetsStore.addNewUploadAsset({
id: deviceAssetId,
file: asset,
progress: 0,
fileExtension: fileExtension,
};
uploadAssetsStore.addNewUploadAsset(newUploadAsset);
});
const response = await axios.post(`/api/asset/upload`, formData, {
params: {