Added export/import features

This commit is contained in:
2026-09-19 13:21:43 +02:00
parent f61b9025eb
commit 870457955b
14 changed files with 3363 additions and 14 deletions

View File

@@ -1,4 +1,5 @@
import Dexie from 'dexie';
import { BackupError, buildBackup, EXPORT_FORMAT, EXPORT_VERSION, parseBackup } from './backup.js';
import { DAY, localDateKey } from './time.js';
/**
@@ -238,3 +239,65 @@ export async function cleanupOldDays(retentionDays) {
});
return keys.length;
}
/**
* Write an imported dataset.
*
* `replace` wipes the local days and entries first and adopts the file's settings.
* `merge` adds only days the device does not already have and leaves settings
* alone — merging two devices' settings is meaningless, and keeping the local copy
* of a day the device already knows about never loses work that was done here.
*
* @param {{days: Array, entries: Array, settings: object|null}} payload
* @param {'merge'|'replace'} mode
* @returns {Promise<{added: number, skipped: number, entries: number}>}
*/
export function importBackup(payload, mode) {
if (mode !== 'merge' && mode !== 'replace') {
throw new BackupError('Mode d’import inconnu.');
}
// Validate again at the write boundary and turn Svelte state proxies into plain
// records. IndexedDB cannot structured-clone a proxy (DataCloneError).
const { days, entries, settings } = parseBackup(JSON.stringify({
...payload,
format: EXPORT_FORMAT,
version: EXPORT_VERSION,
}));
return db.transaction('rw', db.days, db.entries, db.settings, async () => {
if (mode === 'replace') {
await db.days.clear();
await db.entries.clear();
await db.settings.clear();
}
const present = new Set((await db.days.toArray()).map((d) => d.date));
const incoming = days.filter((d) => !present.has(d.date));
const wanted = new Set(incoming.map((d) => d.date));
const incomingEntries = entries
.filter((e) => wanted.has(e.date))
.map(({ id, ...rest }) => rest);
if (incoming.length) await db.days.bulkAdd(incoming);
if (incomingEntries.length) await db.entries.bulkAdd(incomingEntries);
if (mode === 'replace' && settings) {
await db.settings.put({ key: 'app', value: settings });
}
return {
added: incoming.length,
skipped: days.length - incoming.length,
entries: incomingEntries.length,
};
});
}
/** A consistent snapshot of all three stores, even while another tab writes. */
export function exportBackup() {
return db.transaction('r', db.days, db.entries, db.settings, async () => {
const [days, entries, settings] = await Promise.all([
db.days.toArray(), db.entries.toArray(), readSettings(),
]);
return buildBackup({ days, entries, settings });
});
}