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 { // 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 }; } }