"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.StorageCore = void 0;
const node_crypto_1 = require("node:crypto");
const node_path_1 = require("node:path");
const enum_1 = require("../enum");
const asset_util_1 = require("../utils/asset.util");
const config_1 = require("../utils/config");
let instance;
let mediaLocation;
class StorageCore {
    assetRepository;
    configRepository;
    cryptoRepository;
    moveRepository;
    personRepository;
    storageRepository;
    systemMetadataRepository;
    logger;
    constructor(assetRepository, configRepository, cryptoRepository, moveRepository, personRepository, storageRepository, systemMetadataRepository, logger) {
        this.assetRepository = assetRepository;
        this.configRepository = configRepository;
        this.cryptoRepository = cryptoRepository;
        this.moveRepository = moveRepository;
        this.personRepository = personRepository;
        this.storageRepository = storageRepository;
        this.systemMetadataRepository = systemMetadataRepository;
        this.logger = logger;
        this.logger.setContext(StorageCore.name);
    }
    static create(assetRepository, configRepository, cryptoRepository, moveRepository, personRepository, storageRepository, systemMetadataRepository, logger) {
        if (!instance) {
            instance = new StorageCore(assetRepository, configRepository, cryptoRepository, moveRepository, personRepository, storageRepository, systemMetadataRepository, logger);
        }
        return instance;
    }
    static reset() {
        instance = null;
    }
    static getMediaLocation() {
        if (mediaLocation === undefined) {
            throw new Error('Media location is not set.');
        }
        return mediaLocation;
    }
    static setMediaLocation(location) {
        mediaLocation = location;
    }
    static getFolderLocation(folder, userId) {
        return (0, node_path_1.join)(StorageCore.getBaseFolder(folder), userId);
    }
    static getLibraryFolder(user) {
        return (0, node_path_1.join)(StorageCore.getBaseFolder(enum_1.StorageFolder.Library), user.storageLabel || user.id);
    }
    static getBaseFolder(folder) {
        return (0, node_path_1.join)(StorageCore.getMediaLocation(), folder);
    }
    static getPersonThumbnailPath(person) {
        return StorageCore.getNestedPath(enum_1.StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`);
    }
    static getImagePath(asset, { fileType, format, isEdited }) {
        return StorageCore.getNestedPath(enum_1.StorageFolder.Thumbnails, asset.ownerId, `${asset.id}_${fileType}${isEdited ? '_edited' : ''}.${format}`);
    }
    static getEncodedVideoPath(asset) {
        return StorageCore.getNestedPath(enum_1.StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`);
    }
    static getHlsSessionFolder({ ownerId, sessionId }) {
        return StorageCore.getNestedPath(enum_1.StorageFolder.EncodedVideo, ownerId, sessionId);
    }
    static getHlsVariantFolder({ ownerId, sessionId, variantIndex }) {
        return (0, node_path_1.join)(StorageCore.getHlsSessionFolder({ ownerId, sessionId }), variantIndex.toString());
    }
    static getAndroidMotionPath(asset, uuid) {
        return StorageCore.getNestedPath(enum_1.StorageFolder.EncodedVideo, asset.ownerId, `${uuid}-MP.mp4`);
    }
    static isAndroidMotionPath(originalPath) {
        return originalPath.startsWith(StorageCore.getBaseFolder(enum_1.StorageFolder.EncodedVideo));
    }
    static isImmichPath(path) {
        const resolvedPath = (0, node_path_1.resolve)(path);
        const resolvedAppMediaLocation = StorageCore.getMediaLocation();
        const normalizedPath = resolvedPath.endsWith('/') ? resolvedPath : resolvedPath + '/';
        const normalizedAppMediaLocation = resolvedAppMediaLocation.endsWith('/')
            ? resolvedAppMediaLocation
            : resolvedAppMediaLocation + '/';
        return normalizedPath.startsWith(normalizedAppMediaLocation);
    }
    async moveAssetImage(asset, fileType, format) {
        const { id: entityId, files } = asset;
        const oldFile = (0, asset_util_1.getAssetFile)(files, fileType, { isEdited: false });
        return this.moveFile({
            entityId,
            pathType: fileType,
            oldPath: oldFile?.path || null,
            newPath: StorageCore.getImagePath(asset, { fileType, format, isEdited: false }),
        });
    }
    async moveAssetVideo(asset) {
        const encodedVideoFile = (0, asset_util_1.getAssetFile)(asset.files, enum_1.AssetFileType.EncodedVideo, { isEdited: false });
        return this.moveFile({
            entityId: asset.id,
            pathType: enum_1.AssetPathType.EncodedVideo,
            oldPath: encodedVideoFile?.path || null,
            newPath: StorageCore.getEncodedVideoPath(asset),
        });
    }
    async movePersonFile(person, pathType) {
        const { id: entityId, thumbnailPath } = person;
        switch (pathType) {
            case enum_1.PersonPathType.Face: {
                await this.moveFile({
                    entityId,
                    pathType,
                    oldPath: thumbnailPath,
                    newPath: StorageCore.getPersonThumbnailPath(person),
                });
            }
        }
    }
    async moveFile(request) {
        const { entityId, pathType, oldPath, newPath, assetInfo } = request;
        if (!oldPath || oldPath === newPath) {
            return;
        }
        this.ensureFolders(newPath);
        let move = await this.moveRepository.getByEntity(entityId, pathType);
        if (move) {
            this.logger.log(`Attempting to finish incomplete move: ${move.oldPath} => ${move.newPath}`);
            const isOldPathExists = await this.storageRepository.checkFileExists(move.oldPath);
            const isNewPathExists = await this.storageRepository.checkFileExists(move.newPath);
            const newPathCheck = isNewPathExists ? move.newPath : null;
            const actualPath = isOldPathExists ? move.oldPath : newPathCheck;
            if (!actualPath) {
                this.logger.warn('Unable to complete move. File does not exist at either location.');
                return;
            }
            const isFileAtNewLocation = actualPath === move.newPath;
            this.logger.log(`Found file at ${isFileAtNewLocation ? 'new' : 'old'} location`);
            if (isFileAtNewLocation &&
                !(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo))) {
                this.logger.fatal(`Skipping move as file verification failed, old file is missing and new file is different to what was expected`);
                return;
            }
            move = await this.moveRepository.update(move.id, { id: move.id, oldPath: actualPath, newPath });
        }
        else {
            move = await this.moveRepository.create({ entityId, pathType, oldPath, newPath });
        }
        if (pathType === enum_1.AssetPathType.Original && !assetInfo) {
            this.logger.warn(`Unable to complete move. Missing asset info for ${entityId}`);
            return;
        }
        if (move.oldPath !== newPath) {
            try {
                this.logger.debug(`Attempting to rename file: ${move.oldPath} => ${newPath}`);
                await this.storageRepository.rename(move.oldPath, newPath);
            }
            catch (error) {
                if (error.code !== 'EXDEV') {
                    this.logger.warn(`Unable to complete move. Error renaming file with code ${error.code} and message: ${error.message}`);
                    return;
                }
                this.logger.debug(`Unable to rename file. Falling back to copy, verify and delete`);
                await this.storageRepository.copyFile(move.oldPath, newPath);
                if (!(await this.verifyNewPathContentsMatchesExpected(move.oldPath, newPath, assetInfo))) {
                    this.logger.warn(`Skipping move due to file size mismatch`);
                    await this.storageRepository.unlink(newPath);
                    return;
                }
                const { atime, mtime } = await this.storageRepository.stat(move.oldPath);
                await this.storageRepository.utimes(newPath, atime, mtime);
                try {
                    await this.storageRepository.unlink(move.oldPath);
                }
                catch (error) {
                    this.logger.warn(`Unable to delete old file, it will now no longer be tracked by Immich: ${error.message}`);
                }
            }
        }
        await this.savePath(pathType, entityId, newPath);
        await this.moveRepository.delete(move.id);
    }
    async verifyNewPathContentsMatchesExpected(oldPath, newPath, assetInfo) {
        const oldStat = await this.storageRepository.stat(oldPath);
        const newStat = await this.storageRepository.stat(newPath);
        const oldPathSize = assetInfo ? assetInfo.sizeInBytes : oldStat.size;
        const newPathSize = newStat.size;
        this.logger.debug(`File size check: ${newPathSize} === ${oldPathSize}`);
        if (newPathSize !== oldPathSize) {
            this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
            return false;
        }
        const repos = {
            configRepo: this.configRepository,
            metadataRepo: this.systemMetadataRepository,
            logger: this.logger,
        };
        const config = await (0, config_1.getConfig)(repos, { withCache: true });
        if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
            const { checksum } = assetInfo;
            const newChecksum = await this.cryptoRepository.hashFile(newPath);
            if (!newChecksum.equals(checksum)) {
                this.logger.warn(`Unable to complete move. File checksum mismatch: ${newChecksum.toString('base64')} !== ${checksum.toString('base64')}`);
                return false;
            }
            this.logger.debug(`File checksum check: ${newChecksum.toString('base64')} === ${checksum.toString('base64')}`);
        }
        return true;
    }
    ensureFolders(input) {
        this.storageRepository.mkdirSync((0, node_path_1.dirname)(input));
    }
    removeEmptyDirs(folder) {
        return this.storageRepository.removeEmptyDirs(StorageCore.getBaseFolder(folder));
    }
    async getVideoInterfaces() {
        const [dri, mali] = await Promise.all([this.getDevices(), this.hasMaliOpenCL()]);
        return { dri, mali };
    }
    savePath(pathType, id, newPath) {
        switch (pathType) {
            case enum_1.AssetPathType.Original: {
                return this.assetRepository.update({ id, originalPath: newPath });
            }
            case enum_1.AssetFileType.FullSize:
            case enum_1.AssetFileType.EncodedVideo:
            case enum_1.AssetFileType.Thumbnail:
            case enum_1.AssetFileType.Preview:
            case enum_1.AssetFileType.Sidecar:
            case enum_1.AssetPathType.EncodedVideo: {
                return this.assetRepository.upsertFile({ assetId: id, type: pathType, path: newPath });
            }
            case enum_1.PersonPathType.Face: {
                return this.personRepository.update({ id, thumbnailPath: newPath });
            }
            case enum_1.UserPathType.Profile: {
                this.logger.warn('Unexpected path type:', pathType);
                return;
            }
        }
    }
    static getNestedFolder(folder, ownerId, filename) {
        return (0, node_path_1.join)(StorageCore.getFolderLocation(folder, ownerId), filename.slice(0, 2), filename.slice(2, 4));
    }
    static getNestedPath(folder, ownerId, filename) {
        return (0, node_path_1.join)(StorageCore.getNestedFolder(folder, ownerId, filename), filename);
    }
    static getTempPathInDir(dir) {
        return (0, node_path_1.join)(dir, `${(0, node_crypto_1.randomUUID)()}.tmp`);
    }
    async getDevices() {
        try {
            return await this.storageRepository.readdir('/dev/dri');
        }
        catch {
            this.logger.debug('No devices found in /dev/dri.');
            return [];
        }
    }
    async hasMaliOpenCL() {
        try {
            const [maliIcdStat, maliDeviceStat] = await Promise.all([
                this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd'),
                this.storageRepository.stat('/dev/mali0'),
            ]);
            return maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
        }
        catch {
            this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU tonemapping');
            return false;
        }
    }
}
exports.StorageCore = StorageCore;
//# sourceMappingURL=storage.core.js.map