forked from devsam/image
64 lines
2.4 KiB
JavaScript
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);
|
|
}
|
|
}
|