Files
image/node-hook/src/upload-store.mjs
T

64 lines
2.4 KiB
JavaScript

import { createHash } from 'node:crypto';
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises';
import { dirname, join } from 'node:path';
import { DeploymentError } from './git-service.mjs';
export class UploadStore {
constructor(config) {
this.root = config.uploadRoot;
this.statePath = config.uploadStatePath;
this.queue = Promise.resolve();
this.uploads = [];
}
async initialize() {
await mkdir(this.root, { recursive: true });
await mkdir(dirname(this.statePath), { recursive: true });
try {
const saved = JSON.parse(await readFile(this.statePath, 'utf8'));
this.uploads = Array.isArray(saved.uploads) ? saved.uploads : [];
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
}
store({ requestKey, client, filename, body }) {
const operation = this.queue.then(async () => {
if (!/^[a-z0-9][a-z0-9_-]{1,31}$/.test(client)
|| !/^[a-f0-9]{32}\.(?:avif|webp|jpg|png|gif)$/.test(filename)) {
throw new DeploymentError('Invalid upload path', 400);
}
const relativePath = `${client}/${filename}`;
const path = `icons/users/${relativePath}`;
const digest = createHash('sha256').update(body).digest('hex');
const previous = this.uploads.find((upload) => upload.key === requestKey);
if (previous) {
if (previous.path !== path || previous.digest !== digest) {
throw new DeploymentError('Upload request ID was already used', 409);
}
return { duplicate: true, path: previous.path };
}
const destination = join(this.root, relativePath);
await mkdir(dirname(destination), { recursive: true });
try {
await writeFile(destination, body, { flag: 'wx', mode: 0o644 });
} catch (error) {
if (error.code !== 'EEXIST' || !(await readFile(destination)).equals(body)) {
throw new DeploymentError('Upload path already exists', 409);
}
}
this.uploads = [...this.uploads.slice(-999), { key: requestKey, path, digest }];
await this.#save();
return { duplicate: false, path };
});
this.queue = operation.catch(() => undefined);
return operation;
}
async #save() {
const temporary = `${this.statePath}.tmp-${process.pid}`;
await writeFile(temporary, JSON.stringify({ uploads: this.uploads }, null, 2), { mode: 0o600 });
await rename(temporary, this.statePath);
}
}