Browse Source

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>
Jason Rasmussen 2 years ago
parent
commit
b71d7e33bb

+ 5 - 43
cli/src/api/open-api/api.ts

@@ -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
@@ -5690,9 +5682,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 
@@ -5708,13 +5698,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
@@ -5752,10 +5738,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);
             }
@@ -5772,10 +5754,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);
             }
@@ -6089,9 +6067,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 
@@ -6107,8 +6083,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);
         },
     }
@@ -6339,7 +6315,7 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
          * @throws {RequiredError}
          */
         uploadFile(requestParameters: AssetApiUploadFileRequest, options?: AxiosRequestConfig): AxiosPromise<AssetFileUploadResponseDto> {
-            return localVarFp.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(axios, basePath));
+            return localVarFp.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(axios, basePath));
         },
     };
 };
@@ -6763,13 +6739,6 @@ export interface AssetApiUpdateAssetRequest {
  * @interface AssetApiUploadFileRequest
  */
 export interface AssetApiUploadFileRequest {
-    /**
-     * 
-     * @type {AssetTypeEnum}
-     * @memberof AssetApiUploadFile
-     */
-    readonly assetType: AssetTypeEnum
-
     /**
      * 
      * @type {File}
@@ -6777,13 +6746,6 @@ export interface AssetApiUploadFileRequest {
      */
     readonly assetData: File
 
-    /**
-     * 
-     * @type {string}
-     * @memberof AssetApiUploadFile
-     */
-    readonly fileExtension: string
-
     /**
      * 
      * @type {string}
@@ -7143,7 +7105,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));
     }
 }
 

+ 0 - 3
cli/src/commands/upload.ts

@@ -71,7 +71,6 @@ export default class Upload extends BaseCommand {
           const importData = {
             assetPath: asset.path,
             deviceAssetId: asset.deviceAssetId,
-            assetType: asset.assetType,
             deviceId: this.deviceId,
             fileCreatedAt: asset.fileCreatedAt,
             fileModifiedAt: asset.fileModifiedAt,
@@ -157,8 +156,6 @@ export default class Upload extends BaseCommand {
       uploadFormData.append('fileCreatedAt', asset.fileCreatedAt);
       uploadFormData.append('fileModifiedAt', asset.fileModifiedAt);
       uploadFormData.append('isFavorite', String(false));
-      uploadFormData.append('fileExtension', asset.fileExtension);
-      uploadFormData.append('assetType', asset.assetType);
       uploadFormData.append('assetData', asset.assetData, { filename: asset.path });
 
       if (asset.sidecarData) {

+ 0 - 13
cli/src/cores/models/crawled-asset.ts

@@ -1,19 +1,14 @@
 import * as fs from 'fs';
-import * as mime from 'mime-types';
 import { basename } from 'node:path';
-import * as path from 'path';
 import crypto from 'crypto';
-import { AssetTypeEnum } from 'src/api/open-api';
 
 export class CrawledAsset {
   public path: string;
 
-  public assetType?: AssetTypeEnum;
   public assetData?: fs.ReadStream;
   public deviceAssetId?: string;
   public fileCreatedAt?: string;
   public fileModifiedAt?: string;
-  public fileExtension?: string;
   public sidecarData?: Buffer;
   public sidecarPath?: string;
   public fileSize!: number;
@@ -30,16 +25,8 @@ export class CrawledAsset {
   async process() {
     const stats = await fs.promises.stat(this.path);
     this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
-
-    // TODO: Determine file type from extension only
-    const mimeType = mime.lookup(this.path);
-    if (!mimeType) {
-      throw Error('Cannot determine mime type of asset: ' + this.path);
-    }
-    this.assetType = mimeType.split('/')[0].toUpperCase() as AssetTypeEnum;
     this.fileCreatedAt = stats.ctime.toISOString();
     this.fileModifiedAt = stats.mtime.toISOString();
-    this.fileExtension = path.extname(this.path);
     this.fileSize = stats.size;
 
     // TODO: doesn't xmp replace the file extension? Will need investigation

+ 0 - 1
cli/src/services/upload.service.spec.ts

@@ -21,7 +21,6 @@ describe('UploadService', () => {
 
   it('should upload a single file', async () => {
     const data = new FormData();
-    data.append('assetType', 'image');
 
     uploadService.upload(data);
 

+ 3 - 19
mobile/lib/modules/backup/services/backup.service.dart

@@ -14,15 +14,13 @@ import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/providers/api.provider.dart';
 import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
-import 'package:immich_mobile/utils/files_helper.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
 import 'package:permission_handler/permission_handler.dart';
 import 'package:photo_manager/photo_manager.dart';
-import 'package:http_parser/http_parser.dart';
-import 'package:path/path.dart' as p;
 import 'package:cancellation_token_http/http.dart' as http;
+import 'package:path/path.dart' as p;
 
 final backupServiceProvider = Provider(
   (ref) => BackupService(
@@ -230,18 +228,12 @@ class BackupService {
 
         if (file != null) {
           String originalFileName = await entity.titleAsync;
-          var fileExtension = p.extension(file.path);
-          var mimeType = FileHelper.getMimeType(file.path);
           var fileStream = file.openRead();
           var assetRawUploadData = http.MultipartFile(
             "assetData",
             fileStream,
             file.lengthSync(),
             filename: originalFileName,
-            contentType: MediaType(
-              mimeType["type"],
-              mimeType["subType"],
-            ),
           );
 
           var req = MultipartRequest(
@@ -256,12 +248,10 @@ class BackupService {
 
           req.fields['deviceAssetId'] = entity.id;
           req.fields['deviceId'] = deviceId;
-          req.fields['assetType'] = _getAssetType(entity.type);
           req.fields['fileCreatedAt'] = entity.createDateTime.toIso8601String();
           req.fields['fileModifiedAt'] =
               entity.modifiedDateTime.toIso8601String();
           req.fields['isFavorite'] = entity.isFavorite.toString();
-          req.fields['fileExtension'] = fileExtension;
           req.fields['duration'] = entity.videoDuration.toString();
 
           req.files.add(assetRawUploadData);
@@ -342,18 +332,12 @@ class BackupService {
       var validPath = motionFilePath.replaceAll('file://', '');
       var motionFile = File(validPath);
       var fileStream = motionFile.openRead();
-      String originalFileName = await entity.titleAsync;
-      var mimeType = FileHelper.getMimeType(validPath);
-
+      String fileName = p.basename(motionFile.path);
       return http.MultipartFile(
         "livePhotoData",
         fileStream,
         motionFile.lengthSync(),
-        filename: originalFileName,
-        contentType: MediaType(
-          mimeType["type"],
-          mimeType["subType"],
-        ),
+        filename: fileName,
       );
     }
 

+ 0 - 8
mobile/lib/shared/services/user.service.dart

@@ -1,7 +1,6 @@
 import 'package:collection/collection.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:http/http.dart';
-import 'package:http_parser/http_parser.dart';
 import 'package:image_picker/image_picker.dart';
 import 'package:immich_mobile/modules/partner/services/partner.service.dart';
 import 'package:immich_mobile/shared/models/store.dart';
@@ -11,7 +10,6 @@ import 'package:immich_mobile/shared/providers/db.provider.dart';
 import 'package:immich_mobile/shared/services/api.service.dart';
 import 'package:immich_mobile/shared/services/sync.service.dart';
 import 'package:immich_mobile/utils/diff.dart';
-import 'package:immich_mobile/utils/files_helper.dart';
 import 'package:isar/isar.dart';
 import 'package:logging/logging.dart';
 import 'package:openapi/api.dart';
@@ -59,17 +57,11 @@ class UserService {
 
   Future<CreateProfileImageResponseDto?> uploadProfileImage(XFile image) async {
     try {
-      var mimeType = FileHelper.getMimeType(image.path);
-
       return await _apiService.userApi.createProfileImage(
         MultipartFile.fromBytes(
           'file',
           await image.readAsBytes(),
           filename: image.name,
-          contentType: MediaType(
-            mimeType["type"],
-            mimeType["subType"],
-          ),
         ),
       );
     } catch (e) {

+ 0 - 150
mobile/lib/utils/files_helper.dart

@@ -1,150 +0,0 @@
-import 'package:path/path.dart' as p;
-
-class FileHelper {
-  static getMimeType(String filePath) {
-    var fileExtension = p.extension(filePath).split(".")[1];
-
-    switch (fileExtension.toLowerCase()) {
-      case 'gif':
-        return {"type": "image", "subType": "gif"};
-
-      case 'jpeg':
-        return {"type": "image", "subType": "jpeg"};
-
-      case 'jpg':
-        return {"type": "image", "subType": "jpeg"};
-
-      case 'png':
-        return {"type": "image", "subType": "png"};
-
-      case 'tif':
-        return {"type": "image", "subType": "tiff"};
-
-      case 'mov':
-        return {"type": "video", "subType": "quicktime"};
-
-      case 'mp4':
-        return {"type": "video", "subType": "mp4"};
-
-      case 'avi':
-        return {"type": "video", "subType": "x-msvideo"};
-
-      case 'heic':
-        return {"type": "image", "subType": "heic"};
-
-      case 'heif':
-        return {"type": "image", "subType": "heif"};
-
-      case 'dng':
-        return {"type": "image", "subType": "dng"};
-
-      case 'webp':
-        return {"type": "image", "subType": "webp"};
-
-      case '3gp':
-        return {"type": "video", "subType": "3gpp"};
-
-      case 'webm':
-        return {"type": "video", "subType": "webm"};
-
-      case 'avif':
-        return {"type": "image", "subType": "avif"};
-
-      case 'insp':
-        return {"type": "image", "subType": "jpeg"};
-
-      case 'insv':
-        return {"type": "video", "subType": "mp4"};
-
-      case 'arw':
-        return {"type": "image", "subType": "x-sony-arw"};
-
-      case 'raf':
-        return {"type": "image", "subType": "x-fuji-raf"};
-
-      case 'nef':
-        return {"type": "image", "subType": "x-nikon-nef"};
-
-      case 'srw':
-        return {"type": "image", "subType": "x-samsung-srw"};
-
-      case 'crw':
-        return {"type": "image", "subType": "x-canon-crw"};
-
-      case 'cr2':
-        return {"type": "image", "subType": "x-canon-cr2"};
-
-      case 'cr3':
-        return {"type": "image", "subType": "x-canon-cr3"};
-
-      case 'erf':
-        return {"type": "image", "subType": "x-epson-erf"};
-
-      case 'dcr':
-        return {"type": "image", "subType": "x-kodak-dcr"};
-
-      case 'k25':
-        return {"type": "image", "subType": "x-kodak-k25"};
-
-      case 'kdc':
-        return {"type": "image", "subType": "x-kodak-kdc"};
-
-      case 'mrw':
-        return {"type": "image", "subType": "x-minolta-mrw"};
-
-      case 'orf':
-        return {"type": "image", "subType": "x-olympus-orf"};
-
-      case 'raw':
-        return {"type": "image", "subType": "x-panasonic-raw"};
-
-      case 'pef':
-        return {"type": "image", "subType": "x-panasonic-pef"};
-
-      case 'x3f':
-        return {"type": "image", "subType": "x-sigma-x3f"};
-
-      case 'srf':
-        return {"type": "image", "subType": "x-sony-srf"};
-
-      case 'sr2':
-        return {"type": "image", "subType": "x-sony-sr2"};
-
-      case '3fr':
-        return {"type": "image", "subType": "x-hasselblad-3fr"};
-
-      case 'fff':
-        return {"type": "image", "subType": "x-hasselblad-fff"};
-
-      case 'rwl':
-        return {"type": "image", "subType": "x-leica-rwl"};
-
-      case 'ori':
-        return {"type": "image", "subType": "x-olympus-ori"};
-
-      case 'iiq':
-        return {"type": "image", "subType": "x-phaseone-iiq"};
-
-      case 'ari':
-        return {"type": "image", "subType": "x-arriflex-ari"};
-
-      case 'cap':
-        return {"type": "image", "subType": "x-phaseone-cap"};
-
-      case 'cin':
-        return {"type": "image", "subType": "x-phantom-cin"};
-
-      case 'jxl':
-        return {"type": "image", "subType": "jxl"};
-
-      case 'mts':
-        return {"type": "video", "subType": "mp2t"};
-
-      case 'm2ts':
-        return {"type": "video", "subType": "mp2t"};
-
-      default:
-        return {"type": "unsupport", "subType": "unsupport"};
-    }
-  }
-}

+ 2 - 6
mobile/openapi/doc/AssetApi.md

@@ -1393,7 +1393,7 @@ Name | Type | Description  | Notes
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
 # **uploadFile**
-> AssetFileUploadResponseDto uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration)
+> AssetFileUploadResponseDto uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration)
 
 
 
@@ -1416,9 +1416,7 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = AssetApi();
-final assetType = ; // AssetTypeEnum | 
 final assetData = BINARY_DATA_HERE; // MultipartFile | 
-final fileExtension = fileExtension_example; // String | 
 final deviceAssetId = deviceAssetId_example; // String | 
 final deviceId = deviceId_example; // String | 
 final fileCreatedAt = 2013-10-20T19:20:30+01:00; // DateTime | 
@@ -1433,7 +1431,7 @@ final isVisible = true; // bool |
 final duration = duration_example; // String | 
 
 try {
-    final result = api_instance.uploadFile(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration);
+    final result = api_instance.uploadFile(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite, key, livePhotoData, sidecarData, isReadOnly, isArchived, isVisible, duration);
     print(result);
 } catch (e) {
     print('Exception when calling AssetApi->uploadFile: $e\n');
@@ -1444,9 +1442,7 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
- **assetType** | [**AssetTypeEnum**](AssetTypeEnum.md)|  | 
  **assetData** | **MultipartFile**|  | 
- **fileExtension** | **String**|  | 
  **deviceAssetId** | **String**|  | 
  **deviceId** | **String**|  | 
  **fileCreatedAt** | **DateTime**|  | 

+ 0 - 1
mobile/openapi/doc/ImportAssetDto.md

@@ -8,7 +8,6 @@ import 'package:openapi/api.dart';
 ## Properties
 Name | Type | Description | Notes
 ------------ | ------------- | ------------- | -------------
-**assetType** | [**AssetTypeEnum**](AssetTypeEnum.md) |  | 
 **isReadOnly** | **bool** |  | [optional] [default to true]
 **assetPath** | **String** |  | 
 **sidecarPath** | **String** |  | [optional] 

+ 3 - 19
mobile/openapi/lib/api/asset_api.dart

@@ -1359,12 +1359,8 @@ class AssetApi {
   /// Performs an HTTP 'POST /asset/upload' operation and returns the [Response].
   /// Parameters:
   ///
-  /// * [AssetTypeEnum] assetType (required):
-  ///
   /// * [MultipartFile] assetData (required):
   ///
-  /// * [String] fileExtension (required):
-  ///
   /// * [String] deviceAssetId (required):
   ///
   /// * [String] deviceId (required):
@@ -1388,7 +1384,7 @@ class AssetApi {
   /// * [bool] isVisible:
   ///
   /// * [String] duration:
-  Future<Response> uploadFileWithHttpInfo(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async {
+  Future<Response> uploadFileWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async {
     // ignore: prefer_const_declarations
     final path = r'/asset/upload';
 
@@ -1407,10 +1403,6 @@ class AssetApi {
 
     bool hasFields = false;
     final mp = MultipartRequest('POST', Uri.parse(path));
-    if (assetType != null) {
-      hasFields = true;
-      mp.fields[r'assetType'] = parameterToString(assetType);
-    }
     if (assetData != null) {
       hasFields = true;
       mp.fields[r'assetData'] = assetData.field;
@@ -1430,10 +1422,6 @@ class AssetApi {
       hasFields = true;
       mp.fields[r'isReadOnly'] = parameterToString(isReadOnly);
     }
-    if (fileExtension != null) {
-      hasFields = true;
-      mp.fields[r'fileExtension'] = parameterToString(fileExtension);
-    }
     if (deviceAssetId != null) {
       hasFields = true;
       mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
@@ -1483,12 +1471,8 @@ class AssetApi {
 
   /// Parameters:
   ///
-  /// * [AssetTypeEnum] assetType (required):
-  ///
   /// * [MultipartFile] assetData (required):
   ///
-  /// * [String] fileExtension (required):
-  ///
   /// * [String] deviceAssetId (required):
   ///
   /// * [String] deviceId (required):
@@ -1512,8 +1496,8 @@ class AssetApi {
   /// * [bool] isVisible:
   ///
   /// * [String] duration:
-  Future<AssetFileUploadResponseDto?> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async {
-    final response = await uploadFileWithHttpInfo(assetType, assetData, fileExtension, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite,  key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isReadOnly: isReadOnly, isArchived: isArchived, isVisible: isVisible, duration: duration, );
+  Future<AssetFileUploadResponseDto?> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String? key, MultipartFile? livePhotoData, MultipartFile? sidecarData, bool? isReadOnly, bool? isArchived, bool? isVisible, String? duration, }) async {
+    final response = await uploadFileWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, isFavorite,  key: key, livePhotoData: livePhotoData, sidecarData: sidecarData, isReadOnly: isReadOnly, isArchived: isArchived, isVisible: isVisible, duration: duration, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }

+ 1 - 9
mobile/openapi/lib/model/import_asset_dto.dart

@@ -13,7 +13,6 @@ part of openapi.api;
 class ImportAssetDto {
   /// Returns a new [ImportAssetDto] instance.
   ImportAssetDto({
-    required this.assetType,
     this.isReadOnly = true,
     required this.assetPath,
     this.sidecarPath,
@@ -27,8 +26,6 @@ class ImportAssetDto {
     this.duration,
   });
 
-  AssetTypeEnum assetType;
-
   bool isReadOnly;
 
   String assetPath;
@@ -77,7 +74,6 @@ class ImportAssetDto {
 
   @override
   bool operator ==(Object other) => identical(this, other) || other is ImportAssetDto &&
-     other.assetType == assetType &&
      other.isReadOnly == isReadOnly &&
      other.assetPath == assetPath &&
      other.sidecarPath == sidecarPath &&
@@ -93,7 +89,6 @@ class ImportAssetDto {
   @override
   int get hashCode =>
     // ignore: unnecessary_parenthesis
-    (assetType.hashCode) +
     (isReadOnly.hashCode) +
     (assetPath.hashCode) +
     (sidecarPath == null ? 0 : sidecarPath!.hashCode) +
@@ -107,11 +102,10 @@ class ImportAssetDto {
     (duration == null ? 0 : duration!.hashCode);
 
   @override
-  String toString() => 'ImportAssetDto[assetType=$assetType, isReadOnly=$isReadOnly, assetPath=$assetPath, sidecarPath=$sidecarPath, deviceAssetId=$deviceAssetId, deviceId=$deviceId, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, isFavorite=$isFavorite, isArchived=$isArchived, isVisible=$isVisible, duration=$duration]';
+  String toString() => 'ImportAssetDto[isReadOnly=$isReadOnly, assetPath=$assetPath, sidecarPath=$sidecarPath, deviceAssetId=$deviceAssetId, deviceId=$deviceId, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, isFavorite=$isFavorite, isArchived=$isArchived, isVisible=$isVisible, duration=$duration]';
 
   Map<String, dynamic> toJson() {
     final json = <String, dynamic>{};
-      json[r'assetType'] = this.assetType;
       json[r'isReadOnly'] = this.isReadOnly;
       json[r'assetPath'] = this.assetPath;
     if (this.sidecarPath != null) {
@@ -150,7 +144,6 @@ class ImportAssetDto {
       final json = value.cast<String, dynamic>();
 
       return ImportAssetDto(
-        assetType: AssetTypeEnum.fromJson(json[r'assetType'])!,
         isReadOnly: mapValueOfType<bool>(json, r'isReadOnly') ?? true,
         assetPath: mapValueOfType<String>(json, r'assetPath')!,
         sidecarPath: mapValueOfType<String>(json, r'sidecarPath'),
@@ -209,7 +202,6 @@ class ImportAssetDto {
 
   /// The list of required keys that must be present in a JSON.
   static const requiredKeys = <String>{
-    'assetType',
     'assetPath',
     'deviceAssetId',
     'deviceId',

+ 1 - 1
mobile/openapi/test/asset_api_test.dart

@@ -151,7 +151,7 @@ void main() {
       // TODO
     });
 
-    //Future<AssetFileUploadResponseDto> uploadFile(AssetTypeEnum assetType, MultipartFile assetData, String fileExtension, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isReadOnly, bool isArchived, bool isVisible, String duration }) async
+    //Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, bool isFavorite, { String key, MultipartFile livePhotoData, MultipartFile sidecarData, bool isReadOnly, bool isArchived, bool isVisible, String duration }) async
     test('test uploadFile', () async {
       // TODO
     });

+ 0 - 5
mobile/openapi/test/import_asset_dto_test.dart

@@ -16,11 +16,6 @@ void main() {
   // final instance = ImportAssetDto();
 
   group('test ImportAssetDto', () {
-    // AssetTypeEnum assetType
-    test('to test the property `assetType`', () async {
-      // TODO
-    });
-
     // bool isReadOnly (default value: true)
     test('to test the property `isReadOnly`', () async {
       // TODO

+ 0 - 12
server/immich-openapi-specs.json

@@ -5148,9 +5148,6 @@
       "CreateAssetDto": {
         "type": "object",
         "properties": {
-          "assetType": {
-            "$ref": "#/components/schemas/AssetTypeEnum"
-          },
           "assetData": {
             "type": "string",
             "format": "binary"
@@ -5167,9 +5164,6 @@
             "type": "boolean",
             "default": false
           },
-          "fileExtension": {
-            "type": "string"
-          },
           "deviceAssetId": {
             "type": "string"
           },
@@ -5198,9 +5192,7 @@
           }
         },
         "required": [
-          "assetType",
           "assetData",
-          "fileExtension",
           "deviceAssetId",
           "deviceId",
           "fileCreatedAt",
@@ -5571,9 +5563,6 @@
       "ImportAssetDto": {
         "type": "object",
         "properties": {
-          "assetType": {
-            "$ref": "#/components/schemas/AssetTypeEnum"
-          },
           "isReadOnly": {
             "type": "boolean",
             "default": true
@@ -5612,7 +5601,6 @@
           }
         },
         "required": [
-          "assetType",
           "assetPath",
           "deviceAssetId",
           "deviceId",

+ 15 - 1
server/src/domain/domain.constant.ts

@@ -1,3 +1,4 @@
+import { AssetType } from '@app/infra/entities';
 import { BadRequestException } from '@nestjs/common';
 import { extname } from 'node:path';
 import pkg from 'src/../../package.json';
@@ -91,6 +92,8 @@ const sidecar: Record<string, string> = {
 
 const isType = (filename: string, lookup: Record<string, string>) => !!lookup[extname(filename).toLowerCase()];
 const getType = (filename: string, lookup: Record<string, string>) => lookup[extname(filename).toLowerCase()];
+const lookup = (filename: string) =>
+  getType(filename, { ...image, ...video, ...sidecar }) || 'application/octet-stream';
 
 export const mimeTypes = {
   image,
@@ -102,5 +105,16 @@ export const mimeTypes = {
   isProfile: (filename: string) => isType(filename, profile),
   isSidecar: (filename: string) => isType(filename, sidecar),
   isVideo: (filename: string) => isType(filename, video),
-  lookup: (filename: string) => getType(filename, { ...image, ...video, ...sidecar }) || 'application/octet-stream',
+  lookup,
+  assetType: (filename: string) => {
+    const contentType = lookup(filename).split('/')[0];
+    switch (contentType) {
+      case 'image':
+        return AssetType.IMAGE;
+      case 'video':
+        return AssetType.VIDEO;
+      default:
+        return AssetType.OTHER;
+    }
+  },
 };

+ 2 - 2
server/src/immich/api-v1/asset/asset.core.ts

@@ -1,4 +1,4 @@
-import { AuthUserDto, IJobRepository, JobName, UploadFile } from '@app/domain';
+import { AuthUserDto, IJobRepository, JobName, mimeTypes, UploadFile } from '@app/domain';
 import { AssetEntity, UserEntity } from '@app/infra/entities';
 import { parse } from 'node:path';
 import { IAssetRepository } from './asset-repository';
@@ -26,7 +26,7 @@ export class AssetCore {
       fileCreatedAt: dto.fileCreatedAt,
       fileModifiedAt: dto.fileModifiedAt,
 
-      type: dto.assetType,
+      type: mimeTypes.assetType(file.originalPath),
       isFavorite: dto.isFavorite,
       isArchived: dto.isArchived ?? false,
       duration: dto.duration || null,

+ 0 - 1
server/src/immich/api-v1/asset/asset.service.spec.ts

@@ -32,7 +32,6 @@ const _getCreateAssetDto = (): CreateAssetDto => {
   const createAssetDto = new CreateAssetDto();
   createAssetDto.deviceAssetId = 'deviceAssetId';
   createAssetDto.deviceId = 'deviceId';
-  createAssetDto.assetType = AssetType.OTHER;
   createAssetDto.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z');
   createAssetDto.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z');
   createAssetDto.isFavorite = false;

+ 1 - 10
server/src/immich/api-v1/asset/dto/create-asset.dto.ts

@@ -1,8 +1,7 @@
 import { toBoolean, toSanitized, UploadFieldName } from '@app/domain';
-import { AssetType } from '@app/infra/entities';
 import { ApiProperty } from '@nestjs/swagger';
 import { Transform } from 'class-transformer';
-import { IsBoolean, IsEnum, IsNotEmpty, IsOptional, IsString } from 'class-validator';
+import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
 
 export class CreateAssetBase {
   @IsNotEmpty()
@@ -11,11 +10,6 @@ export class CreateAssetBase {
   @IsNotEmpty()
   deviceId!: string;
 
-  @IsNotEmpty()
-  @IsEnum(AssetType)
-  @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
-  assetType!: AssetType;
-
   @IsNotEmpty()
   fileCreatedAt!: Date;
 
@@ -43,9 +37,6 @@ export class CreateAssetDto extends CreateAssetBase {
   @Transform(toBoolean)
   isReadOnly?: boolean = false;
 
-  @IsNotEmpty()
-  fileExtension!: string;
-
   // The properties below are added to correctly generate the API docs
   // and client SDKs. Validation should be handled in the controller.
   @ApiProperty({ type: 'string', format: 'binary' })

+ 6 - 46
web/src/api/open-api/api.ts

@@ -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));
     }
 }
 

+ 2 - 1
web/src/lib/components/shared-components/upload-asset-preview.svelte

@@ -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>

+ 0 - 1
web/src/lib/models/upload-asset.ts

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

+ 1 - 86
web/src/lib/utils/asset-utils.spec.ts

@@ -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);
-    });
-  });
-});

+ 0 - 60
web/src/lib/utils/asset-utils.ts

@@ -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;
 }

+ 55 - 82
web/src/lib/utils/file-uploader.ts

@@ -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: {