diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 891c64cc9a..6e2f5c856d 100644 --- a/mobile/openapi/README.md +++ b/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 diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 4695c961a9..640baeeddf 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/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('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 diff --git a/mobile/openapi/lib/api/asset_api.dart b/mobile/openapi/lib/api/asset_api.dart index 861c9dc155..d6198665fc 100644 --- a/mobile/openapi/lib/api/asset_api.dart +++ b/mobile/openapi/lib/api/asset_api.dart @@ -1164,8 +1164,28 @@ class AssetApi { /// /// Parameters: /// + /// * [String] assetType (required): + /// /// * [MultipartFile] assetData (required): - Future 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 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 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 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)); } diff --git a/mobile/openapi/test/asset_api_test.dart b/mobile/openapi/test/asset_api_test.dart index 3bf405b8fa..619d75ec68 100644 --- a/mobile/openapi/test/asset_api_test.dart +++ b/mobile/openapi/test/asset_api_test.dart @@ -166,7 +166,7 @@ void main() { // // - //Future uploadFile(MultipartFile assetData) async + //Future 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 }); diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 391db66936..aaafd74f4e 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/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 { const file = mapToUploadFile(files.assetData[0]); diff --git a/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts b/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts index 0d6af77bca..fa7154ba10 100644 --- a/server/apps/immich/src/api-v1/asset/dto/create-asset.dto.ts +++ b/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 { diff --git a/server/apps/immich/src/api-v1/validation/file-not-empty-validator.ts b/server/apps/immich/src/api-v1/validation/file-not-empty-validator.ts new file mode 100644 index 0000000000..f75899eecb --- /dev/null +++ b/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`; + } +} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 29b272322c..65f6e0d150 100644 --- a/server/immich-openapi-specs.json +++ b/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": { diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 4789ee960e..7c7f1aa608 100644 --- a/web/src/api/open-api/api.ts +++ b/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 => { + 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 => { + // 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> { - 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> { + 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 { - 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 { + 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)); } } diff --git a/web/src/api/open-api/base.ts b/web/src/api/open-api/base.ts index eb55f2cd71..1ffb5e4ffc 100644 --- a/web/src/api/open-api/base.ts +++ b/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). diff --git a/web/src/api/open-api/common.ts b/web/src/api/open-api/common.ts index 974b67d37d..4d7d11c836 100644 --- a/web/src/api/open-api/common.ts +++ b/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). diff --git a/web/src/api/open-api/configuration.ts b/web/src/api/open-api/configuration.ts index 7223695d8b..e0596e4b17 100644 --- a/web/src/api/open-api/configuration.ts +++ b/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). diff --git a/web/src/api/open-api/index.ts b/web/src/api/open-api/index.ts index 382b9a0b88..49f0b7a38f 100644 --- a/web/src/api/open-api/index.ts +++ b/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).