gastownhall/beads · error

failed to remove source after copy: %w

Error message

failed to remove source after copy: %w

What it means

moveFile falls back to copy-then-remove when a cross-filesystem rename fails with EXDEV. If the copy succeeds but removing the original source file fails, the error is wrapped as "failed to remove source after copy: %w" — the data now exists at BOTH src and dst, and the caller must know the move is only half-complete.

Source

Thrown at cmd/bd/doctor/fix/fs.go:26

	"syscall"
)

var (
	renameFile = os.Rename
	removeFile = os.Remove
	openFileRO = os.Open
	openFileRW = os.OpenFile
)

func moveFile(src, dst string) error {
	if err := renameFile(src, dst); err == nil {
		return nil
	} else if isEXDEV(err) {
		if err := copyFile(src, dst); err != nil {
			return err
		}
		if err := removeFile(src); err != nil {
			return fmt.Errorf("failed to remove source after copy: %w", err)
		}
		return nil
	} else {
		return err
	}
}

func copyFile(src, dst string) error {
	in, err := openFileRO(src) // #nosec G304 -- src is within the workspace
	if err != nil {
		return err
	}
	defer in.Close()
	out, err := openFileRW(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
	if err != nil {
		return err
	}
	defer func() { _ = out.Close() }()

View on GitHub (pinned to 71377f2769)

Solutions

  1. Delete the leftover source file manually once the destination is verified (rm <src>)
  2. Fix permissions on the source file/directory so it can be unlinked (need write on the parent dir)
  3. Verify the copied destination is intact before removing anything; if it is not, redo the move

Example fix

// before (source left behind)
err := fix.MoveFile("/mnt/a/db.x", "/mnt/b/db.x") // failed to remove source after copy: permission denied
// after: make source dir writable first
$ chmod u+w /mnt/a && rm /mnt/a/db.x
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure the source can be removed before attempting a cross-device move
srcDir writable check: if f, err := os.OpenFile(filepath.Dir(src), os.O_WRONLY, 0); err != nil {
	return fmt.Errorf("cannot unlink from %s: %w", filepath.Dir(src), err)
} else { f.Close() }

Try / catch

if err := moveFile(src, dst); err != nil {
	if strings.Contains(err.Error(), "failed to remove source after copy") {
		// dst exists; clean up leftover src manually or retry removal
		if rmErr := os.Remove(src); rmErr != nil {
			return fmt.Errorf("both move cleanup attempts failed: %v; %w", rmErr, err)
		}
		return nil
	}
	return err
}

Prevention

When it happens

Trigger: Calling moveFile (directly or via doctor filesystem fixes) across filesystems where os.Rename returns EXDEV, copyFile succeeds, then removeFile fails because the source file is read-only, its directory is not writable, the file was deleted/replaced concurrently, or the source mount is read-only.

Common situations: Moving database files between volumes with mismatched ownership; source directory on a read-only mount or immutable file; another process holding/rewriting the file during the doctor fix.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/7c8599b204268226. Report an issue: GitHub.