Kareadita/Kavita · critical · InvalidOperationException
Failed to create database backup at {backupPath}
Error message
Failed to create database backup at {backupPath} What it means
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).
Source
Thrown at Kavita.Services/BackupService.cs:167
if (backupPath.Contains('\''))
{
throw new ArgumentException("Backup path contains invalid characters", nameof(tempDirectory));
}
try
{
// Use VACUUM INTO to create a safe backup of the database while it's running
// This creates a consistent snapshot without locking the main database
// Note: VACUUM INTO requires a literal path and cannot use SQL parameters
#pragma warning disable EF1002 // The backup path is validated above to not contain SQL injection characters
await unitOfWork.DataContext.Database.ExecuteSqlRawAsync($"VACUUM INTO '{backupPath}'");
#pragma warning restore EF1002
logger.LogDebug("Database backup created successfully at {BackupPath}", backupPath);
}
catch (Exception ex)
{
logger.LogError(ex, "Failed to create database backup using VACUUM INTO at {BackupPath}", backupPath);
throw new InvalidOperationException($"Failed to create database backup at {backupPath}", ex);
}
}
private void CopyFaviconsToBackupDirectory(string tempDirectory)
{
directoryService.CopyDirectoryToDirectory(directoryService.FaviconDirectory, directoryService.FileSystem.Path.Join(tempDirectory, "favicons"));
}
private void CopyTemplatesToBackupDirectory(string tempDirectory)
{
directoryService.CopyDirectoryToDirectory(directoryService.TemplateDirectory, directoryService.FileSystem.Path.Join(tempDirectory, "templates"));
}
private async Task CopyCoverImagesToBackupDirectory(string tempDirectory)
{
var outputTempDir = Path.Join(tempDirectory, "covers");
directoryService.ExistOrCreate(outputTempDir);
View on GitHub (pinned to 9c3e540000)
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').
Example fix
// before
await unitOfWork.DataContext.Database.ExecuteSqlRawAsync($"VACUUM INTO '{backupPath}'");
// fails if backupPath already exists -> InvalidOperationException
// after — ensure a clean target before vacuum
if (directoryService.FileSystem.File.Exists(backupPath))
directoryService.FileSystem.File.Delete(backupPath);
await unitOfWork.DataContext.Database.ExecuteSqlRawAsync($"VACUUM INTO '{backupPath}'"); Defensive patterns
Strategy: try-catch
Validate before calling
// Ensure a clean, writable target before the backup job runs var backupDir = (await unitOfWork.SettingsRepository.GetSettingAsync(ServerSettingKey.BackupDirectory)).Value; if (!directoryService.ExistOrCreate(backupDir)) return; // already logged Critical in the job // Ensure no stale kavita.db at the temp target var target = Path.Join(tempDir, "kavita.db"); if (directoryService.FileSystem.File.Exists(target)) directoryService.FileSystem.File.Delete(target);
Try / catch
try { await unitOfWork.DataContext.Database.ExecuteSqlRawAsync($"VACUUM INTO '{backupPath}'"); }
catch (Exception ex) { logger.LogError(ex, ...); throw new InvalidOperationException(...); } Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- annotation-failed-create
- generic-error
- bad-copy-files-for-download
- generic-create-temp-archive
- {archivePath} does not exist on disk
AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13).
Data as JSON: /api/errors/ef6c0cb014275681.
Report an issue: GitHub.