{"record":{"id":"ef6c0cb014275681","repo":"Kareadita/Kavita","slug":"failed-to-create-database-backup-at-backuppath","errorCode":null,"errorMessage":"Failed to create database backup at {backupPath}","messagePattern":"Failed to create database backup at (.+?)","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"critical","filePath":"Kavita.Services/BackupService.cs","lineNumber":167,"sourceCode":"        if (backupPath.Contains('\\''))\n        {\n            throw new ArgumentException(\"Backup path contains invalid characters\", nameof(tempDirectory));\n        }\n\n        try\n        {\n            // Use VACUUM INTO to create a safe backup of the database while it's running\n            // This creates a consistent snapshot without locking the main database\n            // Note: VACUUM INTO requires a literal path and cannot use SQL parameters\n            #pragma warning disable EF1002 // The backup path is validated above to not contain SQL injection characters\n            await unitOfWork.DataContext.Database.ExecuteSqlRawAsync($\"VACUUM INTO '{backupPath}'\");\n            #pragma warning restore EF1002\n            logger.LogDebug(\"Database backup created successfully at {BackupPath}\", backupPath);\n        }\n        catch (Exception ex)\n        {\n            logger.LogError(ex, \"Failed to create database backup using VACUUM INTO at {BackupPath}\", backupPath);\n            throw new InvalidOperationException($\"Failed to create database backup at {backupPath}\", ex);\n        }\n    }\n\n    private void CopyFaviconsToBackupDirectory(string tempDirectory)\n    {\n        directoryService.CopyDirectoryToDirectory(directoryService.FaviconDirectory, directoryService.FileSystem.Path.Join(tempDirectory, \"favicons\"));\n    }\n\n    private void CopyTemplatesToBackupDirectory(string tempDirectory)\n    {\n        directoryService.CopyDirectoryToDirectory(directoryService.TemplateDirectory, directoryService.FileSystem.Path.Join(tempDirectory, \"templates\"));\n    }\n\n    private async Task CopyCoverImagesToBackupDirectory(string tempDirectory)\n    {\n        var outputTempDir = Path.Join(tempDirectory, \"covers\");\n        directoryService.ExistOrCreate(outputTempDir);\n","sourceCodeStart":149,"sourceCodeEnd":185,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/BackupService.cs#L149-L185","documentation":"Thrown by BackupService.BackupDatabaseFile (line 167) as an InvalidOperationException (NOT a KavitaException) wrapping any failure of the raw SQL 'VACUUM INTO \\'{backupPath}\\''. The original exception is logged with the backup path. VACUUM INTO fails if the destination path already exists, is not writable, the DB is in WAL checkpoint state issues, or the disk is full. This is part of the scheduled Hangfire BackupDatabase job (retried up to 3 times).","triggerScenarios":"Scheduled/manual backup where the target kavita.db path already exists, the temp directory is read-only or full, the SQLite version does not support VACUUM INTO, or the main DB file is locked by another connection in a way that blocks the snapshot.","commonSituations":"Backup directory on a read-only mount or out of space; previous backup temp dir not cleaned (file exists at backupPath); running Kavita with an old SQLite/EF bundle lacking VACUUM INTO; Docker volume permission mismatch.","solutions":["Ensure ServerSetting BackupDirectory exists, is writable, and has free space (the job logs Critical and aborts earlier if not).","Delete or let Kavita clean the temp directory so kavita.db at backupPath does not pre-exist.","Confirm the SQLite/EF Core Microsoft.Data.Sqlite version supports VACUUM INTO (it does on modern bundles; upgrade if old).","Check the Hangfire job retry log — 3 attempts then Fail; the inner exception names the SQLite error (e.g. 'file exists', 'disk I/O error')."],"exampleFix":"// before\nawait unitOfWork.DataContext.Database.ExecuteSqlRawAsync($\"VACUUM INTO '{backupPath}'\");\n// fails if backupPath already exists -> InvalidOperationException\n\n// after — ensure a clean target before vacuum\nif (directoryService.FileSystem.File.Exists(backupPath))\n    directoryService.FileSystem.File.Delete(backupPath);\nawait unitOfWork.DataContext.Database.ExecuteSqlRawAsync($\"VACUUM INTO '{backupPath}'\");","handlingStrategy":"try-catch","validationCode":"// Ensure a clean, writable target before the backup job runs\nvar backupDir = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value;\nif (!directoryService.ExistOrCreate(backupDir)) return; // already logged Critical in the job\n// Ensure no stale kavita.db at the temp target\nvar target = Path.Join(tempDir, \"kavita.db\");\nif (directoryService.FileSystem.File.Exists(target)) directoryService.FileSystem.File.Delete(target);","typeGuard":null,"tryCatchPattern":"try { await unitOfWork.DataContext.Database.ExecuteSqlRawAsync($\"VACUUM INTO '{backupPath}'\"); }\ncatch (Exception ex) { logger.LogError(ex, ...); throw new InvalidOperationException(...); }","preventionTips":["Point BackupDirectory at a writable volume with ample free space.","Let Kavita clean the temp dir so no stale kavita.db blocks VACUUM INTO.","Keep Microsoft.Data.Sqlite/EF Core current so VACUUM INTO is supported.","Check Hangfire: the job retries 3x then Fails — read the inner SQLite error."],"tags":["backup","database","sqlite","io-error","scheduled-job"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}