From b051b29eca418bb867edba58af13df3bccb8230c Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 27 Aug 2024 03:48:39 +0200 Subject: [PATCH] feat(server): Storage template support album condition (#12000) feat(server): Storage template support album condition ([Request](https://github.com/immich-app/immich/discussions/11999)) --- .../services/storage-template.service.spec.ts | 44 ++++++++++++++++++- .../src/services/storage-template.service.ts | 4 +- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index c1e0410a3d..92d11eaa12 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -15,6 +15,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { StorageTemplateService } from 'src/services/storage-template.service'; +import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; @@ -83,7 +84,7 @@ describe(StorageTemplateService.name, () => { newConfig: { storageTemplate: { template: - '{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{album}}', + '{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{#if album}}{{album}}{{else}}other{{/if}}', }, } as SystemConfig, oldConfig: {} as SystemConfig, @@ -163,6 +164,47 @@ describe(StorageTemplateService.name, () => { originalPath: newMotionPicturePath, }); }); + it('Should use handlebar if condition for album', async () => { + const asset = assetStub.image; + const user = userStub.user1; + const album = albumStub.oneAsset; + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}'; + SystemConfigCore.create(systemMock, loggerMock).config$.next(config); + + userMock.get.mockResolvedValue(user); + assetMock.getByIds.mockResolvedValueOnce([asset]); + albumMock.getByAssetId.mockResolvedValueOnce([album]); + + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + + expect(moveMock.create).toHaveBeenCalledWith({ + entityId: asset.id, + newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`, + oldPath: asset.originalPath, + pathType: AssetPathType.ORIGINAL, + }); + }); + it('Should use handlebar else condition for album', async () => { + const asset = assetStub.image; + const user = userStub.user1; + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}'; + SystemConfigCore.create(systemMock, loggerMock).config$.next(config); + + userMock.get.mockResolvedValue(user); + assetMock.getByIds.mockResolvedValueOnce([asset]); + + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + + const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); + expect(moveMock.create).toHaveBeenCalledWith({ + entityId: asset.id, + newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`, + oldPath: asset.originalPath, + pathType: AssetPathType.ORIGINAL, + }); + }); it('should migrate previously failed move from original path when it still exists', async () => { userMock.get.mockResolvedValue(userStub.user1); const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`; diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 0ee5bdd3b5..4855d602d7 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -308,7 +308,7 @@ export class StorageTemplateService { filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO', assetId: asset.id, //just throw into the root if it doesn't belong to an album - album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '.', + album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '', }; const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -329,6 +329,6 @@ export class StorageTemplateService { substitutions[token] = dt.toFormat(token); } - return template(substitutions); + return template(substitutions).replaceAll(/\/{2,}/gm, '/'); } }