From 3d971f69dc46998308f1db252c5d09c55f37c0b9 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jason@rasm.me>
Date: Thu, 17 Oct 2024 13:11:51 -0400
Subject: [PATCH] refactor(server): storage template options (#13553)

---
 server/src/constants.ts                       | 29 ----------
 .../controllers/system-config.controller.ts   |  8 ++-
 .../services/storage-template.service.spec.ts | 35 +++++++++++
 .../src/services/storage-template.service.ts  | 58 ++++++++++++-------
 .../services/system-config.service.spec.ts    | 35 -----------
 server/src/services/system-config.service.ts  | 27 +--------
 6 files changed, 80 insertions(+), 112 deletions(-)

diff --git a/server/src/constants.ts b/server/src/constants.ts
index eef9ffab05..e99970723a 100644
--- a/server/src/constants.ts
+++ b/server/src/constants.ts
@@ -30,35 +30,6 @@ export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico
 
 export const FACE_THUMBNAIL_SIZE = 250;
 
-export const supportedYearTokens = ['y', 'yy'];
-export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM'];
-export const supportedWeekTokens = ['W', 'WW'];
-export const supportedDayTokens = ['d', 'dd'];
-export const supportedHourTokens = ['h', 'hh', 'H', 'HH'];
-export const supportedMinuteTokens = ['m', 'mm'];
-export const supportedSecondTokens = ['s', 'ss', 'SSS'];
-export const supportedPresetTokens = [
-  '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
-  '{{y}}/{{MM}}-{{dd}}/{{filename}}',
-  '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
-  '{{y}}/{{MM}}/{{filename}}',
-  '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
-  '{{y}}/{{MMM}}/{{filename}}',
-  '{{y}}/{{MMMM}}/{{filename}}',
-  '{{y}}/{{MM}}/{{dd}}/{{filename}}',
-  '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
-  '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
-  '{{y}}-{{MM}}-{{dd}}/{{filename}}',
-  '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
-  '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
-  '{{y}}/{{y}}-{{MM}}/{{filename}}',
-  '{{y}}/{{y}}-{{WW}}/{{filename}}',
-  '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
-  '{{y}}/{{y}}-{{MM}}/{{assetId}}',
-  '{{y}}/{{y}}-{{WW}}/{{assetId}}',
-  '{{album}}/{{filename}}',
-];
-
 type ModelInfo = { dimSize: number };
 export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
   RN101__openai: { dimSize: 512 },
diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts
index f59c8ad66c..58e8bde87b 100644
--- a/server/src/controllers/system-config.controller.ts
+++ b/server/src/controllers/system-config.controller.ts
@@ -3,12 +3,16 @@ import { ApiTags } from '@nestjs/swagger';
 import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
 import { Permission } from 'src/enum';
 import { Authenticated } from 'src/middleware/auth.guard';
+import { StorageTemplateService } from 'src/services/storage-template.service';
 import { SystemConfigService } from 'src/services/system-config.service';
 
 @ApiTags('System Config')
 @Controller('system-config')
 export class SystemConfigController {
-  constructor(private service: SystemConfigService) {}
+  constructor(
+    private service: SystemConfigService,
+    private storageTemplateService: StorageTemplateService,
+  ) {}
 
   @Get()
   @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
@@ -31,6 +35,6 @@ export class SystemConfigController {
   @Get('storage-template-options')
   @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
   getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
-    return this.service.getStorageTemplateOptions();
+    return this.storageTemplateService.getStorageTemplateOptions();
   }
 }
diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts
index 6e5af3baf9..fd063bd50d 100644
--- a/server/src/services/storage-template.service.spec.ts
+++ b/server/src/services/storage-template.service.spec.ts
@@ -70,6 +70,41 @@ describe(StorageTemplateService.name, () => {
     });
   });
 
+  describe('getStorageTemplateOptions', () => {
+    it('should send back the datetime variables', () => {
+      expect(sut.getStorageTemplateOptions()).toEqual({
+        dayOptions: ['d', 'dd'],
+        hourOptions: ['h', 'hh', 'H', 'HH'],
+        minuteOptions: ['m', 'mm'],
+        monthOptions: ['M', 'MM', 'MMM', 'MMMM'],
+        presetOptions: [
+          '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+          '{{y}}/{{MM}}-{{dd}}/{{filename}}',
+          '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
+          '{{y}}/{{MM}}/{{filename}}',
+          '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
+          '{{y}}/{{MMM}}/{{filename}}',
+          '{{y}}/{{MMMM}}/{{filename}}',
+          '{{y}}/{{MM}}/{{dd}}/{{filename}}',
+          '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
+          '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+          '{{y}}-{{MM}}-{{dd}}/{{filename}}',
+          '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
+          '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
+          '{{y}}/{{y}}-{{MM}}/{{filename}}',
+          '{{y}}/{{y}}-{{WW}}/{{filename}}',
+          '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
+          '{{y}}/{{y}}-{{MM}}/{{assetId}}',
+          '{{y}}/{{y}}-{{WW}}/{{assetId}}',
+          '{{album}}/{{filename}}',
+        ],
+        secondOptions: ['s', 'ss', 'SSS'],
+        weekOptions: ['W', 'WW'],
+        yearOptions: ['y', 'yy'],
+      });
+    });
+  });
+
   describe('handleMigrationSingle', () => {
     it('should skip when storage template is disabled', async () => {
       systemMock.get.mockResolvedValue({ storageTemplate: { enabled: false } });
diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts
index e400981f54..d239435660 100644
--- a/server/src/services/storage-template.service.ts
+++ b/server/src/services/storage-template.service.ts
@@ -3,17 +3,9 @@ import handlebar from 'handlebars';
 import { DateTime } from 'luxon';
 import path from 'node:path';
 import sanitize from 'sanitize-filename';
-import {
-  supportedDayTokens,
-  supportedHourTokens,
-  supportedMinuteTokens,
-  supportedMonthTokens,
-  supportedSecondTokens,
-  supportedWeekTokens,
-  supportedYearTokens,
-} from 'src/constants';
 import { StorageCore } from 'src/cores/storage.core';
 import { OnEvent } from 'src/decorators';
+import { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
 import { DatabaseLock } from 'src/interfaces/database.interface';
@@ -23,6 +15,38 @@ import { BaseService } from 'src/services/base.service';
 import { getLivePhotoMotionFilename } from 'src/utils/file';
 import { usePagination } from 'src/utils/pagination';
 
+const storageTokens = {
+  secondOptions: ['s', 'ss', 'SSS'],
+  minuteOptions: ['m', 'mm'],
+  dayOptions: ['d', 'dd'],
+  weekOptions: ['W', 'WW'],
+  hourOptions: ['h', 'hh', 'H', 'HH'],
+  yearOptions: ['y', 'yy'],
+  monthOptions: ['M', 'MM', 'MMM', 'MMMM'],
+};
+
+const storagePresets = [
+  '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+  '{{y}}/{{MM}}-{{dd}}/{{filename}}',
+  '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
+  '{{y}}/{{MM}}/{{filename}}',
+  '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
+  '{{y}}/{{MMM}}/{{filename}}',
+  '{{y}}/{{MMMM}}/{{filename}}',
+  '{{y}}/{{MM}}/{{dd}}/{{filename}}',
+  '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
+  '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+  '{{y}}-{{MM}}-{{dd}}/{{filename}}',
+  '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
+  '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
+  '{{y}}/{{y}}-{{MM}}/{{filename}}',
+  '{{y}}/{{y}}-{{WW}}/{{filename}}',
+  '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
+  '{{y}}/{{y}}-{{MM}}/{{assetId}}',
+  '{{y}}/{{y}}-{{WW}}/{{assetId}}',
+  '{{album}}/{{filename}}',
+];
+
 export interface MoveAssetMetadata {
   storageLabel: string | null;
   filename: string;
@@ -80,6 +104,10 @@ export class StorageTemplateService extends BaseService {
     }
   }
 
+  getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
+    return { ...storageTokens, presetOptions: storagePresets };
+  }
+
   async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
     const config = await this.getConfig({ withCache: true });
     const storageTemplateEnabled = config.storageTemplate.enabled;
@@ -277,17 +305,7 @@ export class StorageTemplateService extends BaseService {
     const zone = asset.exifInfo?.timeZone || systemTimeZone;
     const dt = DateTime.fromJSDate(asset.fileCreatedAt, { zone });
 
-    const dateTokens = [
-      ...supportedYearTokens,
-      ...supportedMonthTokens,
-      ...supportedWeekTokens,
-      ...supportedDayTokens,
-      ...supportedHourTokens,
-      ...supportedMinuteTokens,
-      ...supportedSecondTokens,
-    ];
-
-    for (const token of dateTokens) {
+    for (const token of Object.values(storageTokens).flat()) {
       substitutions[token] = dt.toFormat(token);
     }
 
diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts
index 52a5b1dcd8..807d8299b8 100644
--- a/server/src/services/system-config.service.spec.ts
+++ b/server/src/services/system-config.service.spec.ts
@@ -341,41 +341,6 @@ describe(SystemConfigService.name, () => {
     }
   });
 
-  describe('getStorageTemplateOptions', () => {
-    it('should send back the datetime variables', () => {
-      expect(sut.getStorageTemplateOptions()).toEqual({
-        dayOptions: ['d', 'dd'],
-        hourOptions: ['h', 'hh', 'H', 'HH'],
-        minuteOptions: ['m', 'mm'],
-        monthOptions: ['M', 'MM', 'MMM', 'MMMM'],
-        presetOptions: [
-          '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
-          '{{y}}/{{MM}}-{{dd}}/{{filename}}',
-          '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
-          '{{y}}/{{MM}}/{{filename}}',
-          '{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}',
-          '{{y}}/{{MMM}}/{{filename}}',
-          '{{y}}/{{MMMM}}/{{filename}}',
-          '{{y}}/{{MM}}/{{dd}}/{{filename}}',
-          '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
-          '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
-          '{{y}}-{{MM}}-{{dd}}/{{filename}}',
-          '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
-          '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
-          '{{y}}/{{y}}-{{MM}}/{{filename}}',
-          '{{y}}/{{y}}-{{WW}}/{{filename}}',
-          '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
-          '{{y}}/{{y}}-{{MM}}/{{assetId}}',
-          '{{y}}/{{y}}-{{WW}}/{{assetId}}',
-          '{{album}}/{{filename}}',
-        ],
-        secondOptions: ['s', 'ss', 'SSS'],
-        weekOptions: ['W', 'WW'],
-        yearOptions: ['y', 'yy'],
-      });
-    });
-  });
-
   describe('updateConfig', () => {
     it('should update the config and emit an event', async () => {
       systemMock.get.mockResolvedValue(partialConfig);
diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts
index 96a1f0897b..8f19b22173 100644
--- a/server/src/services/system-config.service.ts
+++ b/server/src/services/system-config.service.ts
@@ -2,18 +2,8 @@ import { BadRequestException, Injectable } from '@nestjs/common';
 import { instanceToPlain } from 'class-transformer';
 import _ from 'lodash';
 import { defaults } from 'src/config';
-import {
-  supportedDayTokens,
-  supportedHourTokens,
-  supportedMinuteTokens,
-  supportedMonthTokens,
-  supportedPresetTokens,
-  supportedSecondTokens,
-  supportedWeekTokens,
-  supportedYearTokens,
-} from 'src/constants';
 import { OnEvent } from 'src/decorators';
-import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
+import { SystemConfigDto, mapConfig } from 'src/dtos/system-config.dto';
 import { ArgOf } from 'src/interfaces/event.interface';
 import { BaseService } from 'src/services/base.service';
 import { clearConfigCache } from 'src/utils/config';
@@ -77,21 +67,6 @@ export class SystemConfigService extends BaseService {
     return mapConfig(newConfig);
   }
 
-  getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
-    const options = new SystemConfigTemplateStorageOptionDto();
-
-    options.dayOptions = supportedDayTokens;
-    options.weekOptions = supportedWeekTokens;
-    options.monthOptions = supportedMonthTokens;
-    options.yearOptions = supportedYearTokens;
-    options.hourOptions = supportedHourTokens;
-    options.secondOptions = supportedSecondTokens;
-    options.minuteOptions = supportedMinuteTokens;
-    options.presetOptions = supportedPresetTokens;
-
-    return options;
-  }
-
   async getCustomCss(): Promise<string> {
     const { theme } = await this.getConfig({ withCache: false });
     return theme.customCss;