68f52818ae
* separate facial clustering job * update api * fixed some tests * invert clustering * hdbscan * update api * remove commented code * wip dbscan * cleanup removed cluster endpoint remove commented code * fixes updated tests minor fixes and formatting fixed queuing refinements * scale search range based on library size * defer non-core faces * optimizations removed unused query option * assign faces individually for correctness fixed unit tests remove unused method * don't select face embedding update sql linting fixed ml typing * updated job mock * paginate people query * select face embeddings because typeorm * fix setting face detection concurrency * update sql formatting linting * simplify logic remove unused imports * more specific delete signature * more accurate typing for face stubs * add migration formatting * chore: better typing * don't select embedding by default remove unused import * updated sql * use normal try/catch * stricter concurrency typing and enforcement * update api * update job concurrency panel to show disabled queues formatting * check jobId in queueAll fix tests * remove outdated comment * better facial recognition icon * wording wording formatting * fixed tests * fix * formatting & sql * try to fix sql check * more detailed description * update sql * formatting * wording * update `minFaces` description --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
137 lines
3.5 KiB
TypeScript
137 lines
3.5 KiB
TypeScript
import {
|
|
CrawlOptionsDto,
|
|
DiskUsage,
|
|
ImmichReadStream,
|
|
ImmichZipStream,
|
|
IStorageRepository,
|
|
mimeTypes,
|
|
} from '@app/domain';
|
|
import { ImmichLogger } from '@app/infra/logger';
|
|
import archiver from 'archiver';
|
|
import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
|
|
import fs, { copyFile, readdir, rename, writeFile } from 'fs/promises';
|
|
import { glob } from 'glob';
|
|
import path from 'path';
|
|
|
|
export class FilesystemProvider implements IStorageRepository {
|
|
private logger = new ImmichLogger(FilesystemProvider.name);
|
|
|
|
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> {
|
|
const { size } = await fs.stat(filepath);
|
|
await fs.access(filepath, constants.R_OK);
|
|
return {
|
|
stream: createReadStream(filepath),
|
|
length: size,
|
|
type: mimeType || undefined,
|
|
};
|
|
}
|
|
|
|
async readFile(filepath: string, options?: fs.FileReadOptions<Buffer>): Promise<Buffer> {
|
|
const file = await fs.open(filepath);
|
|
try {
|
|
const { buffer } = await file.read(options);
|
|
return buffer;
|
|
} finally {
|
|
await file.close();
|
|
}
|
|
}
|
|
|
|
writeFile = writeFile;
|
|
|
|
rename = rename;
|
|
|
|
copyFile = copyFile;
|
|
|
|
async checkFileExists(filepath: string, mode = constants.F_OK): Promise<boolean> {
|
|
try {
|
|
await fs.access(filepath, mode);
|
|
return true;
|
|
} catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async unlink(file: string) {
|
|
try {
|
|
await fs.unlink(file);
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
|
this.logger.warn(`File ${file} does not exist.`);
|
|
} else {
|
|
throw err;
|
|
}
|
|
}
|
|
}
|
|
|
|
stat = fs.stat;
|
|
|
|
async unlinkDir(folder: string, options: { recursive?: boolean; force?: boolean }) {
|
|
await fs.rm(folder, options);
|
|
}
|
|
|
|
async removeEmptyDirs(directory: string, self: boolean = false) {
|
|
// 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 });
|
|
}
|
|
}
|
|
|
|
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,
|
|
};
|
|
}
|
|
|
|
crawl(crawlOptions: CrawlOptionsDto): Promise<string[]> {
|
|
const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions;
|
|
if (!pathsToCrawl) {
|
|
return Promise.resolve([]);
|
|
}
|
|
|
|
const base = pathsToCrawl.length === 1 ? pathsToCrawl[0] : `{${pathsToCrawl.join(',')}}`;
|
|
const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`;
|
|
|
|
return glob(`${base}/**/${extensions}`, {
|
|
absolute: true,
|
|
nocase: true,
|
|
nodir: true,
|
|
dot: includeHidden,
|
|
ignore: exclusionPatterns,
|
|
});
|
|
}
|
|
|
|
readdir = readdir;
|
|
}
|