Add session backup restore and release v1.4.68
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
e7f0b1d1fd
commit
bf2b685e83
21
CHANGELOG.md
21
CHANGELOG.md
@ -2,6 +2,27 @@
|
|||||||
|
|
||||||
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
Alle nennenswerten Aenderungen werden in dieser Datei dokumentiert.
|
||||||
|
|
||||||
|
## 1.4.68 - 2026-03-01
|
||||||
|
|
||||||
|
Stabilitaets-Hotfix fuer Session-Verlust nach Update/Neustart: Session-Dateien haben jetzt ein robustes Backup-/Restore-Fallback.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Session-Backup fuer Queue-Zustand eingefuehrt:
|
||||||
|
- Vor jedem Session-Save wird die vorherige Session als `.bak` gesichert (sync + async Pfad).
|
||||||
|
- Schuetzt gegen defekte/trunkierte Session-Datei beim Start.
|
||||||
|
- Session-Autorestore beim Laden:
|
||||||
|
- Wenn `rd_session_state.json` defekt ist, wird automatisch `rd_session_state.json.bak` geladen.
|
||||||
|
- Das Backup wird danach best-effort wieder als primäre Session-Datei hergestellt.
|
||||||
|
- Klarere Fehlersignale im Log:
|
||||||
|
- Eindeutige Meldung, ob primäre Session defekt war und Backup verwendet wurde.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Neue Tests in `tests/storage.test.ts`:
|
||||||
|
- Laden aus Session-Backup bei defekter primärer Session.
|
||||||
|
- Backup-Erstellung vor sync- und async-Session-Overwrite.
|
||||||
|
|
||||||
## 1.4.67 - 2026-03-01
|
## 1.4.67 - 2026-03-01
|
||||||
|
|
||||||
Hotfix fuer einen kritischen Start-Konflikt-Datenverlust und zusaetzliche Renamer-Haertung fuer reale Scene-Muster.
|
Hotfix fuer einen kritischen Start-Konflikt-Datenverlust und zusaetzliche Renamer-Haertung fuer reale Scene-Muster.
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.67",
|
"version": "1.4.68",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.67",
|
"version": "1.4.68",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "^0.5.16",
|
"adm-zip": "^0.5.16",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "real-debrid-downloader",
|
"name": "real-debrid-downloader",
|
||||||
"version": "1.4.67",
|
"version": "1.4.68",
|
||||||
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@ -365,6 +365,34 @@ function sessionTempPath(sessionFile: string, kind: "sync" | "async"): string {
|
|||||||
return `${sessionFile}.${kind}.tmp`;
|
return `${sessionFile}.${kind}.tmp`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function sessionBackupPath(sessionFile: string): string {
|
||||||
|
return `${sessionFile}.bak`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeLoadedSessionTransientFields(session: SessionState): SessionState {
|
||||||
|
// Reset transient fields that may be stale from a previous crash
|
||||||
|
const ACTIVE_STATUSES = new Set(["downloading", "validating", "extracting", "integrity_check", "paused", "reconnect_wait"]);
|
||||||
|
for (const item of Object.values(session.items)) {
|
||||||
|
if (ACTIVE_STATUSES.has(item.status)) {
|
||||||
|
item.status = "queued";
|
||||||
|
item.lastError = "";
|
||||||
|
}
|
||||||
|
// Always clear stale speed values
|
||||||
|
item.speedBps = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return session;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readSessionFile(filePath: string): SessionState | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
||||||
|
return normalizeLoadedSessionTransientFields(normalizeLoadedSession(parsed));
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function saveSettings(paths: StoragePaths, settings: AppSettings): void {
|
export function saveSettings(paths: StoragePaths, settings: AppSettings): void {
|
||||||
ensureBaseDir(paths.baseDir);
|
ensureBaseDir(paths.baseDir);
|
||||||
// Create a backup of the existing config before overwriting
|
// Create a backup of the existing config before overwriting
|
||||||
@ -404,30 +432,40 @@ export function loadSession(paths: StoragePaths): SessionState {
|
|||||||
if (!fs.existsSync(paths.sessionFile)) {
|
if (!fs.existsSync(paths.sessionFile)) {
|
||||||
return emptySession();
|
return emptySession();
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(fs.readFileSync(paths.sessionFile, "utf8")) as unknown;
|
|
||||||
const session = normalizeLoadedSession(parsed);
|
|
||||||
|
|
||||||
// Reset transient fields that may be stale from a previous crash
|
const primary = readSessionFile(paths.sessionFile);
|
||||||
const ACTIVE_STATUSES = new Set(["downloading", "validating", "extracting", "integrity_check", "paused", "reconnect_wait"]);
|
if (primary) {
|
||||||
for (const item of Object.values(session.items)) {
|
return primary;
|
||||||
if (ACTIVE_STATUSES.has(item.status)) {
|
|
||||||
item.status = "queued";
|
|
||||||
item.lastError = "";
|
|
||||||
}
|
|
||||||
// Always clear stale speed values
|
|
||||||
item.speedBps = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return session;
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Session konnte nicht geladen werden: ${String(error)}`);
|
|
||||||
return emptySession();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const backupFile = sessionBackupPath(paths.sessionFile);
|
||||||
|
const backup = fs.existsSync(backupFile) ? readSessionFile(backupFile) : null;
|
||||||
|
if (backup) {
|
||||||
|
logger.warn("Session defekt, Backup-Datei wird verwendet");
|
||||||
|
try {
|
||||||
|
const payload = JSON.stringify({ ...backup, updatedAt: Date.now() });
|
||||||
|
const tempPath = sessionTempPath(paths.sessionFile, "sync");
|
||||||
|
fs.writeFileSync(tempPath, payload, "utf8");
|
||||||
|
syncRenameWithExdevFallback(tempPath, paths.sessionFile);
|
||||||
|
} catch {
|
||||||
|
// ignore restore write failure
|
||||||
|
}
|
||||||
|
return backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Session konnte nicht geladen werden (auch Backup fehlgeschlagen)");
|
||||||
|
return emptySession();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveSession(paths: StoragePaths, session: SessionState): void {
|
export function saveSession(paths: StoragePaths, session: SessionState): void {
|
||||||
ensureBaseDir(paths.baseDir);
|
ensureBaseDir(paths.baseDir);
|
||||||
|
if (fs.existsSync(paths.sessionFile)) {
|
||||||
|
try {
|
||||||
|
fs.copyFileSync(paths.sessionFile, sessionBackupPath(paths.sessionFile));
|
||||||
|
} catch {
|
||||||
|
// Best-effort backup; proceed even if it fails
|
||||||
|
}
|
||||||
|
}
|
||||||
const payload = JSON.stringify({ ...session, updatedAt: Date.now() });
|
const payload = JSON.stringify({ ...session, updatedAt: Date.now() });
|
||||||
const tempPath = sessionTempPath(paths.sessionFile, "sync");
|
const tempPath = sessionTempPath(paths.sessionFile, "sync");
|
||||||
fs.writeFileSync(tempPath, payload, "utf8");
|
fs.writeFileSync(tempPath, payload, "utf8");
|
||||||
@ -439,6 +477,9 @@ let asyncSaveQueued: { paths: StoragePaths; payload: string } | null = null;
|
|||||||
|
|
||||||
async function writeSessionPayload(paths: StoragePaths, payload: string): Promise<void> {
|
async function writeSessionPayload(paths: StoragePaths, payload: string): Promise<void> {
|
||||||
await fs.promises.mkdir(paths.baseDir, { recursive: true });
|
await fs.promises.mkdir(paths.baseDir, { recursive: true });
|
||||||
|
if (fs.existsSync(paths.sessionFile)) {
|
||||||
|
await fsp.copyFile(paths.sessionFile, sessionBackupPath(paths.sessionFile)).catch(() => {});
|
||||||
|
}
|
||||||
const tempPath = sessionTempPath(paths.sessionFile, "async");
|
const tempPath = sessionTempPath(paths.sessionFile, "async");
|
||||||
await fsp.writeFile(tempPath, payload, "utf8");
|
await fsp.writeFile(tempPath, payload, "utf8");
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -304,6 +304,58 @@ describe("settings storage", () => {
|
|||||||
expect(loaded.packageOrder).toEqual(empty.packageOrder);
|
expect(loaded.packageOrder).toEqual(empty.packageOrder);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("loads backup session when primary session is corrupted", () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const paths = createStoragePaths(dir);
|
||||||
|
|
||||||
|
const backupSession = emptySession();
|
||||||
|
backupSession.packageOrder = ["pkg-backup"];
|
||||||
|
backupSession.packages["pkg-backup"] = {
|
||||||
|
id: "pkg-backup",
|
||||||
|
name: "Backup Package",
|
||||||
|
outputDir: path.join(dir, "out"),
|
||||||
|
extractDir: path.join(dir, "extract"),
|
||||||
|
status: "queued",
|
||||||
|
itemIds: ["item-backup"],
|
||||||
|
cancelled: false,
|
||||||
|
enabled: true,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now()
|
||||||
|
};
|
||||||
|
backupSession.items["item-backup"] = {
|
||||||
|
id: "item-backup",
|
||||||
|
packageId: "pkg-backup",
|
||||||
|
url: "https://example.com/backup-file",
|
||||||
|
provider: null,
|
||||||
|
status: "queued",
|
||||||
|
retries: 0,
|
||||||
|
speedBps: 0,
|
||||||
|
downloadedBytes: 0,
|
||||||
|
totalBytes: null,
|
||||||
|
progressPercent: 0,
|
||||||
|
fileName: "backup-file.rar",
|
||||||
|
targetPath: path.join(dir, "out", "backup-file.rar"),
|
||||||
|
resumable: true,
|
||||||
|
attempts: 0,
|
||||||
|
lastError: "",
|
||||||
|
fullStatus: "Wartet",
|
||||||
|
createdAt: Date.now(),
|
||||||
|
updatedAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(`${paths.sessionFile}.bak`, JSON.stringify(backupSession), "utf8");
|
||||||
|
fs.writeFileSync(paths.sessionFile, "{broken-session-json", "utf8");
|
||||||
|
|
||||||
|
const loaded = loadSession(paths);
|
||||||
|
expect(loaded.packageOrder).toEqual(["pkg-backup"]);
|
||||||
|
expect(loaded.packages["pkg-backup"]?.name).toBe("Backup Package");
|
||||||
|
expect(loaded.items["item-backup"]?.fileName).toBe("backup-file.rar");
|
||||||
|
|
||||||
|
const restoredPrimary = JSON.parse(fs.readFileSync(paths.sessionFile, "utf8")) as { packages?: Record<string, unknown> };
|
||||||
|
expect(restoredPrimary.packages && "pkg-backup" in restoredPrimary.packages).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it("returns defaults when config file contains invalid JSON", () => {
|
it("returns defaults when config file contains invalid JSON", () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
tempDirs.push(dir);
|
tempDirs.push(dir);
|
||||||
@ -395,6 +447,32 @@ describe("settings storage", () => {
|
|||||||
expect(persisted.summaryText).toBe("before-mutation");
|
expect(persisted.summaryText).toBe("before-mutation");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("creates session backup before sync and async session overwrites", async () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const paths = createStoragePaths(dir);
|
||||||
|
|
||||||
|
const first = emptySession();
|
||||||
|
first.summaryText = "first";
|
||||||
|
saveSession(paths, first);
|
||||||
|
|
||||||
|
const second = emptySession();
|
||||||
|
second.summaryText = "second";
|
||||||
|
saveSession(paths, second);
|
||||||
|
|
||||||
|
const backupAfterSync = JSON.parse(fs.readFileSync(`${paths.sessionFile}.bak`, "utf8")) as { summaryText?: string };
|
||||||
|
expect(backupAfterSync.summaryText).toBe("first");
|
||||||
|
|
||||||
|
const third = emptySession();
|
||||||
|
third.summaryText = "third";
|
||||||
|
await saveSessionAsync(paths, third);
|
||||||
|
|
||||||
|
const backupAfterAsync = JSON.parse(fs.readFileSync(`${paths.sessionFile}.bak`, "utf8")) as { summaryText?: string };
|
||||||
|
const primaryAfterAsync = JSON.parse(fs.readFileSync(paths.sessionFile, "utf8")) as { summaryText?: string };
|
||||||
|
expect(backupAfterAsync.summaryText).toBe("second");
|
||||||
|
expect(primaryAfterAsync.summaryText).toBe("third");
|
||||||
|
});
|
||||||
|
|
||||||
it("applies defaults for missing fields when loading old config", () => {
|
it("applies defaults for missing fields when loading old config", () => {
|
||||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-"));
|
||||||
tempDirs.push(dir);
|
tempDirs.push(dir);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user