From e9038193dbbbd4f5796c729802ac0ced7cc90014 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Fri, 31 Oct 2025 00:40:58 +0100 Subject: [PATCH 1/2] fix: asset copy validation error (#23387) --- .../src/controllers/asset.controller.spec.ts | 22 +++++++++++++++++++ server/src/controllers/asset.controller.ts | 14 ++++++------ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 7a7a37fe2e..649c80e850 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -57,6 +57,28 @@ describe(AssetController.name, () => { }); }); + describe('PUT /assets/copy', () => { + it('should be an authenticated route', async () => { + await request(ctx.getHttpServer()).get(`/assets/copy`); + expect(ctx.authenticate).toHaveBeenCalled(); + }); + + it('should require target and source id', async () => { + const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({}); + expect(status).toBe(400); + expect(body).toEqual( + factory.responses.badRequest(expect.arrayContaining(['sourceId must be a UUID', 'targetId must be a UUID'])), + ); + }); + + it('should work', async () => { + const { status } = await request(ctx.getHttpServer()) + .put('/assets/copy') + .send({ sourceId: factory.uuid(), targetId: factory.uuid() }); + expect(status).toBe(204); + }); + }); + describe('PUT /assets/:id', () => { it('should be an authenticated route', async () => { await request(ctx.getHttpServer()).get(`/assets/123`); diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index fb528a5830..a6f8c7921d 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -81,6 +81,13 @@ export class AssetController { return this.service.get(auth, id) as Promise; } + @Put('copy') + @Authenticated({ permission: Permission.AssetCopy }) + @HttpCode(HttpStatus.NO_CONTENT) + copyAsset(@Auth() auth: AuthDto, @Body() dto: AssetCopyDto): Promise { + return this.service.copy(auth, dto); + } + @Put(':id') @Authenticated({ permission: Permission.AssetUpdate }) updateAsset( @@ -91,13 +98,6 @@ export class AssetController { return this.service.update(auth, id, dto); } - @Put('copy') - @Authenticated({ permission: Permission.AssetCopy }) - @HttpCode(HttpStatus.NO_CONTENT) - copyAsset(@Auth() auth: AuthDto, @Body() dto: AssetCopyDto): Promise { - return this.service.copy(auth, dto); - } - @Get(':id/metadata') @Authenticated({ permission: Permission.AssetRead }) getAssetMetadata(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { From 61a2c3ace32522b9511e58ccd397280e791a86b3 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Fri, 31 Oct 2025 00:55:39 +0100 Subject: [PATCH 2/2] chore(server): clarify asset copy parameters (#23396) --- server/src/services/asset.service.ts | 38 ++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index c1c2fb53c8..23cc6791dd 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -217,7 +217,7 @@ export class AssetService extends BaseService { } if (stack) { - await this.copyStack(sourceAsset, targetAsset); + await this.copyStack({ sourceAsset, targetAsset }); } if (favorite) { @@ -225,14 +225,17 @@ export class AssetService extends BaseService { } if (sidecar) { - await this.copySidecar(sourceAsset, targetAsset); + await this.copySidecar({ sourceAsset, targetAsset }); } } - private async copyStack( - sourceAsset: { id: string; stackId: string | null }, - targetAsset: { id: string; stackId: string | null }, - ) { + private async copyStack({ + sourceAsset, + targetAsset, + }: { + sourceAsset: { id: string; stackId: string | null }; + targetAsset: { id: string; stackId: string | null }; + }) { if (!sourceAsset.stackId) { return; } @@ -245,21 +248,24 @@ export class AssetService extends BaseService { } } - private async copySidecar( - targetAsset: { sidecarPath: string | null }, - sourceAsset: { id: string; sidecarPath: string | null; originalPath: string }, - ) { - if (!targetAsset.sidecarPath) { + private async copySidecar({ + sourceAsset, + targetAsset, + }: { + sourceAsset: { sidecarPath: string | null }; + targetAsset: { id: string; sidecarPath: string | null; originalPath: string }; + }) { + if (!sourceAsset.sidecarPath) { return; } - if (sourceAsset.sidecarPath) { - await this.storageRepository.unlink(sourceAsset.sidecarPath); + if (targetAsset.sidecarPath) { + await this.storageRepository.unlink(targetAsset.sidecarPath); } - await this.storageRepository.copyFile(targetAsset.sidecarPath, `${sourceAsset.originalPath}.xmp`); - await this.assetRepository.update({ id: sourceAsset.id, sidecarPath: `${sourceAsset.originalPath}.xmp` }); - await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: sourceAsset.id } }); + await this.storageRepository.copyFile(sourceAsset.sidecarPath, `${targetAsset.originalPath}.xmp`); + await this.assetRepository.update({ id: targetAsset.id, sidecarPath: `${targetAsset.originalPath}.xmp` }); + await this.jobRepository.queue({ name: JobName.AssetExtractMetadata, data: { id: targetAsset.id } }); } @OnJob({ name: JobName.AssetDeleteCheck, queue: QueueName.BackgroundTask })