fix: atomic config writes to prevent data loss on update/crash

- All config writes now go through _atomicWrite() (write to .tmp, backup
  to .bak, rename .tmp to main config)
- load() falls back to .bak if main config is empty or corrupt
- Prevents 0KB config files caused by process termination during write

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Administrator 2026-03-11 04:06:41 +01:00
parent 6945b42886
commit 153ea2b193
2 changed files with 40 additions and 21 deletions

View File

@ -82,10 +82,24 @@ class ConfigStore {
} catch {}
}
_readAndParse(filePath) {
const raw = fs.readFileSync(filePath, 'utf-8');
if (!raw || raw.trim().length < 2) return null;
return JSON.parse(raw);
}
load() {
try {
const raw = fs.readFileSync(this.filePath, 'utf-8');
const data = JSON.parse(raw);
let data = null;
// Try main config
try { data = this._readAndParse(this.filePath); } catch {}
// Fallback to backup if main is empty/corrupt
if (!data) {
const backupPath = this.filePath + '.bak';
try { data = this._readAndParse(backupPath); } catch {}
}
if (!data) return JSON.parse(JSON.stringify(DEFAULTS));
// Merge with defaults so new hosters are always present
const hosters = { ...DEFAULTS.hosters };
for (const [name, val] of Object.entries(data.hosters || {})) {
@ -116,12 +130,7 @@ class ConfigStore {
if (config.hosters) current.hosters = config.hosters;
if (config.hosterSettings) current.hosterSettings = config.hosterSettings;
if (config.globalSettings) current.globalSettings = config.globalSettings;
const data = JSON.stringify(current, null, 2);
return new Promise((resolve, reject) => {
fs.writeFile(this.filePath, data, 'utf-8', (err) => {
if (err) reject(err); else resolve();
});
});
return this._atomicWrite(JSON.stringify(current, null, 2));
}
loadHistory() {
@ -129,29 +138,39 @@ class ConfigStore {
return config.history || [];
}
_atomicWrite(data) {
return new Promise((resolve, reject) => {
const tmpPath = this.filePath + '.tmp';
const backupPath = this.filePath + '.bak';
fs.writeFile(tmpPath, data, 'utf-8', (err) => {
if (err) return reject(err);
try {
if (fs.existsSync(this.filePath)) {
const existing = fs.readFileSync(this.filePath, 'utf-8');
if (existing && existing.trim().length > 2) {
fs.writeFileSync(backupPath, existing, 'utf-8');
}
}
fs.renameSync(tmpPath, this.filePath);
} catch (e) { return reject(e); }
resolve();
});
});
}
appendHistory(entry) {
const config = this.load();
config.history.push(entry);
if (config.history.length > MAX_HISTORY) {
config.history = config.history.slice(-MAX_HISTORY);
}
const data = JSON.stringify(config, null, 2);
return new Promise((resolve, reject) => {
fs.writeFile(this.filePath, data, 'utf-8', (err) => {
if (err) reject(err); else resolve();
});
});
return this._atomicWrite(JSON.stringify(config, null, 2));
}
clearHistory() {
const config = this.load();
config.history = [];
const data = JSON.stringify(config, null, 2);
return new Promise((resolve, reject) => {
fs.writeFile(this.filePath, data, 'utf-8', (err) => {
if (err) reject(err); else resolve();
});
});
return this._atomicWrite(JSON.stringify(config, null, 2));
}
}

View File

@ -1,6 +1,6 @@
{
"name": "multi-hoster-uploader",
"version": "1.6.7",
"version": "1.6.8",
"description": "Upload files to doodstream, voe, vidmoly, byse simultaneously",
"main": "main.js",
"scripts": {