Add daily log rotation with monthly folder structure
All log output is now additionally written to daily log files: daily-logs/YYYY-MM/YYYY-MM-DD.log (main log) daily-logs/YYYY-MM/YYYY-MM-DD-rename.log (rename log) Automatic cleanup of daily logs older than 30 days. The existing rd_downloader.log and rename.log continue to work as before. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1c78bb61c6
commit
d7149829ea
@ -24,26 +24,27 @@ import { APP_VERSION } from "./constants";
|
|||||||
import { DownloadManager } from "./download-manager";
|
import { DownloadManager } from "./download-manager";
|
||||||
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
||||||
import { parseCollectorInput } from "./link-parser";
|
import { parseCollectorInput } from "./link-parser";
|
||||||
import { configureLogger, getLogFilePath, logger } from "./logger";
|
import { configureLogger, getLogFilePath, logger } from "./logger";
|
||||||
import { AllDebridWebFallback } from "./all-debrid-web";
|
import { AllDebridWebFallback } from "./all-debrid-web";
|
||||||
import { BestDebridWebFallback } from "./bestdebrid-web";
|
import { BestDebridWebFallback } from "./bestdebrid-web";
|
||||||
import { RealDebridWebFallback } from "./realdebrid-web";
|
import { RealDebridWebFallback } from "./realdebrid-web";
|
||||||
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log";
|
import { getItemLogPath, initItemLogs, shutdownItemLogs } from "./item-log";
|
||||||
import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log";
|
import { getPackageLogPath, initPackageLogs, shutdownPackageLogs } from "./package-log";
|
||||||
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
||||||
import { MegaWebFallback } from "./mega-web-fallback";
|
import { MegaWebFallback } from "./mega-web-fallback";
|
||||||
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
|
import { addHistoryEntry, addHistoryEntryForRetention, cancelPendingAsyncSaves, clearHistory, createStoragePaths, loadHistoryForRetention, loadSession, loadSettings, normalizeHistoryEntry, normalizeLoadedSession, normalizeLoadedSessionTransientFields, normalizeSettings, removeHistoryEntry, resetHistoryForRetention, saveHistory, saveSession, saveSettings } from "./storage";
|
||||||
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
import { abortActiveUpdateDownload, checkGitHubUpdate, installLatestUpdate } from "./update";
|
||||||
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
import { rotateDebugToken, startDebugServer, stopDebugServer } from "./debug-server";
|
||||||
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
import { encryptBackup, decryptBackup } from "./backup-crypto";
|
||||||
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
import { getAuditLogPath, initAuditLog, logAuditEvent, shutdownAuditLog } from "./audit-log";
|
||||||
import { getDebugSetupCheck } from "./debug-setup";
|
import { getDebugSetupCheck } from "./debug-setup";
|
||||||
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
|
import { buildLinkExportSelection, serializeLinkExportText } from "./link-export";
|
||||||
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log";
|
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "./rename-log";
|
||||||
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
import { initDailyLog, shutdownDailyLog } from "./daily-log";
|
||||||
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
import { buildAccountSummary, diffAccountSummary } from "./support-data";
|
||||||
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
|
import { buildSupportBundle, getSupportBundleDefaultFileName } from "./support-bundle";
|
||||||
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
import { getTraceConfig, getTraceLogPath, initTraceLog, logTraceEvent, setTraceEnabled, shutdownTraceLog } from "./trace-log";
|
||||||
|
import type { DebugSetupCheckResult, SupportTraceConfig } from "../shared/types";
|
||||||
|
|
||||||
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettings> {
|
||||||
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
const entries = Object.entries(partial || {}).filter(([, value]) => value !== undefined);
|
||||||
@ -73,22 +74,23 @@ export class AppController {
|
|||||||
|
|
||||||
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
|
private storagePaths = createStoragePaths(path.join(app.getPath("userData"), "runtime"));
|
||||||
|
|
||||||
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
|
private onStateHandler: ((snapshot: UiSnapshot) => void) | null = null;
|
||||||
|
|
||||||
private autoResumePending = false;
|
|
||||||
private runtimeStatsTimer: NodeJS.Timeout | null = null;
|
|
||||||
|
|
||||||
public constructor() {
|
private autoResumePending = false;
|
||||||
configureLogger(this.storagePaths.baseDir);
|
private runtimeStatsTimer: NodeJS.Timeout | null = null;
|
||||||
initSessionLog(this.storagePaths.baseDir);
|
|
||||||
initPackageLogs(this.storagePaths.baseDir);
|
public constructor() {
|
||||||
initItemLogs(this.storagePaths.baseDir);
|
configureLogger(this.storagePaths.baseDir);
|
||||||
initAuditLog(this.storagePaths.baseDir);
|
initSessionLog(this.storagePaths.baseDir);
|
||||||
initRenameLog(this.storagePaths.baseDir);
|
initPackageLogs(this.storagePaths.baseDir);
|
||||||
initTraceLog(this.storagePaths.baseDir);
|
initItemLogs(this.storagePaths.baseDir);
|
||||||
this.settings = loadSettings(this.storagePaths);
|
initAuditLog(this.storagePaths.baseDir);
|
||||||
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
initRenameLog(this.storagePaths.baseDir);
|
||||||
const session = loadSession(this.storagePaths);
|
initDailyLog(this.storagePaths.baseDir);
|
||||||
|
initTraceLog(this.storagePaths.baseDir);
|
||||||
|
this.settings = loadSettings(this.storagePaths);
|
||||||
|
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
|
const session = loadSession(this.storagePaths);
|
||||||
this.megaWebFallback = new MegaWebFallback(() => ({
|
this.megaWebFallback = new MegaWebFallback(() => ({
|
||||||
login: this.settings.megaLogin,
|
login: this.settings.megaLogin,
|
||||||
password: this.settings.megaPassword
|
password: this.settings.megaPassword
|
||||||
@ -98,31 +100,31 @@ export class AppController {
|
|||||||
this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken);
|
this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken);
|
||||||
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
|
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
|
||||||
megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal),
|
megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal),
|
||||||
allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal),
|
allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal),
|
||||||
realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal),
|
realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal),
|
||||||
bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal),
|
bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal),
|
||||||
invalidateMegaSession: () => this.megaWebFallback.invalidateSession(),
|
invalidateMegaSession: () => this.megaWebFallback.invalidateSession(),
|
||||||
onHistoryEntry: (entry: HistoryEntry) => {
|
onHistoryEntry: (entry: HistoryEntry) => {
|
||||||
addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry);
|
addHistoryEntryForRetention(this.storagePaths, this.settings.historyRetentionMode, entry);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.manager.on("state", (snapshot: UiSnapshot) => {
|
this.manager.on("state", (snapshot: UiSnapshot) => {
|
||||||
this.onStateHandler?.(snapshot);
|
this.onStateHandler?.(snapshot);
|
||||||
});
|
});
|
||||||
logger.info(`App gestartet v${APP_VERSION}`);
|
logger.info(`App gestartet v${APP_VERSION}`);
|
||||||
logger.info(`Log-Datei: ${getLogFilePath()}`);
|
logger.info(`Log-Datei: ${getLogFilePath()}`);
|
||||||
logAuditEvent("INFO", "App gestartet", {
|
logAuditEvent("INFO", "App gestartet", {
|
||||||
appVersion: APP_VERSION,
|
appVersion: APP_VERSION,
|
||||||
runtimeDir: this.storagePaths.baseDir
|
runtimeDir: this.storagePaths.baseDir
|
||||||
});
|
});
|
||||||
startDebugServer(this.manager, this.storagePaths.baseDir);
|
startDebugServer(this.manager, this.storagePaths.baseDir);
|
||||||
this.runtimeStatsTimer = setInterval(() => {
|
this.runtimeStatsTimer = setInterval(() => {
|
||||||
this.manager.persistRuntimeStats();
|
this.manager.persistRuntimeStats();
|
||||||
this.settings = this.manager.getSettings();
|
this.settings = this.manager.getSettings();
|
||||||
}, 60_000);
|
}, 60_000);
|
||||||
this.runtimeStatsTimer.unref?.();
|
this.runtimeStatsTimer.unref?.();
|
||||||
|
|
||||||
if (this.settings.autoResumeOnStart) {
|
if (this.settings.autoResumeOnStart) {
|
||||||
const snapshot = this.manager.getSnapshot();
|
const snapshot = this.manager.getSnapshot();
|
||||||
const hasPending = Object.values(snapshot.session.items).some((item) => item.status === "queued" || item.status === "reconnect_wait");
|
const hasPending = Object.values(snapshot.session.items).some((item) => item.status === "queued" || item.status === "reconnect_wait");
|
||||||
if (hasPending) {
|
if (hasPending) {
|
||||||
@ -187,46 +189,46 @@ export class AppController {
|
|||||||
return APP_VERSION;
|
return APP_VERSION;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSettings(): AppSettings {
|
public getSettings(): AppSettings {
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAuditLogPath(): string | null {
|
public getAuditLogPath(): string | null {
|
||||||
return getAuditLogPath();
|
return getAuditLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRenameLogPath(): string | null {
|
public getRenameLogPath(): string | null {
|
||||||
return getRenameLogPath();
|
return getRenameLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTraceLogPath(): string | null {
|
public getTraceLogPath(): string | null {
|
||||||
return getTraceLogPath();
|
return getTraceLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTraceConfig(): SupportTraceConfig {
|
public getTraceConfig(): SupportTraceConfig {
|
||||||
return getTraceConfig();
|
return getTraceConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public rotateDebugToken(): { path: string; token: string } {
|
public rotateDebugToken(): { path: string; token: string } {
|
||||||
const rotated = rotateDebugToken(this.storagePaths.baseDir);
|
const rotated = rotateDebugToken(this.storagePaths.baseDir);
|
||||||
this.audit("WARN", "Debug-Token rotiert", { path: rotated.path });
|
this.audit("WARN", "Debug-Token rotiert", { path: rotated.path });
|
||||||
return rotated;
|
return rotated;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getDebugSetupCheck(): DebugSetupCheckResult {
|
public getDebugSetupCheck(): DebugSetupCheckResult {
|
||||||
return getDebugSetupCheck(this.storagePaths.baseDir);
|
return getDebugSetupCheck(this.storagePaths.baseDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
private audit(level: "INFO" | "WARN" | "ERROR", message: string, fields?: Record<string, unknown>): void {
|
||||||
logAuditEvent(level, message, fields);
|
logAuditEvent(level, message, fields);
|
||||||
logTraceEvent(level, "audit", message, fields);
|
logTraceEvent(level, "audit", message, fields);
|
||||||
}
|
}
|
||||||
|
|
||||||
public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig {
|
public setTraceEnabled(enabled: boolean, note = "", durationMs?: number): SupportTraceConfig {
|
||||||
const next = setTraceEnabled(enabled, note, durationMs);
|
const next = setTraceEnabled(enabled, note, durationMs);
|
||||||
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
this.audit("INFO", enabled ? "Support-Trace aktiviert" : "Support-Trace deaktiviert", { note });
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
public updateSettings(partial: Partial<AppSettings>): AppSettings {
|
public updateSettings(partial: Partial<AppSettings>): AppSettings {
|
||||||
const sanitizedPatch = sanitizeSettingsPatch(partial);
|
const sanitizedPatch = sanitizeSettingsPatch(partial);
|
||||||
@ -240,32 +242,32 @@ export class AppController {
|
|||||||
return previousSettings;
|
return previousSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preserve the live all-time counters from the download manager
|
// Preserve the live all-time counters from the download manager
|
||||||
const liveSettings = this.manager.getSettings();
|
const liveSettings = this.manager.getSettings();
|
||||||
nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
nextSettings.totalDownloadedAllTime = Math.max(nextSettings.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
||||||
nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
nextSettings.totalCompletedFilesAllTime = Math.max(nextSettings.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
||||||
nextSettings.totalRuntimeAllTimeMs = Math.max(nextSettings.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs());
|
nextSettings.totalRuntimeAllTimeMs = Math.max(nextSettings.totalRuntimeAllTimeMs || 0, this.manager.getLiveTotalRuntimeMs());
|
||||||
nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
nextSettings.providerDailyUsageDay = liveSettings.providerDailyUsageDay;
|
||||||
nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
nextSettings.providerDailyUsageBytes = { ...(liveSettings.providerDailyUsageBytes || {}) };
|
||||||
nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
nextSettings.providerTotalUsageBytes = { ...(liveSettings.providerTotalUsageBytes || {}) };
|
||||||
nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
|
nextSettings.debridLinkApiKeyDailyUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
|
Object.entries(liveSettings.debridLinkApiKeyDailyUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
|
||||||
);
|
);
|
||||||
nextSettings.debridLinkApiKeyTotalUsageBytes = Object.fromEntries(
|
nextSettings.debridLinkApiKeyTotalUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
|
Object.entries(liveSettings.debridLinkApiKeyTotalUsageBytes || {}).filter(([keyId]) => getDebridLinkApiKeyIds(nextSettings.debridLinkApiKeys).includes(keyId))
|
||||||
);
|
);
|
||||||
const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode;
|
const retentionChanged = previousSettings.historyRetentionMode !== nextSettings.historyRetentionMode;
|
||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
if (retentionChanged) {
|
if (retentionChanged) {
|
||||||
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
}
|
}
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Einstellungen aktualisiert", {
|
this.audit("INFO", "Einstellungen aktualisiert", {
|
||||||
changedKeys: Object.keys(sanitizedPatch),
|
changedKeys: Object.keys(sanitizedPatch),
|
||||||
accountChanges: diffAccountSummary(previousSettings, this.settings)
|
accountChanges: diffAccountSummary(previousSettings, this.settings)
|
||||||
});
|
});
|
||||||
if (previousSettings.rememberToken && !this.settings.rememberToken) {
|
if (previousSettings.rememberToken && !this.settings.rememberToken) {
|
||||||
void this.realDebridWebFallback.clearSessions().catch((error) => {
|
void this.realDebridWebFallback.clearSessions().catch((error) => {
|
||||||
logger.warn(`Real-Debrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
logger.warn(`Real-Debrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
||||||
});
|
});
|
||||||
@ -285,12 +287,12 @@ export class AppController {
|
|||||||
...liveSettings,
|
...liveSettings,
|
||||||
...resetProviderDailyUsage(liveSettings, provider)
|
...resetProviderDailyUsage(liveSettings, provider)
|
||||||
});
|
});
|
||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider });
|
this.audit("INFO", "Provider-Tagesnutzung zurückgesetzt", { provider });
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings {
|
public resetDebridLinkApiKeyDailyUsage(keyId: string): AppSettings {
|
||||||
const liveSettings = this.manager.getSettings();
|
const liveSettings = this.manager.getSettings();
|
||||||
@ -298,31 +300,31 @@ export class AppController {
|
|||||||
...liveSettings,
|
...liveSettings,
|
||||||
...resetDebridLinkApiKeyDailyUsage(liveSettings, keyId)
|
...resetDebridLinkApiKeyDailyUsage(liveSettings, keyId)
|
||||||
});
|
});
|
||||||
this.settings = nextSettings;
|
this.settings = nextSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
this.manager.setSettings(this.settings);
|
this.manager.setSettings(this.settings);
|
||||||
this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId });
|
this.audit("INFO", "Debrid-Link-Key-Tagesnutzung zurückgesetzt", { keyId });
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openRealDebridLoginWindow(): Promise<void> {
|
public async openRealDebridLoginWindow(): Promise<void> {
|
||||||
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
|
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
|
||||||
await this.realDebridWebFallback.openLoginWindow();
|
await this.realDebridWebFallback.openLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openAllDebridLoginWindow(): Promise<void> {
|
public async openAllDebridLoginWindow(): Promise<void> {
|
||||||
this.audit("INFO", "AllDebrid Login-Fenster geöffnet");
|
this.audit("INFO", "AllDebrid Login-Fenster geöffnet");
|
||||||
await this.allDebridWebFallback.openLoginWindow();
|
await this.allDebridWebFallback.openLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async importBestDebridCookies(filePath: string): Promise<number> {
|
public async importBestDebridCookies(filePath: string): Promise<number> {
|
||||||
const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
const imported = await this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
||||||
this.audit("INFO", "BestDebrid Cookies importiert", {
|
this.audit("INFO", "BestDebrid Cookies importiert", {
|
||||||
filePath,
|
filePath,
|
||||||
imported
|
imported
|
||||||
});
|
});
|
||||||
return imported;
|
return imported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
||||||
if (this.settings.allDebridUseWebLogin) {
|
if (this.settings.allDebridUseWebLogin) {
|
||||||
@ -367,38 +369,38 @@ export class AppController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } {
|
public addLinks(payload: AddLinksPayload): { addedPackages: number; addedLinks: number; invalidCount: number } {
|
||||||
const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName);
|
const parsed = parseCollectorInput(payload.rawText, payload.packageName || this.settings.packageName);
|
||||||
if (parsed.length === 0) {
|
if (parsed.length === 0) {
|
||||||
this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", {
|
this.audit("WARN", "Links hinzufügen ohne gültigen Inhalt", {
|
||||||
hasPackageName: Boolean(payload.packageName)
|
hasPackageName: Boolean(payload.packageName)
|
||||||
});
|
});
|
||||||
return { addedPackages: 0, addedLinks: 0, invalidCount: 1 };
|
return { addedPackages: 0, addedLinks: 0, invalidCount: 1 };
|
||||||
}
|
}
|
||||||
const result = this.manager.addPackages(parsed);
|
const result = this.manager.addPackages(parsed);
|
||||||
this.audit("INFO", "Links hinzugefügt", {
|
this.audit("INFO", "Links hinzugefügt", {
|
||||||
addedPackages: result.addedPackages,
|
addedPackages: result.addedPackages,
|
||||||
addedLinks: result.addedLinks,
|
addedLinks: result.addedLinks,
|
||||||
requestedPackages: parsed.length
|
requestedPackages: parsed.length
|
||||||
});
|
});
|
||||||
return { ...result, invalidCount: 0 };
|
return { ...result, invalidCount: 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
public async addContainers(filePaths: string[]): Promise<{ addedPackages: number; addedLinks: number }> {
|
public async addContainers(filePaths: string[]): Promise<{ addedPackages: number; addedLinks: number }> {
|
||||||
const packages = await importDlcContainers(filePaths);
|
const packages = await importDlcContainers(filePaths);
|
||||||
const merged: ParsedPackageInput[] = packages.map((pkg) => ({
|
const merged: ParsedPackageInput[] = packages.map((pkg) => ({
|
||||||
name: pkg.name,
|
name: pkg.name,
|
||||||
links: pkg.links,
|
links: pkg.links,
|
||||||
...(pkg.fileNames ? { fileNames: pkg.fileNames } : {})
|
...(pkg.fileNames ? { fileNames: pkg.fileNames } : {})
|
||||||
}));
|
}));
|
||||||
const result = this.manager.addPackages(merged);
|
const result = this.manager.addPackages(merged);
|
||||||
this.audit("INFO", "Container importiert", {
|
this.audit("INFO", "Container importiert", {
|
||||||
files: filePaths.length,
|
files: filePaths.length,
|
||||||
addedPackages: result.addedPackages,
|
addedPackages: result.addedPackages,
|
||||||
addedLinks: result.addedLinks
|
addedLinks: result.addedLinks
|
||||||
});
|
});
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getStartConflicts(): Promise<StartConflictEntry[]> {
|
public async getStartConflicts(): Promise<StartConflictEntry[]> {
|
||||||
return this.manager.getStartConflicts();
|
return this.manager.getStartConflicts();
|
||||||
@ -408,163 +410,163 @@ export class AppController {
|
|||||||
return this.manager.resolveStartConflict(packageId, policy);
|
return this.manager.resolveStartConflict(packageId, policy);
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearAll(): void {
|
public clearAll(): void {
|
||||||
this.audit("WARN", "Queue komplett geleert");
|
this.audit("WARN", "Queue komplett geleert");
|
||||||
this.manager.clearAll();
|
this.manager.clearAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start(): Promise<void> {
|
|
||||||
this.audit("INFO", "Session-Start ausgelöst");
|
|
||||||
await this.manager.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async startPackages(packageIds: string[]): Promise<void> {
|
|
||||||
this.audit("INFO", "Paket-Start ausgelöst", { packageIds });
|
|
||||||
await this.manager.startPackages(packageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async startItems(itemIds: string[]): Promise<void> {
|
|
||||||
this.audit("INFO", "Item-Start ausgelöst", { itemIds });
|
|
||||||
await this.manager.startItems(itemIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public stop(): void {
|
|
||||||
this.audit("INFO", "Session-Stopp ausgelöst");
|
|
||||||
this.manager.stop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public togglePause(): boolean {
|
|
||||||
const paused = this.manager.togglePause();
|
|
||||||
this.audit("INFO", "Pause umgeschaltet", { paused });
|
|
||||||
return paused;
|
|
||||||
}
|
|
||||||
|
|
||||||
public retryExtraction(packageId: string): void {
|
|
||||||
this.audit("INFO", "Extraktion manuell wiederholt", { packageId });
|
|
||||||
this.manager.retryExtraction(packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public extractNow(packageId: string): void {
|
|
||||||
this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId });
|
|
||||||
this.manager.extractNow(packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetPackage(packageId: string): void {
|
|
||||||
this.audit("INFO", "Paket zurückgesetzt", { packageId });
|
|
||||||
this.manager.resetPackage(packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public cancelPackage(packageId: string): void {
|
|
||||||
this.audit("WARN", "Paket abgebrochen", { packageId });
|
|
||||||
this.manager.cancelPackage(packageId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public renamePackage(packageId: string, newName: string): void {
|
|
||||||
this.audit("INFO", "Paket umbenannt", { packageId, newName });
|
|
||||||
this.manager.renamePackage(packageId, newName);
|
|
||||||
}
|
|
||||||
|
|
||||||
public reorderPackages(packageIds: string[]): void {
|
|
||||||
this.audit("INFO", "Paketreihenfolge geändert", { packageIds });
|
|
||||||
this.manager.reorderPackages(packageIds);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeItem(itemId: string): void {
|
|
||||||
this.audit("WARN", "Item entfernt", { itemId });
|
|
||||||
this.manager.removeItem(itemId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public togglePackage(packageId: string): void {
|
public async start(): Promise<void> {
|
||||||
this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId });
|
this.audit("INFO", "Session-Start ausgelöst");
|
||||||
this.manager.togglePackage(packageId);
|
await this.manager.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
public exportPackageSelection(packageIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } {
|
|
||||||
const selection = buildLinkExportSelection(this.manager.getSnapshot(), packageIds, []);
|
|
||||||
this.audit("INFO", "Paket-Auswahl exportiert", {
|
|
||||||
packageCount: selection.packageCount,
|
|
||||||
linkCount: selection.linkCount,
|
|
||||||
packageIds
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
text: serializeLinkExportText(selection.packages),
|
|
||||||
defaultFileName: selection.defaultFileName,
|
|
||||||
packageCount: selection.packageCount,
|
|
||||||
linkCount: selection.linkCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public exportItemSelection(itemIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } {
|
|
||||||
const selection = buildLinkExportSelection(this.manager.getSnapshot(), [], itemIds);
|
|
||||||
this.audit("INFO", "Item-Auswahl exportiert", {
|
|
||||||
packageCount: selection.packageCount,
|
|
||||||
linkCount: selection.linkCount,
|
|
||||||
itemIds
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
text: serializeLinkExportText(selection.packages),
|
|
||||||
defaultFileName: selection.defaultFileName,
|
|
||||||
packageCount: selection.packageCount,
|
|
||||||
linkCount: selection.linkCount
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public exportQueue(): string {
|
|
||||||
return this.manager.exportQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
public importQueue(json: string): { addedPackages: number; addedLinks: number } {
|
|
||||||
const result = this.manager.importQueue(json);
|
|
||||||
this.audit("INFO", "Import-Datei verarbeitet", result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getSessionStats(): SessionStats {
|
public async startPackages(packageIds: string[]): Promise<void> {
|
||||||
return this.manager.getSessionStats();
|
this.audit("INFO", "Paket-Start ausgelöst", { packageIds });
|
||||||
}
|
await this.manager.startPackages(packageIds);
|
||||||
|
}
|
||||||
public resetSessionStats(): void {
|
|
||||||
this.audit("INFO", "Session-Statistik zurückgesetzt");
|
|
||||||
this.manager.resetSessionStats();
|
|
||||||
}
|
|
||||||
|
|
||||||
public resetDownloadStats(): void {
|
|
||||||
this.manager.resetDownloadStats();
|
|
||||||
this.settings = this.manager.getSettings();
|
|
||||||
this.audit("INFO", "Download-Statistik zurückgesetzt");
|
|
||||||
}
|
|
||||||
|
|
||||||
public exportBackup(): Buffer {
|
public async startItems(itemIds: string[]): Promise<void> {
|
||||||
const settings = { ...this.settings };
|
this.audit("INFO", "Item-Start ausgelöst", { itemIds });
|
||||||
const session = this.manager.getSession();
|
await this.manager.startItems(itemIds);
|
||||||
const history = loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
}
|
||||||
|
|
||||||
|
public stop(): void {
|
||||||
|
this.audit("INFO", "Session-Stopp ausgelöst");
|
||||||
|
this.manager.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public togglePause(): boolean {
|
||||||
|
const paused = this.manager.togglePause();
|
||||||
|
this.audit("INFO", "Pause umgeschaltet", { paused });
|
||||||
|
return paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
public retryExtraction(packageId: string): void {
|
||||||
|
this.audit("INFO", "Extraktion manuell wiederholt", { packageId });
|
||||||
|
this.manager.retryExtraction(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public extractNow(packageId: string): void {
|
||||||
|
this.audit("INFO", "Jetzt entpacken ausgelöst", { packageId });
|
||||||
|
this.manager.extractNow(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetPackage(packageId: string): void {
|
||||||
|
this.audit("INFO", "Paket zurückgesetzt", { packageId });
|
||||||
|
this.manager.resetPackage(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelPackage(packageId: string): void {
|
||||||
|
this.audit("WARN", "Paket abgebrochen", { packageId });
|
||||||
|
this.manager.cancelPackage(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public renamePackage(packageId: string, newName: string): void {
|
||||||
|
this.audit("INFO", "Paket umbenannt", { packageId, newName });
|
||||||
|
this.manager.renamePackage(packageId, newName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public reorderPackages(packageIds: string[]): void {
|
||||||
|
this.audit("INFO", "Paketreihenfolge geändert", { packageIds });
|
||||||
|
this.manager.reorderPackages(packageIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
public removeItem(itemId: string): void {
|
||||||
|
this.audit("WARN", "Item entfernt", { itemId });
|
||||||
|
this.manager.removeItem(itemId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public togglePackage(packageId: string): void {
|
||||||
|
this.audit("INFO", "Paket aktiviert/deaktiviert", { packageId });
|
||||||
|
this.manager.togglePackage(packageId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportPackageSelection(packageIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } {
|
||||||
|
const selection = buildLinkExportSelection(this.manager.getSnapshot(), packageIds, []);
|
||||||
|
this.audit("INFO", "Paket-Auswahl exportiert", {
|
||||||
|
packageCount: selection.packageCount,
|
||||||
|
linkCount: selection.linkCount,
|
||||||
|
packageIds
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
text: serializeLinkExportText(selection.packages),
|
||||||
|
defaultFileName: selection.defaultFileName,
|
||||||
|
packageCount: selection.packageCount,
|
||||||
|
linkCount: selection.linkCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportItemSelection(itemIds: string[]): { text: string; defaultFileName: string; packageCount: number; linkCount: number } {
|
||||||
|
const selection = buildLinkExportSelection(this.manager.getSnapshot(), [], itemIds);
|
||||||
|
this.audit("INFO", "Item-Auswahl exportiert", {
|
||||||
|
packageCount: selection.packageCount,
|
||||||
|
linkCount: selection.linkCount,
|
||||||
|
itemIds
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
text: serializeLinkExportText(selection.packages),
|
||||||
|
defaultFileName: selection.defaultFileName,
|
||||||
|
packageCount: selection.packageCount,
|
||||||
|
linkCount: selection.linkCount
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportQueue(): string {
|
||||||
|
return this.manager.exportQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public importQueue(json: string): { addedPackages: number; addedLinks: number } {
|
||||||
|
const result = this.manager.importQueue(json);
|
||||||
|
this.audit("INFO", "Import-Datei verarbeitet", result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getSessionStats(): SessionStats {
|
||||||
|
return this.manager.getSessionStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetSessionStats(): void {
|
||||||
|
this.audit("INFO", "Session-Statistik zurückgesetzt");
|
||||||
|
this.manager.resetSessionStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetDownloadStats(): void {
|
||||||
|
this.manager.resetDownloadStats();
|
||||||
|
this.settings = this.manager.getSettings();
|
||||||
|
this.audit("INFO", "Download-Statistik zurückgesetzt");
|
||||||
|
}
|
||||||
|
|
||||||
|
public exportBackup(): Buffer {
|
||||||
|
const settings = { ...this.settings };
|
||||||
|
const session = this.manager.getSession();
|
||||||
|
const history = loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
const payload = JSON.stringify({
|
const payload = JSON.stringify({
|
||||||
version: 2,
|
version: 2,
|
||||||
appVersion: APP_VERSION,
|
appVersion: APP_VERSION,
|
||||||
exportedAt: new Date().toISOString(),
|
exportedAt: new Date().toISOString(),
|
||||||
settings,
|
settings,
|
||||||
session,
|
session,
|
||||||
history
|
history
|
||||||
});
|
});
|
||||||
this.audit("INFO", "Backup exportiert", {
|
this.audit("INFO", "Backup exportiert", {
|
||||||
historyEntries: history.length,
|
historyEntries: history.length,
|
||||||
sessionItems: Object.keys(session.items).length,
|
sessionItems: Object.keys(session.items).length,
|
||||||
sessionPackages: Object.keys(session.packages).length
|
sessionPackages: Object.keys(session.packages).length
|
||||||
});
|
});
|
||||||
return encryptBackup(payload);
|
return encryptBackup(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } {
|
public exportSupportBundle(): { buffer: Buffer; defaultFileName: string } {
|
||||||
this.audit("INFO", "Support-Bundle exportiert");
|
this.audit("INFO", "Support-Bundle exportiert");
|
||||||
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
|
logTraceEvent("INFO", "support", "Support-Bundle erstellt", {
|
||||||
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
|
packageCount: Object.keys(this.manager.getSnapshot().session.packages).length,
|
||||||
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
|
itemCount: Object.keys(this.manager.getSnapshot().session.items).length
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir),
|
buffer: buildSupportBundle(this.manager, this.storagePaths.baseDir),
|
||||||
defaultFileName: getSupportBundleDefaultFileName()
|
defaultFileName: getSupportBundleDefaultFileName()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public importBackup(data: Buffer): { restored: boolean; message: string } {
|
public importBackup(data: Buffer): { restored: boolean; message: string } {
|
||||||
let parsed: Record<string, unknown>;
|
let parsed: Record<string, unknown>;
|
||||||
@ -586,21 +588,21 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Restore settings — ALL credentials are included (no more masking)
|
// Restore settings — ALL credentials are included (no more masking)
|
||||||
const importedSettings = parsed.settings as AppSettings;
|
const importedSettings = parsed.settings as AppSettings;
|
||||||
const importedSettingsRecord = importedSettings as unknown as Record<string, unknown>;
|
const importedSettingsRecord = importedSettings as unknown as Record<string, unknown>;
|
||||||
const currentSettingsRecord = this.settings as unknown as Record<string, unknown>;
|
const currentSettingsRecord = this.settings as unknown as Record<string, unknown>;
|
||||||
// Legacy backup compatibility: if credentials were masked with ***, keep current values
|
// Legacy backup compatibility: if credentials were masked with ***, keep current values
|
||||||
const SENSITIVE_KEYS: (keyof AppSettings)[] = [
|
const SENSITIVE_KEYS: (keyof AppSettings)[] = [
|
||||||
"token", "megaLogin", "megaPassword", "bestToken", "allDebridToken",
|
"token", "megaLogin", "megaPassword", "bestToken", "allDebridToken",
|
||||||
"ddownloadLogin", "ddownloadPassword", "oneFichierApiKey",
|
"ddownloadLogin", "ddownloadPassword", "oneFichierApiKey",
|
||||||
"debridLinkApiKeys", "linkSnappyLogin", "linkSnappyPassword"
|
"debridLinkApiKeys", "linkSnappyLogin", "linkSnappyPassword"
|
||||||
];
|
];
|
||||||
for (const key of SENSITIVE_KEYS) {
|
for (const key of SENSITIVE_KEYS) {
|
||||||
const val = importedSettingsRecord[key];
|
const val = importedSettingsRecord[key];
|
||||||
if (typeof val === "string" && val.startsWith("***")) {
|
if (typeof val === "string" && val.startsWith("***")) {
|
||||||
importedSettingsRecord[key] = currentSettingsRecord[key];
|
importedSettingsRecord[key] = currentSettingsRecord[key];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const restoredSettings = normalizeSettings(importedSettings);
|
const restoredSettings = normalizeSettings(importedSettings);
|
||||||
this.settings = restoredSettings;
|
this.settings = restoredSettings;
|
||||||
saveSettings(this.storagePaths, this.settings);
|
saveSettings(this.storagePaths, this.settings);
|
||||||
@ -629,93 +631,94 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
resetHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
|
|
||||||
// Prevent prepareForShutdown from overwriting the restored data
|
// Prevent prepareForShutdown from overwriting the restored data
|
||||||
this.manager.skipShutdownPersist = true;
|
this.manager.skipShutdownPersist = true;
|
||||||
this.manager.blockAllPersistence = true;
|
this.manager.blockAllPersistence = true;
|
||||||
logger.info("Backup wiederhergestellt (verschlüsseltes Format)");
|
logger.info("Backup wiederhergestellt (verschlüsseltes Format)");
|
||||||
this.audit("WARN", "Backup importiert", {
|
this.audit("WARN", "Backup importiert", {
|
||||||
historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0,
|
historyEntries: Array.isArray(parsed.history) ? parsed.history.length : 0,
|
||||||
accountSummary: buildAccountSummary(this.settings)
|
accountSummary: buildAccountSummary(this.settings)
|
||||||
});
|
});
|
||||||
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
|
||||||
}
|
}
|
||||||
|
|
||||||
public getSessionLogPath(): string | null {
|
public getSessionLogPath(): string | null {
|
||||||
return getSessionLogPath();
|
return getSessionLogPath();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getPackageLogPath(packageId: string): string | null {
|
public getPackageLogPath(packageId: string): string | null {
|
||||||
return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId);
|
return this.manager.getPackageLogPath(packageId) || getPackageLogPath(packageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getItemLogPath(itemId: string): string | null {
|
public getItemLogPath(itemId: string): string | null {
|
||||||
return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId);
|
return this.manager.getItemLogPath(itemId) || getItemLogPath(itemId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public shutdown(): void {
|
public shutdown(): void {
|
||||||
if (this.runtimeStatsTimer) {
|
if (this.runtimeStatsTimer) {
|
||||||
clearInterval(this.runtimeStatsTimer);
|
clearInterval(this.runtimeStatsTimer);
|
||||||
this.runtimeStatsTimer = null;
|
this.runtimeStatsTimer = null;
|
||||||
}
|
}
|
||||||
stopDebugServer();
|
stopDebugServer();
|
||||||
abortActiveUpdateDownload();
|
abortActiveUpdateDownload();
|
||||||
this.manager.prepareForShutdown();
|
this.manager.prepareForShutdown();
|
||||||
this.megaWebFallback.dispose();
|
this.megaWebFallback.dispose();
|
||||||
this.realDebridWebFallback.dispose();
|
this.realDebridWebFallback.dispose();
|
||||||
this.allDebridWebFallback.dispose();
|
this.allDebridWebFallback.dispose();
|
||||||
this.bestDebridWebFallback.dispose();
|
this.bestDebridWebFallback.dispose();
|
||||||
shutdownSessionLog();
|
shutdownSessionLog();
|
||||||
shutdownPackageLogs();
|
shutdownPackageLogs();
|
||||||
shutdownItemLogs();
|
shutdownItemLogs();
|
||||||
shutdownRenameLog();
|
shutdownRenameLog();
|
||||||
this.audit("INFO", "App beendet");
|
shutdownDailyLog();
|
||||||
shutdownTraceLog();
|
this.audit("INFO", "App beendet");
|
||||||
shutdownAuditLog();
|
shutdownTraceLog();
|
||||||
if (this.settings.historyRetentionMode === "session") {
|
shutdownAuditLog();
|
||||||
clearHistory(this.storagePaths);
|
if (this.settings.historyRetentionMode === "session") {
|
||||||
}
|
clearHistory(this.storagePaths);
|
||||||
logger.info("App beendet");
|
}
|
||||||
}
|
logger.info("App beendet");
|
||||||
|
}
|
||||||
public getHistory(): HistoryEntry[] {
|
|
||||||
return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearHistory(): void {
|
public getHistory(): HistoryEntry[] {
|
||||||
this.audit("WARN", "Verlauf geleert");
|
return loadHistoryForRetention(this.storagePaths, this.settings.historyRetentionMode);
|
||||||
clearHistory(this.storagePaths);
|
}
|
||||||
}
|
|
||||||
|
public clearHistory(): void {
|
||||||
public setPackagePriority(packageId: string, priority: PackagePriority): void {
|
this.audit("WARN", "Verlauf geleert");
|
||||||
this.audit("INFO", "Paket-Priorität geändert", { packageId, priority });
|
clearHistory(this.storagePaths);
|
||||||
this.manager.setPackagePriority(packageId, priority);
|
}
|
||||||
}
|
|
||||||
|
public setPackagePriority(packageId: string, priority: PackagePriority): void {
|
||||||
public skipItems(itemIds: string[]): void {
|
this.audit("INFO", "Paket-Priorität geändert", { packageId, priority });
|
||||||
this.audit("INFO", "Items übersprungen", { itemIds });
|
this.manager.setPackagePriority(packageId, priority);
|
||||||
this.manager.skipItems(itemIds);
|
}
|
||||||
}
|
|
||||||
|
public skipItems(itemIds: string[]): void {
|
||||||
public resetItems(itemIds: string[]): void {
|
this.audit("INFO", "Items übersprungen", { itemIds });
|
||||||
this.audit("INFO", "Items zurückgesetzt", { itemIds });
|
this.manager.skipItems(itemIds);
|
||||||
this.manager.resetItems(itemIds);
|
}
|
||||||
}
|
|
||||||
|
public resetItems(itemIds: string[]): void {
|
||||||
public removeHistoryEntry(entryId: string): void {
|
this.audit("INFO", "Items zurückgesetzt", { itemIds });
|
||||||
this.audit("INFO", "Verlaufseintrag entfernt", { entryId });
|
this.manager.resetItems(itemIds);
|
||||||
removeHistoryEntry(this.storagePaths, entryId);
|
}
|
||||||
}
|
|
||||||
|
public removeHistoryEntry(entryId: string): void {
|
||||||
public addToHistory(entry: HistoryEntry): void {
|
this.audit("INFO", "Verlaufseintrag entfernt", { entryId });
|
||||||
this.audit("INFO", "Verlaufseintrag hinzugefügt", {
|
removeHistoryEntry(this.storagePaths, entryId);
|
||||||
id: entry.id,
|
}
|
||||||
name: entry.name,
|
|
||||||
status: entry.status,
|
public addToHistory(entry: HistoryEntry): void {
|
||||||
provider: entry.provider,
|
this.audit("INFO", "Verlaufseintrag hinzugefügt", {
|
||||||
fileCount: entry.fileCount
|
id: entry.id,
|
||||||
});
|
name: entry.name,
|
||||||
addHistoryEntry(this.storagePaths, entry);
|
status: entry.status,
|
||||||
}
|
provider: entry.provider,
|
||||||
|
fileCount: entry.fileCount
|
||||||
|
});
|
||||||
|
addHistoryEntry(this.storagePaths, entry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
193
src/main/daily-log.ts
Normal file
193
src/main/daily-log.ts
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { addLogListener, removeLogListener } from "./logger";
|
||||||
|
|
||||||
|
const DAILY_LOG_RETENTION_DAYS = 30;
|
||||||
|
const CLEANUP_CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000; // every 6 hours
|
||||||
|
|
||||||
|
let dailyLogDir = "";
|
||||||
|
let currentDayKey = "";
|
||||||
|
let currentLogFd: number | null = null;
|
||||||
|
let currentRenameFd: number | null = null;
|
||||||
|
let logListener: ((line: string) => void) | null = null;
|
||||||
|
let cleanupTimer: NodeJS.Timeout | null = null;
|
||||||
|
let lastCleanupAt = 0;
|
||||||
|
|
||||||
|
function getDayKey(now = new Date()): string {
|
||||||
|
const year = now.getFullYear();
|
||||||
|
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||||
|
const day = String(now.getDate()).padStart(2, "0");
|
||||||
|
return `${year}-${month}-${day}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMonthDir(dayKey: string): string {
|
||||||
|
return dayKey.slice(0, 7); // "YYYY-MM"
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureDayFile(dayKey: string): number | null {
|
||||||
|
if (currentDayKey === dayKey && currentLogFd !== null) {
|
||||||
|
return currentLogFd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close previous day's fd
|
||||||
|
if (currentLogFd !== null) {
|
||||||
|
try { fs.closeSync(currentLogFd); } catch { /* ignore */ }
|
||||||
|
currentLogFd = null;
|
||||||
|
}
|
||||||
|
if (currentRenameFd !== null) {
|
||||||
|
try { fs.closeSync(currentRenameFd); } catch { /* ignore */ }
|
||||||
|
currentRenameFd = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDayKey = dayKey;
|
||||||
|
const monthDir = path.join(dailyLogDir, getMonthDir(dayKey));
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(monthDir, { recursive: true });
|
||||||
|
const filePath = path.join(monthDir, `${dayKey}.log`);
|
||||||
|
currentLogFd = fs.openSync(filePath, "a");
|
||||||
|
return currentLogFd;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureRenameFd(dayKey: string): number | null {
|
||||||
|
if (currentDayKey === dayKey && currentRenameFd !== null) {
|
||||||
|
return currentRenameFd;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDayFile handles day transitions
|
||||||
|
if (currentDayKey !== dayKey) {
|
||||||
|
ensureDayFile(dayKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentRenameFd !== null) {
|
||||||
|
return currentRenameFd;
|
||||||
|
}
|
||||||
|
|
||||||
|
const monthDir = path.join(dailyLogDir, getMonthDir(dayKey));
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(monthDir, { recursive: true });
|
||||||
|
const filePath = path.join(monthDir, `${dayKey}-rename.log`);
|
||||||
|
currentRenameFd = fs.openSync(filePath, "a");
|
||||||
|
return currentRenameFd;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeToDailyLog(line: string): void {
|
||||||
|
if (!dailyLogDir) return;
|
||||||
|
|
||||||
|
const dayKey = getDayKey();
|
||||||
|
const fd = ensureDayFile(dayKey);
|
||||||
|
if (fd === null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeSync(fd, line);
|
||||||
|
} catch {
|
||||||
|
// Close and retry on next write
|
||||||
|
try { fs.closeSync(fd); } catch { /* ignore */ }
|
||||||
|
currentLogFd = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function writeToDailyRenameLog(line: string): void {
|
||||||
|
if (!dailyLogDir) return;
|
||||||
|
|
||||||
|
const dayKey = getDayKey();
|
||||||
|
const fd = ensureRenameFd(dayKey);
|
||||||
|
if (fd === null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.writeSync(fd, line);
|
||||||
|
} catch {
|
||||||
|
try { fs.closeSync(fd); } catch { /* ignore */ }
|
||||||
|
currentRenameFd = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function cleanupOldDailyLogs(): void {
|
||||||
|
if (!dailyLogDir) return;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
if (now - lastCleanupAt < CLEANUP_CHECK_INTERVAL_MS) return;
|
||||||
|
lastCleanupAt = now;
|
||||||
|
|
||||||
|
const cutoffMs = now - DAILY_LOG_RETENTION_DAYS * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const monthDirs = fs.readdirSync(dailyLogDir, { withFileTypes: true })
|
||||||
|
.filter((e) => e.isDirectory() && /^\d{4}-\d{2}$/.test(e.name));
|
||||||
|
|
||||||
|
for (const monthDir of monthDirs) {
|
||||||
|
const monthPath = path.join(dailyLogDir, monthDir.name);
|
||||||
|
const files = fs.readdirSync(monthPath, { withFileTypes: true })
|
||||||
|
.filter((e) => e.isFile() && /^\d{4}-\d{2}-\d{2}/.test(e.name));
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(monthPath, file.name);
|
||||||
|
try {
|
||||||
|
const stat = fs.statSync(filePath);
|
||||||
|
if (stat.mtimeMs < cutoffMs) {
|
||||||
|
fs.rmSync(filePath, { force: true });
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove empty month dirs
|
||||||
|
try {
|
||||||
|
const remaining = fs.readdirSync(monthPath);
|
||||||
|
if (remaining.length === 0) {
|
||||||
|
fs.rmdirSync(monthPath);
|
||||||
|
}
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore cleanup errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initDailyLog(baseDir: string): void {
|
||||||
|
dailyLogDir = path.join(baseDir, "daily-logs");
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(dailyLogDir, { recursive: true });
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
|
||||||
|
// Attach listener to main logger
|
||||||
|
logListener = (line: string) => writeToDailyLog(line);
|
||||||
|
addLogListener(logListener);
|
||||||
|
|
||||||
|
// Initial cleanup
|
||||||
|
cleanupOldDailyLogs();
|
||||||
|
|
||||||
|
// Periodic cleanup
|
||||||
|
cleanupTimer = setInterval(cleanupOldDailyLogs, CLEANUP_CHECK_INTERVAL_MS);
|
||||||
|
if (cleanupTimer.unref) cleanupTimer.unref();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shutdownDailyLog(): void {
|
||||||
|
if (logListener) {
|
||||||
|
removeLogListener(logListener);
|
||||||
|
logListener = null;
|
||||||
|
}
|
||||||
|
if (cleanupTimer) {
|
||||||
|
clearInterval(cleanupTimer);
|
||||||
|
cleanupTimer = null;
|
||||||
|
}
|
||||||
|
if (currentLogFd !== null) {
|
||||||
|
try { fs.closeSync(currentLogFd); } catch { /* ignore */ }
|
||||||
|
currentLogFd = null;
|
||||||
|
}
|
||||||
|
if (currentRenameFd !== null) {
|
||||||
|
try { fs.closeSync(currentRenameFd); } catch { /* ignore */ }
|
||||||
|
currentRenameFd = null;
|
||||||
|
}
|
||||||
|
currentDayKey = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDailyLogDir(): string {
|
||||||
|
return dailyLogDir;
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { writeToDailyRenameLog } from "./daily-log";
|
||||||
|
|
||||||
type RenameLogLevel = "INFO" | "WARN" | "ERROR";
|
type RenameLogLevel = "INFO" | "WARN" | "ERROR";
|
||||||
|
|
||||||
@ -93,11 +94,9 @@ export function logRenameEvent(level: RenameLogLevel, message: string, fields?:
|
|||||||
if (!fs.existsSync(renameLogPath)) {
|
if (!fs.existsSync(renameLogPath)) {
|
||||||
fs.writeFileSync(renameLogPath, "", "utf8");
|
fs.writeFileSync(renameLogPath, "", "utf8");
|
||||||
}
|
}
|
||||||
fs.appendFileSync(
|
const line = `${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`;
|
||||||
renameLogPath,
|
fs.appendFileSync(renameLogPath, line, "utf8");
|
||||||
`${new Date().toISOString()} [${level}] ${message}${formatFields(fields)}\n`,
|
writeToDailyRenameLog(line);
|
||||||
"utf8"
|
|
||||||
);
|
|
||||||
} catch {
|
} catch {
|
||||||
// ignore write errors
|
// ignore write errors
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user