소스 검색

fix(server): more asset upload validation and docs (#1720)

* fix(server): more asset upload validation and docs

* remove unused DTO

* changed Object.keys() to Object.values()

* apply patch to openapi generator for web

* revert CreateAssetDto assetType enum

* resolve merge conflict

* Revert "resolve merge conflict"

This reverts commit 0e0080518759a05d029f2f7a761537c58cb586d0.
Michel Heusschen 2 년 전
부모
커밋
05630776a0

+ 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 = ; // AssetTypeEnum | 
 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** | [**AssetTypeEnum**](AssetTypeEnum.md)|  | 
  **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:
   ///
+  /// * [AssetTypeEnum] 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(AssetTypeEnum 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:
   ///
+  /// * [AssetTypeEnum] 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(AssetTypeEnum 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(AssetTypeEnum 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]);

+ 0 - 8
server/apps/immich/src/api-v1/asset/dto/asset-file-upload.dto.ts

@@ -1,8 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsNotEmpty } from 'class-validator';
-
-export class AssetFileUploadDto {
-  @IsNotEmpty()
-  @ApiProperty({ type: 'string', format: 'binary' })
-  assetData!: any;
-}

+ 10 - 1
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,6 +11,7 @@ export class CreateAssetDto {
   deviceId!: string;
 
   @IsNotEmpty()
+  @IsEnum(AssetType)
   @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
   assetType!: AssetType;
 
@@ -32,6 +33,14 @@ export class CreateAssetDto {
 
   @IsOptional()
   duration?: string;
+
+  // The properties below are added to correctly generate the API docs
+  // and client SDKs. Validation should be handled in the controller.
+  @ApiProperty({ type: 'string', format: 'binary' })
+  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`;
+  }
+}

+ 8 - 4
server/bin/generate-open-api.sh

@@ -2,11 +2,11 @@
 
 function mobile {
   rm -rf ../mobile/openapi
-  cd ./openapi-generator/templates/serialization/native
+  cd ./openapi-generator/templates/mobile/serialization/native
   wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/master/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache
   patch -u native_class.mustache <native_class.mustache.patch
-  cd ../../../..
-  npx openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./openapi-generator/templates
+  cd ../../../../..
+  npx openapi-generator-cli generate -g dart -i ./immich-openapi-specs.json -o ../mobile/openapi -t ./openapi-generator/templates/mobile
 
   # Post generate patches
   patch --no-backup-if-mismatch -u ../mobile/openapi/lib/api_client.dart <./openapi-generator/patch/api_client.dart.patch
@@ -15,7 +15,11 @@ function mobile {
 
 function web {
   rm -rf ../web/src/api/open-api
-  npx openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../web/src/api/open-api
+  cd ./openapi-generator/templates/web
+  wget -O apiInner.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/v6.0.1/modules/openapi-generator/src/main/resources/typescript-axios/apiInner.mustache
+  patch -u apiInner.mustache < apiInner.mustache.patch
+  cd ../../..
+  npx openapi-generator-cli generate -g typescript-axios -i ./immich-openapi-specs.json -o ../web/src/api/open-api -t ./openapi-generator/templates/web
 }
 
 if [[ $1 == 'mobile' ]]; then

+ 41 - 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,54 @@
           "profileImagePath"
         ]
       },
-      "AssetFileUploadDto": {
+      "CreateAssetDto": {
         "type": "object",
         "properties": {
+          "assetType": {
+            "$ref": "#/components/schemas/AssetTypeEnum"
+          },
           "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": {

+ 1 - 1
server/openapi-generator/patch/api_client.dart.patch

@@ -18,4 +18,4 @@
        ? json
 -      : _deserialize(jsonDecode(json), targetType, growable: growable);
 +      : _deserialize(await compute((String j) => jsonDecode(j), json), targetType, growable: growable);
-   }
+   }

+ 0 - 0
server/openapi-generator/templates/serialization/native/native_class.mustache → server/openapi-generator/templates/mobile/serialization/native/native_class.mustache


+ 0 - 0
server/openapi-generator/templates/serialization/native/native_class.mustache.patch → server/openapi-generator/templates/mobile/serialization/native/native_class.mustache.patch


+ 372 - 0
server/openapi-generator/templates/web/apiInner.mustache

@@ -0,0 +1,372 @@
+{{#withSeparateModelsAndApi}}
+/* tslint:disable */
+/* eslint-disable */
+{{>licenseInfo}}
+
+import globalAxios, { AxiosPromise, AxiosInstance, AxiosRequestConfig } from 'axios';
+import { Configuration } from '{{apiRelativeToRoot}}configuration';
+{{#withNodeImports}}
+// URLSearchParams not necessarily used
+// @ts-ignore
+import { URL, URLSearchParams } from 'url';
+{{#multipartFormData}}
+import FormData from 'form-data'
+{{/multipartFormData}}
+{{/withNodeImports}}
+// Some imports not used depending on template conditions
+// @ts-ignore
+import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '{{apiRelativeToRoot}}common';
+// @ts-ignore
+import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '{{apiRelativeToRoot}}base';
+{{#imports}}
+// @ts-ignore
+import { {{classname}} } from '{{apiRelativeToRoot}}{{tsModelPackage}}';
+{{/imports}}
+{{/withSeparateModelsAndApi}}
+{{^withSeparateModelsAndApi}}
+{{/withSeparateModelsAndApi}}
+{{#operations}}
+/**
+ * {{classname}} - axios parameter creator{{#description}}
+ * {{&description}}{{/description}}
+ * @export
+ */
+export const {{classname}}AxiosParamCreator = function (configuration?: Configuration) {
+    return {
+    {{#operation}}
+        /**
+         * {{&notes}}
+         {{#summary}}
+         * @summary {{&summary}}
+         {{/summary}}
+         {{#allParams}}
+         * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
+         {{/allParams}}
+         * @param {*} [options] Override http request option.{{#isDeprecated}}
+         * @deprecated{{/isDeprecated}}
+         * @throws {RequiredError}
+         */
+        {{nickname}}: async ({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+    {{#allParams}}
+    {{#required}}
+            // verify required parameter '{{paramName}}' is not null or undefined
+            assertParamExists('{{nickname}}', '{{paramName}}', {{paramName}})
+    {{/required}}
+    {{/allParams}}
+            const localVarPath = `{{{path}}}`{{#pathParams}}
+                .replace(`{${"{{baseName}}"}}`, encodeURIComponent(String({{paramName}}))){{/pathParams}};
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+
+            const localVarRequestOptions = { method: '{{httpMethod}}', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;{{#vendorExtensions}}{{#hasFormParams}}
+            const localVarFormParams = new {{^multipartFormData}}URLSearchParams(){{/multipartFormData}}{{#multipartFormData}}((configuration && configuration.formDataCtor) || FormData)(){{/multipartFormData}};{{/hasFormParams}}{{/vendorExtensions}}
+
+    {{#authMethods}}
+            // authentication {{name}} required
+            {{#isApiKey}}
+            {{#isKeyInHeader}}
+            await setApiKeyToObject(localVarHeaderParameter, "{{keyParamName}}", configuration)
+            {{/isKeyInHeader}}
+            {{#isKeyInQuery}}
+            await setApiKeyToObject(localVarQueryParameter, "{{keyParamName}}", configuration)
+            {{/isKeyInQuery}}
+            {{/isApiKey}}
+            {{#isBasicBasic}}
+            // http basic authentication required
+            setBasicAuthToObject(localVarRequestOptions, configuration)
+            {{/isBasicBasic}}
+            {{#isBasicBearer}}
+            // http bearer authentication required
+            await setBearerAuthToObject(localVarHeaderParameter, configuration)
+            {{/isBasicBearer}}
+            {{#isOAuth}}
+            // oauth required
+            await setOAuthToObject(localVarHeaderParameter, "{{name}}", [{{#scopes}}"{{{scope}}}"{{^-last}}, {{/-last}}{{/scopes}}], configuration)
+            {{/isOAuth}}
+
+    {{/authMethods}}
+    {{#queryParams}}
+            {{#isArray}}
+            if ({{paramName}}) {
+            {{#isCollectionFormatMulti}}
+                {{#uniqueItems}}
+                localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}});
+                {{/uniqueItems}}
+                {{^uniqueItems}}
+                localVarQueryParameter['{{baseName}}'] = {{paramName}};
+                {{/uniqueItems}}
+            {{/isCollectionFormatMulti}}
+            {{^isCollectionFormatMulti}}
+                {{#uniqueItems}}
+                localVarQueryParameter['{{baseName}}'] = Array.from({{paramName}}).join(COLLECTION_FORMATS.{{collectionFormat}});
+                {{/uniqueItems}}
+                {{^uniqueItems}}
+                localVarQueryParameter['{{baseName}}'] = {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}});
+                {{/uniqueItems}}
+            {{/isCollectionFormatMulti}}
+            }
+            {{/isArray}}
+            {{^isArray}}
+            if ({{paramName}} !== undefined) {
+                {{#isDateTime}}
+                localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ?
+                    ({{paramName}} as any).toISOString() :
+                    {{paramName}};
+                {{/isDateTime}}
+                {{^isDateTime}}
+                {{#isDate}}
+                localVarQueryParameter['{{baseName}}'] = ({{paramName}} as any instanceof Date) ?
+                    ({{paramName}} as any).toISOString().substr(0,10) :
+                    {{paramName}};
+                {{/isDate}}
+                {{^isDate}}
+                localVarQueryParameter['{{baseName}}'] = {{paramName}};
+                {{/isDate}}
+                {{/isDateTime}}
+            }
+            {{/isArray}}
+
+    {{/queryParams}}
+    {{#headerParams}}
+            {{#isArray}}
+            if ({{paramName}}) {
+                {{#uniqueItems}}
+                let mapped = Array.from({{paramName}}).map(value => (<any>"{{{dataType}}}" !== "Set<string>") ? JSON.stringify(value) : (value || ""));
+                {{/uniqueItems}}
+                {{^uniqueItems}}
+                let mapped = {{paramName}}.map(value => (<any>"{{{dataType}}}" !== "Array<string>") ? JSON.stringify(value) : (value || ""));
+                {{/uniqueItems}}
+                localVarHeaderParameter['{{baseName}}'] = mapped.join(COLLECTION_FORMATS["{{collectionFormat}}"]);
+            }
+            {{/isArray}}
+            {{^isArray}}
+            if ({{paramName}} !== undefined && {{paramName}} !== null) {
+                {{#isString}}
+                localVarHeaderParameter['{{baseName}}'] = String({{paramName}});
+                {{/isString}}
+                {{^isString}}
+                localVarHeaderParameter['{{baseName}}'] = String(JSON.stringify({{paramName}}));
+                {{/isString}}
+            }
+            {{/isArray}}
+
+    {{/headerParams}}
+    {{#vendorExtensions}}
+    {{#formParams}}
+            {{#isArray}}
+            if ({{paramName}}) {
+            {{#isCollectionFormatMulti}}
+                {{paramName}}.forEach((element) => {
+                    localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', element as any);
+                })
+            {{/isCollectionFormatMulti}}
+            {{^isCollectionFormatMulti}}
+                localVarFormParams.{{#multipartFormData}}append{{/multipartFormData}}{{^multipartFormData}}set{{/multipartFormData}}('{{baseName}}', {{paramName}}.join(COLLECTION_FORMATS.{{collectionFormat}}));
+            {{/isCollectionFormatMulti}}
+            }{{/isArray}}
+            {{^isArray}}
+            if ({{paramName}} !== undefined) { {{^multipartFormData}}
+                localVarFormParams.set('{{baseName}}', {{paramName}} as any);{{/multipartFormData}}{{#multipartFormData}}{{#isPrimitiveType}}
+                localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isEnum}}
+                localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isEnum}}{{^isEnum}}
+                localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isEnum}}{{/isPrimitiveType}}{{/multipartFormData}}
+            }{{/isArray}}
+    {{/formParams}}{{/vendorExtensions}}
+    {{#vendorExtensions}}{{#hasFormParams}}{{^multipartFormData}}
+            localVarHeaderParameter['Content-Type'] = 'application/x-www-form-urlencoded';{{/multipartFormData}}{{#multipartFormData}}
+            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';{{/multipartFormData}}
+    {{/hasFormParams}}{{/vendorExtensions}}
+    {{#bodyParam}}
+            {{^consumes}}
+            localVarHeaderParameter['Content-Type'] = 'application/json';
+            {{/consumes}}
+            {{#consumes.0}}
+            localVarHeaderParameter['Content-Type'] = '{{{mediaType}}}';
+            {{/consumes.0}}
+
+    {{/bodyParam}}
+            setSearchParams(localVarUrlObj, localVarQueryParameter);
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions,{{#hasFormParams}}{{#multipartFormData}} ...(localVarFormParams as any).getHeaders?.(),{{/multipartFormData}}{{/hasFormParams}} ...options.headers};
+    {{#hasFormParams}}
+            localVarRequestOptions.data = localVarFormParams{{#vendorExtensions}}{{^multipartFormData}}.toString(){{/multipartFormData}}{{/vendorExtensions}};
+    {{/hasFormParams}}
+    {{#bodyParam}}
+            localVarRequestOptions.data = serializeDataIfNeeded({{paramName}}, localVarRequestOptions, configuration)
+    {{/bodyParam}}
+
+            return {
+                url: toPathString(localVarUrlObj),
+                options: localVarRequestOptions,
+            };
+        },
+    {{/operation}}
+    }
+};
+
+/**
+ * {{classname}} - functional programming interface{{#description}}
+ * {{{.}}}{{/description}}
+ * @export
+ */
+export const {{classname}}Fp = function(configuration?: Configuration) {
+    const localVarAxiosParamCreator = {{classname}}AxiosParamCreator(configuration)
+    return {
+    {{#operation}}
+        /**
+         * {{&notes}}
+         {{#summary}}
+         * @summary {{&summary}}
+         {{/summary}}
+         {{#allParams}}
+         * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
+         {{/allParams}}
+         * @param {*} [options] Override http request option.{{#isDeprecated}}
+         * @deprecated{{/isDeprecated}}
+         * @throws {RequiredError}
+         */
+        async {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>> {
+            const localVarAxiosArgs = await localVarAxiosParamCreator.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options);
+            return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
+        },
+    {{/operation}}
+    }
+};
+
+/**
+ * {{classname}} - factory interface{{#description}}
+ * {{&description}}{{/description}}
+ * @export
+ */
+export const {{classname}}Factory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+    const localVarFp = {{classname}}Fp(configuration)
+    return {
+    {{#operation}}
+        /**
+         * {{&notes}}
+         {{#summary}}
+         * @summary {{&summary}}
+         {{/summary}}
+         {{#allParams}}
+         * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
+         {{/allParams}}
+         * @param {*} [options] Override http request option.{{#isDeprecated}}
+         * @deprecated{{/isDeprecated}}
+         * @throws {RequiredError}
+         */
+        {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: any): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}> {
+            return localVarFp.{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(axios, basePath));
+        },
+    {{/operation}}
+    };
+};
+
+{{#withInterfaces}}
+/**
+ * {{classname}} - interface{{#description}}
+ * {{&description}}{{/description}}
+ * @export
+ * @interface {{classname}}
+ */
+export interface {{classname}}Interface {
+{{#operation}}
+    /**
+     * {{&notes}}
+     {{#summary}}
+     * @summary {{&summary}}
+     {{/summary}}
+     {{#allParams}}
+     * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
+     {{/allParams}}
+     * @param {*} [options] Override http request option.{{#isDeprecated}}
+     * @deprecated{{/isDeprecated}}
+     * @throws {RequiredError}
+     * @memberof {{classname}}Interface
+     */
+    {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig): AxiosPromise<{{{returnType}}}{{^returnType}}void{{/returnType}}>;
+
+{{/operation}}
+}
+
+{{/withInterfaces}}
+{{#useSingleRequestParameter}}
+{{#operation}}
+{{#allParams.0}}
+/**
+ * Request parameters for {{nickname}} operation in {{classname}}.
+ * @export
+ * @interface {{classname}}{{operationIdCamelCase}}Request
+ */
+export interface {{classname}}{{operationIdCamelCase}}Request {
+    {{#allParams}}
+    /**
+     * {{description}}
+     * @type {{=<% %>=}}{<%&dataType%>}<%={{ }}=%>
+     * @memberof {{classname}}{{operationIdCamelCase}}
+     */
+    readonly {{paramName}}{{^required}}?{{/required}}: {{{dataType}}}
+    {{^-last}}
+
+    {{/-last}}
+    {{/allParams}}
+}
+
+{{/allParams.0}}
+{{/operation}}
+{{/useSingleRequestParameter}}
+/**
+ * {{classname}} - object-oriented interface{{#description}}
+ * {{{.}}}{{/description}}
+ * @export
+ * @class {{classname}}
+ * @extends {BaseAPI}
+ */
+{{#withInterfaces}}
+export class {{classname}} extends BaseAPI implements {{classname}}Interface {
+{{/withInterfaces}}
+{{^withInterfaces}}
+export class {{classname}} extends BaseAPI {
+{{/withInterfaces}}
+    {{#operation}}
+    /**
+     * {{&notes}}
+     {{#summary}}
+     * @summary {{&summary}}
+     {{/summary}}
+     {{#useSingleRequestParameter}}
+     {{#allParams.0}}
+     * @param {{=<% %>=}}{<%& classname %><%& operationIdCamelCase %>Request}<%={{ }}=%> requestParameters Request parameters.
+     {{/allParams.0}}
+     {{/useSingleRequestParameter}}
+     {{^useSingleRequestParameter}}
+     {{#allParams}}
+     * @param {{=<% %>=}}{<%&dataType%>}<%={{ }}=%> {{^required}}[{{/required}}{{paramName}}{{^required}}]{{/required}} {{description}}
+     {{/allParams}}
+     {{/useSingleRequestParameter}}
+     * @param {*} [options] Override http request option.{{#isDeprecated}}
+     * @deprecated{{/isDeprecated}}
+     * @throws {RequiredError}
+     * @memberof {{classname}}
+     */
+    {{#useSingleRequestParameter}}
+    public {{nickname}}({{#allParams.0}}requestParameters: {{classname}}{{operationIdCamelCase}}Request{{^hasRequiredParams}} = {}{{/hasRequiredParams}}, {{/allParams.0}}options?: AxiosRequestConfig) {
+        return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams.0}}{{#allParams}}requestParameters.{{paramName}}, {{/allParams}}{{/allParams.0}}options).then((request) => request(this.axios, this.basePath));
+    }
+    {{/useSingleRequestParameter}}
+    {{^useSingleRequestParameter}}
+    public {{nickname}}({{#allParams}}{{paramName}}{{^required}}?{{/required}}: {{{dataType}}}, {{/allParams}}options?: AxiosRequestConfig) {
+        return {{classname}}Fp(this.configuration).{{nickname}}({{#allParams}}{{paramName}}, {{/allParams}}options).then((request) => request(this.axios, this.basePath));
+    }
+    {{/useSingleRequestParameter}}
+    {{^-last}}
+
+    {{/-last}}
+    {{/operation}}
+}
+{{/operations}}

+ 14 - 0
server/openapi-generator/templates/web/apiInner.mustache.patch

@@ -0,0 +1,14 @@
+--- apiInner.mustache   2023-02-10 17:44:20.945845049 +0000
++++ apiInner.mustache.patch     2023-02-10 17:46:28.669054112 +0000
+@@ -173,8 +173,9 @@
+             {{^isArray}}
+             if ({{paramName}} !== undefined) { {{^multipartFormData}}
+                 localVarFormParams.set('{{baseName}}', {{paramName}} as any);{{/multipartFormData}}{{#multipartFormData}}{{#isPrimitiveType}}
+-                localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}
+-                localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isPrimitiveType}}{{/multipartFormData}}
++                localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isPrimitiveType}}{{^isPrimitiveType}}{{#isEnum}}
++                localVarFormParams.append('{{baseName}}', {{paramName}} as any);{{/isEnum}}{{^isEnum}}
++                localVarFormParams.append('{{baseName}}', new Blob([JSON.stringify({{paramName}})], { type: "application/json", }));{{/isEnum}}{{/isPrimitiveType}}{{/multipartFormData}}
+             }{{/isArray}}
+     {{/formParams}}{{/vendorExtensions}}
+     {{#vendorExtensions}}{{#hasFormParams}}{{^multipartFormData}}

+ 101 - 7
web/src/api/open-api/api.ts

@@ -4402,13 +4402,37 @@ export const AssetApiAxiosParamCreator = function (configuration?: Configuration
         },
         /**
          * 
+         * @param {AssetTypeEnum} 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: AssetTypeEnum, 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', new Blob([JSON.stringify(assetType)], { type: "application/json", }));
+            }
+    
             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 {AssetTypeEnum} 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: AssetTypeEnum, 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 {AssetTypeEnum} 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: AssetTypeEnum, 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 {AssetTypeEnum} 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: AssetTypeEnum, 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));
     }
 }