170 lines
No EOL
4.9 KiB
TypeScript
170 lines
No EOL
4.9 KiB
TypeScript
// ! Hashed File Store (not file system!!)
|
|
|
|
import { join } from "path";
|
|
import { existsSync, mkdirSync, createWriteStream, rename, stat, writeFile, rm, rmSync, createReadStream } from "fs";
|
|
import { Console } from "hsconsole";
|
|
import { yellow } from "dyetty";
|
|
import { createHash, randomBytes } from "crypto";
|
|
import FunkyArray from "funky-array";
|
|
import HashFSFileInformation from "./HashFSFileInformation";
|
|
import type { BusboyFileStream } from "@fastify/busboy";
|
|
import { pipeline } from "stream/promises";
|
|
|
|
export default class HashFS {
|
|
public static STARTUP_DIR: string;
|
|
private static HASHFS_INSTANCES: FunkyArray<string, HashFS> = new FunkyArray<string, HashFS>();
|
|
|
|
public static GetHashFSInstance(name: string) {
|
|
const instance = this.HASHFS_INSTANCES.get(name);
|
|
if (!instance) {
|
|
throw `Attempted to get nonexistent HashFS instance "${name}"`;
|
|
}
|
|
|
|
return instance;
|
|
}
|
|
|
|
public readonly path: string;
|
|
private readonly tempPath: string;
|
|
private readonly folder: string;
|
|
|
|
private logInfo(logText: string) {
|
|
Console.printInfo(`[ ${yellow(`HashFS: ${this.folder}`)} ] ${logText}`);
|
|
}
|
|
|
|
public constructor(folder: string) {
|
|
HashFS.HASHFS_INSTANCES.set(folder, this);
|
|
|
|
this.folder = folder;
|
|
this.path = join(HashFS.STARTUP_DIR, folder);
|
|
|
|
let firstCreation = false;
|
|
if (!existsSync(this.path)) {
|
|
this.logInfo(`Creating HashFS for "${folder}"...`);
|
|
mkdirSync(this.path);
|
|
firstCreation = true;
|
|
}
|
|
|
|
this.logInfo(`Validating file store...`);
|
|
let issuesRepaired = 0;
|
|
for (let i = 0; i < 16; i++) {
|
|
const hashRootFolderPath = join(this.path, i.toString(16));
|
|
if (!existsSync(hashRootFolderPath)) {
|
|
mkdirSync(hashRootFolderPath);
|
|
this.logInfo(`"${i.toString(16)}" does not exist, creating...`);
|
|
issuesRepaired++;
|
|
}
|
|
for (let i1 = 0; i1 < 16; i1++) {
|
|
const subFolderPath = join(hashRootFolderPath, (i * 16 + i1).toString(16).padStart(2, "0"));
|
|
if (!existsSync(subFolderPath)) {
|
|
this.logInfo(`"${i.toString(16)}/${(i * 16 + i1).toString(16).padStart(2, "0")}" does not exist, creating...`);
|
|
mkdirSync(subFolderPath);
|
|
issuesRepaired++;
|
|
}
|
|
}
|
|
}
|
|
// TODO: Validate the files in the file store
|
|
this.logInfo(`File Store Validated Successfully${!firstCreation && issuesRepaired > 0 ? `. Repaired ${issuesRepaired} issues.` : " with no issues."}`);
|
|
|
|
this.tempPath = join(this.path, "temp");
|
|
if (existsSync(this.tempPath)) {
|
|
rmSync(this.tempPath, { recursive: true });
|
|
}
|
|
mkdirSync(this.tempPath);
|
|
this.logInfo(`Created temp working folder at "${this.tempPath}"`);
|
|
|
|
this.logInfo(`Ready!`);
|
|
}
|
|
|
|
public GetFilePath(hash: string) {
|
|
return join(this.path, hash[0], `${hash[0]}${hash[1]}`, hash);
|
|
}
|
|
|
|
public GetRelativePath(hash: string) {
|
|
return join(hash[0], `${hash[0]}${hash[1]}`, hash);
|
|
}
|
|
|
|
public AddFile(contents: Buffer | string) {
|
|
return new Promise<HashFSFileInformation>(async (resolve, reject) => {
|
|
const hasher = createHash("sha1");
|
|
hasher.setEncoding("hex");
|
|
hasher.write(contents);
|
|
hasher.end();
|
|
const hash: string = hasher.read();
|
|
|
|
const fileInfo = new HashFSFileInformation();
|
|
fileInfo.fileHash = hash;
|
|
fileInfo.fileSize = contents.length;
|
|
|
|
if (await this.FileExists(hash)) {
|
|
fileInfo.fileExistsAlready = true;
|
|
this.logInfo(`File with hash "${hash}" already exists.`);
|
|
return resolve(fileInfo);
|
|
}
|
|
|
|
writeFile(this.GetFilePath(hash), contents, (err) => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
|
|
resolve(fileInfo);
|
|
})
|
|
});
|
|
}
|
|
|
|
public AddFromStream(stream: BusboyFileStream) {
|
|
return new Promise<HashFSFileInformation>(async (resolve, reject) => {
|
|
const hasher = createHash("sha1");
|
|
hasher.setEncoding("hex");
|
|
|
|
let fileSize = 0;
|
|
const tempFilePath = join(this.tempPath, randomBytes(16).toString("base64url"));
|
|
await pipeline(stream, createWriteStream(tempFilePath));
|
|
const readStream = createReadStream(tempFilePath);
|
|
for await (const chunk of readStream) {
|
|
fileSize += chunk.length;
|
|
hasher.write(chunk)
|
|
}
|
|
hasher.end();
|
|
const hash: string = hasher.read();
|
|
const fileInfo = new HashFSFileInformation();
|
|
fileInfo.fileHash = hash;
|
|
fileInfo.fileSize = fileSize;
|
|
if (await this.FileExists(hash)) {
|
|
fileInfo.fileExistsAlready = true;
|
|
rm(tempFilePath, err => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
|
|
this.logInfo(`File with hash "${hash}" already exists.`);
|
|
resolve(fileInfo);
|
|
});
|
|
} else {
|
|
rename(tempFilePath, this.GetFilePath(hash), err => {
|
|
if (err) {
|
|
return reject(err);
|
|
}
|
|
|
|
this.logInfo(`Stored file as ${hash}`);
|
|
resolve(fileInfo);
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
public FileExists(hash: string) {
|
|
return new Promise<boolean>((resolve, reject) => {
|
|
stat(this.GetFilePath(hash), (err, _stat) => {
|
|
if (err) {
|
|
if (err.code === "ENOENT") {
|
|
resolve(false);
|
|
} else {
|
|
reject(err);
|
|
}
|
|
} else {
|
|
resolve(true);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
} |