Przeglądaj źródła

fix(server): more asset upload validation and docs

Michel Heusschen 2 lat temu
rodzic
commit
22a973e6e0

+ 1 - 1
mobile/openapi/README.md

@@ -3,7 +3,7 @@ Immich API
 
 This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
 
-- API version: 1.45.0
+- API version: 1.46.1
 - Build package: org.openapitools.codegen.languages.DartClientCodegen
 
 ## Requirements

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

@@ -1059,7 +1059,7 @@ Name | Type | Description  | Notes
 [[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to Model list]](../README.md#documentation-for-models) [[Back to README]](../README.md)
 
 # **uploadFile**
-> AssetFileUploadResponseDto uploadFile(assetData)
+> AssetFileUploadResponseDto uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration)
 
 
 
@@ -1076,10 +1076,20 @@ import 'package:openapi/api.dart';
 //defaultApiClient.getAuthentication<HttpBearerAuth>('bearer').setAccessToken(yourTokenGeneratorFunction);
 
 final api_instance = AssetApi();
+final assetType = assetType_example; // String | 
 final assetData = BINARY_DATA_HERE; // MultipartFile | 
+final deviceAssetId = deviceAssetId_example; // String | 
+final deviceId = deviceId_example; // String | 
+final createdAt = createdAt_example; // String | 
+final modifiedAt = modifiedAt_example; // String | 
+final isFavorite = true; // bool | 
+final fileExtension = fileExtension_example; // String | 
+final livePhotoData = BINARY_DATA_HERE; // MultipartFile | 
+final isVisible = true; // bool | 
+final duration = duration_example; // String | 
 
 try {
-    final result = api_instance.uploadFile(assetData);
+    final result = api_instance.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration);
     print(result);
 } catch (e) {
     print('Exception when calling AssetApi->uploadFile: $e\n');
@@ -1090,7 +1100,17 @@ try {
 
 Name | Type | Description  | Notes
 ------------- | ------------- | ------------- | -------------
+ **assetType** | **String**|  | 
  **assetData** | **MultipartFile**|  | 
+ **deviceAssetId** | **String**|  | 
+ **deviceId** | **String**|  | 
+ **createdAt** | **String**|  | 
+ **modifiedAt** | **String**|  | 
+ **isFavorite** | **bool**|  | 
+ **fileExtension** | **String**|  | 
+ **livePhotoData** | **MultipartFile**|  | [optional] 
+ **isVisible** | **bool**|  | [optional] 
+ **duration** | **String**|  | [optional] 
 
 ### Return type
 

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

@@ -1164,8 +1164,28 @@ class AssetApi {
   ///
   /// Parameters:
   ///
+  /// * [String] assetType (required):
+  ///
   /// * [MultipartFile] assetData (required):
-  Future<Response> uploadFileWithHttpInfo(MultipartFile assetData,) async {
+  ///
+  /// * [String] deviceAssetId (required):
+  ///
+  /// * [String] deviceId (required):
+  ///
+  /// * [String] createdAt (required):
+  ///
+  /// * [String] modifiedAt (required):
+  ///
+  /// * [bool] isFavorite (required):
+  ///
+  /// * [String] fileExtension (required):
+  ///
+  /// * [MultipartFile] livePhotoData:
+  ///
+  /// * [bool] isVisible:
+  ///
+  /// * [String] duration:
+  Future<Response> uploadFileWithHttpInfo(String assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile? livePhotoData, bool? isVisible, String? duration, }) async {
     // ignore: prefer_const_declarations
     final path = r'/asset/upload';
 
@@ -1180,11 +1200,52 @@ class AssetApi {
 
     bool hasFields = false;
     final mp = MultipartRequest('POST', Uri.parse(path));
+    if (assetType != null) {
+      hasFields = true;
+      mp.fields[r'assetType'] = parameterToString(assetType);
+    }
     if (assetData != null) {
       hasFields = true;
       mp.fields[r'assetData'] = assetData.field;
       mp.files.add(assetData);
     }
+    if (livePhotoData != null) {
+      hasFields = true;
+      mp.fields[r'livePhotoData'] = livePhotoData.field;
+      mp.files.add(livePhotoData);
+    }
+    if (deviceAssetId != null) {
+      hasFields = true;
+      mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId);
+    }
+    if (deviceId != null) {
+      hasFields = true;
+      mp.fields[r'deviceId'] = parameterToString(deviceId);
+    }
+    if (createdAt != null) {
+      hasFields = true;
+      mp.fields[r'createdAt'] = parameterToString(createdAt);
+    }
+    if (modifiedAt != null) {
+      hasFields = true;
+      mp.fields[r'modifiedAt'] = parameterToString(modifiedAt);
+    }
+    if (isFavorite != null) {
+      hasFields = true;
+      mp.fields[r'isFavorite'] = parameterToString(isFavorite);
+    }
+    if (isVisible != null) {
+      hasFields = true;
+      mp.fields[r'isVisible'] = parameterToString(isVisible);
+    }
+    if (fileExtension != null) {
+      hasFields = true;
+      mp.fields[r'fileExtension'] = parameterToString(fileExtension);
+    }
+    if (duration != null) {
+      hasFields = true;
+      mp.fields[r'duration'] = parameterToString(duration);
+    }
     if (hasFields) {
       postBody = mp;
     }
@@ -1204,9 +1265,29 @@ class AssetApi {
   ///
   /// Parameters:
   ///
+  /// * [String] assetType (required):
+  ///
   /// * [MultipartFile] assetData (required):
-  Future<AssetFileUploadResponseDto?> uploadFile(MultipartFile assetData,) async {
-    final response = await uploadFileWithHttpInfo(assetData,);
+  ///
+  /// * [String] deviceAssetId (required):
+  ///
+  /// * [String] deviceId (required):
+  ///
+  /// * [String] createdAt (required):
+  ///
+  /// * [String] modifiedAt (required):
+  ///
+  /// * [bool] isFavorite (required):
+  ///
+  /// * [String] fileExtension (required):
+  ///
+  /// * [MultipartFile] livePhotoData:
+  ///
+  /// * [bool] isVisible:
+  ///
+  /// * [String] duration:
+  Future<AssetFileUploadResponseDto?> uploadFile(String assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile? livePhotoData, bool? isVisible, String? duration, }) async {
+    final response = await uploadFileWithHttpInfo(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension,  livePhotoData: livePhotoData, isVisible: isVisible, duration: duration, );
     if (response.statusCode >= HttpStatus.badRequest) {
       throw ApiException(response.statusCode, await _decodeBodyBytes(response));
     }

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

@@ -166,7 +166,7 @@ void main() {
 
     // 
     //
-    //Future<AssetFileUploadResponseDto> uploadFile(MultipartFile assetData) async
+    //Future<AssetFileUploadResponseDto> uploadFile(String assetType, MultipartFile assetData, String deviceAssetId, String deviceId, String createdAt, String modifiedAt, bool isFavorite, String fileExtension, { MultipartFile livePhotoData, bool isVisible, String duration }) async
     test('test uploadFile', () async {
       // TODO
     });

+ 6 - 4
server/apps/immich/src/api-v1/asset/asset.controller.ts

@@ -16,6 +16,7 @@ import {
   UploadedFiles,
   Patch,
   StreamableFile,
+  ParseFilePipe,
 } from '@nestjs/common';
 import { Authenticated } from '../../decorators/authenticated.decorator';
 import { AssetService } from './asset.service';
@@ -31,7 +32,6 @@ import { CuratedObjectsResponseDto } from './response-dto/curated-objects-respon
 import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
 import { AssetResponseDto, ImmichReadStream } from '@app/domain';
 import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
-import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
 import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
 import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
 import { DeleteAssetResponseDto } from './response-dto/delete-asset-response.dto';
@@ -55,6 +55,7 @@ import { SharedLinkResponseDto } from '@app/domain';
 import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
 import { AssetSearchDto } from './dto/asset-search.dto';
 import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
+import FileNotEmptyValidator from '../validation/file-not-empty-validator';
 
 function asStreamableFile({ stream, type, length }: ImmichReadStream) {
   return new StreamableFile(stream, { type, length });
@@ -80,12 +81,13 @@ export class AssetController {
   @ApiConsumes('multipart/form-data')
   @ApiBody({
     description: 'Asset Upload Information',
-    type: AssetFileUploadDto,
+    type: CreateAssetDto,
   })
   async uploadFile(
     @GetAuthUser() authUser: AuthUserDto,
-    @UploadedFiles() files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[] },
-    @Body(ValidationPipe) dto: CreateAssetDto,
+    @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator(['assetData'])] }))
+    files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[] },
+    @Body(new ValidationPipe()) dto: CreateAssetDto,
     @Response({ passthrough: true }) res: Res,
   ): Promise<AssetFileUploadResponseDto> {
     const file = mapToUploadFile(files.assetData[0]);

+ 9 - 2
server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts

@@ -1,6 +1,6 @@
 import { AssetType } from '@app/infra';
 import { ApiProperty } from '@nestjs/swagger';
-import { IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
+import { IsBoolean, IsEnum, IsNotEmpty, IsOptional } from 'class-validator';
 import { ImmichFile } from '../../../config/asset-upload.config';
 
 export class CreateAssetDto {
@@ -11,7 +11,8 @@ export class CreateAssetDto {
   deviceId!: string;
 
   @IsNotEmpty()
-  @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
+  @IsEnum(AssetType)
+  @ApiProperty({ enum: Object.keys(AssetType) })
   assetType!: AssetType;
 
   @IsNotEmpty()
@@ -32,6 +33,12 @@ export class CreateAssetDto {
 
   @IsOptional()
   duration?: string;
+
+  @ApiProperty({ type: 'string', format: 'binary' })
+  assetData!: any;
+
+  @ApiProperty({ type: 'string', format: 'binary' })
+  livePhotoData?: any;
 }
 
 export interface UploadFile {

+ 25 - 0
server/apps/immich/src/api-v1/validation/file-not-empty-validator.ts

@@ -0,0 +1,25 @@
+import { FileValidator, Injectable } from '@nestjs/common';
+
+@Injectable()
+export default class FileNotEmptyValidator extends FileValidator {
+  requiredFields: string[];
+
+  constructor(requiredFields: string[]) {
+    super({});
+    this.requiredFields = requiredFields;
+  }
+
+  isValid(files?: any): boolean {
+    if (!files) {
+      return false;
+    }
+
+    return this.requiredFields.every((field) => {
+      return files[field];
+    });
+  }
+
+  buildErrorMessage(): string {
+    return `Field(s) ${this.requiredFields.join(', ')} should not be empty`;
+  }
+}

+ 47 - 3
server/immich-openapi-specs.json

@@ -1077,7 +1077,7 @@
           "content": {
             "multipart/form-data": {
               "schema": {
-                "$ref": "#/components/schemas/AssetFileUploadDto"
+                "$ref": "#/components/schemas/CreateAssetDto"
               }
             }
           }
@@ -3758,16 +3758,60 @@
           "profileImagePath"
         ]
       },
-      "AssetFileUploadDto": {
+      "CreateAssetDto": {
         "type": "object",
         "properties": {
+          "assetType": {
+            "enum": [
+              "IMAGE",
+              "VIDEO",
+              "AUDIO",
+              "OTHER"
+            ],
+            "type": "string"
+          },
           "assetData": {
             "type": "string",
             "format": "binary"
+          },
+          "livePhotoData": {
+            "type": "string",
+            "format": "binary"
+          },
+          "deviceAssetId": {
+            "type": "string"
+          },
+          "deviceId": {
+            "type": "string"
+          },
+          "createdAt": {
+            "type": "string"
+          },
+          "modifiedAt": {
+            "type": "string"
+          },
+          "isFavorite": {
+            "type": "boolean"
+          },
+          "isVisible": {
+            "type": "boolean"
+          },
+          "fileExtension": {
+            "type": "string"
+          },
+          "duration": {
+            "type": "string"
           }
         },
         "required": [
-          "assetData"
+          "assetType",
+          "assetData",
+          "deviceAssetId",
+          "deviceId",
+          "createdAt",
+          "modifiedAt",
+          "isFavorite",
+          "fileExtension"
         ]
       },
       "AssetFileUploadResponseDto": {

+ 102 - 8
web/src/api/open-api/api.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.45.0
+ * The version of the OpenAPI document: 1.46.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@@ -4402,13 +4402,37 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
+         * @param {string} assetType 
          * @param {any} assetData 
+         * @param {string} deviceAssetId 
+         * @param {string} deviceId 
+         * @param {string} createdAt 
+         * @param {string} modifiedAt 
+         * @param {boolean} isFavorite 
+         * @param {string} fileExtension 
+         * @param {any} [livePhotoData] 
+         * @param {boolean} [isVisible] 
+         * @param {string} [duration] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        uploadFile: async (assetData: any, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        uploadFile: async (assetType: string, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'assetType' is not null or undefined
+            assertParamExists('uploadFile', 'assetType', assetType)
             // verify required parameter 'assetData' is not null or undefined
             assertParamExists('uploadFile', 'assetData', assetData)
+            // verify required parameter 'deviceAssetId' is not null or undefined
+            assertParamExists('uploadFile', 'deviceAssetId', deviceAssetId)
+            // verify required parameter 'deviceId' is not null or undefined
+            assertParamExists('uploadFile', 'deviceId', deviceId)
+            // verify required parameter 'createdAt' is not null or undefined
+            assertParamExists('uploadFile', 'createdAt', createdAt)
+            // verify required parameter 'modifiedAt' is not null or undefined
+            assertParamExists('uploadFile', 'modifiedAt', modifiedAt)
+            // verify required parameter 'isFavorite' is not null or undefined
+            assertParamExists('uploadFile', 'isFavorite', isFavorite)
+            // verify required parameter 'fileExtension' is not null or undefined
+            assertParamExists('uploadFile', 'fileExtension', fileExtension)
             const localVarPath = `/asset/upload`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
@@ -4427,10 +4451,50 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
             await setBearerAuthToObject(localVarHeaderParameter, configuration)
 
 
+            if (assetType !== undefined) { 
+                localVarFormParams.append('assetType', assetType as any);
+            }
+    
             if (assetData !== undefined) { 
                 localVarFormParams.append('assetData', assetData as any);
             }
     
+            if (livePhotoData !== undefined) { 
+                localVarFormParams.append('livePhotoData', livePhotoData as any);
+            }
+    
+            if (deviceAssetId !== undefined) { 
+                localVarFormParams.append('deviceAssetId', deviceAssetId as any);
+            }
+    
+            if (deviceId !== undefined) { 
+                localVarFormParams.append('deviceId', deviceId as any);
+            }
+    
+            if (createdAt !== undefined) { 
+                localVarFormParams.append('createdAt', createdAt as any);
+            }
+    
+            if (modifiedAt !== undefined) { 
+                localVarFormParams.append('modifiedAt', modifiedAt as any);
+            }
+    
+            if (isFavorite !== undefined) { 
+                localVarFormParams.append('isFavorite', isFavorite as any);
+            }
+    
+            if (isVisible !== undefined) { 
+                localVarFormParams.append('isVisible', isVisible as any);
+            }
+    
+            if (fileExtension !== undefined) { 
+                localVarFormParams.append('fileExtension', fileExtension as any);
+            }
+    
+            if (duration !== undefined) { 
+                localVarFormParams.append('duration', duration as any);
+            }
+    
     
             localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
     
@@ -4668,12 +4732,22 @@ export const AssetApiFp = function(configuration?: Configuration) {
         },
         /**
          * 
+         * @param {string} assetType 
          * @param {any} assetData 
+         * @param {string} deviceAssetId 
+         * @param {string} deviceId 
+         * @param {string} createdAt 
+         * @param {string} modifiedAt 
+         * @param {boolean} isFavorite 
+         * @param {string} fileExtension 
+         * @param {any} [livePhotoData] 
+         * @param {boolean} [isVisible] 
+         * @param {string} [duration] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async uploadFile(assetData: any, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
-            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetData, options);
+        async uploadFile(assetType: string, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<AssetFileUploadResponseDto>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options);
             return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
         },
     }
@@ -4879,12 +4953,22 @@ export const AssetApiFactory = function (configuration?: Configuration, basePath
         },
         /**
          * 
+         * @param {string} assetType 
          * @param {any} assetData 
+         * @param {string} deviceAssetId 
+         * @param {string} deviceId 
+         * @param {string} createdAt 
+         * @param {string} modifiedAt 
+         * @param {boolean} isFavorite 
+         * @param {string} fileExtension 
+         * @param {any} [livePhotoData] 
+         * @param {boolean} [isVisible] 
+         * @param {string} [duration] 
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        uploadFile(assetData: any, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
-            return localVarFp.uploadFile(assetData, options).then((request) => request(axios, basePath));
+        uploadFile(assetType: string, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: any): AxiosPromise<AssetFileUploadResponseDto> {
+            return localVarFp.uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(axios, basePath));
         },
     };
 };
@@ -5131,13 +5215,23 @@ export class AssetApi extends BaseAPI {
 
     /**
      * 
+     * @param {string} assetType 
      * @param {any} assetData 
+     * @param {string} deviceAssetId 
+     * @param {string} deviceId 
+     * @param {string} createdAt 
+     * @param {string} modifiedAt 
+     * @param {boolean} isFavorite 
+     * @param {string} fileExtension 
+     * @param {any} [livePhotoData] 
+     * @param {boolean} [isVisible] 
+     * @param {string} [duration] 
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof AssetApi
      */
-    public uploadFile(assetData: any, options?: AxiosRequestConfig) {
-        return AssetApiFp(this.configuration).uploadFile(assetData, options).then((request) => request(this.axios, this.basePath));
+    public uploadFile(assetType: string, assetData: any, deviceAssetId: string, deviceId: string, createdAt: string, modifiedAt: string, isFavorite: boolean, fileExtension: string, livePhotoData?: any, isVisible?: boolean, duration?: string, options?: AxiosRequestConfig) {
+        return AssetApiFp(this.configuration).uploadFile(assetType, assetData, deviceAssetId, deviceId, createdAt, modifiedAt, isFavorite, fileExtension, livePhotoData, isVisible, duration, options).then((request) => request(this.axios, this.basePath));
     }
 }
 

+ 1 - 1
web/src/api/open-api/base.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.45.0
+ * The version of the OpenAPI document: 1.46.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/common.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.45.0
+ * The version of the OpenAPI document: 1.46.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/configuration.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.45.0
+ * The version of the OpenAPI document: 1.46.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

+ 1 - 1
web/src/api/open-api/index.ts

@@ -4,7 +4,7 @@
  * Immich
  * Immich API
  *
- * The version of the OpenAPI document: 1.45.0
+ * The version of the OpenAPI document: 1.46.1
  * 
  *
  * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).