2023-06-30 12:24:28 -04:00
|
|
|
import { DiskUsage, ImmichReadStream, ImmichZipStream, IStorageRepository } from '@app/domain';
|
|
|
|
|
import archiver from 'archiver';
|
2023-02-25 09:12:03 -05:00
|
|
|
import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
|
2023-02-03 10:16:25 -05:00
|
|
|
import fs from 'fs/promises';
|
2023-02-25 09:12:03 -05:00
|
|
|
import mv from 'mv';
|
|
|
|
|
import { promisify } from 'node:util';
|
|
|
|
|
import path from 'path';
|
2023-02-03 10:16:25 -05:00
|
|
|
|
2023-02-25 09:12:03 -05:00
|
|
|
const moveFile = promisify<string, string, mv.Options>(mv);
|
2023-02-03 10:16:25 -05:00
|
|
|
|
|
|
|
|
export class FilesystemProvider implements IStorageRepository {
|
2023-06-30 12:24:28 -04:00
|
|
|
createZipStream(): ImmichZipStream {
|
|
|
|
|
const archive = archiver('zip', { store: true });
|
|
|
|
|
|
|
|
|
|
const addFile = (input: string, filename: string) => {
|
|
|
|
|
archive.file(input, { name: filename });
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const finalize = () => archive.finalize();
|
|
|
|
|
|
|
|
|
|
return { stream: archive, addFile, finalize };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream> {
|
2023-02-25 09:12:03 -05:00
|
|
|
const { size } = await fs.stat(filepath);
|
2023-06-30 12:25:08 -04:00
|
|
|
await fs.access(filepath, constants.R_OK);
|
2023-02-03 10:16:25 -05:00
|
|
|
return {
|
|
|
|
|
stream: createReadStream(filepath),
|
|
|
|
|
length: size,
|
2023-06-30 12:24:28 -04:00
|
|
|
type: mimeType || undefined,
|
2023-02-03 10:16:25 -05:00
|
|
|
};
|
|
|
|
|
}
|
2023-02-25 09:12:03 -05:00
|
|
|
|
|
|
|
|
async moveFile(source: string, destination: string): Promise<void> {
|
2023-06-19 18:58:10 +02:00
|
|
|
if (await this.checkFileExists(destination)) {
|
|
|
|
|
throw new Error(`Destination file already exists: ${destination}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await moveFile(source, destination, { mkdirp: true, clobber: true });
|
2023-02-25 09:12:03 -05:00
|
|
|
}
|
|
|
|
|
|
2023-05-26 08:52:52 -04:00
|
|
|
async checkFileExists(filepath: string, mode = constants.F_OK): Promise<boolean> {
|
2023-02-25 09:12:03 -05:00
|
|
|
try {
|
2023-05-26 08:52:52 -04:00
|
|
|
await fs.access(filepath, mode);
|
2023-02-25 09:12:03 -05:00
|
|
|
return true;
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async unlink(file: string) {
|
|
|
|
|
await fs.unlink(file);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async unlinkDir(folder: string, options: { recursive?: boolean; force?: boolean }) {
|
|
|
|
|
await fs.rm(folder, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async removeEmptyDirs(directory: string) {
|
|
|
|
|
this._removeEmptyDirs(directory, false);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async _removeEmptyDirs(directory: string, self: boolean) {
|
|
|
|
|
// lstat does not follow symlinks (in contrast to stat)
|
|
|
|
|
const stats = await fs.lstat(directory);
|
|
|
|
|
if (!stats.isDirectory()) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const files = await fs.readdir(directory);
|
|
|
|
|
await Promise.all(files.map((file) => this._removeEmptyDirs(path.join(directory, file), true)));
|
|
|
|
|
|
|
|
|
|
if (self) {
|
|
|
|
|
const updated = await fs.readdir(directory);
|
|
|
|
|
if (updated.length === 0) {
|
|
|
|
|
await fs.rmdir(directory);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
mkdirSync(filepath: string): void {
|
|
|
|
|
if (!existsSync(filepath)) {
|
|
|
|
|
mkdirSync(filepath, { recursive: true });
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-03-21 22:49:19 -04:00
|
|
|
|
2023-05-18 17:56:33 +02:00
|
|
|
async checkDiskUsage(folder: string): Promise<DiskUsage> {
|
|
|
|
|
const stats = await fs.statfs(folder);
|
|
|
|
|
return {
|
|
|
|
|
available: stats.bavail * stats.bsize,
|
|
|
|
|
free: stats.bfree * stats.bsize,
|
|
|
|
|
total: stats.blocks * stats.bsize,
|
|
|
|
|
};
|
2023-03-21 22:49:19 -04:00
|
|
|
}
|
2023-02-03 10:16:25 -05:00
|
|
|
}
|