Persist recovery before accepting upload work
Fail closed if the initial or extended recovery marker cannot be written, retain exact terminal outcomes after catastrophic start failures, and localize the resulting user-visible errors.
This commit is contained in:
@@ -132,6 +132,7 @@ const configStore = new ConfigStore(app);
|
|||||||
configStore.setPerfLog((m) => { try { logInfo(m); } catch {} });
|
configStore.setPerfLog((m) => { try { logInfo(m); } catch {} });
|
||||||
let uploadManager = null;
|
let uploadManager = null;
|
||||||
const uploadBatchMutationGates = new WeakMap();
|
const uploadBatchMutationGates = new WeakMap();
|
||||||
|
const uploadRecoveryStates = new WeakMap();
|
||||||
let lastSessionSummary = null;
|
let lastSessionSummary = null;
|
||||||
let startupRecoveryCoordinator = null;
|
let startupRecoveryCoordinator = null;
|
||||||
let sourceDeleteJournal = null;
|
let sourceDeleteJournal = null;
|
||||||
@@ -2092,7 +2093,17 @@ async function executeReservedUploadStart(payload, startLease) {
|
|||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
jobIds: tasks.map(task => task.jobId).filter(Boolean)
|
jobIds: tasks.map(task => task.jobId).filter(Boolean)
|
||||||
};
|
};
|
||||||
try { await configStore.saveUploadRecovery(recovery); } catch (error) { debugLog(`upload recovery state could not be saved: ${error.message}`); }
|
try {
|
||||||
|
await configStore.saveUploadRecovery(recovery);
|
||||||
|
} catch (error) {
|
||||||
|
debugLog(`upload recovery state could not be saved: ${error.message}`);
|
||||||
|
if (uploadManager === _thisManager) {
|
||||||
|
uploadManager = null;
|
||||||
|
globalThis._mhuUploadManagerRef = null;
|
||||||
|
}
|
||||||
|
return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' };
|
||||||
|
}
|
||||||
|
uploadRecoveryStates.set(_thisManager, recovery);
|
||||||
|
|
||||||
// Pre-resolve a fallback for every hoster that has one. Lets the upload
|
// Pre-resolve a fallback for every hoster that has one. Lets the upload
|
||||||
// manager break out of the retry loop after a single generic failure and
|
// manager break out of the retry loop after a single generic failure and
|
||||||
@@ -2436,6 +2447,21 @@ ipcMain.handle('add-jobs-to-batch', async (_event, payload) => {
|
|||||||
);
|
);
|
||||||
if (!auditedAdd.ok) return { error: getUploadAuditFailureMessage(getConfiguredLanguage()) };
|
if (!auditedAdd.ok) return { error: getUploadAuditFailureMessage(getConfiguredLanguage()) };
|
||||||
}
|
}
|
||||||
|
const batchRecovery = uploadRecoveryStates.get(batchManager);
|
||||||
|
const newRecoveryJobIds = tasks.map(task => task.jobId).filter(Boolean);
|
||||||
|
if (batchRecovery && newRecoveryJobIds.length > 0) {
|
||||||
|
const nextRecovery = {
|
||||||
|
...batchRecovery,
|
||||||
|
jobIds: Array.from(new Set([...(batchRecovery.jobIds || []), ...newRecoveryJobIds]))
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await configStore.saveUploadRecovery(nextRecovery);
|
||||||
|
} catch (error) {
|
||||||
|
debugLog(`upload recovery state could not be extended: ${error.message}`);
|
||||||
|
return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' };
|
||||||
|
}
|
||||||
|
Object.assign(batchRecovery, nextRecovery);
|
||||||
|
}
|
||||||
persistRotation(pick);
|
persistRotation(pick);
|
||||||
const sourceCleanupFingerprints = batchManager.sourceFileCleanup
|
const sourceCleanupFingerprints = batchManager.sourceFileCleanup
|
||||||
? await batchManager.sourceFileCleanup.registerGroups(sourceCleanupGroups)
|
? await batchManager.sourceFileCleanup.registerGroups(sourceCleanupGroups)
|
||||||
|
|||||||
@@ -578,9 +578,12 @@
|
|||||||
['Update wird installiert. Die Anwendung startet neu…', 'Installing update. The application is restarting…'],
|
['Update wird installiert. Die Anwendung startet neu…', 'Installing update. The application is restarting…'],
|
||||||
['Updateprüfung fehlgeschlagen', 'Update check failed'],
|
['Updateprüfung fehlgeschlagen', 'Update check failed'],
|
||||||
['Upload läuft...', 'Uploading...'],
|
['Upload läuft...', 'Uploading...'],
|
||||||
|
['Upload konnte nicht gestartet werden', 'The upload could not be started'],
|
||||||
|
['Upload wurde abgebrochen', 'The upload was canceled'],
|
||||||
['Upload-Log', 'Upload log'],
|
['Upload-Log', 'Upload log'],
|
||||||
['Upload-Status', 'Upload status'],
|
['Upload-Status', 'Upload status'],
|
||||||
['Upload-Übersicht', 'Upload overview'],
|
['Upload-Übersicht', 'Upload overview'],
|
||||||
|
['Upload-Wiederherstellung konnte nicht gespeichert werden', 'Upload recovery could not be saved'],
|
||||||
['Verlauf als CSV exportieren?\n\nOK = CSV\nAbbrechen = JSON', 'Export history as CSV?\n\nOK = CSV\nCancel = JSON'],
|
['Verlauf als CSV exportieren?\n\nOK = CSV\nAbbrechen = JSON', 'Export history as CSV?\n\nOK = CSV\nCancel = JSON'],
|
||||||
['Verlauf wirklich löschen?', 'Are you sure you want to delete the history?'],
|
['Verlauf wirklich löschen?', 'Are you sure you want to delete the history?'],
|
||||||
['Verschlüssele und speichere Einstellungen…', 'Encrypting and saving settings…'],
|
['Verschlüssele und speichere Einstellungen…', 'Encrypting and saving settings…'],
|
||||||
|
|||||||
@@ -88,6 +88,12 @@ test('main and renderer keep recovery evidence until final queue persistence suc
|
|||||||
assert.match(startFailure, /saveUploadRecovery\(terminalRecovery\)/);
|
assert.match(startFailure, /saveUploadRecovery\(terminalRecovery\)/);
|
||||||
assert.match(startFailure, /requestUploadFinalization\(errorSummary, historyPersisted\)/);
|
assert.match(startFailure, /requestUploadFinalization\(errorSummary, historyPersisted\)/);
|
||||||
assert.match(startFailure, /if \(queuePersisted && terminalRecoveryPersisted\)[\s\S]*saveUploadRecovery\(null\)/);
|
assert.match(startFailure, /if \(queuePersisted && terminalRecoveryPersisted\)[\s\S]*saveUploadRecovery\(null\)/);
|
||||||
|
const startHandler = mainSource.slice(mainSource.indexOf("ipcMain.handle('start-upload'"), mainSource.indexOf("ipcMain.handle('cancel-upload'"));
|
||||||
|
assert.match(startHandler, /await configStore\.saveUploadRecovery\(recovery\)/);
|
||||||
|
assert.match(startHandler, /catch \(error\)[\s\S]*return { error: 'Upload-Wiederherstellung konnte nicht gespeichert werden' }/);
|
||||||
|
const addHandler = mainSource.slice(mainSource.indexOf("ipcMain.handle('add-jobs-to-batch'"), mainSource.indexOf("ipcMain.handle('finish-after-active'"));
|
||||||
|
assert.match(addHandler, /await configStore\.saveUploadRecovery\(nextRecovery\)/);
|
||||||
|
assert.ok(addHandler.indexOf('saveUploadRecovery(nextRecovery)') < addHandler.indexOf('batchManager.addJobs(tasks)'));
|
||||||
assert.match(rendererSource, /window\.UploadRecovery\.getRecoveryOutcome/);
|
assert.match(rendererSource, /window\.UploadRecovery\.getRecoveryOutcome/);
|
||||||
assert.match(rendererSource, /data\.historyPersisted !== true/);
|
assert.match(rendererSource, /data\.historyPersisted !== true/);
|
||||||
assert.ok(indexSource.indexOf('../lib/upload-recovery.js') < indexSource.indexOf('app.js'));
|
assert.ok(indexSource.indexOf('../lib/upload-recovery.js') < indexSource.indexOf('app.js'));
|
||||||
|
|||||||
Reference in New Issue
Block a user