immich/server/src/services/storage.service.ts

129 lines
4.6 KiB
TypeScript
Raw Normal View History

import { Injectable } from '@nestjs/common';
import { join, resolve } from 'node:path';
2024-09-27 10:28:42 -04:00
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent, OnJob } from 'src/decorators';
2025-02-11 17:15:56 -05:00
import { DatabaseLock, JobName, JobStatus, QueueName, StorageFolder, SystemMetadataKey } from 'src/enum';
import { BaseService } from 'src/services/base.service';
2025-03-28 10:40:09 -04:00
import { JobOf, SystemFlags } from 'src/types';
import { ImmichStartupError } from 'src/utils/misc';
const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`;
@Injectable()
export class StorageService extends BaseService {
2025-07-15 13:41:19 -04:00
@OnEvent({ name: 'AppBootstrap' })
async onBootstrap() {
const envData = this.configRepository.getEnv();
await this.databaseRepository.withLock(DatabaseLock.SystemFileMounts, async () => {
const flags =
(await this.systemMetadataRepository.get(SystemMetadataKey.SYSTEM_FLAGS)) ||
({ mountChecks: {} } as SystemFlags);
if (!flags.mountChecks) {
flags.mountChecks = {};
}
let updated = false;
this.logger.log(`Verifying system mount folder checks, current state: ${JSON.stringify(flags)}`);
try {
// check each folder exists and is writable
for (const folder of Object.values(StorageFolder)) {
if (!flags.mountChecks[folder]) {
this.logger.log(`Writing initial mount file for the ${folder} folder`);
await this.createMountFile(folder);
}
await this.verifyReadAccess(folder);
await this.verifyWriteAccess(folder);
if (!flags.mountChecks[folder]) {
flags.mountChecks[folder] = true;
updated = true;
}
}
if (updated) {
await this.systemMetadataRepository.set(SystemMetadataKey.SYSTEM_FLAGS, flags);
this.logger.log('Successfully enabled system mount folders checks');
}
this.logger.log('Successfully verified system mount folder checks');
} catch (error) {
if (envData.storage.ignoreMountCheckErrors) {
2025-02-27 14:59:50 -05:00
this.logger.error(error as Error);
this.logger.warn('Ignoring mount folder errors');
} else {
throw error;
}
}
});
}
@OnJob({ name: JobName.DELETE_FILES, queue: QueueName.BACKGROUND_TASK })
async handleDeleteFiles(job: JobOf<JobName.DELETE_FILES>): Promise<JobStatus> {
const { files } = job;
// TODO: one job per file
for (const file of files) {
if (!file) {
continue;
}
try {
await this.storageRepository.unlink(file);
} catch (error: any) {
this.logger.warn('Unable to remove file from disk', error);
}
}
return JobStatus.SUCCESS;
}
private async verifyReadAccess(folder: StorageFolder) {
const { internalPath, externalPath } = this.getMountFilePaths(folder);
try {
await this.storageRepository.readFile(internalPath);
} catch (error) {
const fullyQualifiedPath = resolve(process.cwd(), internalPath);
this.logger.error(`Failed to read ${fullyQualifiedPath} (${internalPath}): ${error}`);
throw new ImmichStartupError(`Failed to read: "${externalPath} (${fullyQualifiedPath}) - ${docsMessage}"`);
}
}
private async createMountFile(folder: StorageFolder) {
const { folderPath, internalPath, externalPath } = this.getMountFilePaths(folder);
try {
this.storageRepository.mkdirSync(folderPath);
await this.storageRepository.createFile(internalPath, Buffer.from(`${Date.now()}`));
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
this.logger.warn('Found existing mount file, skipping creation');
return;
}
this.logger.error(`Failed to create ${internalPath}: ${error}`);
throw new ImmichStartupError(`Failed to create "${externalPath} - ${docsMessage}"`);
}
}
private async verifyWriteAccess(folder: StorageFolder) {
const { internalPath, externalPath } = this.getMountFilePaths(folder);
try {
await this.storageRepository.overwriteFile(internalPath, Buffer.from(`${Date.now()}`));
} catch (error) {
this.logger.error(`Failed to write ${internalPath}: ${error}`);
throw new ImmichStartupError(`Failed to write "${externalPath} - ${docsMessage}"`);
}
}
private getMountFilePaths(folder: StorageFolder) {
const folderPath = StorageCore.getBaseFolder(folder);
const internalPath = join(folderPath, '.immich');
const externalPath = `<UPLOAD_LOCATION>/${folder}/.immich`;
return { folderPath, internalPath, externalPath };
}
}