Created
March 17, 2024 09:27
-
-
Save pastleo/cab5cfcd709747b162574337e60e53de to your computer and use it in GitHub Desktop.
patched adding {{{albumPath}}} for immich
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"use strict"; | |
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { | |
if (k2 === undefined) k2 = k; | |
var desc = Object.getOwnPropertyDescriptor(m, k); | |
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { | |
desc = { enumerable: true, get: function() { return m[k]; } }; | |
} | |
Object.defineProperty(o, k2, desc); | |
}) : (function(o, m, k, k2) { | |
if (k2 === undefined) k2 = k; | |
o[k2] = m[k]; | |
})); | |
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { | |
Object.defineProperty(o, "default", { enumerable: true, value: v }); | |
}) : function(o, v) { | |
o["default"] = v; | |
}); | |
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { | |
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; | |
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); | |
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; | |
return c > 3 && r && Object.defineProperty(target, key, r), r; | |
}; | |
var __importStar = (this && this.__importStar) || function (mod) { | |
if (mod && mod.__esModule) return mod; | |
var result = {}; | |
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); | |
__setModuleDefault(result, mod); | |
return result; | |
}; | |
var __metadata = (this && this.__metadata) || function (k, v) { | |
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); | |
}; | |
var __param = (this && this.__param) || function (paramIndex, decorator) { | |
return function (target, key) { decorator(target, key, paramIndex); } | |
}; | |
var __importDefault = (this && this.__importDefault) || function (mod) { | |
return (mod && mod.__esModule) ? mod : { "default": mod }; | |
}; | |
var StorageTemplateService_1; | |
Object.defineProperty(exports, "__esModule", { value: true }); | |
exports.StorageTemplateService = void 0; | |
const entities_1 = require("../../infra/entities"); | |
const logger_1 = require("../../infra/logger"); | |
const common_1 = require("@nestjs/common"); | |
const handlebars_1 = __importDefault(require("handlebars")); | |
const luxon = __importStar(require("luxon")); | |
const node_path_1 = __importDefault(require("node:path")); | |
const sanitize_filename_1 = __importDefault(require("sanitize-filename")); | |
const domain_util_1 = require("../domain.util"); | |
const job_1 = require("../job"); | |
const repositories_1 = require("../repositories"); | |
const storage_1 = require("../storage"); | |
const system_config_1 = require("../system-config"); | |
const system_config_core_1 = require("../system-config/system-config.core"); | |
let StorageTemplateService = StorageTemplateService_1 = class StorageTemplateService { | |
albumRepository; | |
assetRepository; | |
storageRepository; | |
userRepository; | |
databaseRepository; | |
logger = new logger_1.ImmichLogger(StorageTemplateService_1.name); | |
configCore; | |
storageCore; | |
_template = null; | |
get template() { | |
if (!this._template) { | |
throw new Error('Template not initialized'); | |
} | |
return this._template; | |
} | |
constructor(albumRepository, assetRepository, configRepository, moveRepository, personRepository, storageRepository, userRepository, cryptoRepository, databaseRepository) { | |
this.albumRepository = albumRepository; | |
this.assetRepository = assetRepository; | |
this.storageRepository = storageRepository; | |
this.userRepository = userRepository; | |
this.databaseRepository = databaseRepository; | |
this.configCore = system_config_core_1.SystemConfigCore.create(configRepository); | |
this.configCore.addValidator((config) => this.validate(config)); | |
this.configCore.config$.subscribe((config) => this.onConfig(config)); | |
this.storageCore = storage_1.StorageCore.create(assetRepository, moveRepository, personRepository, cryptoRepository, configRepository, storageRepository); | |
} | |
async handleMigrationSingle({ id }) { | |
const config = await this.configCore.getConfig(); | |
const storageTemplateEnabled = config.storageTemplate.enabled; | |
if (!storageTemplateEnabled) { | |
return true; | |
} | |
const [asset] = await this.assetRepository.getByIds([id]); | |
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 }); | |
if (asset.livePhotoVideoId) { | |
const [livePhotoVideo] = await this.assetRepository.getByIds([asset.livePhotoVideoId]); | |
const motionFilename = (0, domain_util_1.getLivePhotoMotionFilename)(filename, livePhotoVideo.originalPath); | |
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename }); | |
} | |
return true; | |
} | |
async handleMigration() { | |
this.logger.log('Starting storage template migration'); | |
const { storageTemplate } = await this.configCore.getConfig(); | |
const { enabled } = storageTemplate; | |
if (!enabled) { | |
this.logger.log('Storage template migration disabled, skipping'); | |
return true; | |
} | |
const assetPagination = (0, domain_util_1.usePagination)(job_1.JOBS_ASSET_PAGINATION_SIZE, (pagination) => this.assetRepository.getAll(pagination, { withExif: true })); | |
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 }); | |
} | |
} | |
this.logger.debug('Cleaning up empty directories...'); | |
const libraryFolder = storage_1.StorageCore.getBaseFolder(storage_1.StorageFolder.LIBRARY); | |
await this.storageRepository.removeEmptyDirs(libraryFolder); | |
this.logger.log('Finished storage template migration'); | |
return true; | |
} | |
async moveAsset(asset, metadata) { | |
if (asset.isReadOnly || asset.isExternal || storage_1.StorageCore.isAndroidMotionPath(asset.originalPath)) { | |
return; | |
} | |
return this.databaseRepository.withLock(repositories_1.DatabaseLock.StorageTemplateMigration, async () => { | |
const { id, sidecarPath, originalPath, exifInfo, checksum } = asset; | |
const oldPath = originalPath; | |
const newPath = await this.getTemplatePath(asset, metadata); | |
if (!exifInfo || !exifInfo.fileSizeInByte) { | |
this.logger.error(`Asset ${id} missing exif info, skipping storage template migration`); | |
return; | |
} | |
try { | |
await this.storageCore.moveFile({ | |
entityId: id, | |
pathType: entities_1.AssetPathType.ORIGINAL, | |
oldPath, | |
newPath, | |
assetInfo: { sizeInBytes: exifInfo.fileSizeInByte, checksum }, | |
}); | |
if (sidecarPath) { | |
await this.storageCore.moveFile({ | |
entityId: id, | |
pathType: entities_1.AssetPathType.SIDECAR, | |
oldPath: sidecarPath, | |
newPath: `${newPath}.xmp`, | |
}); | |
} | |
} | |
catch (error) { | |
this.logger.error(`Problem applying storage template`, error?.stack, { id, oldPath, newPath }); | |
} | |
}); | |
} | |
async getTemplatePath(asset, metadata) { | |
const { storageLabel, filename } = metadata; | |
try { | |
const source = asset.originalPath; | |
const extension = node_path_1.default.extname(source).split('.').pop(); | |
const sanitized = (0, sanitize_filename_1.default)(node_path_1.default.basename(filename, `.${extension}`)); | |
const rootPath = storage_1.StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel }); | |
let albumName = null; | |
if (this.template.needsAlbum) { | |
const albums = await this.albumRepository.getByAssetId(asset.ownerId, asset.id); | |
albumName = albums?.[0]?.albumName || null; | |
} | |
const storagePath = this.render(this.template.compiled, { | |
asset, | |
filename: sanitized, | |
extension: extension, | |
albumName, | |
}); | |
const fullPath = node_path_1.default.normalize(node_path_1.default.join(rootPath, storagePath)); | |
let destination = `${fullPath}.${extension}`; | |
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; | |
} | |
if (source.startsWith(fullPath) && source.endsWith(`.${extension}`)) { | |
const diff = source.replace(fullPath, '').replace(`.${extension}`, ''); | |
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}.${extension}`; | |
} | |
return destination; | |
} | |
catch (error) { | |
this.logger.error(`Unable to get template path for ${filename}`, error); | |
return asset.originalPath; | |
} | |
} | |
validate(config) { | |
try { | |
const { compiled } = this.compile(config.storageTemplate.template); | |
this.render(compiled, { | |
asset: { | |
fileCreatedAt: new Date(), | |
originalPath: '/upload/test/IMG_123.jpg', | |
type: entities_1.AssetType.IMAGE, | |
id: 'd587e44b-f8c0-4832-9ba3-43268bbf5d4e', | |
}, | |
filename: 'IMG_123', | |
extension: 'jpg', | |
albumName: 'album', | |
}); | |
} | |
catch (error) { | |
this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`); | |
throw new Error(`Invalid storage template: ${error}`); | |
} | |
} | |
onConfig(config) { | |
const template = config.storageTemplate.template; | |
if (!this._template || template !== this.template.raw) { | |
this.logger.debug(`Compiling new storage template: ${template}`); | |
this._template = this.compile(template); | |
} | |
} | |
compile(template) { | |
return { | |
raw: template, | |
compiled: handlebars_1.default.compile(template, { knownHelpers: undefined, strict: true }), | |
needsAlbum: template.includes('{{album}}') || template.includes('{{albumPath}}'), | |
}; | |
} | |
render(template, options) { | |
const { filename, extension, asset, albumName } = options; | |
this.logger.log('storage template migration albumName: ' + albumName); | |
let albumSubstitutions = { | |
album: '.', | |
albumPath: '.', | |
} | |
if (albumName) { | |
const album = albumName.replaceAll(/\.+/g, '') | |
albumSubstitutions = { | |
album: (0, sanitize_filename_1.default)(album), | |
albumPath: (0, sanitize_filename_1.default)(album.replaceAll(/\/+/g, '.')).replaceAll('.', '/'), | |
} | |
} | |
const substitutions = { | |
filename, | |
ext: extension, | |
filetype: asset.type == entities_1.AssetType.IMAGE ? 'IMG' : 'VID', | |
filetypefull: asset.type == entities_1.AssetType.IMAGE ? 'IMAGE' : 'VIDEO', | |
assetId: asset.id, | |
...albumSubstitutions, | |
}; | |
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; | |
const zone = asset.exifInfo?.timeZone || systemTimeZone; | |
const dt = luxon.DateTime.fromJSDate(asset.fileCreatedAt, { zone }); | |
const dateTokens = [ | |
...system_config_1.supportedYearTokens, | |
...system_config_1.supportedMonthTokens, | |
...system_config_1.supportedWeekTokens, | |
...system_config_1.supportedDayTokens, | |
...system_config_1.supportedHourTokens, | |
...system_config_1.supportedMinuteTokens, | |
...system_config_1.supportedSecondTokens, | |
]; | |
for (const token of dateTokens) { | |
substitutions[token] = dt.toFormat(token); | |
} | |
this.logger.log('storage template migration substitutions: ' + JSON.stringify(substitutions)); | |
return template(substitutions); | |
} | |
}; | |
exports.StorageTemplateService = StorageTemplateService; | |
exports.StorageTemplateService = StorageTemplateService = StorageTemplateService_1 = __decorate([ | |
(0, common_1.Injectable)(), | |
__param(0, (0, common_1.Inject)(repositories_1.IAlbumRepository)), | |
__param(1, (0, common_1.Inject)(repositories_1.IAssetRepository)), | |
__param(2, (0, common_1.Inject)(repositories_1.ISystemConfigRepository)), | |
__param(3, (0, common_1.Inject)(repositories_1.IMoveRepository)), | |
__param(4, (0, common_1.Inject)(repositories_1.IPersonRepository)), | |
__param(5, (0, common_1.Inject)(repositories_1.IStorageRepository)), | |
__param(6, (0, common_1.Inject)(repositories_1.IUserRepository)), | |
__param(7, (0, common_1.Inject)(repositories_1.ICryptoRepository)), | |
__param(8, (0, common_1.Inject)(repositories_1.IDatabaseRepository)), | |
__metadata("design:paramtypes", [Object, Object, Object, Object, Object, Object, Object, Object, Object]) | |
], StorageTemplateService); | |
//# sourceMappingURL=storage-template.service.js.map |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment