release: Multi-Hoster-Upload 2.1.7
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
class SourceDeleteJournal {
|
||||
constructor(filePath, onEvent, options = {}) {
|
||||
this.filePath = filePath;
|
||||
this.onEvent = typeof onEvent === 'function' ? onEvent : () => {};
|
||||
this.fs = options.fs || fs;
|
||||
this.wait = typeof options.wait === 'function' ? options.wait : (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds));
|
||||
this.pending = Promise.resolve();
|
||||
}
|
||||
|
||||
append(entry) {
|
||||
const operation = this.pending.then(async () => {
|
||||
await this.fs.promises.mkdir(path.dirname(this.filePath), { recursive: true });
|
||||
const handle = await this.fs.promises.open(this.filePath, 'a');
|
||||
try {
|
||||
await handle.writeFile(`${JSON.stringify(entry)}\n`, 'utf-8');
|
||||
await handle.sync();
|
||||
} finally {
|
||||
await handle.close();
|
||||
}
|
||||
});
|
||||
this.pending = operation.catch(() => {});
|
||||
return operation;
|
||||
}
|
||||
|
||||
plan(entry) {
|
||||
return this.append({ action: 'plan', timestamp: new Date().toISOString(), ...entry });
|
||||
}
|
||||
|
||||
clear(token) {
|
||||
return this.append({ action: 'clear', timestamp: new Date().toISOString(), token });
|
||||
}
|
||||
|
||||
async recover() {
|
||||
await this.pending;
|
||||
let text;
|
||||
try {
|
||||
text = await this.fs.promises.readFile(this.filePath, 'utf-8');
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ENOENT') return [];
|
||||
throw error;
|
||||
}
|
||||
const active = new Map();
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
if (!entry || typeof entry.token !== 'string') continue;
|
||||
if (entry.action === 'plan') active.set(entry.token, entry);
|
||||
if (entry.action === 'clear') active.delete(entry.token);
|
||||
} catch {}
|
||||
}
|
||||
const outcomes = [];
|
||||
for (const entry of active.values()) {
|
||||
try {
|
||||
const sourceExists = await this.exists(entry.file);
|
||||
const stagedExists = await this.exists(entry.stagedFile);
|
||||
if (!sourceExists && stagedExists) {
|
||||
try {
|
||||
await this.renameWithRetries(entry.stagedFile, entry.file);
|
||||
outcomes.push({ token: entry.token, outcome: 'restored', file: entry.file });
|
||||
await this.clear(entry.token);
|
||||
} catch (error) {
|
||||
outcomes.push({ token: entry.token, outcome: 'restore-failed', file: entry.file, error: error.message });
|
||||
}
|
||||
} else if (!stagedExists) {
|
||||
outcomes.push({ token: entry.token, outcome: 'cleared', file: entry.file });
|
||||
await this.clear(entry.token);
|
||||
} else {
|
||||
outcomes.push({ token: entry.token, outcome: 'conflict', file: entry.file, stagedFile: entry.stagedFile });
|
||||
}
|
||||
} catch (error) {
|
||||
outcomes.push({ token: entry.token, outcome: 'recovery-failed', file: entry.file, error: error.message });
|
||||
}
|
||||
}
|
||||
for (const outcome of outcomes) this.onEvent(outcome);
|
||||
return outcomes;
|
||||
}
|
||||
|
||||
async exists(target) {
|
||||
try {
|
||||
await this.fs.promises.lstat(target);
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (error && error.code === 'ENOENT') return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async renameWithRetries(from, to) {
|
||||
const delays = [100, 250, 500];
|
||||
for (let attempt = 0; ; attempt += 1) {
|
||||
try {
|
||||
await this.fs.promises.rename(from, to);
|
||||
return;
|
||||
} catch (error) {
|
||||
if (!error || !['EBUSY', 'EPERM'].includes(error.code) || attempt >= delays.length) throw error;
|
||||
await this.wait(delays[attempt]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = SourceDeleteJournal;
|
||||
Reference in New Issue
Block a user