mirror of https://github.com/sylv/micro.git
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import { Injectable } from "@nestjs/common";
|
|
import crypto from "crypto";
|
|
import fs from "fs";
|
|
import { nanoid } from "nanoid";
|
|
import path from "path";
|
|
import stream from "stream";
|
|
import getSizeTransform from "stream-size";
|
|
import { promisify } from "util";
|
|
import { ExifTransformer } from "../../classes/ExifTransformer";
|
|
import { config } from "../../config";
|
|
import { isObject } from "../../../../common/src/helpers/is-object.helper";
|
|
|
|
const pipeline = promisify(stream.pipeline);
|
|
|
|
@Injectable()
|
|
export class StorageService {
|
|
private readonly createdPaths = new Set();
|
|
|
|
async create(stream: NodeJS.ReadableStream) {
|
|
// using .tmp in the upload dir solves cross-device link issues
|
|
// todo: using .tmp instead of the actual temp folder means if the process exits half way through an upload,
|
|
// that file will be left behind. we could just use `fs-extra` and `/tmp` but that isn't great either since it means
|
|
// we're copying the file which is slow, especially for large files.
|
|
const uploadId = nanoid(6);
|
|
const uploadPath = path.join(config.storagePath, ".tmp", `.micro${uploadId}`);
|
|
await this.ensureDirectoryExists(uploadPath);
|
|
|
|
try {
|
|
const hashStream = crypto.createHash("sha256");
|
|
const exifTransform = new ExifTransformer();
|
|
const sizeTransform = getSizeTransform(config.uploadLimit);
|
|
const writeStream = fs.createWriteStream(uploadPath);
|
|
await Promise.all([
|
|
// prettier-ignore
|
|
pipeline(stream, hashStream),
|
|
pipeline(stream, exifTransform, sizeTransform, writeStream),
|
|
]);
|
|
|
|
const digest = hashStream.digest("hex");
|
|
const filePath = this.getPathFromHash(digest);
|
|
await this.ensureDirectoryExists(filePath);
|
|
await fs.promises.rename(uploadPath, filePath);
|
|
return {
|
|
hash: digest,
|
|
size: sizeTransform.sizeInBytes,
|
|
};
|
|
} catch (error: unknown) {
|
|
await fs.promises.unlink(uploadPath).catch(() => false);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
async delete(hash: string) {
|
|
try {
|
|
const filePath = this.getPathFromHash(hash);
|
|
await fs.promises.unlink(filePath);
|
|
} catch (error: unknown) {
|
|
if (isObject(error) && error.code === "ENOENT") {
|
|
return;
|
|
}
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
createReadStream(hash: string, range?: { start?: number | null; end?: number | null } | null) {
|
|
// todo: we should throw an error if the file doesn't exist, at the moment
|
|
// the entire app will crash.
|
|
const filePath = this.getPathFromHash(hash);
|
|
return fs.createReadStream(filePath, {
|
|
start: range?.start ?? undefined,
|
|
end: range?.end ?? undefined,
|
|
});
|
|
}
|
|
|
|
private getPathFromHash(hash: string) {
|
|
return path.join(config.storagePath, hash[0], hash[1], hash);
|
|
}
|
|
|
|
private async ensureDirectoryExists(filePath: string) {
|
|
const fileDirectory = path.dirname(filePath);
|
|
if (!this.createdPaths.has(fileDirectory)) {
|
|
await fs.promises.mkdir(fileDirectory, { recursive: true });
|
|
this.createdPaths.add(fileDirectory);
|
|
}
|
|
}
|
|
}
|