targo-backend/src/modules/attachments/services/disk-storage.service.ts

71 lines
2.5 KiB
TypeScript

import { Injectable } from '@nestjs/common';
import { createHash } from 'node:crypto';
import { promises as fsp } from 'node:fs';
import { createWriteStream, statSync, existsSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { pipeline } from 'node:stream/promises';
import { ATT_TMP_DIR, resolveAttachmentsRoot } from 'src/config/attachment.config';
export type SaveResult = { sha256:string, storage_path:string, size:number};
@Injectable()
export class DiskStorageService {
private root = resolveAttachmentsRoot();
private casPath(hash: string) {
const a = hash.slice(0,2), b = hash.slice(2,4);
return `sha256/${a}/${b}/${hash}`;
}
//chemin absolue du storage
getAbsolutePath(storagePathRel: string) {
return join(this.root, storagePathRel);
}
async exists(storagePathRel: string) {
try {
statSync(this.getAbsolutePath(storagePathRel));
return true;
}catch {
return false;
}
}
//adds file and hash it
async saveStreamAndHash(input: NodeJS.ReadableStream): Promise<SaveResult> {
// 1- writing in ROOT:/_tmp while streaming and hashing
const tmpDir = ATT_TMP_DIR();
await fsp.mkdir(tmpDir, { recursive: true });
const tmpPath = join(tmpDir, `up_${Date.now()}_${Math.random().toString(36).slice(2)}`);
const hash = createHash('sha256');
const tmpOut = createWriteStream(tmpPath);
input.on('data', (chunk) => hash.update(chunk));
await pipeline(input, tmpOut); //await end of writing stream
const sha = hash.digest('hex');
const rel = this.casPath(sha);
const finalAbs = this.getAbsolutePath(rel);
// 2- is there is no destination => move (atomic renaming on the same volume)
if(!existsSync(finalAbs)) {
await fsp.mkdir(dirname(finalAbs), { recursive:true });
try {
await fsp.rename(tmpPath, finalAbs);
}catch (e) {
//if someone is faster and used the same hash
if(existsSync(finalAbs)) {
await fsp.rm(tmpPath, { force:true });
} else {
throw e;
}
}
} else {
//remove duplicata if already exists
await fsp.rm(tmpPath, { force:true });
}
const size = statSync(finalAbs).size;
return { sha256: sha, storage_path: rel, size };
}
}