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 } from 'src/config/attachment.config'; import { casPathFor, getAbsolutePath } from 'src/modules/attachments/utils/cas.util'; export type SaveResult = { sha256: string, storage_path: string, size: number }; @Injectable() export class DiskStorageService { // async exists(storagePathRel: string) { // try { // statSync(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 = casPathFor(sha); const finalAbs = 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 }; } }