mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	refactor(server): mime types (#3197)
* refactor(server): mime type check * chore: open api * chore: remove duplicate test
This commit is contained in:
		
							parent
							
								
									785f61ba70
								
							
						
					
					
						commit
						6180828ed2
					
				@ -679,12 +679,6 @@ export interface AssetResponseDto {
 | 
				
			|||||||
     * @memberof AssetResponseDto
 | 
					     * @memberof AssetResponseDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'isArchived': boolean;
 | 
					    'isArchived': boolean;
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     * @memberof AssetResponseDto
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    'mimeType': string | null;
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/AssetResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/AssetResponseDto.md
									
									
									
										generated
									
									
									
								
							@ -22,7 +22,6 @@ Name | Type | Description | Notes
 | 
				
			|||||||
**updatedAt** | [**DateTime**](DateTime.md) |  | 
 | 
					**updatedAt** | [**DateTime**](DateTime.md) |  | 
 | 
				
			||||||
**isFavorite** | **bool** |  | 
 | 
					**isFavorite** | **bool** |  | 
 | 
				
			||||||
**isArchived** | **bool** |  | 
 | 
					**isArchived** | **bool** |  | 
 | 
				
			||||||
**mimeType** | **String** |  | 
 | 
					 | 
				
			||||||
**duration** | **String** |  | 
 | 
					**duration** | **String** |  | 
 | 
				
			||||||
**exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) |  | [optional] 
 | 
					**exifInfo** | [**ExifResponseDto**](ExifResponseDto.md) |  | [optional] 
 | 
				
			||||||
**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) |  | [optional] 
 | 
					**smartInfo** | [**SmartInfoResponseDto**](SmartInfoResponseDto.md) |  | [optional] 
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										14
									
								
								mobile/openapi/lib/model/asset_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								mobile/openapi/lib/model/asset_response_dto.dart
									
									
									
										generated
									
									
									
								
							@ -27,7 +27,6 @@ class AssetResponseDto {
 | 
				
			|||||||
    required this.updatedAt,
 | 
					    required this.updatedAt,
 | 
				
			||||||
    required this.isFavorite,
 | 
					    required this.isFavorite,
 | 
				
			||||||
    required this.isArchived,
 | 
					    required this.isArchived,
 | 
				
			||||||
    required this.mimeType,
 | 
					 | 
				
			||||||
    required this.duration,
 | 
					    required this.duration,
 | 
				
			||||||
    this.exifInfo,
 | 
					    this.exifInfo,
 | 
				
			||||||
    this.smartInfo,
 | 
					    this.smartInfo,
 | 
				
			||||||
@ -66,8 +65,6 @@ class AssetResponseDto {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  bool isArchived;
 | 
					  bool isArchived;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String? mimeType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  String duration;
 | 
					  String duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ///
 | 
					  ///
 | 
				
			||||||
@ -111,7 +108,6 @@ class AssetResponseDto {
 | 
				
			|||||||
     other.updatedAt == updatedAt &&
 | 
					     other.updatedAt == updatedAt &&
 | 
				
			||||||
     other.isFavorite == isFavorite &&
 | 
					     other.isFavorite == isFavorite &&
 | 
				
			||||||
     other.isArchived == isArchived &&
 | 
					     other.isArchived == isArchived &&
 | 
				
			||||||
     other.mimeType == mimeType &&
 | 
					 | 
				
			||||||
     other.duration == duration &&
 | 
					     other.duration == duration &&
 | 
				
			||||||
     other.exifInfo == exifInfo &&
 | 
					     other.exifInfo == exifInfo &&
 | 
				
			||||||
     other.smartInfo == smartInfo &&
 | 
					     other.smartInfo == smartInfo &&
 | 
				
			||||||
@ -137,7 +133,6 @@ class AssetResponseDto {
 | 
				
			|||||||
    (updatedAt.hashCode) +
 | 
					    (updatedAt.hashCode) +
 | 
				
			||||||
    (isFavorite.hashCode) +
 | 
					    (isFavorite.hashCode) +
 | 
				
			||||||
    (isArchived.hashCode) +
 | 
					    (isArchived.hashCode) +
 | 
				
			||||||
    (mimeType == null ? 0 : mimeType!.hashCode) +
 | 
					 | 
				
			||||||
    (duration.hashCode) +
 | 
					    (duration.hashCode) +
 | 
				
			||||||
    (exifInfo == null ? 0 : exifInfo!.hashCode) +
 | 
					    (exifInfo == null ? 0 : exifInfo!.hashCode) +
 | 
				
			||||||
    (smartInfo == null ? 0 : smartInfo!.hashCode) +
 | 
					    (smartInfo == null ? 0 : smartInfo!.hashCode) +
 | 
				
			||||||
@ -147,7 +142,7 @@ class AssetResponseDto {
 | 
				
			|||||||
    (checksum.hashCode);
 | 
					    (checksum.hashCode);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, originalFileName=$originalFileName, resized=$resized, thumbhash=$thumbhash, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, isArchived=$isArchived, mimeType=$mimeType, duration=$duration, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags, people=$people, checksum=$checksum]';
 | 
					  String toString() => 'AssetResponseDto[type=$type, id=$id, deviceAssetId=$deviceAssetId, ownerId=$ownerId, deviceId=$deviceId, originalPath=$originalPath, originalFileName=$originalFileName, resized=$resized, thumbhash=$thumbhash, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, updatedAt=$updatedAt, isFavorite=$isFavorite, isArchived=$isArchived, duration=$duration, exifInfo=$exifInfo, smartInfo=$smartInfo, livePhotoVideoId=$livePhotoVideoId, tags=$tags, people=$people, checksum=$checksum]';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Map<String, dynamic> toJson() {
 | 
					  Map<String, dynamic> toJson() {
 | 
				
			||||||
    final json = <String, dynamic>{};
 | 
					    final json = <String, dynamic>{};
 | 
				
			||||||
@ -169,11 +164,6 @@ class AssetResponseDto {
 | 
				
			|||||||
      json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
 | 
					      json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String();
 | 
				
			||||||
      json[r'isFavorite'] = this.isFavorite;
 | 
					      json[r'isFavorite'] = this.isFavorite;
 | 
				
			||||||
      json[r'isArchived'] = this.isArchived;
 | 
					      json[r'isArchived'] = this.isArchived;
 | 
				
			||||||
    if (this.mimeType != null) {
 | 
					 | 
				
			||||||
      json[r'mimeType'] = this.mimeType;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
    //  json[r'mimeType'] = null;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
      json[r'duration'] = this.duration;
 | 
					      json[r'duration'] = this.duration;
 | 
				
			||||||
    if (this.exifInfo != null) {
 | 
					    if (this.exifInfo != null) {
 | 
				
			||||||
      json[r'exifInfo'] = this.exifInfo;
 | 
					      json[r'exifInfo'] = this.exifInfo;
 | 
				
			||||||
@ -218,7 +208,6 @@ class AssetResponseDto {
 | 
				
			|||||||
        updatedAt: mapDateTime(json, r'updatedAt', r'')!,
 | 
					        updatedAt: mapDateTime(json, r'updatedAt', r'')!,
 | 
				
			||||||
        isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
 | 
					        isFavorite: mapValueOfType<bool>(json, r'isFavorite')!,
 | 
				
			||||||
        isArchived: mapValueOfType<bool>(json, r'isArchived')!,
 | 
					        isArchived: mapValueOfType<bool>(json, r'isArchived')!,
 | 
				
			||||||
        mimeType: mapValueOfType<String>(json, r'mimeType'),
 | 
					 | 
				
			||||||
        duration: mapValueOfType<String>(json, r'duration')!,
 | 
					        duration: mapValueOfType<String>(json, r'duration')!,
 | 
				
			||||||
        exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
 | 
					        exifInfo: ExifResponseDto.fromJson(json[r'exifInfo']),
 | 
				
			||||||
        smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
 | 
					        smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
 | 
				
			||||||
@ -287,7 +276,6 @@ class AssetResponseDto {
 | 
				
			|||||||
    'updatedAt',
 | 
					    'updatedAt',
 | 
				
			||||||
    'isFavorite',
 | 
					    'isFavorite',
 | 
				
			||||||
    'isArchived',
 | 
					    'isArchived',
 | 
				
			||||||
    'mimeType',
 | 
					 | 
				
			||||||
    'duration',
 | 
					    'duration',
 | 
				
			||||||
    'checksum',
 | 
					    'checksum',
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/asset_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/asset_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							@ -87,11 +87,6 @@ void main() {
 | 
				
			|||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // String mimeType
 | 
					 | 
				
			||||||
    test('to test the property `mimeType`', () async {
 | 
					 | 
				
			||||||
      // TODO
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // String duration
 | 
					    // String duration
 | 
				
			||||||
    test('to test the property `duration`', () async {
 | 
					    test('to test the property `duration`', () async {
 | 
				
			||||||
      // TODO
 | 
					      // TODO
 | 
				
			||||||
 | 
				
			|||||||
@ -4866,10 +4866,6 @@
 | 
				
			|||||||
          "isArchived": {
 | 
					          "isArchived": {
 | 
				
			||||||
            "type": "boolean"
 | 
					            "type": "boolean"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
          "mimeType": {
 | 
					 | 
				
			||||||
            "type": "string",
 | 
					 | 
				
			||||||
            "nullable": true
 | 
					 | 
				
			||||||
          },
 | 
					 | 
				
			||||||
          "duration": {
 | 
					          "duration": {
 | 
				
			||||||
            "type": "string"
 | 
					            "type": "string"
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@ -4915,7 +4911,6 @@
 | 
				
			|||||||
          "updatedAt",
 | 
					          "updatedAt",
 | 
				
			||||||
          "isFavorite",
 | 
					          "isFavorite",
 | 
				
			||||||
          "isArchived",
 | 
					          "isArchived",
 | 
				
			||||||
          "mimeType",
 | 
					 | 
				
			||||||
          "duration",
 | 
					          "duration",
 | 
				
			||||||
          "checksum"
 | 
					          "checksum"
 | 
				
			||||||
        ]
 | 
					        ]
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										26
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										26
									
								
								server/package-lock.json
									
									
									
										generated
									
									
									
								
							@ -21,7 +21,6 @@
 | 
				
			|||||||
        "@nestjs/typeorm": "^9.0.1",
 | 
					        "@nestjs/typeorm": "^9.0.1",
 | 
				
			||||||
        "@nestjs/websockets": "^9.2.1",
 | 
					        "@nestjs/websockets": "^9.2.1",
 | 
				
			||||||
        "@socket.io/redis-adapter": "^8.0.1",
 | 
					        "@socket.io/redis-adapter": "^8.0.1",
 | 
				
			||||||
        "@types/mime-types": "^2.1.1",
 | 
					 | 
				
			||||||
        "archiver": "^5.3.1",
 | 
					        "archiver": "^5.3.1",
 | 
				
			||||||
        "axios": "^0.26.0",
 | 
					        "axios": "^0.26.0",
 | 
				
			||||||
        "bcrypt": "^5.0.1",
 | 
					        "bcrypt": "^5.0.1",
 | 
				
			||||||
@ -40,7 +39,6 @@
 | 
				
			|||||||
        "local-reverse-geocoder": "0.12.5",
 | 
					        "local-reverse-geocoder": "0.12.5",
 | 
				
			||||||
        "lodash": "^4.17.21",
 | 
					        "lodash": "^4.17.21",
 | 
				
			||||||
        "luxon": "^3.0.3",
 | 
					        "luxon": "^3.0.3",
 | 
				
			||||||
        "mime-types": "^2.1.35",
 | 
					 | 
				
			||||||
        "mv": "^2.1.1",
 | 
					        "mv": "^2.1.1",
 | 
				
			||||||
        "nest-commander": "^3.3.0",
 | 
					        "nest-commander": "^3.3.0",
 | 
				
			||||||
        "openid-client": "^5.2.1",
 | 
					        "openid-client": "^5.2.1",
 | 
				
			||||||
@ -55,8 +53,8 @@
 | 
				
			|||||||
        "ua-parser-js": "^1.0.35"
 | 
					        "ua-parser-js": "^1.0.35"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "bin": {
 | 
					      "bin": {
 | 
				
			||||||
        "immich": "./bin/cli.sh",
 | 
					        "immich": "bin/cli.sh",
 | 
				
			||||||
        "immich-admin": "./bin/admin-cli.sh"
 | 
					        "immich-admin": "bin/admin-cli.sh"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      "devDependencies": {
 | 
					      "devDependencies": {
 | 
				
			||||||
        "@nestjs/cli": "^9.1.8",
 | 
					        "@nestjs/cli": "^9.1.8",
 | 
				
			||||||
@ -3022,11 +3020,6 @@
 | 
				
			|||||||
      "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
 | 
					      "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "node_modules/@types/mime-types": {
 | 
					 | 
				
			||||||
      "version": "2.1.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw=="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "node_modules/@types/multer": {
 | 
					    "node_modules/@types/multer": {
 | 
				
			||||||
      "version": "1.4.7",
 | 
					      "version": "1.4.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
 | 
				
			||||||
@ -9079,7 +9072,6 @@
 | 
				
			|||||||
      "version": "3.1.0",
 | 
					      "version": "3.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
 | 
					      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "dependencies": {
 | 
					      "dependencies": {
 | 
				
			||||||
        "yocto-queue": "^0.1.0"
 | 
					        "yocto-queue": "^0.1.0"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -9327,7 +9319,7 @@
 | 
				
			|||||||
      "version": "2.3.1",
 | 
					      "version": "2.3.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
 | 
					      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
 | 
				
			||||||
      "dev": true,
 | 
					      "devOptional": true,
 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">=8.6"
 | 
					        "node": ">=8.6"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -12213,7 +12205,6 @@
 | 
				
			|||||||
      "version": "0.1.0",
 | 
					      "version": "0.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
 | 
					      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "engines": {
 | 
					      "engines": {
 | 
				
			||||||
        "node": ">=10"
 | 
					        "node": ">=10"
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
@ -14458,11 +14449,6 @@
 | 
				
			|||||||
      "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
 | 
					      "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
 | 
				
			||||||
      "dev": true
 | 
					      "dev": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "@types/mime-types": {
 | 
					 | 
				
			||||||
      "version": "2.1.1",
 | 
					 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.1.tgz",
 | 
					 | 
				
			||||||
      "integrity": "sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw=="
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
    "@types/multer": {
 | 
					    "@types/multer": {
 | 
				
			||||||
      "version": "1.4.7",
 | 
					      "version": "1.4.7",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/@types/multer/-/multer-1.4.7.tgz",
 | 
				
			||||||
@ -19088,7 +19074,6 @@
 | 
				
			|||||||
      "version": "3.1.0",
 | 
					      "version": "3.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
 | 
					      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
 | 
				
			||||||
      "dev": true,
 | 
					 | 
				
			||||||
      "requires": {
 | 
					      "requires": {
 | 
				
			||||||
        "yocto-queue": "^0.1.0"
 | 
					        "yocto-queue": "^0.1.0"
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@ -19271,7 +19256,7 @@
 | 
				
			|||||||
      "version": "2.3.1",
 | 
					      "version": "2.3.1",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
 | 
				
			||||||
      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
 | 
					      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
 | 
				
			||||||
      "dev": true
 | 
					      "devOptional": true
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "pirates": {
 | 
					    "pirates": {
 | 
				
			||||||
      "version": "4.0.5",
 | 
					      "version": "4.0.5",
 | 
				
			||||||
@ -21284,8 +21269,7 @@
 | 
				
			|||||||
    "yocto-queue": {
 | 
					    "yocto-queue": {
 | 
				
			||||||
      "version": "0.1.0",
 | 
					      "version": "0.1.0",
 | 
				
			||||||
      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
 | 
					      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
 | 
				
			||||||
      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
 | 
					      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="
 | 
				
			||||||
      "dev": true
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    "zip-stream": {
 | 
					    "zip-stream": {
 | 
				
			||||||
      "version": "4.1.0",
 | 
					      "version": "4.1.0",
 | 
				
			||||||
 | 
				
			|||||||
@ -51,7 +51,6 @@
 | 
				
			|||||||
    "@nestjs/typeorm": "^9.0.1",
 | 
					    "@nestjs/typeorm": "^9.0.1",
 | 
				
			||||||
    "@nestjs/websockets": "^9.2.1",
 | 
					    "@nestjs/websockets": "^9.2.1",
 | 
				
			||||||
    "@socket.io/redis-adapter": "^8.0.1",
 | 
					    "@socket.io/redis-adapter": "^8.0.1",
 | 
				
			||||||
    "@types/mime-types": "^2.1.1",
 | 
					 | 
				
			||||||
    "archiver": "^5.3.1",
 | 
					    "archiver": "^5.3.1",
 | 
				
			||||||
    "axios": "^0.26.0",
 | 
					    "axios": "^0.26.0",
 | 
				
			||||||
    "bcrypt": "^5.0.1",
 | 
					    "bcrypt": "^5.0.1",
 | 
				
			||||||
@ -64,12 +63,12 @@
 | 
				
			|||||||
    "fluent-ffmpeg": "^2.1.2",
 | 
					    "fluent-ffmpeg": "^2.1.2",
 | 
				
			||||||
    "handlebars": "^4.7.7",
 | 
					    "handlebars": "^4.7.7",
 | 
				
			||||||
    "i18n-iso-countries": "^7.5.0",
 | 
					    "i18n-iso-countries": "^7.5.0",
 | 
				
			||||||
 | 
					    "immich": "^0.39.0",
 | 
				
			||||||
    "ioredis": "^5.3.1",
 | 
					    "ioredis": "^5.3.1",
 | 
				
			||||||
    "joi": "^17.5.0",
 | 
					    "joi": "^17.5.0",
 | 
				
			||||||
    "local-reverse-geocoder": "0.12.5",
 | 
					    "local-reverse-geocoder": "0.12.5",
 | 
				
			||||||
    "lodash": "^4.17.21",
 | 
					    "lodash": "^4.17.21",
 | 
				
			||||||
    "luxon": "^3.0.3",
 | 
					    "luxon": "^3.0.3",
 | 
				
			||||||
    "mime-types": "^2.1.35",
 | 
					 | 
				
			||||||
    "mv": "^2.1.1",
 | 
					    "mv": "^2.1.1",
 | 
				
			||||||
    "nest-commander": "^3.3.0",
 | 
					    "nest-commander": "^3.3.0",
 | 
				
			||||||
    "openid-client": "^5.2.1",
 | 
					    "openid-client": "^5.2.1",
 | 
				
			||||||
@ -81,8 +80,7 @@
 | 
				
			|||||||
    "thumbhash": "^0.1.1",
 | 
					    "thumbhash": "^0.1.1",
 | 
				
			||||||
    "typeorm": "^0.3.11",
 | 
					    "typeorm": "^0.3.11",
 | 
				
			||||||
    "typesense": "^1.5.3",
 | 
					    "typesense": "^1.5.3",
 | 
				
			||||||
    "ua-parser-js": "^1.0.35",
 | 
					    "ua-parser-js": "^1.0.35"
 | 
				
			||||||
    "immich": "^0.39.0"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@nestjs/cli": "^9.1.8",
 | 
					    "@nestjs/cli": "^9.1.8",
 | 
				
			||||||
 | 
				
			|||||||
@ -156,10 +156,7 @@ describe(AssetService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      await expect(sut.downloadFile(authStub.admin, 'asset-1')).resolves.toEqual({ stream });
 | 
					      await expect(sut.downloadFile(authStub.admin, 'asset-1')).resolves.toEqual({ stream });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(storageMock.createReadStream).toHaveBeenCalledWith(
 | 
					      expect(storageMock.createReadStream).toHaveBeenCalledWith(assetEntityStub.image.originalPath, 'image/jpeg');
 | 
				
			||||||
        assetEntityStub.image.originalPath,
 | 
					 | 
				
			||||||
        assetEntityStub.image.mimeType,
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should download an archive', async () => {
 | 
					    it('should download an archive', async () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -4,6 +4,7 @@ import { DateTime } from 'luxon';
 | 
				
			|||||||
import { extname } from 'path';
 | 
					import { extname } from 'path';
 | 
				
			||||||
import { AccessCore, IAccessRepository, Permission } from '../access';
 | 
					import { AccessCore, IAccessRepository, Permission } from '../access';
 | 
				
			||||||
import { AuthUserDto } from '../auth';
 | 
					import { AuthUserDto } from '../auth';
 | 
				
			||||||
 | 
					import { mimeTypes } from '../domain.constant';
 | 
				
			||||||
import { HumanReadableSize, usePagination } from '../domain.util';
 | 
					import { HumanReadableSize, usePagination } from '../domain.util';
 | 
				
			||||||
import { ImmichReadStream, IStorageRepository } from '../storage';
 | 
					import { ImmichReadStream, IStorageRepository } from '../storage';
 | 
				
			||||||
import { IAssetRepository } from './asset.repository';
 | 
					import { IAssetRepository } from './asset.repository';
 | 
				
			||||||
@ -20,7 +21,6 @@ export enum UploadFieldName {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface UploadFile {
 | 
					export interface UploadFile {
 | 
				
			||||||
  mimeType: string;
 | 
					 | 
				
			||||||
  checksum: Buffer;
 | 
					  checksum: Buffer;
 | 
				
			||||||
  originalPath: string;
 | 
					  originalPath: string;
 | 
				
			||||||
  originalName: string;
 | 
					  originalName: string;
 | 
				
			||||||
@ -68,7 +68,7 @@ export class AssetService {
 | 
				
			|||||||
      throw new BadRequestException('Asset not found');
 | 
					      throw new BadRequestException('Asset not found');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return this.storageRepository.createReadStream(asset.originalPath, asset.mimeType);
 | 
					    return this.storageRepository.createReadStream(asset.originalPath, mimeTypes.lookup(asset.originalPath));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getDownloadInfo(authUser: AuthUserDto, dto: DownloadDto): Promise<DownloadResponseDto> {
 | 
					  async getDownloadInfo(authUser: AuthUserDto, dto: DownloadDto): Promise<DownloadResponseDto> {
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,6 @@ export class AssetResponseDto {
 | 
				
			|||||||
  updatedAt!: Date;
 | 
					  updatedAt!: Date;
 | 
				
			||||||
  isFavorite!: boolean;
 | 
					  isFavorite!: boolean;
 | 
				
			||||||
  isArchived!: boolean;
 | 
					  isArchived!: boolean;
 | 
				
			||||||
  mimeType!: string | null;
 | 
					 | 
				
			||||||
  duration!: string;
 | 
					  duration!: string;
 | 
				
			||||||
  exifInfo?: ExifResponseDto;
 | 
					  exifInfo?: ExifResponseDto;
 | 
				
			||||||
  smartInfo?: SmartInfoResponseDto;
 | 
					  smartInfo?: SmartInfoResponseDto;
 | 
				
			||||||
@ -50,7 +49,6 @@ export function mapAsset(entity: AssetEntity): AssetResponseDto {
 | 
				
			|||||||
    updatedAt: entity.updatedAt,
 | 
					    updatedAt: entity.updatedAt,
 | 
				
			||||||
    isFavorite: entity.isFavorite,
 | 
					    isFavorite: entity.isFavorite,
 | 
				
			||||||
    isArchived: entity.isArchived,
 | 
					    isArchived: entity.isArchived,
 | 
				
			||||||
    mimeType: entity.mimeType,
 | 
					 | 
				
			||||||
    duration: entity.duration ?? '0:00:00.00000',
 | 
					    duration: entity.duration ?? '0:00:00.00000',
 | 
				
			||||||
    exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
 | 
					    exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
 | 
				
			||||||
    smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
 | 
					    smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
 | 
				
			||||||
@ -77,7 +75,6 @@ export function mapAssetWithoutExif(entity: AssetEntity): AssetResponseDto {
 | 
				
			|||||||
    updatedAt: entity.updatedAt,
 | 
					    updatedAt: entity.updatedAt,
 | 
				
			||||||
    isFavorite: entity.isFavorite,
 | 
					    isFavorite: entity.isFavorite,
 | 
				
			||||||
    isArchived: entity.isArchived,
 | 
					    isArchived: entity.isArchived,
 | 
				
			||||||
    mimeType: entity.mimeType,
 | 
					 | 
				
			||||||
    duration: entity.duration ?? '0:00:00.00000',
 | 
					    duration: entity.duration ?? '0:00:00.00000',
 | 
				
			||||||
    exifInfo: undefined,
 | 
					    exifInfo: undefined,
 | 
				
			||||||
    smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
 | 
					    smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined,
 | 
				
			||||||
 | 
				
			|||||||
@ -1,4 +1,5 @@
 | 
				
			|||||||
import { BadRequestException } from '@nestjs/common';
 | 
					import { BadRequestException } from '@nestjs/common';
 | 
				
			||||||
 | 
					import { extname } from 'node:path';
 | 
				
			||||||
import pkg from 'src/../../package.json';
 | 
					import pkg from 'src/../../package.json';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const [major, minor, patch] = pkg.version.split('.');
 | 
					const [major, minor, patch] = pkg.version.split('.');
 | 
				
			||||||
@ -28,92 +29,78 @@ export function assertMachineLearningEnabled() {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ASSET_MIME_TYPES = [
 | 
					const profile: Record<string, string> = {
 | 
				
			||||||
  'image/3fr',
 | 
					  '.avif': 'image/avif',
 | 
				
			||||||
  'image/ari',
 | 
					  '.dng': 'image/x-adobe-dng',
 | 
				
			||||||
  'image/arw',
 | 
					  '.heic': 'image/heic',
 | 
				
			||||||
  'image/avif',
 | 
					  '.heif': 'image/heif',
 | 
				
			||||||
  'image/cap',
 | 
					  '.jpeg': 'image/jpeg',
 | 
				
			||||||
  'image/cin',
 | 
					  '.jpg': 'image/jpeg',
 | 
				
			||||||
  'image/cr2',
 | 
					  '.png': 'image/png',
 | 
				
			||||||
  'image/cr3',
 | 
					  '.webp': 'image/webp',
 | 
				
			||||||
  'image/crw',
 | 
					};
 | 
				
			||||||
  'image/dcr',
 | 
					
 | 
				
			||||||
  'image/dng',
 | 
					const image: Record<string, string> = {
 | 
				
			||||||
  'image/erf',
 | 
					  ...profile,
 | 
				
			||||||
  'image/fff',
 | 
					  '.3fr': 'image/x-hasselblad-3fr',
 | 
				
			||||||
  'image/gif',
 | 
					  '.ari': 'image/x-arriflex-ari',
 | 
				
			||||||
  'image/heic',
 | 
					  '.arw': 'image/x-sony-arw',
 | 
				
			||||||
  'image/heif',
 | 
					  '.cap': 'image/x-phaseone-cap',
 | 
				
			||||||
  'image/iiq',
 | 
					  '.cin': 'image/x-phantom-cin',
 | 
				
			||||||
  'image/jpeg',
 | 
					  '.cr2': 'image/x-canon-cr2',
 | 
				
			||||||
  'image/jxl',
 | 
					  '.cr3': 'image/x-canon-cr3',
 | 
				
			||||||
  'image/k25',
 | 
					  '.crw': 'image/x-canon-crw',
 | 
				
			||||||
  'image/kdc',
 | 
					  '.dcr': 'image/x-kodak-dcr',
 | 
				
			||||||
  'image/mrw',
 | 
					  '.erf': 'image/x-epson-erf',
 | 
				
			||||||
  'image/nef',
 | 
					  '.fff': 'image/x-hasselblad-fff',
 | 
				
			||||||
  'image/orf',
 | 
					  '.gif': 'image/gif',
 | 
				
			||||||
  'image/ori',
 | 
					  '.iiq': 'image/x-phaseone-iiq',
 | 
				
			||||||
  'image/pef',
 | 
					  '.k25': 'image/x-kodak-k25',
 | 
				
			||||||
  'image/png',
 | 
					  '.kdc': 'image/x-kodak-kdc',
 | 
				
			||||||
  'image/raf',
 | 
					  '.mrw': 'image/x-minolta-mrw',
 | 
				
			||||||
  'image/raw',
 | 
					  '.nef': 'image/x-nikon-nef',
 | 
				
			||||||
  'image/rwl',
 | 
					  '.orf': 'image/x-olympus-orf',
 | 
				
			||||||
  'image/sr2',
 | 
					  '.ori': 'image/x-olympus-ori',
 | 
				
			||||||
  'image/srf',
 | 
					  '.pef': 'image/x-pentax-pef',
 | 
				
			||||||
  'image/srw',
 | 
					  '.raf': 'image/x-fuji-raf',
 | 
				
			||||||
  'image/tiff',
 | 
					  '.raw': 'image/x-panasonic-raw',
 | 
				
			||||||
  'image/webp',
 | 
					  '.rwl': 'image/x-leica-rwl',
 | 
				
			||||||
  'image/x-adobe-dng',
 | 
					  '.sr2': 'image/x-sony-sr2',
 | 
				
			||||||
  'image/x-arriflex-ari',
 | 
					  '.srf': 'image/x-sony-srf',
 | 
				
			||||||
  'image/x-canon-cr2',
 | 
					  '.srw': 'image/x-samsung-srw',
 | 
				
			||||||
  'image/x-canon-cr3',
 | 
					  '.tiff': 'image/tiff',
 | 
				
			||||||
  'image/x-canon-crw',
 | 
					  '.x3f': 'image/x-sigma-x3f',
 | 
				
			||||||
  'image/x-epson-erf',
 | 
					};
 | 
				
			||||||
  'image/x-fuji-raf',
 | 
					
 | 
				
			||||||
  'image/x-hasselblad-3fr',
 | 
					const video: Record<string, string> = {
 | 
				
			||||||
  'image/x-hasselblad-fff',
 | 
					  '.3gp': 'video/3gpp',
 | 
				
			||||||
  'image/x-kodak-dcr',
 | 
					  '.avi': 'video/x-msvideo',
 | 
				
			||||||
  'image/x-kodak-k25',
 | 
					  '.flv': 'video/x-flv',
 | 
				
			||||||
  'image/x-kodak-kdc',
 | 
					  '.mkv': 'video/x-matroska',
 | 
				
			||||||
  'image/x-leica-rwl',
 | 
					  '.mov': 'video/quicktime',
 | 
				
			||||||
  'image/x-minolta-mrw',
 | 
					  '.mp2t': 'video/mp2t',
 | 
				
			||||||
  'image/x-nikon-nef',
 | 
					  '.mp4': 'video/mp4',
 | 
				
			||||||
  'image/x-olympus-orf',
 | 
					  '.mpeg': 'video/mpeg',
 | 
				
			||||||
  'image/x-olympus-ori',
 | 
					  '.webm': 'video/webm',
 | 
				
			||||||
  'image/x-panasonic-raw',
 | 
					  '.wmv': 'video/x-ms-wmv',
 | 
				
			||||||
  'image/x-pentax-pef',
 | 
					};
 | 
				
			||||||
  'image/x-phantom-cin',
 | 
					
 | 
				
			||||||
  'image/x-phaseone-cap',
 | 
					const sidecar: Record<string, string> = {
 | 
				
			||||||
  'image/x-phaseone-iiq',
 | 
					  '.xmp': 'application/xml',
 | 
				
			||||||
  'image/x-samsung-srw',
 | 
					};
 | 
				
			||||||
  'image/x-sigma-x3f',
 | 
					
 | 
				
			||||||
  'image/x-sony-arw',
 | 
					const isType = (filename: string, lookup: Record<string, string>) => !!lookup[extname(filename).toLowerCase()];
 | 
				
			||||||
  'image/x-sony-sr2',
 | 
					const getType = (filename: string, lookup: Record<string, string>) => lookup[extname(filename).toLowerCase()];
 | 
				
			||||||
  'image/x-sony-srf',
 | 
					
 | 
				
			||||||
  'image/x3f',
 | 
					export const mimeTypes = {
 | 
				
			||||||
  'video/3gpp',
 | 
					  image,
 | 
				
			||||||
  'video/avi',
 | 
					  profile,
 | 
				
			||||||
  'video/mp2t',
 | 
					  sidecar,
 | 
				
			||||||
  'video/mp4',
 | 
					  video,
 | 
				
			||||||
  'video/mpeg',
 | 
					
 | 
				
			||||||
  'video/msvideo',
 | 
					  isAsset: (filename: string) => isType(filename, image) || isType(filename, video),
 | 
				
			||||||
  'video/quicktime',
 | 
					  isProfile: (filename: string) => isType(filename, profile),
 | 
				
			||||||
  'video/vnd.avi',
 | 
					  isSidecar: (filename: string) => isType(filename, sidecar),
 | 
				
			||||||
  'video/webm',
 | 
					  isVideo: (filename: string) => isType(filename, video),
 | 
				
			||||||
  'video/x-flv',
 | 
					  lookup: (filename: string) => getType(filename, { ...image, ...video, ...sidecar }) || 'application/octet-stream',
 | 
				
			||||||
  'video/x-matroska',
 | 
					};
 | 
				
			||||||
  'video/x-ms-wmv',
 | 
					 | 
				
			||||||
  'video/x-msvideo',
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
export const LIVE_PHOTO_MIME_TYPES = ASSET_MIME_TYPES;
 | 
					 | 
				
			||||||
export const SIDECAR_MIME_TYPES = ['application/xml', 'text/xml'];
 | 
					 | 
				
			||||||
export const PROFILE_MIME_TYPES = [
 | 
					 | 
				
			||||||
  'image/jpeg',
 | 
					 | 
				
			||||||
  'image/png',
 | 
					 | 
				
			||||||
  'image/heic',
 | 
					 | 
				
			||||||
  'image/heif',
 | 
					 | 
				
			||||||
  'image/dng',
 | 
					 | 
				
			||||||
  'image/webp',
 | 
					 | 
				
			||||||
  'image/avif',
 | 
					 | 
				
			||||||
];
 | 
					 | 
				
			||||||
 | 
				
			|||||||
@ -268,7 +268,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      expect(assetMock.getByIds).toHaveBeenCalledWith(['asset-1']);
 | 
					      expect(assetMock.getByIds).toHaveBeenCalledWith(['asset-1']);
 | 
				
			||||||
      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id');
 | 
					      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id');
 | 
				
			||||||
      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.ext', {
 | 
					      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
				
			||||||
        left: 95,
 | 
					        left: 95,
 | 
				
			||||||
        top: 95,
 | 
					        top: 95,
 | 
				
			||||||
        width: 110,
 | 
					        width: 110,
 | 
				
			||||||
@ -289,7 +289,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      await sut.handleGenerateFaceThumbnail(face.start);
 | 
					      await sut.handleGenerateFaceThumbnail(face.start);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.ext', {
 | 
					      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
				
			||||||
        left: 0,
 | 
					        left: 0,
 | 
				
			||||||
        top: 0,
 | 
					        top: 0,
 | 
				
			||||||
        width: 510,
 | 
					        width: 510,
 | 
				
			||||||
@ -306,7 +306,7 @@ describe(FacialRecognitionService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      await sut.handleGenerateFaceThumbnail(face.end);
 | 
					      await sut.handleGenerateFaceThumbnail(face.end);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.ext', {
 | 
					      expect(mediaMock.crop).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg', {
 | 
				
			||||||
        left: 297,
 | 
					        left: 297,
 | 
				
			||||||
        top: 297,
 | 
					        top: 297,
 | 
				
			||||||
        width: 202,
 | 
					        width: 202,
 | 
				
			||||||
 | 
				
			|||||||
@ -116,7 +116,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
      await sut.handleGenerateJpegThumbnail({ id: assetEntityStub.image.id });
 | 
					      await sut.handleGenerateJpegThumbnail({ id: assetEntityStub.image.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id');
 | 
					      expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/user-id');
 | 
				
			||||||
      expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.ext', 'upload/thumbs/user-id/asset-id.jpeg', {
 | 
					      expect(mediaMock.resize).toHaveBeenCalledWith('/original/path.jpg', 'upload/thumbs/user-id/asset-id.jpeg', {
 | 
				
			||||||
        size: 1440,
 | 
					        size: 1440,
 | 
				
			||||||
        format: 'jpeg',
 | 
					        format: 'jpeg',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@ -167,11 +167,11 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
      await sut.handleGenerateWebpThumbnail({ id: assetEntityStub.image.id });
 | 
					      await sut.handleGenerateWebpThumbnail({ id: assetEntityStub.image.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(mediaMock.resize).toHaveBeenCalledWith(
 | 
					      expect(mediaMock.resize).toHaveBeenCalledWith(
 | 
				
			||||||
        '/uploads/user-id/thumbs/path.ext',
 | 
					        '/uploads/user-id/thumbs/path.jpg',
 | 
				
			||||||
        '/uploads/user-id/thumbs/path.ext',
 | 
					        '/uploads/user-id/thumbs/path.webp',
 | 
				
			||||||
        { format: 'webp', size: 250 },
 | 
					        { format: 'webp', size: 250 },
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-id', webpPath: '/uploads/user-id/thumbs/path.ext' });
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-id', webpPath: '/uploads/user-id/thumbs/path.webp' });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -195,7 +195,7 @@ describe(MediaService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      await sut.handleGenerateThumbhashThumbnail({ id: assetEntityStub.image.id });
 | 
					      await sut.handleGenerateThumbhashThumbnail({ id: assetEntityStub.image.id });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.ext');
 | 
					      expect(mediaMock.generateThumbhash).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg');
 | 
				
			||||||
      expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({ id: 'asset-id', thumbhash: thumbhashBuffer });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
				
			|||||||
@ -89,10 +89,10 @@ export class MediaService {
 | 
				
			|||||||
      return false;
 | 
					      return false;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const webpPath = asset.resizePath.replace('jpeg', 'webp');
 | 
					    const webpPath = asset.resizePath.replace('jpeg', 'webp').replace('jpg', 'webp');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this.mediaRepository.resize(asset.resizePath, webpPath, { size: WEBP_THUMBNAIL_SIZE, format: 'webp' });
 | 
					    await this.mediaRepository.resize(asset.resizePath, webpPath, { size: WEBP_THUMBNAIL_SIZE, format: 'webp' });
 | 
				
			||||||
    await this.assetRepository.save({ id: asset.id, webpPath: webpPath });
 | 
					    await this.assetRepository.save({ id: asset.id, webpPath });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return true;
 | 
					    return true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -82,10 +82,10 @@ describe(MetadataService.name, () => {
 | 
				
			|||||||
      assetMock.save.mockResolvedValue(assetEntityStub.image);
 | 
					      assetMock.save.mockResolvedValue(assetEntityStub.image);
 | 
				
			||||||
      storageMock.checkFileExists.mockResolvedValue(true);
 | 
					      storageMock.checkFileExists.mockResolvedValue(true);
 | 
				
			||||||
      await sut.handleSidecarDiscovery({ id: assetEntityStub.image.id });
 | 
					      await sut.handleSidecarDiscovery({ id: assetEntityStub.image.id });
 | 
				
			||||||
      expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.ext.xmp', constants.R_OK);
 | 
					      expect(storageMock.checkFileExists).toHaveBeenCalledWith('/original/path.jpg.xmp', constants.R_OK);
 | 
				
			||||||
      expect(assetMock.save).toHaveBeenCalledWith({
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
        id: assetEntityStub.image.id,
 | 
					        id: assetEntityStub.image.id,
 | 
				
			||||||
        sidecarPath: '/original/path.ext.xmp',
 | 
					        sidecarPath: '/original/path.jpg.xmp',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,7 @@ import { PersonService } from './person.service';
 | 
				
			|||||||
const responseDto: PersonResponseDto = {
 | 
					const responseDto: PersonResponseDto = {
 | 
				
			||||||
  id: 'person-1',
 | 
					  id: 'person-1',
 | 
				
			||||||
  name: 'Person 1',
 | 
					  name: 'Person 1',
 | 
				
			||||||
  thumbnailPath: '/path/to/thumbnail',
 | 
					  thumbnailPath: '/path/to/thumbnail.jpg',
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe(PersonService.name, () => {
 | 
					describe(PersonService.name, () => {
 | 
				
			||||||
@ -74,7 +74,7 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
    it('should serve the thumbnail', async () => {
 | 
					    it('should serve the thumbnail', async () => {
 | 
				
			||||||
      personMock.getById.mockResolvedValue(personStub.noName);
 | 
					      personMock.getById.mockResolvedValue(personStub.noName);
 | 
				
			||||||
      await sut.getThumbnail(authStub.admin, 'person-1');
 | 
					      await sut.getThumbnail(authStub.admin, 'person-1');
 | 
				
			||||||
      expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail', 'image/jpeg');
 | 
					      expect(storageMock.createReadStream).toHaveBeenCalledWith('/path/to/thumbnail.jpg', 'image/jpeg');
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -150,7 +150,7 @@ describe(PersonService.name, () => {
 | 
				
			|||||||
      expect(personMock.delete).toHaveBeenCalledWith(personStub.noName);
 | 
					      expect(personMock.delete).toHaveBeenCalledWith(personStub.noName);
 | 
				
			||||||
      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
					      expect(jobMock.queue).toHaveBeenCalledWith({
 | 
				
			||||||
        name: JobName.DELETE_FILES,
 | 
					        name: JobName.DELETE_FILES,
 | 
				
			||||||
        data: { files: ['/path/to/thumbnail'] },
 | 
					        data: { files: ['/path/to/thumbnail.jpg'] },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
				
			|||||||
@ -2,6 +2,7 @@ import { PersonEntity } from '@app/infra/entities';
 | 
				
			|||||||
import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
 | 
					import { BadRequestException, Inject, Injectable, Logger, NotFoundException } from '@nestjs/common';
 | 
				
			||||||
import { AssetResponseDto, mapAsset } from '../asset';
 | 
					import { AssetResponseDto, mapAsset } from '../asset';
 | 
				
			||||||
import { AuthUserDto } from '../auth';
 | 
					import { AuthUserDto } from '../auth';
 | 
				
			||||||
 | 
					import { mimeTypes } from '../domain.constant';
 | 
				
			||||||
import { IJobRepository, JobName } from '../job';
 | 
					import { IJobRepository, JobName } from '../job';
 | 
				
			||||||
import { ImmichReadStream, IStorageRepository } from '../storage';
 | 
					import { ImmichReadStream, IStorageRepository } from '../storage';
 | 
				
			||||||
import { mapPerson, PersonResponseDto, PersonUpdateDto } from './person.dto';
 | 
					import { mapPerson, PersonResponseDto, PersonUpdateDto } from './person.dto';
 | 
				
			||||||
@ -44,7 +45,7 @@ export class PersonService {
 | 
				
			|||||||
      throw new NotFoundException();
 | 
					      throw new NotFoundException();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return this.storageRepository.createReadStream(person.thumbnailPath, 'image/jpeg');
 | 
					    return this.storageRepository.createReadStream(person.thumbnailPath, mimeTypes.lookup(person.thumbnailPath));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async getAssets(authUser: AuthUserDto, personId: string): Promise<AssetResponseDto[]> {
 | 
					  async getAssets(authUser: AuthUserDto, personId: string): Promise<AssetResponseDto[]> {
 | 
				
			||||||
 | 
				
			|||||||
@ -56,11 +56,11 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
      userMock.getList.mockResolvedValue([userEntityStub.user1]);
 | 
					      userMock.getList.mockResolvedValue([userEntityStub.user1]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      when(storageMock.checkFileExists)
 | 
					      when(storageMock.checkFileExists)
 | 
				
			||||||
        .calledWith('upload/library/user-id/2023/2023-02-23/asset-id.ext')
 | 
					        .calledWith('upload/library/user-id/2023/2023-02-23/asset-id.jpg')
 | 
				
			||||||
        .mockResolvedValue(true);
 | 
					        .mockResolvedValue(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      when(storageMock.checkFileExists)
 | 
					      when(storageMock.checkFileExists)
 | 
				
			||||||
        .calledWith('upload/library/user-id/2023/2023-02-23/asset-id+1.ext')
 | 
					        .calledWith('upload/library/user-id/2023/2023-02-23/asset-id+1.jpg')
 | 
				
			||||||
        .mockResolvedValue(false);
 | 
					        .mockResolvedValue(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      await sut.handleMigration();
 | 
					      await sut.handleMigration();
 | 
				
			||||||
@ -69,7 +69,7 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
      expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2);
 | 
					      expect(storageMock.checkFileExists).toHaveBeenCalledTimes(2);
 | 
				
			||||||
      expect(assetMock.save).toHaveBeenCalledWith({
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
        id: assetEntityStub.image.id,
 | 
					        id: assetEntityStub.image.id,
 | 
				
			||||||
        originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.ext',
 | 
					        originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(userMock.getList).toHaveBeenCalled();
 | 
					      expect(userMock.getList).toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -79,7 +79,7 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
        items: [
 | 
					        items: [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            ...assetEntityStub.image,
 | 
					            ...assetEntityStub.image,
 | 
				
			||||||
            originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.ext',
 | 
					            originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        hasNextPage: false,
 | 
					        hasNextPage: false,
 | 
				
			||||||
@ -99,7 +99,7 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
        items: [
 | 
					        items: [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            ...assetEntityStub.image,
 | 
					            ...assetEntityStub.image,
 | 
				
			||||||
            originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.ext',
 | 
					            originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg',
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
        hasNextPage: false,
 | 
					        hasNextPage: false,
 | 
				
			||||||
@ -126,12 +126,12 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      expect(assetMock.getAll).toHaveBeenCalled();
 | 
					      expect(assetMock.getAll).toHaveBeenCalled();
 | 
				
			||||||
      expect(storageMock.moveFile).toHaveBeenCalledWith(
 | 
					      expect(storageMock.moveFile).toHaveBeenCalledWith(
 | 
				
			||||||
        '/original/path.ext',
 | 
					        '/original/path.jpg',
 | 
				
			||||||
        'upload/library/user-id/2023/2023-02-23/asset-id.ext',
 | 
					        'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      expect(assetMock.save).toHaveBeenCalledWith({
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
        id: assetEntityStub.image.id,
 | 
					        id: assetEntityStub.image.id,
 | 
				
			||||||
        originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.ext',
 | 
					        originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -147,12 +147,12 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      expect(assetMock.getAll).toHaveBeenCalled();
 | 
					      expect(assetMock.getAll).toHaveBeenCalled();
 | 
				
			||||||
      expect(storageMock.moveFile).toHaveBeenCalledWith(
 | 
					      expect(storageMock.moveFile).toHaveBeenCalledWith(
 | 
				
			||||||
        '/original/path.ext',
 | 
					        '/original/path.jpg',
 | 
				
			||||||
        'upload/library/label-1/2023/2023-02-23/asset-id.ext',
 | 
					        'upload/library/label-1/2023/2023-02-23/asset-id.jpg',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      expect(assetMock.save).toHaveBeenCalledWith({
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
        id: assetEntityStub.image.id,
 | 
					        id: assetEntityStub.image.id,
 | 
				
			||||||
        originalPath: 'upload/library/label-1/2023/2023-02-23/asset-id.ext',
 | 
					        originalPath: 'upload/library/label-1/2023/2023-02-23/asset-id.jpg',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -168,8 +168,8 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      expect(assetMock.getAll).toHaveBeenCalled();
 | 
					      expect(assetMock.getAll).toHaveBeenCalled();
 | 
				
			||||||
      expect(storageMock.moveFile).toHaveBeenCalledWith(
 | 
					      expect(storageMock.moveFile).toHaveBeenCalledWith(
 | 
				
			||||||
        '/original/path.ext',
 | 
					        '/original/path.jpg',
 | 
				
			||||||
        'upload/library/user-id/2023/2023-02-23/asset-id.ext',
 | 
					        'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      expect(assetMock.save).not.toHaveBeenCalled();
 | 
					      expect(assetMock.save).not.toHaveBeenCalled();
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@ -187,11 +187,11 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
      expect(assetMock.getAll).toHaveBeenCalled();
 | 
					      expect(assetMock.getAll).toHaveBeenCalled();
 | 
				
			||||||
      expect(assetMock.save).toHaveBeenCalledWith({
 | 
					      expect(assetMock.save).toHaveBeenCalledWith({
 | 
				
			||||||
        id: assetEntityStub.image.id,
 | 
					        id: assetEntityStub.image.id,
 | 
				
			||||||
        originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.ext',
 | 
					        originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id.jpg',
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      expect(storageMock.moveFile.mock.calls).toEqual([
 | 
					      expect(storageMock.moveFile.mock.calls).toEqual([
 | 
				
			||||||
        ['/original/path.ext', 'upload/library/user-id/2023/2023-02-23/asset-id.ext'],
 | 
					        ['/original/path.jpg', 'upload/library/user-id/2023/2023-02-23/asset-id.jpg'],
 | 
				
			||||||
        ['upload/library/user-id/2023/2023-02-23/asset-id.ext', '/original/path.ext'],
 | 
					        ['upload/library/user-id/2023/2023-02-23/asset-id.jpg', '/original/path.jpg'],
 | 
				
			||||||
      ]);
 | 
					      ]);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -200,7 +200,7 @@ describe(StorageTemplateService.name, () => {
 | 
				
			|||||||
        items: [
 | 
					        items: [
 | 
				
			||||||
          {
 | 
					          {
 | 
				
			||||||
            ...assetEntityStub.image,
 | 
					            ...assetEntityStub.image,
 | 
				
			||||||
            originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.ext',
 | 
					            originalPath: 'upload/library/user-id/2023/2023-02-23/asset-id+1.jpg',
 | 
				
			||||||
            isReadOnly: true,
 | 
					            isReadOnly: true,
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
 | 
				
			|||||||
@ -17,7 +17,6 @@ export class AssetCore {
 | 
				
			|||||||
    const asset = await this.repository.create({
 | 
					    const asset = await this.repository.create({
 | 
				
			||||||
      owner: { id: authUser.id } as UserEntity,
 | 
					      owner: { id: authUser.id } as UserEntity,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      mimeType: file.mimeType,
 | 
					 | 
				
			||||||
      checksum: file.checksum,
 | 
					      checksum: file.checksum,
 | 
				
			||||||
      originalPath: file.originalPath,
 | 
					      originalPath: file.originalPath,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,12 +1,9 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  ASSET_MIME_TYPES,
 | 
					 | 
				
			||||||
  ICryptoRepository,
 | 
					  ICryptoRepository,
 | 
				
			||||||
  IJobRepository,
 | 
					  IJobRepository,
 | 
				
			||||||
  IStorageRepository,
 | 
					  IStorageRepository,
 | 
				
			||||||
  JobName,
 | 
					  JobName,
 | 
				
			||||||
  LIVE_PHOTO_MIME_TYPES,
 | 
					  mimeTypes,
 | 
				
			||||||
  PROFILE_MIME_TYPES,
 | 
					 | 
				
			||||||
  SIDECAR_MIME_TYPES,
 | 
					 | 
				
			||||||
  UploadFieldName,
 | 
					  UploadFieldName,
 | 
				
			||||||
} from '@app/domain';
 | 
					} from '@app/domain';
 | 
				
			||||||
import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
					import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities';
 | 
				
			||||||
@ -60,7 +57,6 @@ const _getAsset_1 = () => {
 | 
				
			|||||||
  asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z');
 | 
					  asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z');
 | 
				
			||||||
  asset_1.isFavorite = false;
 | 
					  asset_1.isFavorite = false;
 | 
				
			||||||
  asset_1.isArchived = false;
 | 
					  asset_1.isArchived = false;
 | 
				
			||||||
  asset_1.mimeType = 'image/jpeg';
 | 
					 | 
				
			||||||
  asset_1.webpPath = '';
 | 
					  asset_1.webpPath = '';
 | 
				
			||||||
  asset_1.encodedVideoPath = '';
 | 
					  asset_1.encodedVideoPath = '';
 | 
				
			||||||
  asset_1.duration = '0:00:00.000000';
 | 
					  asset_1.duration = '0:00:00.000000';
 | 
				
			||||||
@ -85,7 +81,6 @@ const _getAsset_2 = () => {
 | 
				
			|||||||
  asset_2.updatedAt = new Date('2022-06-19T23:41:36.910Z');
 | 
					  asset_2.updatedAt = new Date('2022-06-19T23:41:36.910Z');
 | 
				
			||||||
  asset_2.isFavorite = false;
 | 
					  asset_2.isFavorite = false;
 | 
				
			||||||
  asset_2.isArchived = false;
 | 
					  asset_2.isArchived = false;
 | 
				
			||||||
  asset_2.mimeType = 'image/jpeg';
 | 
					 | 
				
			||||||
  asset_2.webpPath = '';
 | 
					  asset_2.webpPath = '';
 | 
				
			||||||
  asset_2.encodedVideoPath = '';
 | 
					  asset_2.encodedVideoPath = '';
 | 
				
			||||||
  asset_2.duration = '0:00:00.000000';
 | 
					  asset_2.duration = '0:00:00.000000';
 | 
				
			||||||
@ -132,24 +127,11 @@ const uploadFile = {
 | 
				
			|||||||
    authUser: null,
 | 
					    authUser: null,
 | 
				
			||||||
    fieldName: UploadFieldName.ASSET_DATA,
 | 
					    fieldName: UploadFieldName.ASSET_DATA,
 | 
				
			||||||
    file: {
 | 
					    file: {
 | 
				
			||||||
      mimeType: 'image/jpeg',
 | 
					 | 
				
			||||||
      checksum: Buffer.from('checksum', 'utf8'),
 | 
					      checksum: Buffer.from('checksum', 'utf8'),
 | 
				
			||||||
      originalPath: 'upload/admin/image.jpeg',
 | 
					      originalPath: 'upload/admin/image.jpeg',
 | 
				
			||||||
      originalName: 'image.jpeg',
 | 
					      originalName: 'image.jpeg',
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  mimeType: (fieldName: UploadFieldName, mimeType: string) => {
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      authUser: authStub.admin,
 | 
					 | 
				
			||||||
      fieldName,
 | 
					 | 
				
			||||||
      file: {
 | 
					 | 
				
			||||||
        mimeType,
 | 
					 | 
				
			||||||
        checksum: Buffer.from('checksum', 'utf8'),
 | 
					 | 
				
			||||||
        originalPath: 'upload/admin/image.jpeg',
 | 
					 | 
				
			||||||
        originalName: 'image.jpeg',
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  filename: (fieldName: UploadFieldName, filename: string) => {
 | 
					  filename: (fieldName: UploadFieldName, filename: string) => {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      authUser: authStub.admin,
 | 
					      authUser: authStub.admin,
 | 
				
			||||||
@ -164,6 +146,33 @@ const uploadFile = {
 | 
				
			|||||||
  },
 | 
					  },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const uploadTests = [
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    label: 'asset',
 | 
				
			||||||
 | 
					    fieldName: UploadFieldName.ASSET_DATA,
 | 
				
			||||||
 | 
					    filetypes: Object.keys({ ...mimeTypes.image, ...mimeTypes.video }),
 | 
				
			||||||
 | 
					    invalid: ['.xml', '.html'],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    label: 'live photo',
 | 
				
			||||||
 | 
					    fieldName: UploadFieldName.LIVE_PHOTO_DATA,
 | 
				
			||||||
 | 
					    filetypes: Object.keys(mimeTypes.video),
 | 
				
			||||||
 | 
					    invalid: ['.xml', '.html', '.jpg', '.jpeg'],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    label: 'sidecar',
 | 
				
			||||||
 | 
					    fieldName: UploadFieldName.SIDECAR_DATA,
 | 
				
			||||||
 | 
					    filetypes: Object.keys(mimeTypes.sidecar),
 | 
				
			||||||
 | 
					    invalid: ['.xml', '.html', '.jpg', '.jpeg', '.mov', '.mp4'],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    label: 'profile',
 | 
				
			||||||
 | 
					    fieldName: UploadFieldName.PROFILE_DATA,
 | 
				
			||||||
 | 
					    filetypes: Object.keys(mimeTypes.profile),
 | 
				
			||||||
 | 
					    invalid: ['.xml', '.html', '.cr2', '.arf', '.mov', '.mp4'],
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
describe('AssetService', () => {
 | 
					describe('AssetService', () => {
 | 
				
			||||||
  let sut: AssetService;
 | 
					  let sut: AssetService;
 | 
				
			||||||
  let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
 | 
					  let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
 | 
				
			||||||
@ -195,8 +204,6 @@ describe('AssetService', () => {
 | 
				
			|||||||
      getByOriginalPath: jest.fn(),
 | 
					      getByOriginalPath: jest.fn(),
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    cryptoMock = newCryptoRepositoryMock();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    accessMock = newAccessRepositoryMock();
 | 
					    accessMock = newAccessRepositoryMock();
 | 
				
			||||||
    cryptoMock = newCryptoRepositoryMock();
 | 
					    cryptoMock = newCryptoRepositoryMock();
 | 
				
			||||||
    jobMock = newJobRepositoryMock();
 | 
					    jobMock = newJobRepositoryMock();
 | 
				
			||||||
@ -212,60 +219,106 @@ describe('AssetService', () => {
 | 
				
			|||||||
      .mockResolvedValue(assetEntityStub.livePhotoMotionAsset);
 | 
					      .mockResolvedValue(assetEntityStub.livePhotoMotionAsset);
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const tests = [
 | 
					  describe('mime types linting', () => {
 | 
				
			||||||
    { label: 'asset', fieldName: UploadFieldName.ASSET_DATA, mimeTypes: ASSET_MIME_TYPES },
 | 
					    describe('profile', () => {
 | 
				
			||||||
    { label: 'live photo', fieldName: UploadFieldName.LIVE_PHOTO_DATA, mimeTypes: LIVE_PHOTO_MIME_TYPES },
 | 
					      it('should contain only lowercase mime types', () => {
 | 
				
			||||||
    { label: 'sidecar', fieldName: UploadFieldName.SIDECAR_DATA, mimeTypes: SIDECAR_MIME_TYPES },
 | 
					        const keys = Object.keys(mimeTypes.profile);
 | 
				
			||||||
    { label: 'profile', fieldName: UploadFieldName.PROFILE_DATA, mimeTypes: PROFILE_MIME_TYPES },
 | 
					        expect(keys).toEqual(keys.map((mimeType) => mimeType.toLowerCase()));
 | 
				
			||||||
  ];
 | 
					        const values = Object.values(mimeTypes.profile);
 | 
				
			||||||
 | 
					        expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase()));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  for (const { label, fieldName, mimeTypes } of tests) {
 | 
					 | 
				
			||||||
    describe(`${label} mime types linting`, () => {
 | 
					 | 
				
			||||||
      it('should be a sorted list', () => {
 | 
					      it('should be a sorted list', () => {
 | 
				
			||||||
        expect(mimeTypes).toEqual(mimeTypes.sort());
 | 
					        const keys = Object.keys(mimeTypes.profile);
 | 
				
			||||||
 | 
					        expect(keys).toEqual([...keys].sort());
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      it('should contain only unique values', () => {
 | 
					    describe('image', () => {
 | 
				
			||||||
        expect(mimeTypes).toEqual([...new Set(mimeTypes)]);
 | 
					      it('should contain only lowercase mime types', () => {
 | 
				
			||||||
 | 
					        const keys = Object.keys(mimeTypes.image);
 | 
				
			||||||
 | 
					        expect(keys).toEqual(keys.map((mimeType) => mimeType.toLowerCase()));
 | 
				
			||||||
 | 
					        const values = Object.values(mimeTypes.image);
 | 
				
			||||||
 | 
					        expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase()));
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (fieldName !== UploadFieldName.SIDECAR_DATA) {
 | 
					      it('should be a sorted list', () => {
 | 
				
			||||||
        it('should contain only image or video mime types', () => {
 | 
					        const keys = Object.keys(mimeTypes.image).filter((key) => key in mimeTypes.profile === false);
 | 
				
			||||||
          expect(mimeTypes).toEqual(
 | 
					        expect(keys).toEqual([...keys].sort());
 | 
				
			||||||
            mimeTypes.filter((mimeType) => mimeType.startsWith('image/') || mimeType.startsWith('video/')),
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it('should contain only image mime types', () => {
 | 
				
			||||||
 | 
					        expect(Object.values(mimeTypes.image)).toEqual(
 | 
				
			||||||
 | 
					          Object.values(mimeTypes.image).filter((mimeType) => mimeType.startsWith('image/')),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      }
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe('video', () => {
 | 
				
			||||||
      it('should contain only lowercase mime types', () => {
 | 
					      it('should contain only lowercase mime types', () => {
 | 
				
			||||||
        expect(mimeTypes).toEqual(mimeTypes.map((mimeType) => mimeType.toLowerCase()));
 | 
					        const keys = Object.keys(mimeTypes.video);
 | 
				
			||||||
 | 
					        expect(keys).toEqual(keys.map((mimeType) => mimeType.toLowerCase()));
 | 
				
			||||||
 | 
					        const values = Object.values(mimeTypes.video);
 | 
				
			||||||
 | 
					        expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase()));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it('should be a sorted list', () => {
 | 
				
			||||||
 | 
					        const keys = Object.keys(mimeTypes.video);
 | 
				
			||||||
 | 
					        expect(keys).toEqual([...keys].sort());
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it('should contain only video mime types', () => {
 | 
				
			||||||
 | 
					        expect(Object.values(mimeTypes.video)).toEqual(
 | 
				
			||||||
 | 
					          Object.values(mimeTypes.video).filter((mimeType) => mimeType.startsWith('video/')),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe('sidecar', () => {
 | 
				
			||||||
 | 
					      it('should contain only lowercase mime types', () => {
 | 
				
			||||||
 | 
					        const keys = Object.keys(mimeTypes.sidecar);
 | 
				
			||||||
 | 
					        expect(keys).toEqual(keys.map((mimeType) => mimeType.toLowerCase()));
 | 
				
			||||||
 | 
					        const values = Object.values(mimeTypes.sidecar);
 | 
				
			||||||
 | 
					        expect(values).toEqual(values.map((mimeType) => mimeType.toLowerCase()));
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      it('should be a sorted list', () => {
 | 
				
			||||||
 | 
					        const keys = Object.keys(mimeTypes.sidecar);
 | 
				
			||||||
 | 
					        expect(keys).toEqual([...keys].sort());
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    describe('sidecar', () => {
 | 
				
			||||||
 | 
					      it('should contain only be xml mime type', () => {
 | 
				
			||||||
 | 
					        expect(Object.values(mimeTypes.sidecar)).toEqual(
 | 
				
			||||||
 | 
					          Object.values(mimeTypes.sidecar).filter((mimeType) => mimeType === 'application/xml'),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('canUpload', () => {
 | 
					  describe('canUpload', () => {
 | 
				
			||||||
    it('should require an authenticated user', () => {
 | 
					    it('should require an authenticated user', () => {
 | 
				
			||||||
      expect(() => sut.canUploadFile(uploadFile.nullAuth)).toThrowError(UnauthorizedException);
 | 
					      expect(() => sut.canUploadFile(uploadFile.nullAuth)).toThrowError(UnauthorizedException);
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should accept all accepted mime types', () => {
 | 
					    for (const { fieldName, filetypes, invalid } of uploadTests) {
 | 
				
			||||||
      for (const { fieldName, mimeTypes } of tests) {
 | 
					      describe(`${fieldName}`, () => {
 | 
				
			||||||
        for (const mimeType of mimeTypes) {
 | 
					        for (const filetype of filetypes) {
 | 
				
			||||||
          expect(sut.canUploadFile(uploadFile.mimeType(fieldName, mimeType))).toEqual(true);
 | 
					          it(`should accept ${filetype}`, () => {
 | 
				
			||||||
        }
 | 
					            expect(sut.canUploadFile(uploadFile.filename(fieldName, `asset${filetype}`))).toEqual(true);
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    it('should reject other mime types', () => {
 | 
					        for (const filetype of invalid) {
 | 
				
			||||||
      for (const { fieldName, mimeType } of [
 | 
					          it(`should reject ${filetype}`, () => {
 | 
				
			||||||
        { fieldName: UploadFieldName.ASSET_DATA, mimeType: 'application/html' },
 | 
					            expect(() => sut.canUploadFile(uploadFile.filename(fieldName, `asset${filetype}`))).toThrowError(
 | 
				
			||||||
        { fieldName: UploadFieldName.LIVE_PHOTO_DATA, mimeType: 'application/html' },
 | 
					              BadRequestException,
 | 
				
			||||||
        { fieldName: UploadFieldName.PROFILE_DATA, mimeType: 'application/html' },
 | 
					            );
 | 
				
			||||||
        { fieldName: UploadFieldName.SIDECAR_DATA, mimeType: 'image/jpeg' },
 | 
					          });
 | 
				
			||||||
      ]) {
 | 
					 | 
				
			||||||
        expect(() => sut.canUploadFile(uploadFile.mimeType(fieldName, mimeType))).toThrowError(BadRequestException);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  describe('getUploadFilename', () => {
 | 
					  describe('getUploadFilename', () => {
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,6 @@
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  AccessCore,
 | 
					  AccessCore,
 | 
				
			||||||
  AssetResponseDto,
 | 
					  AssetResponseDto,
 | 
				
			||||||
  ASSET_MIME_TYPES,
 | 
					 | 
				
			||||||
  AuthUserDto,
 | 
					  AuthUserDto,
 | 
				
			||||||
  getLivePhotoMotionFilename,
 | 
					  getLivePhotoMotionFilename,
 | 
				
			||||||
  IAccessRepository,
 | 
					  IAccessRepository,
 | 
				
			||||||
@ -9,12 +8,10 @@ import {
 | 
				
			|||||||
  IJobRepository,
 | 
					  IJobRepository,
 | 
				
			||||||
  IStorageRepository,
 | 
					  IStorageRepository,
 | 
				
			||||||
  JobName,
 | 
					  JobName,
 | 
				
			||||||
  LIVE_PHOTO_MIME_TYPES,
 | 
					 | 
				
			||||||
  mapAsset,
 | 
					  mapAsset,
 | 
				
			||||||
  mapAssetWithoutExif,
 | 
					  mapAssetWithoutExif,
 | 
				
			||||||
 | 
					  mimeTypes,
 | 
				
			||||||
  Permission,
 | 
					  Permission,
 | 
				
			||||||
  PROFILE_MIME_TYPES,
 | 
					 | 
				
			||||||
  SIDECAR_MIME_TYPES,
 | 
					 | 
				
			||||||
  StorageCore,
 | 
					  StorageCore,
 | 
				
			||||||
  StorageFolder,
 | 
					  StorageFolder,
 | 
				
			||||||
  UploadFieldName,
 | 
					  UploadFieldName,
 | 
				
			||||||
@ -33,7 +30,6 @@ import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			|||||||
import { Response as Res } from 'express';
 | 
					import { Response as Res } from 'express';
 | 
				
			||||||
import { constants, createReadStream } from 'fs';
 | 
					import { constants, createReadStream } from 'fs';
 | 
				
			||||||
import fs from 'fs/promises';
 | 
					import fs from 'fs/promises';
 | 
				
			||||||
import mime from 'mime-types';
 | 
					 | 
				
			||||||
import path, { extname } from 'path';
 | 
					import path, { extname } from 'path';
 | 
				
			||||||
import sanitize from 'sanitize-filename';
 | 
					import sanitize from 'sanitize-filename';
 | 
				
			||||||
import { pipeline } from 'stream/promises';
 | 
					import { pipeline } from 'stream/promises';
 | 
				
			||||||
@ -71,11 +67,6 @@ import { CuratedLocationsResponseDto } from './response-dto/curated-locations-re
 | 
				
			|||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 | 
					import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
 | 
				
			||||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
 | 
					import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ServableFile {
 | 
					 | 
				
			||||||
  filepath: string;
 | 
					 | 
				
			||||||
  contentType: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
export class AssetService {
 | 
					export class AssetService {
 | 
				
			||||||
  readonly logger = new Logger(AssetService.name);
 | 
					  readonly logger = new Logger(AssetService.name);
 | 
				
			||||||
@ -98,35 +89,36 @@ export class AssetService {
 | 
				
			|||||||
  canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
 | 
					  canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
 | 
				
			||||||
    this.access.requireUploadAccess(authUser);
 | 
					    this.access.requireUploadAccess(authUser);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const filename = file.originalName;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    switch (fieldName) {
 | 
					    switch (fieldName) {
 | 
				
			||||||
      case UploadFieldName.ASSET_DATA:
 | 
					      case UploadFieldName.ASSET_DATA:
 | 
				
			||||||
        if (ASSET_MIME_TYPES.includes(file.mimeType)) {
 | 
					        if (mimeTypes.isAsset(filename)) {
 | 
				
			||||||
          return true;
 | 
					          return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case UploadFieldName.LIVE_PHOTO_DATA:
 | 
					      case UploadFieldName.LIVE_PHOTO_DATA:
 | 
				
			||||||
        if (LIVE_PHOTO_MIME_TYPES.includes(file.mimeType)) {
 | 
					        if (mimeTypes.isVideo(filename)) {
 | 
				
			||||||
          return true;
 | 
					          return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case UploadFieldName.SIDECAR_DATA:
 | 
					      case UploadFieldName.SIDECAR_DATA:
 | 
				
			||||||
        if (SIDECAR_MIME_TYPES.includes(file.mimeType)) {
 | 
					        if (mimeTypes.isSidecar(filename)) {
 | 
				
			||||||
          return true;
 | 
					          return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      case UploadFieldName.PROFILE_DATA:
 | 
					      case UploadFieldName.PROFILE_DATA:
 | 
				
			||||||
        if (PROFILE_MIME_TYPES.includes(file.mimeType)) {
 | 
					        if (mimeTypes.isProfile(filename)) {
 | 
				
			||||||
          return true;
 | 
					          return true;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        break;
 | 
					        break;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const ext = extname(file.originalName);
 | 
					    this.logger.error(`Unsupported file type ${filename}`);
 | 
				
			||||||
    this.logger.error(`Unsupported file type ${ext} file MIME type ${file.mimeType}`);
 | 
					    throw new BadRequestException(`Unsupported file type ${filename}`);
 | 
				
			||||||
    throw new BadRequestException(`Unsupported file type ${ext}`);
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  getUploadFilename({ authUser, fieldName, file }: UploadRequest): string {
 | 
					  getUploadFilename({ authUser, fieldName, file }: UploadRequest): string {
 | 
				
			||||||
@ -208,16 +200,13 @@ export class AssetService {
 | 
				
			|||||||
      sidecarPath: dto.sidecarPath ? path.resolve(dto.sidecarPath) : undefined,
 | 
					      sidecarPath: dto.sidecarPath ? path.resolve(dto.sidecarPath) : undefined,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const mimeType = mime.lookup(dto.assetPath) as string;
 | 
					    if (!mimeTypes.isAsset(dto.assetPath)) {
 | 
				
			||||||
    if (!ASSET_MIME_TYPES.includes(mimeType)) {
 | 
					      throw new BadRequestException(`Unsupported file type ${dto.assetPath}`);
 | 
				
			||||||
      throw new BadRequestException(`Unsupported file type ${mimeType}`);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (dto.sidecarPath) {
 | 
					    if (dto.sidecarPath && !mimeTypes.isSidecar(dto.sidecarPath)) {
 | 
				
			||||||
      if (path.extname(dto.sidecarPath).toLowerCase() !== '.xmp') {
 | 
					 | 
				
			||||||
      throw new BadRequestException(`Unsupported sidecar file type`);
 | 
					      throw new BadRequestException(`Unsupported sidecar file type`);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    for (const filepath of [dto.assetPath, dto.sidecarPath]) {
 | 
					    for (const filepath of [dto.assetPath, dto.sidecarPath]) {
 | 
				
			||||||
      if (!filepath) {
 | 
					      if (!filepath) {
 | 
				
			||||||
@ -236,7 +225,6 @@ export class AssetService {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const assetFile: UploadFile = {
 | 
					    const assetFile: UploadFile = {
 | 
				
			||||||
      checksum: await this.cryptoRepository.hashFile(dto.assetPath),
 | 
					      checksum: await this.cryptoRepository.hashFile(dto.assetPath),
 | 
				
			||||||
      mimeType,
 | 
					 | 
				
			||||||
      originalPath: dto.assetPath,
 | 
					      originalPath: dto.assetPath,
 | 
				
			||||||
      originalName: path.parse(dto.assetPath).name,
 | 
					      originalName: path.parse(dto.assetPath).name,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
@ -328,8 +316,7 @@ export class AssetService {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
      const [thumbnailPath, contentType] = this.getThumbnailPath(asset, query.format);
 | 
					      return this.streamFile(this.getThumbnailPath(asset, query.format), res, headers);
 | 
				
			||||||
      return this.streamFile(thumbnailPath, res, headers, contentType);
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					    } catch (e) {
 | 
				
			||||||
      res.header('Cache-Control', 'none');
 | 
					      res.header('Cache-Control', 'none');
 | 
				
			||||||
      this.logger.error(`Cannot create read stream for asset ${asset.id}`, 'getAssetThumbnail');
 | 
					      this.logger.error(`Cannot create read stream for asset ${asset.id}`, 'getAssetThumbnail');
 | 
				
			||||||
@ -360,8 +347,7 @@ export class AssetService {
 | 
				
			|||||||
    // Handle Sending Images
 | 
					    // Handle Sending Images
 | 
				
			||||||
    if (asset.type == AssetType.IMAGE) {
 | 
					    if (asset.type == AssetType.IMAGE) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const { filepath, contentType } = this.getServePath(asset, query, allowOriginalFile);
 | 
					        return this.streamFile(this.getServePath(asset, query, allowOriginalFile), res, headers);
 | 
				
			||||||
        return this.streamFile(filepath, res, headers, contentType);
 | 
					 | 
				
			||||||
      } catch (e) {
 | 
					      } catch (e) {
 | 
				
			||||||
        this.logger.error(`Cannot create read stream for asset ${asset.id} ${JSON.stringify(e)}`, 'serveFile[IMAGE]');
 | 
					        this.logger.error(`Cannot create read stream for asset ${asset.id} ${JSON.stringify(e)}`, 'serveFile[IMAGE]');
 | 
				
			||||||
        throw new InternalServerErrorException(
 | 
					        throw new InternalServerErrorException(
 | 
				
			||||||
@ -371,10 +357,7 @@ export class AssetService {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const videoPath = asset.encodedVideoPath ? asset.encodedVideoPath : asset.originalPath;
 | 
					        return this.streamFile(asset.encodedVideoPath || asset.originalPath, res, headers);
 | 
				
			||||||
        const mimeType = asset.encodedVideoPath ? 'video/mp4' : asset.mimeType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        return this.streamFile(videoPath, res, headers, mimeType);
 | 
					 | 
				
			||||||
      } catch (e: Error | any) {
 | 
					      } catch (e: Error | any) {
 | 
				
			||||||
        this.logger.error(`Error serving VIDEO asset=${asset.id}`, e?.stack);
 | 
					        this.logger.error(`Error serving VIDEO asset=${asset.id}`, e?.stack);
 | 
				
			||||||
        throw new InternalServerErrorException(`Failed to serve video asset ${e}`, 'ServeFile');
 | 
					        throw new InternalServerErrorException(`Failed to serve video asset ${e}`, 'ServeFile');
 | 
				
			||||||
@ -595,7 +578,7 @@ export class AssetService {
 | 
				
			|||||||
    switch (format) {
 | 
					    switch (format) {
 | 
				
			||||||
      case GetAssetThumbnailFormatEnum.WEBP:
 | 
					      case GetAssetThumbnailFormatEnum.WEBP:
 | 
				
			||||||
        if (asset.webpPath) {
 | 
					        if (asset.webpPath) {
 | 
				
			||||||
          return [asset.webpPath, 'image/webp'];
 | 
					          return asset.webpPath;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`);
 | 
					        this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -604,48 +587,48 @@ export class AssetService {
 | 
				
			|||||||
        if (!asset.resizePath) {
 | 
					        if (!asset.resizePath) {
 | 
				
			||||||
          throw new NotFoundException(`No thumbnail found for asset ${asset.id}`);
 | 
					          throw new NotFoundException(`No thumbnail found for asset ${asset.id}`);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        return [asset.resizePath, 'image/jpeg'];
 | 
					        return asset.resizePath;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private getServePath(asset: AssetEntity, query: ServeFileDto, allowOriginalFile: boolean): ServableFile {
 | 
					  private getServePath(asset: AssetEntity, query: ServeFileDto, allowOriginalFile: boolean): string {
 | 
				
			||||||
 | 
					    const mimeType = mimeTypes.lookup(asset.originalPath);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Serve file viewer on the web
 | 
					     * Serve file viewer on the web
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    if (query.isWeb && asset.mimeType != 'image/gif') {
 | 
					    if (query.isWeb && mimeType != 'image/gif') {
 | 
				
			||||||
      if (!asset.resizePath) {
 | 
					      if (!asset.resizePath) {
 | 
				
			||||||
        this.logger.error('Error serving IMAGE asset for web');
 | 
					        this.logger.error('Error serving IMAGE asset for web');
 | 
				
			||||||
        throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile');
 | 
					        throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile');
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return { filepath: asset.resizePath, contentType: 'image/jpeg' };
 | 
					      return asset.resizePath;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * Serve thumbnail image for both web and mobile app
 | 
					     * Serve thumbnail image for both web and mobile app
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    if ((!query.isThumb && allowOriginalFile) || (query.isWeb && asset.mimeType === 'image/gif')) {
 | 
					    if ((!query.isThumb && allowOriginalFile) || (query.isWeb && mimeType === 'image/gif')) {
 | 
				
			||||||
      return { filepath: asset.originalPath, contentType: asset.mimeType as string };
 | 
					      return asset.originalPath;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (asset.webpPath && asset.webpPath.length > 0) {
 | 
					    if (asset.webpPath && asset.webpPath.length > 0) {
 | 
				
			||||||
      return { filepath: asset.webpPath, contentType: 'image/webp' };
 | 
					      return asset.webpPath;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!asset.resizePath) {
 | 
					    if (!asset.resizePath) {
 | 
				
			||||||
      throw new Error('resizePath not set');
 | 
					      throw new Error('resizePath not set');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return { filepath: asset.resizePath, contentType: 'image/jpeg' };
 | 
					    return asset.resizePath;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async streamFile(filepath: string, res: Res, headers: Record<string, string>, contentType?: string | null) {
 | 
					  private async streamFile(filepath: string, res: Res, headers: Record<string, string>) {
 | 
				
			||||||
    await fs.access(filepath, constants.R_OK);
 | 
					    await fs.access(filepath, constants.R_OK);
 | 
				
			||||||
    const { size, mtimeNs } = await fs.stat(filepath, { bigint: true });
 | 
					    const { size, mtimeNs } = await fs.stat(filepath, { bigint: true });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (contentType) {
 | 
					    res.header('Content-Type', mimeTypes.lookup(filepath));
 | 
				
			||||||
      res.header('Content-Type', contentType);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const range = this.setResRange(res, headers, Number(size));
 | 
					    const range = this.setResRange(res, headers, Number(size));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -23,7 +23,6 @@ export interface ImmichFile extends Express.Multer.File {
 | 
				
			|||||||
export function mapToUploadFile(file: ImmichFile): UploadFile {
 | 
					export function mapToUploadFile(file: ImmichFile): UploadFile {
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    checksum: file.checksum,
 | 
					    checksum: file.checksum,
 | 
				
			||||||
    mimeType: file.mimetype,
 | 
					 | 
				
			||||||
    originalPath: file.path,
 | 
					    originalPath: file.path,
 | 
				
			||||||
    originalName: Buffer.from(file.originalname, 'latin1').toString('utf8'),
 | 
					    originalName: Buffer.from(file.originalname, 'latin1').toString('utf8'),
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
				
			|||||||
@ -78,9 +78,6 @@ export class AssetEntity {
 | 
				
			|||||||
  @Column({ type: 'boolean', default: false })
 | 
					  @Column({ type: 'boolean', default: false })
 | 
				
			||||||
  isReadOnly!: boolean;
 | 
					  isReadOnly!: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @Column({ type: 'varchar', nullable: true })
 | 
					 | 
				
			||||||
  mimeType!: string | null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @Column({ type: 'bytea' })
 | 
					  @Column({ type: 'bytea' })
 | 
				
			||||||
  @Index()
 | 
					  @Index()
 | 
				
			||||||
  checksum!: Buffer; // sha1 checksum
 | 
					  checksum!: Buffer; // sha1 checksum
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					import { MigrationInterface, QueryRunner } from "typeorm";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export class DropMimeTypeColumn1689001889950 implements MigrationInterface {
 | 
				
			||||||
 | 
					    name = 'DropMimeTypeColumn1689001889950'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async up(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "mimeType"`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public async down(queryRunner: QueryRunner): Promise<void> {
 | 
				
			||||||
 | 
					        await queryRunner.query(`ALTER TABLE "assets" ADD "mimeType" character varying`);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -190,13 +190,11 @@ export const userEntityStub = {
 | 
				
			|||||||
export const fileStub = {
 | 
					export const fileStub = {
 | 
				
			||||||
  livePhotoStill: Object.freeze({
 | 
					  livePhotoStill: Object.freeze({
 | 
				
			||||||
    originalPath: 'fake_path/asset_1.jpeg',
 | 
					    originalPath: 'fake_path/asset_1.jpeg',
 | 
				
			||||||
    mimeType: 'image/jpg',
 | 
					 | 
				
			||||||
    checksum: Buffer.from('file hash', 'utf8'),
 | 
					    checksum: Buffer.from('file hash', 'utf8'),
 | 
				
			||||||
    originalName: 'asset_1.jpeg',
 | 
					    originalName: 'asset_1.jpeg',
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  livePhotoMotion: Object.freeze({
 | 
					  livePhotoMotion: Object.freeze({
 | 
				
			||||||
    originalPath: 'fake_path/asset_1.mp4',
 | 
					    originalPath: 'fake_path/asset_1.mp4',
 | 
				
			||||||
    mimeType: 'image/jpeg',
 | 
					 | 
				
			||||||
    checksum: Buffer.from('live photo file hash', 'utf8'),
 | 
					    checksum: Buffer.from('live photo file hash', 'utf8'),
 | 
				
			||||||
    originalName: 'asset_1.mp4',
 | 
					    originalName: 'asset_1.mp4',
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
@ -221,7 +219,6 @@ export const assetEntityStub = {
 | 
				
			|||||||
    encodedVideoPath: null,
 | 
					    encodedVideoPath: null,
 | 
				
			||||||
    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    mimeType: null,
 | 
					 | 
				
			||||||
    isFavorite: true,
 | 
					    isFavorite: true,
 | 
				
			||||||
    isArchived: false,
 | 
					    isArchived: false,
 | 
				
			||||||
    duration: null,
 | 
					    duration: null,
 | 
				
			||||||
@ -251,7 +248,6 @@ export const assetEntityStub = {
 | 
				
			|||||||
    encodedVideoPath: null,
 | 
					    encodedVideoPath: null,
 | 
				
			||||||
    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    mimeType: null,
 | 
					 | 
				
			||||||
    isFavorite: true,
 | 
					    isFavorite: true,
 | 
				
			||||||
    isArchived: false,
 | 
					    isArchived: false,
 | 
				
			||||||
    duration: null,
 | 
					    duration: null,
 | 
				
			||||||
@ -285,7 +281,6 @@ export const assetEntityStub = {
 | 
				
			|||||||
    encodedVideoPath: null,
 | 
					    encodedVideoPath: null,
 | 
				
			||||||
    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    mimeType: null,
 | 
					 | 
				
			||||||
    isFavorite: true,
 | 
					    isFavorite: true,
 | 
				
			||||||
    isArchived: false,
 | 
					    isArchived: false,
 | 
				
			||||||
    isReadOnly: false,
 | 
					    isReadOnly: false,
 | 
				
			||||||
@ -307,8 +302,8 @@ export const assetEntityStub = {
 | 
				
			|||||||
    owner: userEntityStub.user1,
 | 
					    owner: userEntityStub.user1,
 | 
				
			||||||
    ownerId: 'user-id',
 | 
					    ownerId: 'user-id',
 | 
				
			||||||
    deviceId: 'device-id',
 | 
					    deviceId: 'device-id',
 | 
				
			||||||
    originalPath: '/original/path.ext',
 | 
					    originalPath: '/original/path.jpg',
 | 
				
			||||||
    resizePath: '/uploads/user-id/thumbs/path.ext',
 | 
					    resizePath: '/uploads/user-id/thumbs/path.jpg',
 | 
				
			||||||
    checksum: Buffer.from('file hash', 'utf8'),
 | 
					    checksum: Buffer.from('file hash', 'utf8'),
 | 
				
			||||||
    type: AssetType.IMAGE,
 | 
					    type: AssetType.IMAGE,
 | 
				
			||||||
    webpPath: '/uploads/user-id/webp/path.ext',
 | 
					    webpPath: '/uploads/user-id/webp/path.ext',
 | 
				
			||||||
@ -316,7 +311,6 @@ export const assetEntityStub = {
 | 
				
			|||||||
    encodedVideoPath: null,
 | 
					    encodedVideoPath: null,
 | 
				
			||||||
    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    mimeType: null,
 | 
					 | 
				
			||||||
    isFavorite: true,
 | 
					    isFavorite: true,
 | 
				
			||||||
    isArchived: false,
 | 
					    isArchived: false,
 | 
				
			||||||
    isReadOnly: false,
 | 
					    isReadOnly: false,
 | 
				
			||||||
@ -326,7 +320,7 @@ export const assetEntityStub = {
 | 
				
			|||||||
    livePhotoVideoId: null,
 | 
					    livePhotoVideoId: null,
 | 
				
			||||||
    tags: [],
 | 
					    tags: [],
 | 
				
			||||||
    sharedLinks: [],
 | 
					    sharedLinks: [],
 | 
				
			||||||
    originalFileName: 'asset-id.ext',
 | 
					    originalFileName: 'asset-id.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
    sidecarPath: null,
 | 
					    sidecarPath: null,
 | 
				
			||||||
    exifInfo: {
 | 
					    exifInfo: {
 | 
				
			||||||
@ -351,7 +345,6 @@ export const assetEntityStub = {
 | 
				
			|||||||
    encodedVideoPath: null,
 | 
					    encodedVideoPath: null,
 | 
				
			||||||
    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    mimeType: null,
 | 
					 | 
				
			||||||
    isFavorite: true,
 | 
					    isFavorite: true,
 | 
				
			||||||
    isArchived: false,
 | 
					    isArchived: false,
 | 
				
			||||||
    isReadOnly: false,
 | 
					    isReadOnly: false,
 | 
				
			||||||
@ -412,7 +405,6 @@ export const assetEntityStub = {
 | 
				
			|||||||
    encodedVideoPath: null,
 | 
					    encodedVideoPath: null,
 | 
				
			||||||
    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    mimeType: null,
 | 
					 | 
				
			||||||
    isFavorite: false,
 | 
					    isFavorite: false,
 | 
				
			||||||
    isArchived: false,
 | 
					    isArchived: false,
 | 
				
			||||||
    isReadOnly: false,
 | 
					    isReadOnly: false,
 | 
				
			||||||
@ -447,7 +439,6 @@ export const assetEntityStub = {
 | 
				
			|||||||
    encodedVideoPath: null,
 | 
					    encodedVideoPath: null,
 | 
				
			||||||
    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    createdAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
					    updatedAt: new Date('2023-02-23T05:06:29.716Z'),
 | 
				
			||||||
    mimeType: null,
 | 
					 | 
				
			||||||
    isFavorite: true,
 | 
					    isFavorite: true,
 | 
				
			||||||
    isArchived: false,
 | 
					    isArchived: false,
 | 
				
			||||||
    isReadOnly: false,
 | 
					    isReadOnly: false,
 | 
				
			||||||
@ -621,7 +612,6 @@ const assetResponse: AssetResponseDto = {
 | 
				
			|||||||
  updatedAt: today,
 | 
					  updatedAt: today,
 | 
				
			||||||
  isFavorite: false,
 | 
					  isFavorite: false,
 | 
				
			||||||
  isArchived: false,
 | 
					  isArchived: false,
 | 
				
			||||||
  mimeType: 'image/jpeg',
 | 
					 | 
				
			||||||
  smartInfo: {
 | 
					  smartInfo: {
 | 
				
			||||||
    tags: [],
 | 
					    tags: [],
 | 
				
			||||||
    objects: ['a', 'b', 'c'],
 | 
					    objects: ['a', 'b', 'c'],
 | 
				
			||||||
@ -909,7 +899,6 @@ export const sharedLinkStub = {
 | 
				
			|||||||
          isFavorite: false,
 | 
					          isFavorite: false,
 | 
				
			||||||
          isArchived: false,
 | 
					          isArchived: false,
 | 
				
			||||||
          isReadOnly: false,
 | 
					          isReadOnly: false,
 | 
				
			||||||
          mimeType: 'image/jpeg',
 | 
					 | 
				
			||||||
          smartInfo: {
 | 
					          smartInfo: {
 | 
				
			||||||
            assetId: 'id_1',
 | 
					            assetId: 'id_1',
 | 
				
			||||||
            tags: [],
 | 
					            tags: [],
 | 
				
			||||||
@ -1136,7 +1125,7 @@ export const personStub = {
 | 
				
			|||||||
    ownerId: userEntityStub.admin.id,
 | 
					    ownerId: userEntityStub.admin.id,
 | 
				
			||||||
    owner: userEntityStub.admin,
 | 
					    owner: userEntityStub.admin,
 | 
				
			||||||
    name: '',
 | 
					    name: '',
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail',
 | 
					    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  withName: Object.freeze<PersonEntity>({
 | 
					  withName: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -1146,7 +1135,7 @@ export const personStub = {
 | 
				
			|||||||
    ownerId: userEntityStub.admin.id,
 | 
					    ownerId: userEntityStub.admin.id,
 | 
				
			||||||
    owner: userEntityStub.admin,
 | 
					    owner: userEntityStub.admin,
 | 
				
			||||||
    name: 'Person 1',
 | 
					    name: 'Person 1',
 | 
				
			||||||
    thumbnailPath: '/path/to/thumbnail',
 | 
					    thumbnailPath: '/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
  noThumbnail: Object.freeze<PersonEntity>({
 | 
					  noThumbnail: Object.freeze<PersonEntity>({
 | 
				
			||||||
@ -1166,7 +1155,7 @@ export const personStub = {
 | 
				
			|||||||
    ownerId: userEntityStub.admin.id,
 | 
					    ownerId: userEntityStub.admin.id,
 | 
				
			||||||
    owner: userEntityStub.admin,
 | 
					    owner: userEntityStub.admin,
 | 
				
			||||||
    name: '',
 | 
					    name: '',
 | 
				
			||||||
    thumbnailPath: '/new/path/to/thumbnail',
 | 
					    thumbnailPath: '/new/path/to/thumbnail.jpg',
 | 
				
			||||||
    faces: [],
 | 
					    faces: [],
 | 
				
			||||||
  }),
 | 
					  }),
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										6
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										6
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							@ -679,12 +679,6 @@ export interface AssetResponseDto {
 | 
				
			|||||||
     * @memberof AssetResponseDto
 | 
					     * @memberof AssetResponseDto
 | 
				
			||||||
     */
 | 
					     */
 | 
				
			||||||
    'isArchived': boolean;
 | 
					    'isArchived': boolean;
 | 
				
			||||||
    /**
 | 
					 | 
				
			||||||
     * 
 | 
					 | 
				
			||||||
     * @type {string}
 | 
					 | 
				
			||||||
     * @memberof AssetResponseDto
 | 
					 | 
				
			||||||
     */
 | 
					 | 
				
			||||||
    'mimeType': string | null;
 | 
					 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
     * 
 | 
					     * 
 | 
				
			||||||
     * @type {string}
 | 
					     * @type {string}
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user