54 lines
3.3 KiB
JavaScript
54 lines
3.3 KiB
JavaScript
import crypto from "node:crypto";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
export const SERVER_JOB_DIRECTORY_SCHEMA = 1;
|
|
const JOB_ID = /^[-A-Za-z0-9:_./]{1,256}$/;
|
|
|
|
function validateJobId(jobId) {
|
|
if (typeof jobId !== "string" || !JOB_ID.test(jobId) || jobId.includes("..")) throw new Error("SERVER_JOB_DIRECTORY_INVALID: jobId is invalid");
|
|
}
|
|
|
|
function validateRoot(root) {
|
|
if (typeof root !== "string" || !path.isAbsolute(root)) throw new Error("SERVER_JOB_DIRECTORY_INVALID: root must be absolute");
|
|
return path.resolve(root);
|
|
}
|
|
|
|
export async function createServerJobDirectory(root, jobId) {
|
|
const resolvedRoot = validateRoot(root);
|
|
validateJobId(jobId);
|
|
await fs.mkdir(resolvedRoot, { recursive: true, mode: 0o700 });
|
|
const randomPrefix = `.blender-job-${crypto.randomUUID()}-`;
|
|
const directory = await fs.mkdtemp(path.join(resolvedRoot, randomPrefix), { encoding: "utf8" });
|
|
await fs.chmod(directory, 0o700);
|
|
return Object.freeze({ schemaVersion: SERVER_JOB_DIRECTORY_SCHEMA, jobId, root: resolvedRoot, path: directory, directoryName: path.basename(directory), state: "ALLOCATED", cleanupCount: 0 });
|
|
}
|
|
|
|
export async function cleanupServerJobDirectory(job) {
|
|
if (!job || job.schemaVersion !== SERVER_JOB_DIRECTORY_SCHEMA || typeof job.path !== "string" || typeof job.root !== "string") throw new Error("SERVER_JOB_DIRECTORY_INVALID: receipt is invalid");
|
|
if (job.state === "CLEANED") return job;
|
|
const root = validateRoot(job.root);
|
|
const directory = path.resolve(job.path);
|
|
if (path.dirname(directory) !== root || !path.basename(directory).startsWith(".blender-job-")) throw new Error("SERVER_JOB_DIRECTORY_INVALID: directory escaped the job root");
|
|
await fs.chmod(directory, 0o700);
|
|
try { await fs.chmod(path.join(directory, "source"), 0o700); } catch (error) { if (error?.code !== "ENOENT") throw error; }
|
|
await fs.rm(directory, { recursive: true, force: false });
|
|
return Object.freeze({ ...job, state: "CLEANED", cleanupCount: 1 });
|
|
}
|
|
|
|
export async function prepareServerJobWorkspace(job, sourceBytes, sourceName = "source.blend") {
|
|
if (!job || job.schemaVersion !== SERVER_JOB_DIRECTORY_SCHEMA || job.state !== "ALLOCATED") throw new Error("SERVER_JOB_WORKSPACE_INVALID: job receipt is not allocated");
|
|
if (!(sourceBytes instanceof Uint8Array) || sourceBytes.byteLength < 1) throw new Error("SERVER_JOB_WORKSPACE_INVALID: source bytes are empty");
|
|
if (typeof sourceName !== "string" || !/^[A-Za-z0-9_.-]{1,128}$/.test(sourceName) || sourceName.includes("..")) throw new Error("SERVER_JOB_WORKSPACE_INVALID: source name is invalid");
|
|
const sourceDirectory = path.join(job.path, "source");
|
|
const outputDirectory = path.join(job.path, "output");
|
|
await fs.mkdir(sourceDirectory, { mode: 0o700 });
|
|
await fs.mkdir(outputDirectory, { mode: 0o700 });
|
|
const sourcePath = path.join(sourceDirectory, sourceName);
|
|
await fs.writeFile(sourcePath, sourceBytes, { mode: 0o444, flag: "wx" });
|
|
await fs.chmod(sourceDirectory, 0o555);
|
|
await fs.chmod(sourcePath, 0o444);
|
|
await fs.chmod(outputDirectory, 0o700);
|
|
return Object.freeze({ schemaVersion: SERVER_JOB_DIRECTORY_SCHEMA, sourceDirectory, sourcePath, outputDirectory, sourceMode: "0444", sourceDirectoryMode: "0555", outputDirectoryMode: "0700", sourceReadOnly: true, outputWritable: true });
|
|
}
|