126 lines
3.2 KiB
JavaScript
126 lines
3.2 KiB
JavaScript
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
|
|
export class JobStore {
|
|
constructor() {
|
|
this.jobs = new Map();
|
|
this.nextId = 1;
|
|
}
|
|
|
|
create(type, task) {
|
|
const id = String(this.nextId++);
|
|
const job = {
|
|
id,
|
|
type,
|
|
status: 'queued',
|
|
progress: 0,
|
|
message: '',
|
|
error: null,
|
|
createdAt: new Date().toISOString(),
|
|
updatedAt: new Date().toISOString(),
|
|
cancelRequested: false
|
|
};
|
|
|
|
this.jobs.set(id, job);
|
|
queueMicrotask(async () => {
|
|
await this.run(job, task);
|
|
});
|
|
return this.publicJob(job);
|
|
}
|
|
|
|
get(id) {
|
|
const job = this.jobs.get(String(id));
|
|
return job ? this.publicJob(job) : null;
|
|
}
|
|
|
|
cancel(id) {
|
|
const job = this.jobs.get(String(id));
|
|
if (!job) {
|
|
return null;
|
|
}
|
|
job.cancelRequested = true;
|
|
this.touch(job, 'cancel_requested', job.progress, 'Cancel requested');
|
|
return this.publicJob(job);
|
|
}
|
|
|
|
async run(job, task) {
|
|
try {
|
|
this.touch(job, 'running', 1, 'Running');
|
|
await task({
|
|
isCanceled: () => job.cancelRequested,
|
|
progress: (progress, message = job.message) => this.touch(job, 'running', progress, message)
|
|
});
|
|
if (job.cancelRequested) {
|
|
this.touch(job, 'canceled', job.progress, 'Canceled');
|
|
} else {
|
|
this.touch(job, 'completed', 100, 'Completed');
|
|
}
|
|
} catch (error) {
|
|
this.touch(job, job.cancelRequested ? 'canceled' : 'failed', job.progress, error.message);
|
|
job.error = error.message;
|
|
}
|
|
}
|
|
|
|
touch(job, status, progress, message) {
|
|
job.status = status;
|
|
job.progress = Math.max(0, Math.min(100, Math.round(progress)));
|
|
job.message = message;
|
|
job.updatedAt = new Date().toISOString();
|
|
}
|
|
|
|
publicJob(job) {
|
|
const { cancelRequested, ...publicFields } = job;
|
|
return publicFields;
|
|
}
|
|
}
|
|
|
|
export async function movePath(source, destination, ctx) {
|
|
if (ctx.isCanceled()) {
|
|
throw new Error('Canceled');
|
|
}
|
|
ctx.progress(10, 'Moving');
|
|
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
await fs.rename(source, destination);
|
|
ctx.progress(95, 'Move finished');
|
|
}
|
|
|
|
export async function copyPath(source, destination, ctx) {
|
|
if (ctx.isCanceled()) {
|
|
throw new Error('Canceled');
|
|
}
|
|
ctx.progress(5, 'Preparing copy');
|
|
await fs.mkdir(path.dirname(destination), { recursive: true });
|
|
await copyRecursive(source, destination, ctx);
|
|
ctx.progress(95, 'Copy finished');
|
|
}
|
|
|
|
async function copyRecursive(source, destination, ctx) {
|
|
if (ctx.isCanceled()) {
|
|
throw new Error('Canceled');
|
|
}
|
|
|
|
const stat = await fs.lstat(source);
|
|
if (stat.isSymbolicLink()) {
|
|
throw new Error('Copying symlinks is not allowed');
|
|
}
|
|
|
|
if (stat.isDirectory()) {
|
|
await fs.mkdir(destination, { recursive: true, mode: stat.mode });
|
|
const entries = await fs.readdir(source);
|
|
let done = 0;
|
|
for (const entry of entries) {
|
|
await copyRecursive(path.join(source, entry), path.join(destination, entry), ctx);
|
|
done += 1;
|
|
ctx.progress(10 + (done / Math.max(entries.length, 1)) * 80, 'Copying directory');
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!stat.isFile()) {
|
|
throw new Error('Only files and directories can be copied');
|
|
}
|
|
|
|
await fs.copyFile(source, destination);
|
|
ctx.progress(85, 'Copying file');
|
|
}
|