59 lines
2.0 KiB
TypeScript
59 lines
2.0 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { promises as fs } from "node:fs";
|
|
import path from "node:path";
|
|
import type { ImportJob } from "@/lib/furni/import-job";
|
|
export const importRoot = () =>
|
|
path.join(process.cwd(), "storage", "furniture-imports");
|
|
export const validJobId = (value: unknown): value is string =>
|
|
typeof value === "string" &&
|
|
/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
|
|
value,
|
|
);
|
|
export class ImportJobStore {
|
|
constructor(readonly root = importRoot()) {}
|
|
async save(job: ImportJob) {
|
|
await fs.mkdir(this.root, { recursive: true });
|
|
const target = path.join(this.root, `${job.id}.json`),
|
|
tmp = `${target}.${randomUUID()}.tmp`;
|
|
job.updatedAt = new Date().toISOString();
|
|
await fs.writeFile(tmp, JSON.stringify(job));
|
|
await fs.rename(tmp, target);
|
|
}
|
|
async create(job: ImportJob) {
|
|
if (!validJobId(job.id)) throw Error("Invalid import ID");
|
|
await fs.mkdir(this.root, { recursive: true });
|
|
const tmp = path.join(this.root, `${randomUUID()}.tmp`);
|
|
await fs.writeFile(tmp, JSON.stringify(job));
|
|
try {
|
|
await fs.link(tmp, path.join(this.root, `${job.id}.json`));
|
|
return job;
|
|
} catch (error) {
|
|
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
|
|
const existing = await this.read(job.id);
|
|
if (existing.userId !== job.userId)
|
|
throw Error("Request ID already used");
|
|
return existing;
|
|
} finally {
|
|
await fs.unlink(tmp);
|
|
}
|
|
}
|
|
async read(id: string): Promise<ImportJob> {
|
|
if (!validJobId(id)) throw Error("Invalid import ID");
|
|
return JSON.parse(
|
|
await fs.readFile(path.join(this.root, `${id}.json`), "utf8"),
|
|
);
|
|
}
|
|
async list(): Promise<ImportJob[]> {
|
|
const files = await fs.readdir(this.root).catch((error) => {
|
|
if (error.code === "ENOENT") return [];
|
|
throw error;
|
|
});
|
|
const jobs = await Promise.all(
|
|
files
|
|
.filter((f) => f.endsWith(".json") && validJobId(f.slice(0, -5)))
|
|
.map((f) => this.read(f.slice(0, -5))),
|
|
);
|
|
return jobs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
}
|
|
}
|