feat: expose managed online backup actions
This commit is contained in:
@@ -18,7 +18,9 @@ const { createAccountPicker } = require('./lib/account-rotation');
|
|||||||
const ClouddropUploader = require('./lib/clouddrop-upload');
|
const ClouddropUploader = require('./lib/clouddrop-upload');
|
||||||
const { checkForUpdate, prepareUpdate, launchPreparedUpdate, abortUpdate, createUpdateAnnouncementState } = require('./lib/updater');
|
const { checkForUpdate, prepareUpdate, launchPreparedUpdate, abortUpdate, createUpdateAnnouncementState } = require('./lib/updater');
|
||||||
const backupCrypto = require('./lib/backup-crypto');
|
const backupCrypto = require('./lib/backup-crypto');
|
||||||
const { createOnlineBackup, downloadOnlineBackup, uploadOnlineBackup } = require('./lib/online-backup');
|
const { downloadOnlineBackup } = require('./lib/online-backup');
|
||||||
|
const { createOnlineBackupKeyring } = require('./lib/online-backup-keyring');
|
||||||
|
const { createOnlineBackupManager } = require('./lib/online-backup-manager');
|
||||||
const { createPortableSettingsSnapshot, prepareImportedSettings } = require('./lib/settings-backup');
|
const { createPortableSettingsSnapshot, prepareImportedSettings } = require('./lib/settings-backup');
|
||||||
const { createSettingsImportGate } = require('./lib/settings-import-gate');
|
const { createSettingsImportGate } = require('./lib/settings-import-gate');
|
||||||
const FolderMonitor = require('./lib/folder-monitor');
|
const FolderMonitor = require('./lib/folder-monitor');
|
||||||
@@ -119,6 +121,18 @@ let dropTargetWindow = null;
|
|||||||
let tray = null;
|
let tray = null;
|
||||||
const configStore = new ConfigStore(app);
|
const configStore = new ConfigStore(app);
|
||||||
configStore.setPerfLog((m) => { try { logInfo(m); } catch {} });
|
configStore.setPerfLog((m) => { try { logInfo(m); } catch {} });
|
||||||
|
const onlineBackupKeyring = createOnlineBackupKeyring({
|
||||||
|
filePath: path.join(app.getPath('userData'), 'online-backup-keys.json')
|
||||||
|
});
|
||||||
|
const onlineBackupManager = createOnlineBackupManager({
|
||||||
|
keyring: onlineBackupKeyring,
|
||||||
|
loadSettings: async () => {
|
||||||
|
await waitForConfigStoreWrites();
|
||||||
|
return createPortableSettingsSnapshot(configStore.load());
|
||||||
|
},
|
||||||
|
appVersion: () => app.getVersion(),
|
||||||
|
copyText: value => clipboard.writeText(value)
|
||||||
|
});
|
||||||
let uploadManager = null;
|
let uploadManager = null;
|
||||||
let lastSessionSummary = null;
|
let lastSessionSummary = null;
|
||||||
let sourceDeleteJournal = null;
|
let sourceDeleteJournal = null;
|
||||||
@@ -154,6 +168,17 @@ async function waitForConfigStoreWrites() {
|
|||||||
await configStore.drainWrites();
|
await configStore.drainWrites();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function requireCanonicalOnlineBackupId(id) {
|
||||||
|
if (typeof id !== 'string' || !/^[A-Za-z0-9_-]{22}$/.test(id)) {
|
||||||
|
throw new Error('Online-Sicherungs-ID ist ungültig');
|
||||||
|
}
|
||||||
|
const decoded = Buffer.from(id, 'base64url');
|
||||||
|
if (decoded.length !== 16 || decoded.toString('base64url') !== id) {
|
||||||
|
throw new Error('Online-Sicherungs-ID ist ungültig');
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
function clearCloseFlushTimer() {
|
function clearCloseFlushTimer() {
|
||||||
if (closeFlushTimer) clearTimeout(closeFlushTimer);
|
if (closeFlushTimer) clearTimeout(closeFlushTimer);
|
||||||
closeFlushTimer = null;
|
closeFlushTimer = null;
|
||||||
@@ -2781,13 +2806,21 @@ ipcMain.handle('import-backup', async (_event, legacyPassword) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('online-backup:create', async () => {
|
ipcMain.handle('online-backup:list-managed', () => onlineBackupManager.listManaged());
|
||||||
|
|
||||||
|
ipcMain.handle('online-backup:create-managed', () => onlineBackupManager.createManaged());
|
||||||
|
|
||||||
|
ipcMain.handle('online-backup:copy-managed', (_event, id) => {
|
||||||
try {
|
try {
|
||||||
await waitForConfigStoreWrites();
|
return onlineBackupManager.copyManaged(requireCanonicalOnlineBackupId(id));
|
||||||
const snapshot = createPortableSettingsSnapshot(configStore.load());
|
} catch (error) {
|
||||||
const created = createOnlineBackup(snapshot, app.getVersion());
|
return { ok: false, error: error.message || String(error) };
|
||||||
await uploadOnlineBackup(created.record);
|
}
|
||||||
return { ok: true, key: created.key };
|
});
|
||||||
|
|
||||||
|
ipcMain.handle('online-backup:delete-managed', (_event, id) => {
|
||||||
|
try {
|
||||||
|
return onlineBackupManager.deleteManaged(requireCanonicalOnlineBackupId(id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return { ok: false, error: error.message || String(error) };
|
return { ok: false, error: error.message || String(error) };
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -79,7 +79,11 @@ contextBridge.exposeInMainWorld('api', {
|
|||||||
// Backup
|
// Backup
|
||||||
exportBackup: (options) => ipcRenderer.invoke('export-backup', options),
|
exportBackup: (options) => ipcRenderer.invoke('export-backup', options),
|
||||||
importBackup: (legacyPassword) => ipcRenderer.invoke('import-backup', legacyPassword),
|
importBackup: (legacyPassword) => ipcRenderer.invoke('import-backup', legacyPassword),
|
||||||
createOnlineBackup: () => ipcRenderer.invoke('online-backup:create'),
|
listManagedOnlineBackups: () => ipcRenderer.invoke('online-backup:list-managed'),
|
||||||
|
createManagedOnlineBackup: () => ipcRenderer.invoke('online-backup:create-managed'),
|
||||||
|
copyManagedOnlineBackup: (id) => ipcRenderer.invoke('online-backup:copy-managed', id),
|
||||||
|
deleteManagedOnlineBackup: (id) => ipcRenderer.invoke('online-backup:delete-managed', id),
|
||||||
|
createOnlineBackup: () => ipcRenderer.invoke('online-backup:create-managed'),
|
||||||
restoreOnlineBackup: (key) => ipcRenderer.invoke('online-backup:restore', key),
|
restoreOnlineBackup: (key) => ipcRenderer.invoke('online-backup:restore', key),
|
||||||
|
|
||||||
// Folder Monitor
|
// Folder Monitor
|
||||||
|
|||||||
@@ -1,15 +1,39 @@
|
|||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
const assert = require('node:assert/strict');
|
const assert = require('node:assert/strict');
|
||||||
|
const fs = require('node:fs');
|
||||||
const Module = require('node:module');
|
const Module = require('node:module');
|
||||||
const path = require('node:path');
|
const path = require('node:path');
|
||||||
const packageJson = require('../package.json');
|
const packageJson = require('../package.json');
|
||||||
|
|
||||||
|
const projectRoot = path.join(__dirname, '..');
|
||||||
|
|
||||||
test('packages every Electron preload referenced by the main process', () => {
|
test('packages every Electron preload referenced by the main process', () => {
|
||||||
assert.ok(packageJson.build.files.includes('preload.js'));
|
assert.ok(packageJson.build.files.includes('preload.js'));
|
||||||
assert.ok(packageJson.build.files.includes('preload-drop-target.js'));
|
assert.ok(packageJson.build.files.includes('preload-drop-target.js'));
|
||||||
|
assert.ok(packageJson.build.files.includes('lib/**/*'));
|
||||||
assert.equal(packageJson.build.win.signAndEditExecutable, false);
|
assert.equal(packageJson.build.win.signAndEditExecutable, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('exposes managed online backup operations through narrow IPC boundaries', () => {
|
||||||
|
const preloadSource = fs.readFileSync(path.join(projectRoot, 'preload.js'), 'utf8');
|
||||||
|
const mainSource = fs.readFileSync(path.join(projectRoot, 'main.js'), 'utf8');
|
||||||
|
|
||||||
|
assert.match(preloadSource, /listManagedOnlineBackups:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('online-backup:list-managed'\)/u);
|
||||||
|
assert.match(preloadSource, /createManagedOnlineBackup:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('online-backup:create-managed'\)/u);
|
||||||
|
assert.match(preloadSource, /copyManagedOnlineBackup:\s*\(id\)\s*=>\s*ipcRenderer\.invoke\('online-backup:copy-managed',\s*id\)/u);
|
||||||
|
assert.match(preloadSource, /deleteManagedOnlineBackup:\s*\(id\)\s*=>\s*ipcRenderer\.invoke\('online-backup:delete-managed',\s*id\)/u);
|
||||||
|
assert.match(preloadSource, /createOnlineBackup:\s*\(\)\s*=>\s*ipcRenderer\.invoke\('online-backup:create-managed'\)/u);
|
||||||
|
assert.doesNotMatch(preloadSource, /ipcRenderer\.invoke\('online-backup:create'\)/u);
|
||||||
|
assert.match(mainSource, /path\.join\(app\.getPath\('userData'\),\s*'online-backup-keys\.json'\)/u);
|
||||||
|
assert.match(mainSource, /Buffer\.from\(id,\s*'base64url'\)/u);
|
||||||
|
assert.match(mainSource, /decoded\.toString\('base64url'\)\s*!==\s*id/u);
|
||||||
|
assert.doesNotMatch(mainSource, /ipcMain\.handle\('online-backup:create'/u);
|
||||||
|
assert.match(mainSource, /ipcMain\.handle\('online-backup:list-managed',[\s\S]*?onlineBackupManager\.listManaged\(\)/u);
|
||||||
|
assert.match(mainSource, /ipcMain\.handle\('online-backup:create-managed',[\s\S]*?onlineBackupManager\.createManaged\(\)/u);
|
||||||
|
assert.match(mainSource, /ipcMain\.handle\('online-backup:copy-managed',[\s\S]*?onlineBackupManager\.copyManaged\(/u);
|
||||||
|
assert.match(mainSource, /ipcMain\.handle\('online-backup:delete-managed',[\s\S]*?onlineBackupManager\.deleteManaged\(/u);
|
||||||
|
});
|
||||||
|
|
||||||
test('afterPack brands the executable metadata shown by Windows', async () => {
|
test('afterPack brands the executable metadata shown by Windows', async () => {
|
||||||
let editCall = null;
|
let editCall = null;
|
||||||
const originalLoad = Module._load;
|
const originalLoad = Module._load;
|
||||||
|
|||||||
+5
-2
@@ -1377,8 +1377,11 @@ setTimeout(async () => {
|
|||||||
const onlineBackupKeyContract = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput")?.maxLength + "|" + document.getElementById("onlineBackupKeyInput")?.getAttribute("pattern")');
|
const onlineBackupKeyContract = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput")?.maxLength + "|" + document.getElementById("onlineBackupKeyInput")?.getAttribute("pattern")');
|
||||||
check('Online backup input enforces the 75-character MHU key format', onlineBackupKeyContract === '75|MHU2-[A-Za-z0-9_-]{70}');
|
check('Online backup input enforces the 75-character MHU key format', onlineBackupKeyContract === '75|MHU2-[A-Za-z0-9_-]{70}');
|
||||||
|
|
||||||
const onlineBackupBridge = await wc.executeJavaScript('typeof window.api.createOnlineBackup + "|" + typeof window.api.restoreOnlineBackup');
|
const onlineBackupBridge = await wc.executeJavaScript('["listManagedOnlineBackups", "createManagedOnlineBackup", "copyManagedOnlineBackup", "deleteManagedOnlineBackup"].map(name => typeof window.api[name]).join("|")');
|
||||||
check('Online backup uses a narrow preload bridge', onlineBackupBridge === 'function|function');
|
check('Online backup uses a narrow managed preload bridge', onlineBackupBridge === 'function|function|function|function');
|
||||||
|
|
||||||
|
const managedOnlineBackupCopyResult = await wc.executeJavaScript('window.api.copyManagedOnlineBackup("AAAAAAAAAAAAAAAAAAAAAA").then(result => JSON.stringify(result))');
|
||||||
|
check('Managed online backup copy never returns a complete key', !managedOnlineBackupCopyResult.includes('MHU2-'));
|
||||||
|
|
||||||
const invalidOnlineBackup = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput").value = "MHU2-short"; document.getElementById("onlineBackupKeyInput").dispatchEvent(new Event("input", { bubbles: true })); document.getElementById("restoreOnlineBackupBtn").disabled + "|" + document.getElementById("onlineBackupStatus").textContent');
|
const invalidOnlineBackup = await wc.executeJavaScript('document.getElementById("onlineBackupKeyInput").value = "MHU2-short"; document.getElementById("onlineBackupKeyInput").dispatchEvent(new Event("input", { bubbles: true })); document.getElementById("restoreOnlineBackupBtn").disabled + "|" + document.getElementById("onlineBackupStatus").textContent');
|
||||||
check('Invalid online backup keys stay blocked with visible guidance', invalidOnlineBackup === 'true|Der Schlüssel muss exakt 75 Zeichen lang sein.');
|
check('Invalid online backup keys stay blocked with visible guidance', invalidOnlineBackup === 'true|Der Schlüssel muss exakt 75 Zeichen lang sein.');
|
||||||
|
|||||||
Reference in New Issue
Block a user