{"record":{"id":"7c8599b204268226","repo":"gastownhall/beads","slug":"failed-to-remove-source-after-copy-w","errorCode":null,"errorMessage":"failed to remove source after copy: %w","messagePattern":"failed to remove source after copy: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/doctor/fix/fs.go","lineNumber":26,"sourceCode":"\t\"syscall\"\n)\n\nvar (\n\trenameFile = os.Rename\n\tremoveFile = os.Remove\n\topenFileRO = os.Open\n\topenFileRW = os.OpenFile\n)\n\nfunc moveFile(src, dst string) error {\n\tif err := renameFile(src, dst); err == nil {\n\t\treturn nil\n\t} else if isEXDEV(err) {\n\t\tif err := copyFile(src, dst); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif err := removeFile(src); err != nil {\n\t\t\treturn fmt.Errorf(\"failed to remove source after copy: %w\", err)\n\t\t}\n\t\treturn nil\n\t} else {\n\t\treturn err\n\t}\n}\n\nfunc copyFile(src, dst string) error {\n\tin, err := openFileRO(src) // #nosec G304 -- src is within the workspace\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer in.Close()\n\tout, err := openFileRW(dst, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer func() { _ = out.Close() }()","sourceCodeStart":8,"sourceCodeEnd":44,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/doctor/fix/fs.go#L8-L44","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Delete the leftover source file manually once the destination is verified (rm <src>)","Fix permissions on the source file/directory so it can be unlinked (need write on the parent dir)","Verify the copied destination is intact before removing anything; if it is not, redo the move"],"exampleFix":"// before (source left behind)\nerr := fix.MoveFile(\"/mnt/a/db.x\", \"/mnt/b/db.x\") // failed to remove source after copy: permission denied\n// after: make source dir writable first\n$ chmod u+w /mnt/a && rm /mnt/a/db.x","handlingStrategy":"try-catch","validationCode":"// Ensure the source can be removed before attempting a cross-device move\nsrcDir writable check: if f, err := os.OpenFile(filepath.Dir(src), os.O_WRONLY, 0); err != nil {\n\treturn fmt.Errorf(\"cannot unlink from %s: %w\", filepath.Dir(src), err)\n} else { f.Close() }","typeGuard":null,"tryCatchPattern":"if err := moveFile(src, dst); err != nil {\n\tif strings.Contains(err.Error(), \"failed to remove source after copy\") {\n\t\t// dst exists; clean up leftover src manually or retry removal\n\t\tif rmErr := os.Remove(src); rmErr != nil {\n\t\t\treturn fmt.Errorf(\"both move cleanup attempts failed: %v; %w\", rmErr, err)\n\t\t}\n\t\treturn nil\n\t}\n\treturn err\n}","preventionTips":["Check write permission on the source file's parent directory before cross-device moves","Don't move files concurrently being written by other processes","After any EXDEV fallback, verify both src and dst state before proceeding"],"tags":["filesystem","move","copy","exdev"],"backgroundTag":"file-move-source-remove-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}