Initial U-Navigator prototype
This commit is contained in:
+125
@@ -0,0 +1,125 @@
|
||||
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');
|
||||
}
|
||||
Reference in New Issue
Block a user