Initial U-Navigator prototype

This commit is contained in:
Mikei386
2026-06-22 22:11:54 +02:00
commit 37ddea08f4
11 changed files with 1841 additions and 0 deletions
+125
View File
@@ -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');
}