diff --git a/e2e/src/specs/web/photo-viewer.e2e-spec.ts b/e2e/src/specs/web/photo-viewer.e2e-spec.ts index 76d9d61ed6..71f2145be8 100644 --- a/e2e/src/specs/web/photo-viewer.e2e-spec.ts +++ b/e2e/src/specs/web/photo-viewer.e2e-spec.ts @@ -77,18 +77,4 @@ test.describe('Photo Viewer', () => { }); expect(tagAtCenter).toBe('IMG'); }); - - test('reloads photo when checksum changes', async ({ page }) => { - await page.goto(`/photos/${asset.id}`); - - const preview = page.getByTestId('preview').filter({ visible: true }); - await expect(preview).toHaveAttribute('src', /.+/); - const initialSrc = await preview.getAttribute('src'); - - const websocketEvent = utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); - await utils.replaceAsset(admin.accessToken, asset.id); - await websocketEvent; - - await expect(preview).not.toHaveAttribute('src', initialSrc!); - }); }); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 4d44d99e2f..f9dd11a1ec 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -375,40 +375,6 @@ export const utils = { return body as AssetMediaResponseDto; }, - replaceAsset: async ( - accessToken: string, - assetId: string, - dto?: Partial> & { assetData?: FileData }, - ) => { - const _dto = { - deviceAssetId: 'test-1', - deviceId: 'test', - fileCreatedAt: new Date().toISOString(), - fileModifiedAt: new Date().toISOString(), - ...dto, - }; - - const assetData = dto?.assetData?.bytes || makeRandomImage(); - const filename = dto?.assetData?.filename || 'example.png'; - - if (dto?.assetData?.bytes) { - console.log(`Uploading ${filename}`); - } - - const builder = request(app) - .put(`/assets/${assetId}/original`) - .attach('assetData', assetData, filename) - .set('Authorization', `Bearer ${accessToken}`); - - for (const [key, value] of Object.entries(_dto)) { - void builder.field(key, String(value)); - } - - const { body } = await builder; - - return body as AssetMediaResponseDto; - }, - createImageFile: (path: string) => { if (!existsSync(dirname(path))) { mkdirSync(dirname(path), { recursive: true }); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b1df5f240c..e905fc41e8 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -113,7 +113,6 @@ Class | Method | HTTP request | Description *AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random | Get random assets *AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback | Play asset video *AssetsApi* | [**removeAssetEdits**](doc//AssetsApi.md#removeassetedits) | **DELETE** /assets/{id}/edits | Remove edits from an existing asset -*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | Run an asset job *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | Update an asset *AssetsApi* | [**updateAssetMetadata**](doc//AssetsApi.md#updateassetmetadata) | **PUT** /assets/{id}/metadata | Update asset metadata @@ -149,7 +148,6 @@ Class | Method | HTTP request | Description *DeprecatedApi* | [**getFullSyncForUser**](doc//DeprecatedApi.md#getfullsyncforuser) | **POST** /sync/full-sync | Get full sync for user *DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status *DeprecatedApi* | [**getRandom**](doc//DeprecatedApi.md#getrandom) | **GET** /assets/random | Get random assets -*DeprecatedApi* | [**replaceAsset**](doc//DeprecatedApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace asset *DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs *DownloadApi* | [**downloadArchive**](doc//DownloadApi.md#downloadarchive) | **POST** /download/archive | Download asset archive *DownloadApi* | [**getDownloadInfo**](doc//DownloadApi.md#getdownloadinfo) | **POST** /download/info | Retrieve download information diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index 831c19683d..8e9e5461a1 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -1113,154 +1113,6 @@ class AssetsApi { } } - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/{id}/original' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - - const contentTypes = ['multipart/form-data']; - - bool hasFields = false; - final mp = MultipartRequest('PUT', Uri.parse(apiPath)); - if (assetData != null) { - hasFields = true; - mp.fields[r'assetData'] = assetData.field; - mp.files.add(assetData); - } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } - if (duration != null) { - hasFields = true; - mp.fields[r'duration'] = parameterToString(duration); - } - if (fileCreatedAt != null) { - hasFields = true; - mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt); - } - if (fileModifiedAt != null) { - hasFields = true; - mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); - } - if (filename != null) { - hasFields = true; - mp.fields[r'filename'] = parameterToString(filename); - } - if (hasFields) { - postBody = mp; - } - - return apiClient.invokeAPI( - apiPath, - 'PUT', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMediaResponseDto',) as AssetMediaResponseDto; - - } - return null; - } - /// Run an asset job /// /// Run a specific job on a set of assets. diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 94b7e2e738..f4f3d5f6b1 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -363,154 +363,6 @@ class DeprecatedApi { return null; } - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Note: This method returns the HTTP [Response]. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAssetWithHttpInfo(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - // ignore: prefer_const_declarations - final apiPath = r'/assets/{id}/original' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - if (key != null) { - queryParams.addAll(_queryParams('', 'key', key)); - } - if (slug != null) { - queryParams.addAll(_queryParams('', 'slug', slug)); - } - - const contentTypes = ['multipart/form-data']; - - bool hasFields = false; - final mp = MultipartRequest('PUT', Uri.parse(apiPath)); - if (assetData != null) { - hasFields = true; - mp.fields[r'assetData'] = assetData.field; - mp.files.add(assetData); - } - if (deviceAssetId != null) { - hasFields = true; - mp.fields[r'deviceAssetId'] = parameterToString(deviceAssetId); - } - if (deviceId != null) { - hasFields = true; - mp.fields[r'deviceId'] = parameterToString(deviceId); - } - if (duration != null) { - hasFields = true; - mp.fields[r'duration'] = parameterToString(duration); - } - if (fileCreatedAt != null) { - hasFields = true; - mp.fields[r'fileCreatedAt'] = parameterToString(fileCreatedAt); - } - if (fileModifiedAt != null) { - hasFields = true; - mp.fields[r'fileModifiedAt'] = parameterToString(fileModifiedAt); - } - if (filename != null) { - hasFields = true; - mp.fields[r'filename'] = parameterToString(filename); - } - if (hasFields) { - postBody = mp; - } - - return apiClient.invokeAPI( - apiPath, - 'PUT', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Replace asset - /// - /// Replace the asset with new file, without changing its id. - /// - /// Parameters: - /// - /// * [String] id (required): - /// - /// * [MultipartFile] assetData (required): - /// Asset file data - /// - /// * [String] deviceAssetId (required): - /// Device asset ID - /// - /// * [String] deviceId (required): - /// Device ID - /// - /// * [DateTime] fileCreatedAt (required): - /// File creation date - /// - /// * [DateTime] fileModifiedAt (required): - /// File modification date - /// - /// * [String] key: - /// - /// * [String] slug: - /// - /// * [String] duration: - /// Duration (for videos) - /// - /// * [String] filename: - /// Filename - Future replaceAsset(String id, MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? duration, String? filename, }) async { - final response = await replaceAssetWithHttpInfo(id, assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, duration: duration, filename: filename, ); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AssetMediaResponseDto',) as AssetMediaResponseDto; - - } - return null; - } - /// Run jobs /// /// Queue all assets for a specific job type. Defaults to only queueing assets that have not yet been processed, but the force command can be used to re-process all assets. diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 9092ede786..0ac9461027 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -41,7 +41,6 @@ class Permission { static const assetPeriodView = Permission._(r'asset.view'); static const assetPeriodDownload = Permission._(r'asset.download'); static const assetPeriodUpload = Permission._(r'asset.upload'); - static const assetPeriodReplace = Permission._(r'asset.replace'); static const assetPeriodCopy = Permission._(r'asset.copy'); static const assetPeriodDerive = Permission._(r'asset.derive'); static const assetPeriodEditPeriodGet = Permission._(r'asset.edit.get'); @@ -200,7 +199,6 @@ class Permission { assetPeriodView, assetPeriodDownload, assetPeriodUpload, - assetPeriodReplace, assetPeriodCopy, assetPeriodDerive, assetPeriodEditPeriodGet, @@ -394,7 +392,6 @@ class PermissionTypeTransformer { case r'asset.view': return Permission.assetPeriodView; case r'asset.download': return Permission.assetPeriodDownload; case r'asset.upload': return Permission.assetPeriodUpload; - case r'asset.replace': return Permission.assetPeriodReplace; case r'asset.copy': return Permission.assetPeriodCopy; case r'asset.derive': return Permission.assetPeriodDerive; case r'asset.edit.get': return Permission.assetPeriodEditPeriodGet; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f07898d4e7..4ab8a1d584 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4271,90 +4271,6 @@ ], "x-immich-permission": "asset.download", "x-immich-state": "Stable" - }, - "put": { - "deprecated": true, - "description": "Replace the asset with new file, without changing its id.", - "operationId": "replaceAsset", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", - "type": "string" - } - }, - { - "name": "key", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "slug", - "required": false, - "in": "query", - "schema": { - "type": "string" - } - } - ], - "requestBody": { - "content": { - "multipart/form-data": { - "schema": { - "$ref": "#/components/schemas/AssetMediaReplaceDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AssetMediaResponseDto" - } - } - }, - "description": "Asset replaced successfully" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "summary": "Replace asset", - "tags": [ - "Assets", - "Deprecated" - ], - "x-immich-history": [ - { - "version": "v1", - "state": "Added" - }, - { - "version": "v1", - "state": "Deprecated", - "replacementId": "copyAsset" - } - ], - "x-immich-permission": "asset.replace", - "x-immich-state": "Deprecated" } }, "/assets/{id}/thumbnail": { @@ -16818,53 +16734,6 @@ ], "type": "object" }, - "AssetMediaReplaceDto": { - "properties": { - "assetData": { - "description": "Asset file data", - "format": "binary", - "type": "string" - }, - "deviceAssetId": { - "description": "Device asset ID", - "type": "string" - }, - "deviceId": { - "description": "Device ID", - "type": "string" - }, - "duration": { - "description": "Duration (for videos)", - "type": "string" - }, - "fileCreatedAt": { - "description": "File creation date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", - "type": "string" - }, - "fileModifiedAt": { - "description": "File modification date", - "example": "2024-01-01T00:00:00.000Z", - "format": "date-time", - "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", - "type": "string" - }, - "filename": { - "description": "Filename", - "type": "string" - } - }, - "required": [ - "assetData", - "deviceAssetId", - "deviceId", - "fileCreatedAt", - "fileModifiedAt" - ], - "type": "object" - }, "AssetMediaResponseDto": { "properties": { "id": { @@ -20023,7 +19892,6 @@ "asset.view", "asset.download", "asset.upload", - "asset.replace", "asset.copy", "asset.derive", "asset.edit.get", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 365187e6a7..6bb17f6834 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1010,22 +1010,6 @@ export type AssetOcrResponseDto = { /** Normalized y coordinate of box corner 4 (0-1) */ y4: number; }; -export type AssetMediaReplaceDto = { - /** Asset file data */ - assetData: Blob; - /** Device asset ID */ - deviceAssetId: string; - /** Device ID */ - deviceId: string; - /** Duration (for videos) */ - duration?: string; - /** File creation date */ - fileCreatedAt: string; - /** File modification date */ - fileModifiedAt: string; - /** Filename */ - filename?: string; -}; export type SignUpDto = { /** User email */ email: string; @@ -4272,27 +4256,6 @@ export function downloadAsset({ edited, id, key, slug }: { ...opts })); } -/** - * Replace asset - */ -export function replaceAsset({ id, key, slug, assetMediaReplaceDto }: { - id: string; - key?: string; - slug?: string; - assetMediaReplaceDto: AssetMediaReplaceDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetMediaResponseDto; - }>(`/assets/${encodeURIComponent(id)}/original${QS.query(QS.explode({ - key, - slug - }))}`, oazapfts.multipart({ - ...opts, - method: "PUT", - body: assetMediaReplaceDto - }))); -} /** * View asset thumbnail */ @@ -6932,7 +6895,6 @@ export enum Permission { AssetView = "asset.view", AssetDownload = "asset.download", AssetUpload = "asset.upload", - AssetReplace = "asset.replace", AssetCopy = "asset.copy", AssetDerive = "asset.derive", AssetEditGet = "asset.edit.get", diff --git a/server/src/controllers/asset-media.controller.ts b/server/src/controllers/asset-media.controller.ts index ec6083cfa8..c1ef8c853d 100644 --- a/server/src/controllers/asset-media.controller.ts +++ b/server/src/controllers/asset-media.controller.ts @@ -8,7 +8,6 @@ import { Param, ParseFilePipe, Post, - Put, Query, Req, Res, @@ -28,10 +27,8 @@ import { AssetBulkUploadCheckDto, AssetMediaCreateDto, AssetMediaOptionsDto, - AssetMediaReplaceDto, AssetMediaSize, CheckExistingAssetsDto, - UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -112,36 +109,6 @@ export class AssetMediaController { await sendFile(res, next, () => this.service.downloadOriginal(auth, id, dto), this.logger); } - @Put(':id/original') - @UseInterceptors(FileUploadInterceptor) - @ApiConsumes('multipart/form-data') - @ApiResponse({ - status: 200, - description: 'Asset replaced successfully', - type: AssetMediaResponseDto, - }) - @Endpoint({ - summary: 'Replace asset', - description: 'Replace the asset with new file, without changing its id.', - history: new HistoryBuilder().added('v1').deprecated('v1', { replacementId: 'copyAsset' }), - }) - @Authenticated({ permission: Permission.AssetReplace, sharedLink: true }) - async replaceAsset( - @Auth() auth: AuthDto, - @Param() { id }: UUIDParamDto, - @UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator([UploadFieldName.ASSET_DATA])] })) - files: UploadFiles, - @Body() dto: AssetMediaReplaceDto, - @Res({ passthrough: true }) res: Response, - ): Promise { - const { file } = getFiles(files); - const responseDto = await this.service.replaceAsset(auth, id, dto, file); - if (responseDto.status === AssetMediaStatus.DUPLICATE) { - res.status(HttpStatus.OK); - } - return responseDto; - } - @Get(':id/thumbnail') @FileResponse() @Authenticated({ permission: Permission.AssetView, sharedLink: true }) diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index 6a4c55c5aa..bb1d3826bb 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -53,8 +53,6 @@ const AssetMediaCreateSchema = AssetMediaBaseSchema.extend({ .meta({ type: 'string', format: 'binary' }), }).meta({ id: 'AssetMediaCreateDto' }); -const AssetMediaReplaceSchema = AssetMediaBaseSchema.meta({ id: 'AssetMediaReplaceDto' }); - const AssetBulkUploadCheckItemSchema = z .object({ id: z.string().describe('Asset ID'), @@ -77,6 +75,5 @@ const CheckExistingAssetsSchema = z export class AssetMediaOptionsDto extends createZodDto(AssetMediaOptionsSchema) {} export class AssetMediaCreateDto extends createZodDto(AssetMediaCreateSchema) {} -export class AssetMediaReplaceDto extends createZodDto(AssetMediaReplaceSchema) {} export class AssetBulkUploadCheckDto extends createZodDto(AssetBulkUploadCheckSchema) {} export class CheckExistingAssetsDto extends createZodDto(CheckExistingAssetsSchema) {} diff --git a/server/src/enum.ts b/server/src/enum.ts index cb4835020f..067b5435e4 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -146,7 +146,6 @@ export enum Permission { AssetView = 'asset.view', AssetDownload = 'asset.download', AssetUpload = 'asset.upload', - AssetReplace = 'asset.replace', AssetCopy = 'asset.copy', AssetDerive = 'asset.derive', diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index ddb6f412c5..74e7d08012 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Injectable, InternalServerErrorException, NotFound import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; import { StorageCore } from 'src/cores/storage.core'; -import { Asset, AuthSharedLink } from 'src/database'; +import { AuthSharedLink } from 'src/database'; import { AssetBulkUploadCheckResponseDto, AssetMediaResponseDto, @@ -15,7 +15,6 @@ import { AssetBulkUploadCheckDto, AssetMediaCreateDto, AssetMediaOptionsDto, - AssetMediaReplaceDto, AssetMediaSize, CheckExistingAssetsDto, UploadFieldName, @@ -24,7 +23,6 @@ import { AssetDownloadOriginalDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileType, - AssetStatus, AssetVisibility, CacheControl, ChecksumAlgorithm, @@ -164,40 +162,6 @@ export class AssetMediaService extends BaseService { } } - async replaceAsset( - auth: AuthDto, - id: string, - dto: AssetMediaReplaceDto, - file: UploadFile, - sidecarFile?: UploadFile, - ): Promise { - try { - await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] }); - const asset = await this.assetRepository.getById(id); - - if (!asset) { - throw new Error('Asset not found'); - } - - this.requireQuota(auth, file.size); - - await this.replaceFileData(asset.id, dto, file, sidecarFile?.originalPath); - - // Next, create a backup copy of the existing record. The db record has already been updated above, - // but the local variable holds the original file data paths. - const copiedPhoto = await this.createCopy(asset); - // and immediate trash it - await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.Trashed }); - await this.eventRepository.emit('AssetTrash', { assetId: copiedPhoto.id, userId: auth.user.id }); - - await this.userRepository.updateUsage(auth.user.id, file.size); - - return { status: AssetMediaStatus.REPLACED, id: copiedPhoto.id }; - } catch (error: any) { - return this.handleUploadError(error, auth, file, sidecarFile); - } - } - async downloadOriginal(auth: AuthDto, id: string, dto: AssetDownloadOriginalDto): Promise { await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: [id] }); @@ -367,83 +331,6 @@ export class AssetMediaService extends BaseService { throw error; } - /** - * Updates the specified assetId to the specified photo data file properties: checksum, path, - * timestamps, deviceIds, and sidecar. Derived properties like: faces, smart search info, etc - * are UNTOUCHED. The photo data files modification times on the filesysytem are updated to - * the specified timestamps. The exif db record is upserted, and then A METADATA_EXTRACTION - * job is queued to update these derived properties. - */ - private async replaceFileData( - assetId: string, - dto: AssetMediaReplaceDto, - file: UploadFile, - sidecarPath?: string, - ): Promise { - await this.assetRepository.update({ - id: assetId, - - checksum: file.checksum, - originalPath: file.originalPath, - type: mimeTypes.assetType(file.originalPath), - originalFileName: file.originalName, - - deviceAssetId: dto.deviceAssetId, - deviceId: dto.deviceId, - fileCreatedAt: dto.fileCreatedAt, - fileModifiedAt: dto.fileModifiedAt, - localDateTime: dto.fileCreatedAt, - duration: dto.duration || null, - - livePhotoVideoId: null, - }); - - await (sidecarPath - ? this.assetRepository.upsertFile({ assetId, type: AssetFileType.Sidecar, path: sidecarPath }) - : this.assetRepository.deleteFile({ assetId, type: AssetFileType.Sidecar })); - - await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); - await this.assetRepository.upsertExif( - { assetId, fileSizeInByte: file.size }, - { lockedPropertiesBehavior: 'override' }, - ); - await this.jobRepository.queue({ - name: JobName.AssetExtractMetadata, - data: { id: assetId, source: 'upload' }, - }); - } - - /** - * Create a 'shallow' copy of the specified asset record creating a new asset record in the database. - * Uses only vital properties excluding things like: stacks, faces, smart search info, etc, - * and then queues a METADATA_EXTRACTION job. - */ - private async createCopy(asset: Omit) { - const created = await this.assetRepository.create({ - ownerId: asset.ownerId, - originalPath: asset.originalPath, - originalFileName: asset.originalFileName, - libraryId: asset.libraryId, - deviceAssetId: asset.deviceAssetId, - deviceId: asset.deviceId, - type: asset.type, - checksum: asset.checksum, - checksumAlgorithm: asset.checksumAlgorithm, - fileCreatedAt: asset.fileCreatedAt, - localDateTime: asset.localDateTime, - fileModifiedAt: asset.fileModifiedAt, - livePhotoVideoId: asset.livePhotoVideoId, - }); - - const { size } = await this.storageRepository.stat(created.originalPath); - await this.assetRepository.upsertExif( - { assetId: created.id, fileSizeInByte: size }, - { lockedPropertiesBehavior: 'override' }, - ); - await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: created.id, source: 'copy' } }); - return created; - } - private async create(ownerId: string, dto: AssetMediaCreateDto, file: UploadFile, sidecarFile?: UploadFile) { const asset = await this.assetRepository.create({ ownerId,