MHSanaei/3x-ui · error
sqlite backup destination already exists: %s
Error message
sqlite backup destination already exists: %s
What it means
BackupSQLite performs an online backup to dstPath and refuses to run when the destination path already exists (Lstat succeeds), to prevent silently overwriting a previous backup. It also cleans up the partial file if the backup itself fails. Any existing filesystem entry at dstPath — even a stale zero-byte file — triggers this.
Source
Thrown at internal/database/db.go:2188
func IsSQLiteDB(file io.ReaderAt) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.ReadAt(buf, 0)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}
func BackupSQLite(dstPath string) (err error) {
if IsPostgres() {
return errors.New("sqlite backup is unavailable for PostgreSQL")
}
if db == nil {
return errors.New("database is not initialized")
}
if _, err := os.Lstat(dstPath); err == nil {
return fmt.Errorf("sqlite backup destination already exists: %s", dstPath)
} else if !errors.Is(err, os.ErrNotExist) {
return err
}
defer func() {
if err != nil {
_ = os.Remove(dstPath)
}
}()
ctx, cancel := context.WithTimeout(context.Background(), backupSQLiteTimeout)
defer cancel()
sourceDB, err := db.DB()
if err != nil {
return err
}
sourceConn, err := sourceDB.Conn(ctx)
if err != nil {View on GitHub (pinned to ad32144c42)
Solutions
- Use a unique destination per run, e.g. append a timestamp: backup-$(date +%Y%m%d-%H%M%S).db.
- If the existing file is a stale/unwanted artifact, delete or move it explicitly before re-running.
- Never point dstPath at the live database path (/etc/x-ui/x-ui.db).
Example fix
# before
cp_cmd: BackupSQLite("/etc/x-ui/backup.db")
# after
BackupSQLite(fmt.Sprintf("/etc/x-ui/backup-%s.db", time.Now().Format("20060102-150405"))) Defensive patterns
Strategy: validation
Validate before calling
if _, err := os.Lstat(dst); err == nil {
return fmt.Errorf("refusing to overwrite existing backup %s", dst)
} Prevention
- Always timestamp backup destinations: backup-20060102-150405.db.
- Automate cleanup of old backups separately from creation; never reuse a fixed filename.
When it happens
Trigger: Running the backup API/CLI twice with the same destination path; a prior failed run left a file (only on non-error paths); an automated nightly backup writing to a fixed filename without rotation.
Common situations: Cron/scripts using backup-$(date)? No — hardcoding /etc/x-ui/backup.db; backup jobs that don't timestamp; a leftover file from a killed run.
Related errors
- sqlite backup is unavailable for PostgreSQL
- database is not initialized
- source sqlite not found at %s: %w
- destination already exists: %s
- open sqlite source: %w
AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15).
Data as JSON: /api/errors/83d9642890a7fdd0.
Report an issue: GitHub.