mirror of
https://github.com/immich-app/immich.git
synced 2025-01-28 06:32:44 +01:00
refactor(server): storage template options (#13553)
This commit is contained in:
parent
bb694aeeeb
commit
3d971f69dc
6 changed files with 80 additions and 112 deletions
|
@ -30,35 +30,6 @@ export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico
|
||||||
|
|
||||||
export const FACE_THUMBNAIL_SIZE = 250;
|
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 };
|
type ModelInfo = { dimSize: number };
|
||||||
export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
|
export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
|
||||||
RN101__openai: { dimSize: 512 },
|
RN101__openai: { dimSize: 512 },
|
||||||
|
|
|
@ -3,12 +3,16 @@ import { ApiTags } from '@nestjs/swagger';
|
||||||
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
||||||
import { Permission } from 'src/enum';
|
import { Permission } from 'src/enum';
|
||||||
import { Authenticated } from 'src/middleware/auth.guard';
|
import { Authenticated } from 'src/middleware/auth.guard';
|
||||||
|
import { StorageTemplateService } from 'src/services/storage-template.service';
|
||||||
import { SystemConfigService } from 'src/services/system-config.service';
|
import { SystemConfigService } from 'src/services/system-config.service';
|
||||||
|
|
||||||
@ApiTags('System Config')
|
@ApiTags('System Config')
|
||||||
@Controller('system-config')
|
@Controller('system-config')
|
||||||
export class SystemConfigController {
|
export class SystemConfigController {
|
||||||
constructor(private service: SystemConfigService) {}
|
constructor(
|
||||||
|
private service: SystemConfigService,
|
||||||
|
private storageTemplateService: StorageTemplateService,
|
||||||
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
||||||
|
@ -31,6 +35,6 @@ export class SystemConfigController {
|
||||||
@Get('storage-template-options')
|
@Get('storage-template-options')
|
||||||
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true })
|
||||||
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
|
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
|
||||||
return this.service.getStorageTemplateOptions();
|
return this.storageTemplateService.getStorageTemplateOptions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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', () => {
|
describe('handleMigrationSingle', () => {
|
||||||
it('should skip when storage template is disabled', async () => {
|
it('should skip when storage template is disabled', async () => {
|
||||||
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: false } });
|
systemMock.get.mockResolvedValue({ storageTemplate: { enabled: false } });
|
||||||
|
|
|
@ -3,17 +3,9 @@ import handlebar from 'handlebars';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import {
|
|
||||||
supportedDayTokens,
|
|
||||||
supportedHourTokens,
|
|
||||||
supportedMinuteTokens,
|
|
||||||
supportedMonthTokens,
|
|
||||||
supportedSecondTokens,
|
|
||||||
supportedWeekTokens,
|
|
||||||
supportedYearTokens,
|
|
||||||
} from 'src/constants';
|
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
|
import { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
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 { getLivePhotoMotionFilename } from 'src/utils/file';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
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 {
|
export interface MoveAssetMetadata {
|
||||||
storageLabel: string | null;
|
storageLabel: string | null;
|
||||||
filename: string;
|
filename: string;
|
||||||
|
@ -80,6 +104,10 @@ export class StorageTemplateService extends BaseService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
|
||||||
|
return { ...storageTokens, presetOptions: storagePresets };
|
||||||
|
}
|
||||||
|
|
||||||
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
|
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const config = await this.getConfig({ withCache: true });
|
const config = await this.getConfig({ withCache: true });
|
||||||
const storageTemplateEnabled = config.storageTemplate.enabled;
|
const storageTemplateEnabled = config.storageTemplate.enabled;
|
||||||
|
@ -277,17 +305,7 @@ export class StorageTemplateService extends BaseService {
|
||||||
const zone = asset.exifInfo?.timeZone || systemTimeZone;
|
const zone = asset.exifInfo?.timeZone || systemTimeZone;
|
||||||
const dt = DateTime.fromJSDate(asset.fileCreatedAt, { zone });
|
const dt = DateTime.fromJSDate(asset.fileCreatedAt, { zone });
|
||||||
|
|
||||||
const dateTokens = [
|
for (const token of Object.values(storageTokens).flat()) {
|
||||||
...supportedYearTokens,
|
|
||||||
...supportedMonthTokens,
|
|
||||||
...supportedWeekTokens,
|
|
||||||
...supportedDayTokens,
|
|
||||||
...supportedHourTokens,
|
|
||||||
...supportedMinuteTokens,
|
|
||||||
...supportedSecondTokens,
|
|
||||||
];
|
|
||||||
|
|
||||||
for (const token of dateTokens) {
|
|
||||||
substitutions[token] = dt.toFormat(token);
|
substitutions[token] = dt.toFormat(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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', () => {
|
describe('updateConfig', () => {
|
||||||
it('should update the config and emit an event', async () => {
|
it('should update the config and emit an event', async () => {
|
||||||
systemMock.get.mockResolvedValue(partialConfig);
|
systemMock.get.mockResolvedValue(partialConfig);
|
||||||
|
|
|
@ -2,18 +2,8 @@ import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { instanceToPlain } from 'class-transformer';
|
import { instanceToPlain } from 'class-transformer';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { defaults } from 'src/config';
|
import { defaults } from 'src/config';
|
||||||
import {
|
|
||||||
supportedDayTokens,
|
|
||||||
supportedHourTokens,
|
|
||||||
supportedMinuteTokens,
|
|
||||||
supportedMonthTokens,
|
|
||||||
supportedPresetTokens,
|
|
||||||
supportedSecondTokens,
|
|
||||||
supportedWeekTokens,
|
|
||||||
supportedYearTokens,
|
|
||||||
} from 'src/constants';
|
|
||||||
import { OnEvent } from 'src/decorators';
|
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 { ArgOf } from 'src/interfaces/event.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { clearConfigCache } from 'src/utils/config';
|
import { clearConfigCache } from 'src/utils/config';
|
||||||
|
@ -77,21 +67,6 @@ export class SystemConfigService extends BaseService {
|
||||||
return mapConfig(newConfig);
|
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> {
|
async getCustomCss(): Promise<string> {
|
||||||
const { theme } = await this.getConfig({ withCache: false });
|
const { theme } = await this.getConfig({ withCache: false });
|
||||||
return theme.customCss;
|
return theme.customCss;
|
||||||
|
|
Loading…
Reference in a new issue