feat(cms): improve catalog, editorial recovery and operations
This commit is contained in:
1 parent
2fead134e6
commit
c389c3893d
122 files changed
+8137
-703
No files matched your search
@@ -39,20 +39,68 @@ export class ImportJobStore {
|
||||
}
|
||||
async read(id: string): Promise<ImportJob> {
|
||||
if (!validJobId(id)) throw Error("Invalid import ID");
|
||||
return JSON.parse(
|
||||
const job: ImportJob = JSON.parse(
|
||||
await fs.readFile(path.join(this.root, `${id}.json`), "utf8"),
|
||||
);
|
||||
if (await this.isCancellationRequested(id)) job.cancelRequested = true;
|
||||
return job;
|
||||
}
|
||||
async list(): Promise<ImportJob[]> {
|
||||
async isCancellationRequested(id: string): Promise<boolean> {
|
||||
if (!validJobId(id)) throw Error("Invalid import ID");
|
||||
try {
|
||||
await fs.access(path.join(this.root, `${id}.cancel`));
|
||||
return true;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
/** Cancellation has its own durable marker: a worker save cannot overwrite it. */
|
||||
async requestCancellation(
|
||||
id: string,
|
||||
userId: number,
|
||||
): Promise<ImportJob | null> {
|
||||
const job = await this.read(id).catch((error) => {
|
||||
if (error.code === "ENOENT") return null;
|
||||
throw error;
|
||||
});
|
||||
if (!job || job.userId !== userId) return null;
|
||||
if (job.state !== "queued" && job.state !== "running") return job;
|
||||
await fs
|
||||
.writeFile(path.join(this.root, `${id}.cancel`), "cancel", { flag: "wx" })
|
||||
.catch((error) => {
|
||||
if (error.code !== "EEXIST") throw error;
|
||||
});
|
||||
return this.read(id);
|
||||
}
|
||||
async list(options?: {
|
||||
userId: number;
|
||||
limit: number;
|
||||
}): Promise<ImportJob[]> {
|
||||
const files = await fs.readdir(this.root).catch((error) => {
|
||||
if (error.code === "ENOENT") return [];
|
||||
throw error;
|
||||
});
|
||||
const jobs = await Promise.all(
|
||||
files
|
||||
.filter((f) => f.endsWith(".json") && validJobId(f.slice(0, -5)))
|
||||
.map((f) => this.read(f.slice(0, -5))),
|
||||
const candidates: Array<{ id: string; modified: number }> = [];
|
||||
// Metadata and payload reads are sequential, avoiding thousands of concurrent opens.
|
||||
for (const file of files) {
|
||||
if (!file.endsWith(".json") || !validJobId(file.slice(0, -5))) continue;
|
||||
const stat = await fs.stat(path.join(this.root, file));
|
||||
candidates.push({ id: file.slice(0, -5), modified: stat.mtimeMs });
|
||||
}
|
||||
candidates.sort((a, b) =>
|
||||
options ? b.modified - a.modified : a.modified - b.modified,
|
||||
);
|
||||
return jobs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
||||
const jobs: ImportJob[] = [];
|
||||
const limit = options ? Math.min(30, Math.max(1, options.limit)) : Infinity;
|
||||
for (const candidate of candidates) {
|
||||
const job = await this.read(candidate.id);
|
||||
if (options && job.userId !== options.userId) continue;
|
||||
jobs.push(job);
|
||||
if (jobs.length >= limit) break;
|
||||
}
|
||||
return options
|
||||
? jobs
|
||||
: jobs.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user