2023-07-01 06:43:24 +02:00
|
|
|
import { AssetEntity, AssetType, SystemConfig } from '@app/infra/entities';
|
2023-02-25 15:12:03 +01:00
|
|
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
2023-07-01 06:43:24 +02:00
|
|
|
import handlebar from 'handlebars';
|
|
|
|
import * as luxon from 'luxon';
|
|
|
|
import path from 'node:path';
|
|
|
|
import sanitize from 'sanitize-filename';
|
2023-02-25 15:12:03 +01:00
|
|
|
import { IAssetRepository } from '../asset/asset.repository';
|
2023-05-22 20:05:06 +02:00
|
|
|
import { getLivePhotoMotionFilename, usePagination } from '../domain.util';
|
2023-05-26 21:43:24 +02:00
|
|
|
import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE } from '../job';
|
2023-07-01 06:43:24 +02:00
|
|
|
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
|
|
|
|
import {
|
|
|
|
INITIAL_SYSTEM_CONFIG,
|
|
|
|
ISystemConfigRepository,
|
|
|
|
supportedDayTokens,
|
|
|
|
supportedHourTokens,
|
|
|
|
supportedMinuteTokens,
|
|
|
|
supportedMonthTokens,
|
|
|
|
supportedSecondTokens,
|
|
|
|
supportedYearTokens,
|
|
|
|
} from '../system-config';
|
|
|
|
import { SystemConfigCore } from '../system-config/system-config.core';
|
2023-05-22 05:18:10 +02:00
|
|
|
import { IUserRepository } from '../user/user.repository';
|
2023-02-25 15:12:03 +01:00
|
|
|
|
2023-05-22 05:18:10 +02:00
|
|
|
export interface MoveAssetMetadata {
|
|
|
|
storageLabel: string | null;
|
|
|
|
filename: string;
|
|
|
|
}
|
|
|
|
|
2023-02-25 15:12:03 +01:00
|
|
|
@Injectable()
|
|
|
|
export class StorageTemplateService {
|
|
|
|
private logger = new Logger(StorageTemplateService.name);
|
2023-07-01 06:43:24 +02:00
|
|
|
private configCore: SystemConfigCore;
|
|
|
|
private storageCore = new StorageCore();
|
|
|
|
private storageTemplate: HandlebarsTemplateDelegate<any>;
|
2023-02-25 15:12:03 +01:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
|
|
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
|
|
|
@Inject(INITIAL_SYSTEM_CONFIG) config: SystemConfig,
|
|
|
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
2023-05-22 05:18:10 +02:00
|
|
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
2023-02-25 15:12:03 +01:00
|
|
|
) {
|
2023-07-01 06:43:24 +02:00
|
|
|
this.storageTemplate = this.compile(config.storageTemplate.template);
|
|
|
|
this.configCore = new SystemConfigCore(configRepository);
|
|
|
|
this.configCore.addValidator((config) => this.validate(config));
|
|
|
|
this.configCore.config$.subscribe((config) => this.onConfig(config));
|
2023-02-25 15:12:03 +01:00
|
|
|
}
|
|
|
|
|
2023-05-26 21:43:24 +02:00
|
|
|
async handleMigrationSingle({ id }: IEntityJob) {
|
|
|
|
const [asset] = await this.assetRepository.getByIds([id]);
|
2023-03-28 22:04:11 +02:00
|
|
|
|
2023-05-26 21:43:24 +02:00
|
|
|
const user = await this.userRepository.get(asset.ownerId);
|
|
|
|
const storageLabel = user?.storageLabel || null;
|
|
|
|
const filename = asset.originalFileName || asset.id;
|
|
|
|
await this.moveAsset(asset, { storageLabel, filename });
|
|
|
|
|
|
|
|
// move motion part of live photo
|
|
|
|
if (asset.livePhotoVideoId) {
|
|
|
|
const [livePhotoVideo] = await this.assetRepository.getByIds([asset.livePhotoVideoId]);
|
|
|
|
const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath);
|
|
|
|
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename });
|
2023-03-28 22:04:11 +02:00
|
|
|
}
|
2023-05-26 21:43:24 +02:00
|
|
|
|
|
|
|
return true;
|
2023-03-28 22:04:11 +02:00
|
|
|
}
|
|
|
|
|
2023-05-26 14:52:52 +02:00
|
|
|
async handleMigration() {
|
2023-07-01 06:43:24 +02:00
|
|
|
this.logger.log('Starting storage template migration');
|
|
|
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
|
|
|
this.assetRepository.getAll(pagination),
|
|
|
|
);
|
|
|
|
const users = await this.userRepository.getList();
|
|
|
|
|
|
|
|
for await (const assets of assetPagination) {
|
|
|
|
for (const asset of assets) {
|
|
|
|
const user = users.find((user) => user.id === asset.ownerId);
|
|
|
|
const storageLabel = user?.storageLabel || null;
|
|
|
|
const filename = asset.originalFileName || asset.id;
|
|
|
|
await this.moveAsset(asset, { storageLabel, filename });
|
2023-02-25 15:12:03 +01:00
|
|
|
}
|
|
|
|
}
|
2023-05-26 21:43:24 +02:00
|
|
|
|
2023-07-01 06:43:24 +02:00
|
|
|
this.logger.debug('Cleaning up empty directories...');
|
|
|
|
const libraryFolder = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);
|
|
|
|
await this.storageRepository.removeEmptyDirs(libraryFolder);
|
|
|
|
|
|
|
|
this.logger.log('Finished storage template migration');
|
|
|
|
|
2023-05-26 21:43:24 +02:00
|
|
|
return true;
|
2023-02-25 15:12:03 +01:00
|
|
|
}
|
|
|
|
|
2023-05-22 05:18:10 +02:00
|
|
|
async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) {
|
feat(server): support for read-only assets and importing existing items in the filesystem (#2715)
* Added read-only flag for assets, endpoint to trigger file import vs upload
* updated fixtures with new property
* if upload is 'read-only', ensure there is no existing asset at the designated originalPath
* added test for file import as well as detecting existing image at read-only destination location
* Added storage service test for a case where it should not move read-only assets
* upload doesn't need the read-only flag available, just importing
* default isReadOnly on import endpoint to true
* formatting fixes
* create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation
* updated code to reflect changes in MR
* fixed read stream promise return type
* new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates
* refactor: import asset
* chore: open api
* chore: tests
* Added externalPath support for individual users, updated UI to allow this to be set by admin
* added missing var for externalPath in ui
* chore: open api
* fix: compilation issues
* fix: server test
* built api, fixed user-response dto to include externalPath
* reverted accidental commit
* bad commit of duplicate externalPath in user response dto
* fixed tests to include externalPath on expected result
* fix: unit tests
* centralized supported filetypes, perform file type checking of asset and sidecar during file import process
* centralized supported filetype check method to keep regex DRY
* fixed typo
* combined migrations into one
* update api
* Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not
* update mimetype
* Fixed detect correct mimetype
* revert asset-upload config
* reverted domain.constant
* refactor
* fix mime-type issue
* fix format
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-06-22 04:33:20 +02:00
|
|
|
if (asset.isReadOnly) {
|
|
|
|
this.logger.verbose(`Not moving read-only asset: ${asset.originalPath}`);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-07-01 06:43:24 +02:00
|
|
|
const destination = await this.getTemplatePath(asset, metadata);
|
2023-02-25 15:12:03 +01:00
|
|
|
if (asset.originalPath !== destination) {
|
|
|
|
const source = asset.originalPath;
|
|
|
|
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
let sidecarMoved = false;
|
2023-02-25 15:12:03 +01:00
|
|
|
try {
|
|
|
|
await this.storageRepository.moveFile(asset.originalPath, destination);
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
|
|
|
|
let sidecarDestination;
|
2023-02-25 15:12:03 +01:00
|
|
|
try {
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
if (asset.sidecarPath) {
|
|
|
|
sidecarDestination = `${destination}.xmp`;
|
|
|
|
await this.storageRepository.moveFile(asset.sidecarPath, sidecarDestination);
|
|
|
|
sidecarMoved = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.assetRepository.save({ id: asset.id, originalPath: destination, sidecarPath: sidecarDestination });
|
2023-02-25 15:12:03 +01:00
|
|
|
asset.originalPath = destination;
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
asset.sidecarPath = sidecarDestination || null;
|
2023-02-25 15:12:03 +01:00
|
|
|
} catch (error: any) {
|
2023-06-20 23:24:47 +02:00
|
|
|
this.logger.warn(
|
|
|
|
`Unable to save new originalPath to database, undoing move for path ${asset.originalPath} - filename ${asset.originalFileName} - id ${asset.id}`,
|
|
|
|
error?.stack,
|
|
|
|
);
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
|
2023-09-11 17:56:38 +02:00
|
|
|
// Either sidecar move failed or the save failed. Either way, move media back
|
2023-02-25 15:12:03 +01:00
|
|
|
await this.storageRepository.moveFile(destination, source);
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-25 03:59:30 +02:00
|
|
|
|
|
|
|
if (asset.sidecarPath && sidecarDestination && sidecarMoved) {
|
|
|
|
// If the sidecar was moved, that means the saved failed. So move both the sidecar and the
|
|
|
|
// media back into their original positions
|
|
|
|
await this.storageRepository.moveFile(sidecarDestination, asset.sidecarPath);
|
|
|
|
}
|
2023-02-25 15:12:03 +01:00
|
|
|
}
|
|
|
|
} catch (error: any) {
|
|
|
|
this.logger.error(`Problem applying storage template`, error?.stack, { id: asset.id, source, destination });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return asset;
|
|
|
|
}
|
2023-07-01 06:43:24 +02:00
|
|
|
|
|
|
|
private async getTemplatePath(asset: AssetEntity, metadata: MoveAssetMetadata): Promise<string> {
|
|
|
|
const { storageLabel, filename } = metadata;
|
|
|
|
|
|
|
|
try {
|
|
|
|
const source = asset.originalPath;
|
|
|
|
const ext = path.extname(source).split('.').pop() as string;
|
|
|
|
const sanitized = sanitize(path.basename(filename, `.${ext}`));
|
|
|
|
const rootPath = this.storageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });
|
|
|
|
const storagePath = this.render(this.storageTemplate, asset, sanitized, ext);
|
|
|
|
const fullPath = path.normalize(path.join(rootPath, storagePath));
|
|
|
|
let destination = `${fullPath}.${ext}`;
|
|
|
|
|
|
|
|
if (!fullPath.startsWith(rootPath)) {
|
|
|
|
this.logger.warn(`Skipped attempt to access an invalid path: ${fullPath}. Path should start with ${rootPath}`);
|
|
|
|
return source;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (source === destination) {
|
|
|
|
return source;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* In case of migrating duplicate filename to a new path, we need to check if it is already migrated
|
|
|
|
* Due to the mechanism of appending +1, +2, +3, etc to the filename
|
|
|
|
*
|
|
|
|
* Example:
|
|
|
|
* Source = upload/abc/def/FullSizeRender+7.heic
|
|
|
|
* Expected Destination = upload/abc/def/FullSizeRender.heic
|
|
|
|
*
|
|
|
|
* The file is already at the correct location, but since there are other FullSizeRender.heic files in the
|
|
|
|
* destination, it was renamed to FullSizeRender+7.heic.
|
|
|
|
*
|
|
|
|
* The lines below will be used to check if the differences between the source and destination is only the
|
|
|
|
* +7 suffix, and if so, it will be considered as already migrated.
|
|
|
|
*/
|
|
|
|
if (source.startsWith(fullPath) && source.endsWith(`.${ext}`)) {
|
|
|
|
const diff = source.replace(fullPath, '').replace(`.${ext}`, '');
|
|
|
|
const hasDuplicationAnnotation = /^\+\d+$/.test(diff);
|
|
|
|
if (hasDuplicationAnnotation) {
|
|
|
|
return source;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
let duplicateCount = 0;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
const exists = await this.storageRepository.checkFileExists(destination);
|
|
|
|
if (!exists) {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
duplicateCount++;
|
|
|
|
destination = `${fullPath}+${duplicateCount}.${ext}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
return destination;
|
|
|
|
} catch (error: any) {
|
|
|
|
this.logger.error(`Unable to get template path for ${filename}`, error);
|
|
|
|
return asset.originalPath;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private validate(config: SystemConfig) {
|
|
|
|
const testAsset = {
|
|
|
|
fileCreatedAt: new Date(),
|
|
|
|
originalPath: '/upload/test/IMG_123.jpg',
|
|
|
|
type: AssetType.IMAGE,
|
|
|
|
} as AssetEntity;
|
|
|
|
try {
|
|
|
|
this.render(this.compile(config.storageTemplate.template), testAsset, 'IMG_123', 'jpg');
|
|
|
|
} catch (e) {
|
|
|
|
this.logger.warn(`Storage template validation failed: ${JSON.stringify(e)}`);
|
|
|
|
throw new Error(`Invalid storage template: ${e}`);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private onConfig(config: SystemConfig) {
|
|
|
|
this.logger.debug(`Received new config, recompiling storage template: ${config.storageTemplate.template}`);
|
|
|
|
this.storageTemplate = this.compile(config.storageTemplate.template);
|
|
|
|
}
|
|
|
|
|
|
|
|
private compile(template: string) {
|
|
|
|
return handlebar.compile(template, {
|
|
|
|
knownHelpers: undefined,
|
|
|
|
strict: true,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
private render(template: HandlebarsTemplateDelegate<any>, asset: AssetEntity, filename: string, ext: string) {
|
|
|
|
const substitutions: Record<string, string> = {
|
|
|
|
filename,
|
|
|
|
ext,
|
|
|
|
filetype: asset.type == AssetType.IMAGE ? 'IMG' : 'VID',
|
|
|
|
filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
|
|
|
|
};
|
|
|
|
|
2023-08-02 00:08:07 +02:00
|
|
|
const dt = luxon.DateTime.fromJSDate(asset.fileCreatedAt, { zone: asset.exifInfo?.timeZone || undefined });
|
2023-07-01 06:43:24 +02:00
|
|
|
|
|
|
|
const dateTokens = [
|
|
|
|
...supportedYearTokens,
|
|
|
|
...supportedMonthTokens,
|
|
|
|
...supportedDayTokens,
|
|
|
|
...supportedHourTokens,
|
|
|
|
...supportedMinuteTokens,
|
|
|
|
...supportedSecondTokens,
|
|
|
|
];
|
|
|
|
|
|
|
|
for (const token of dateTokens) {
|
|
|
|
substitutions[token] = dt.toFormat(token);
|
|
|
|
}
|
|
|
|
|
|
|
|
return template(substitutions);
|
|
|
|
}
|
2023-02-25 15:12:03 +01:00
|
|
|
}
|