Skip to content

Instantly share code, notes, and snippets.

@pastleo
Created March 17, 2024 09:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pastleo/cab5cfcd709747b162574337e60e53de to your computer and use it in GitHub Desktop.
Save pastleo/cab5cfcd709747b162574337e60e53de to your computer and use it in GitHub Desktop.
patched adding {{{albumPath}}} for immich
"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