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

  1. Use a unique destination per run, e.g. append a timestamp: backup-$(date +%Y%m%d-%H%M%S).db.
  2. If the existing file is a stale/unwanted artifact, delete or move it explicitly before re-running.
  3. 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

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


AI-assisted analysis of MHSanaei/3x-ui@ad32144c42 (2026-08-15). Data as JSON: /api/errors/83d9642890a7fdd0. Report an issue: GitHub.