A user's server crashed hard during an upload and lost all configured accounts. It
was NOT the v3.3.104 update (a second server updated fine and kept its accounts) —
it was a data-durability hole exposed by the crash:
- Config writes were atomic (tmp + rename) but never fsync'd, so a hard crash could
leave electron-config.json truncated/unflushed on disk.
- On restart, load() reads the truncated file, falls back to .bak, and if that is
also bad returns empty DEFAULTS. The next settings/queue save then persists EMPTY
hosters — permanently wiping the accounts. Worse, the async _atomicWrite blindly
copied the (now truncated) live file over .bak, so an empty live could clobber a
good backup.
Hardening (lib/config-store.js + main.js; no behavior change in the happy path):
- fsync before rename in both write paths — _atomicWrite (openSync/writeSync/
fsyncSync/closeSync) and the synchronous save-global-settings-sync on window close.
A hard crash can no longer leave a truncated config.
- _atomicWrite only refreshes .bak when the current live file is non-trivial
(trim length > 2), so an empty/truncated live can never overwrite a good backup
(the sync-save path already did this).
- Wipe-guard (_guardHosters): save(), saveRotationCursors() and the sync close-save
never intend to change hosters; if after a load() the hosters are all empty and
the write did not explicitly provide hosters, recover them from disk
(_recoverHostersFromDisk: live -> .bak -> .pre-history-split.bak) instead of
persisting the wipe. An explicit save({hosters: {}}) (user deleted all accounts)
is still allowed. Restored hosters are already-encrypted on disk and
encryptCredentials skips already-encrypted fields, so re-serializing is safe.
- load() gained a third fallback tier — the permanent pre-history-split.bak snapshot
(which still holds the accounts) — so load() itself recovers after corruption.
Recovery for the already-affected server: copy
%APPDATA%/multi-hoster-uploader/electron-config.json.pre-history-split.bak (or .bak)
over electron-config.json with the app closed.
2 new regression tests (post-wipe valid-empty live + .bak → guard restores accounts;
an explicit empty-hosters save is not blocked). 409 tests pass; clean boot.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
57 lines
1.4 KiB
JSON
57 lines
1.4 KiB
JSON
{
|
|
"name": "multi-hoster-uploader",
|
|
"version": "3.3.105",
|
|
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
|
|
"main": "main.js",
|
|
"scripts": {
|
|
"start": "electron .",
|
|
"test": "node --test tests/*.test.js tests/ui-smoke.js",
|
|
"dist": "electron-builder --win",
|
|
"release:win": "electron-builder --publish never --win nsis portable",
|
|
"release:gitea": "node scripts/release_gitea.mjs"
|
|
},
|
|
"dependencies": {
|
|
"chokidar": "^3.6.0",
|
|
"undici": "^7.16.0",
|
|
"ws": "^8.19.0"
|
|
},
|
|
"devDependencies": {
|
|
"electron": "^41.3.0",
|
|
"electron-builder": "^26.8.1",
|
|
"eslint": "^10.1.0",
|
|
"eslint-plugin-security": "^4.0.0",
|
|
"rcedit": "^4.0.1"
|
|
},
|
|
"build": {
|
|
"appId": "com.multihoster.uploader",
|
|
"productName": "Multi-Hoster-Upload",
|
|
"directories": {
|
|
"buildResources": "assets",
|
|
"output": "release"
|
|
},
|
|
"files": [
|
|
"main.js",
|
|
"preload.js",
|
|
"lib/**/*",
|
|
"renderer/**/*",
|
|
"assets/app_icon.ico",
|
|
"assets/app_icon.png"
|
|
],
|
|
"win": {
|
|
"target": [
|
|
"nsis",
|
|
"portable"
|
|
],
|
|
"icon": "assets/app_icon.ico",
|
|
"signAndEditExecutable": false
|
|
},
|
|
"nsis": {
|
|
"oneClick": false,
|
|
"perMachine": false,
|
|
"allowToChangeInstallationDirectory": true,
|
|
"createDesktopShortcut": true
|
|
},
|
|
"afterPack": "scripts/afterPack.cjs"
|
|
}
|
|
}
|