yorukot/superfile · error

failed to remove source after move

Error message

failed to remove source after move: %w

What it means

pasteDir wraps os.RemoveAll(src) failures when finishing a cross-device cut (move) that fell back to a manual copy. The move copied successfully but cleanup of the original failed, so the source remains and the wrapped cause (usually EPERM/EBUSY/ENOTEMPTY) is reported.

Solutions

  1. Close applications holding files in the source directory open (editors, indexers, terminals cd'd into it), then retry the move.
  2. Check write/execute permissions on the source directory and its contents (chmod -R u+w).
  3. Remove the leftover source manually once unlocked: rm -rf <src> (verify the copy first).
  4. Same-device moves avoid this path entirely: paste onto the same filesystem so rename() is used instead of copy+delete.

Example fix

// before
if cut && !sameDev {
    err = os.RemoveAll(src)
    if err != nil {
        return fmt.Errorf("failed to remove source after move: %w", err)
    }
}
// after
if cut && !sameDev {
    if err := os.RemoveAll(src); err != nil {
        return fmt.Errorf("move succeeded but could not delete source %s (is a file open elsewhere?): %w", src, err)
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// cross-device move will copy+delete; make sure no process holds the tree
// (best effort) verify write access to source root
if f, err := os.OpenFile(src, os.O_RDONLY, 0); err != nil {
    return fmt.Errorf("source may not be removable: %w", err)
} else {
    f.Close()
}

Try / catch

if err := ops.Paste(src, dst, cut=true); err != nil {
    if strings.Contains(err.Error(), "failed to remove source after move") {
        // copy succeeded; ask user to close apps holding source files, then rm -rf manually
    }
    return err
}

Prevention

When it happens

Trigger: A cut-paste across filesystems (rename fails EXDEV, so pasteDir does copy+delete) where the source cannot be removed: a file inside is held open (EBUSY on Windows), permission denied, or a new file appeared in the source dir making it non-empty for a non-recursive failure.

Common situations: Moving directories between drives/partitions (e.g. home -> USB) while an editor or indexer holds files open; moving dirs with files owned by other users; Windows AV scanners locking files.

Related errors


AI-assisted analysis of yorukot/superfile@b72f550bc6 (2026-09-01). Data as JSON: /api/errors/093b0666c3225f29. Report an issue: GitHub.

Appendix: source

Thrown at src/internal/file_operations.go:195

		}

		relPath, err := filepath.Rel(src, path)
		if err != nil {
			return err
		}
		newPath := filepath.Join(dst, relPath)
		return actualPasteOperation(info, path, newPath, cut, sameDev, p, processBarModel)
	})

	if err != nil {
		return err
	}

	// If this was a cut operation and we had to do a manual copy, remove the source
	if cut && !sameDev {
		err = os.RemoveAll(src)
		if err != nil {
			return fmt.Errorf("failed to remove source after move: %w", err)
		}
	}

	return nil
}

func actualPasteOperation(info os.FileInfo, path string, newPath string, cut bool, sameDev bool,
	p *processbar.Process, processBarModel *processbar.Model) error {
	var err error
	if info.IsDir() {
		// TODO - this is likely not needed because we did
		// dst, err := renameIfDuplicate(dst) above
		newPath, err = renameIfDuplicate(newPath)
		if err != nil {
			return err
		}
		err = os.MkdirAll(newPath, info.Mode())
		return err

View on GitHub (pinned to b72f550bc6)