drop-server-bak/server/internal/objects/fsBackend.ts

269 lines
7.7 KiB
TypeScript
Raw Permalink Normal View History

2025-04-13 21:44:29 -04:00
import type { ObjectMetadata, ObjectReference, Source } from "./objectHandler";
import { ObjectBackend, objectMetadata } from "./objectHandler";
import fs from "fs";
import path from "path";
2025-04-15 21:10:45 -04:00
import { Readable } from "stream";
2025-04-09 14:48:13 -04:00
import { createHash } from "crypto";
2025-04-12 15:54:26 -04:00
import prisma from "../db/database";
2025-05-07 22:13:22 -04:00
import cacheHandler from "../cache";
2025-05-10 16:18:28 -04:00
import { systemConfig } from "../config/sys-conf";
import { type } from "arktype";
import { logger } from "~/server/internal/logging";
import type pino from "pino";
export class FsObjectBackend extends ObjectBackend {
private baseObjectPath: string;
private baseMetadataPath: string;
2025-04-12 15:54:26 -04:00
private hashStore = new FsHashStore();
private metadataCache =
cacheHandler.createCache<ObjectMetadata>("ObjectMetadata");
2025-04-09 14:48:13 -04:00
constructor() {
super();
2025-05-10 16:18:28 -04:00
const basePath = path.join(systemConfig.getDataFolder(), "objects");
this.baseObjectPath = path.join(basePath, "objects");
this.baseMetadataPath = path.join(basePath, "metadata");
fs.mkdirSync(this.baseObjectPath, { recursive: true });
fs.mkdirSync(this.baseMetadataPath, { recursive: true });
}
async fetch(id: ObjectReference) {
const objectPath = path.join(this.baseObjectPath, id);
if (!fs.existsSync(objectPath)) return undefined;
return fs.createReadStream(objectPath);
}
async write(id: ObjectReference, source: Source): Promise<boolean> {
const objectPath = path.join(this.baseObjectPath, id);
if (!fs.existsSync(objectPath)) return false;
2025-04-09 14:48:13 -04:00
// remove item from cache
2025-05-07 22:13:22 -04:00
await this.hashStore.delete(id);
2025-04-09 14:48:13 -04:00
if (source instanceof Readable) {
const outputStream = fs.createWriteStream(objectPath);
source.pipe(outputStream, { end: true });
2025-04-15 21:10:45 -04:00
await new Promise((r, _j) => source.on("end", r));
return true;
}
if (source instanceof Buffer) {
fs.writeFileSync(objectPath, source);
return true;
}
return false;
}
2025-04-01 21:08:57 +11:00
async startWriteStream(id: ObjectReference) {
const objectPath = path.join(this.baseObjectPath, id);
2025-04-01 21:08:57 +11:00
if (!fs.existsSync(objectPath)) return undefined;
2025-04-09 14:48:13 -04:00
// remove item from cache
2025-05-07 22:13:22 -04:00
await this.hashStore.delete(id);
2025-04-01 21:08:57 +11:00
return fs.createWriteStream(objectPath);
}
async create(
id: string,
source: Source,
2025-04-15 21:10:45 -04:00
metadata: ObjectMetadata,
): Promise<ObjectReference | undefined> {
const objectPath = path.join(this.baseObjectPath, id);
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
if (fs.existsSync(objectPath) || fs.existsSync(metadataPath))
return undefined;
// Write metadata
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
// Create file so write passes
fs.writeFileSync(objectPath, "");
// Call write
this.write(id, source);
return id;
}
2025-04-01 21:08:57 +11:00
async createWithWriteStream(id: string, metadata: ObjectMetadata) {
const objectPath = path.join(this.baseObjectPath, id);
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
2025-04-01 21:08:57 +11:00
if (fs.existsSync(objectPath) || fs.existsSync(metadataPath))
return undefined;
// Write metadata
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
// Create file so write passes
fs.writeFileSync(objectPath, "");
const stream = await this.startWriteStream(id);
if (!stream) throw new Error("Could not create write stream");
return stream;
2025-04-01 21:08:57 +11:00
}
async delete(id: ObjectReference): Promise<boolean> {
const objectPath = path.join(this.baseObjectPath, id);
if (!fs.existsSync(objectPath)) return true;
fs.rmSync(objectPath);
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
if (!fs.existsSync(metadataPath)) return true;
fs.rmSync(metadataPath);
// remove item from caches
await this.metadataCache.remove(id);
2025-05-07 22:13:22 -04:00
await this.hashStore.delete(id);
return true;
}
async fetchMetadata(
2025-04-15 21:10:45 -04:00
id: ObjectReference,
): Promise<ObjectMetadata | undefined> {
const cacheResult = await this.metadataCache.get(id);
if (cacheResult !== null) return cacheResult;
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
if (!fs.existsSync(metadataPath)) return undefined;
const metadataRaw = JSON.parse(fs.readFileSync(metadataPath, "utf-8"));
const metadata = objectMetadata(metadataRaw);
if (metadata instanceof type.errors) {
logger.error("FsObjectBackend#fetchMetadata", metadata.summary);
return undefined;
}
await this.metadataCache.set(id, metadata);
return metadata;
}
async writeMetadata(
id: ObjectReference,
2025-04-15 21:10:45 -04:00
metadata: ObjectMetadata,
): Promise<boolean> {
const metadataPath = path.join(this.baseMetadataPath, `${id}.json`);
if (!fs.existsSync(metadataPath)) return false;
fs.writeFileSync(metadataPath, JSON.stringify(metadata));
await this.metadataCache.set(id, metadata);
return true;
}
2025-04-09 14:48:13 -04:00
async fetchHash(id: ObjectReference): Promise<string | undefined> {
const cacheResult = await this.hashStore.get(id);
2025-05-07 22:13:22 -04:00
if (cacheResult !== null) return cacheResult;
2025-04-09 14:48:13 -04:00
const obj = await this.fetch(id);
if (obj === undefined) return;
// hash object
const hash = createHash("md5");
hash.setEncoding("hex");
2025-05-07 22:13:22 -04:00
// local variable to point to object
const store = this.hashStore;
let hashResult = "";
2025-05-07 22:13:22 -04:00
const objEnd = new Promise<void>((r) => {
2025-05-07 22:13:22 -04:00
obj.on("end", async function () {
hash.end();
hashResult = hash.read();
r();
});
});
// read obj into hash
obj.pipe(hash);
await objEnd;
2025-04-09 14:48:13 -04:00
// if hash isn't a string somehow, mark as unknown hash
if (typeof hashResult !== "string") {
return undefined;
}
await store.save(id, hashResult);
return typeof hashResult;
2025-04-12 15:54:26 -04:00
}
2025-05-08 19:19:10 -04:00
async listAll(): Promise<string[]> {
return fs.readdirSync(this.baseObjectPath);
}
2025-05-29 17:27:03 -04:00
async cleanupMetadata(taskLogger: pino.Logger) {
const cleanupLogger = taskLogger ?? logger;
2025-05-29 17:27:03 -04:00
const metadataFiles = fs.readdirSync(this.baseMetadataPath);
const objects = await this.listAll();
const extraFiles = metadataFiles.filter(
(file) => !objects.includes(file.replace(/\.json$/, "")),
);
cleanupLogger.info(
2025-05-29 17:27:03 -04:00
`[FsObjectBackend#cleanupMetadata]: Found ${extraFiles.length} metadata files without corresponding objects.`,
);
for (const file of extraFiles) {
const filePath = path.join(this.baseMetadataPath, file);
try {
fs.rmSync(filePath);
cleanupLogger.info(
`[FsObjectBackend#cleanupMetadata]: Removed ${file}`,
);
2025-05-29 17:27:03 -04:00
} catch (error) {
cleanupLogger.error(
2025-05-29 17:27:03 -04:00
`[FsObjectBackend#cleanupMetadata]: Failed to remove ${file}`,
error,
);
}
}
}
2025-04-12 15:54:26 -04:00
}
class FsHashStore {
2025-05-07 22:13:22 -04:00
private cache = cacheHandler.createCache<string>("ObjectHashStore");
2025-04-12 15:54:26 -04:00
/**
* Gets hash of object
* @param id
* @returns
*/
async get(id: ObjectReference) {
2025-05-07 22:13:22 -04:00
const cacheRes = await this.cache.get(id);
if (cacheRes !== null) {
return cacheRes;
}
2025-04-12 15:54:26 -04:00
const objectHash = await prisma.objectHash.findUnique({
2025-04-12 15:54:26 -04:00
where: {
id,
},
select: {
hash: true,
},
});
if (objectHash === null) return undefined;
2025-05-07 22:13:22 -04:00
await this.cache.set(id, objectHash.hash);
return objectHash.hash;
2025-04-12 15:54:26 -04:00
}
/**
* Saves hash of object
* @param id
*/
async save(id: ObjectReference, hash: string) {
await prisma.objectHash.upsert({
where: {
id,
},
create: {
id,
hash,
},
update: {
hash,
},
});
2025-05-07 22:13:22 -04:00
await this.cache.set(id, hash);
2025-04-12 15:54:26 -04:00
}
/**
* Hash is no longer valid for whatever reason
* @param id
*/
async delete(id: ObjectReference) {
2025-05-07 22:13:22 -04:00
await this.cache.remove(id);
await prisma.objectHash.deleteMany({
where: {
id,
},
});
2025-04-09 14:48:13 -04:00
}
}