fix(paste, delete): non-silent missing dirent, synchronous dst-writable precheck, complete move src cleanup#329
Merged
Merged
Conversation
- delete: DELETE /api/resources no longer silently succeeds when the target dirent is missing (posix pre-stat, sync existence check via ListDirWithPerm, cloud pre-stat before Purge). Posix response shape aligned with sync/cloud: data carries the failed dirents, message is the shared "delete failed paths". - paste: dst writability is precheck'd in the HTTP handler before a task is allocated, so no-write-permission surfaces as a synchronous 403 + Permission denied instead of a flaky async task status. Cross- node posix uses GET /api/resources?probe=write; cloud uses an rclone list-root probe; sync uses CheckFolderPermission. - paste: cloud->sync move and cross-node posix move now actually drop the source after the copy completes (UploadToSync routes cloud src through cmd.Clear; DownloadFromFiles forwards a DELETE to the src files-pod). Clear failures stay log-only, matching the existing rclone/seahub clear convention. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Three independent bugs found while end-to-end testing paste and delete, all
under the same "the request looked like it succeeded but didn't actually do
what the user asked" symptom class:
DELETE /api/resourcessilently returned200 OKfor a dirent that didnot exist, leaking a stale entry as if it had been removed.
PATCH /api/pasteto a destination the caller has no write permissionon returned an HTTP 200 with a task id; the failure only surfaced
asynchronously in the task status, in a backend-dependent shape.
awss3(or any cloud) tosync, and a move between twoPOSIX-family backends on different nodes, both copied the src to the dst
but left the src intact. The UI reported the task as Completed.
Each bug is fixed in isolation; the response shape is aligned with sibling
code paths so the frontend has one less per-backend special case.
1. Delete: surface missing dirents instead of silent success
Problem
os.RemoveAllreturnsnilfor an absent path -> handler returns200 OK, no indication the entry was already goneseaserv.DelFileis idempotent -> handler returns200 OK, dittoPurgesucceeds against an empty/absent prefix -> handler returns200 OK, dittoA user who deletes the same path twice (or whose UI list is stale) has no
way to know the second request did nothing useful.
Fix
Lstat, falling back toStat) beforeRemoveAll.Lstatis used so dangling symlinks remain deletable.ListDirWithPermof theparent before calling
DelFile. Missing entries return a typedErrEntryNotFound{IsDir}. Internal task cleanup (HandleDelete)swallows the typed error so task-side idempotent cleanup is preserved;
the public API path (
SyncStorage.Delete) surfaces it.Purge.Posix response shape aligned with sync/cloud
While we were in
PosixStorage.Delete, the response shape was alsobrought in line with sync/cloud so the UI has one shape per success/failure
across all backends:
datanullfor delete-failed (only filled on path-validation errors)message"<path>: <reason>;<path>: <reason>"(per-error string, dedup'd by reason)"delete failed paths"Per-item reasons are still in
klog.Errorffor backend debugging. Theparent-missing branch (
s.getFilesfailure ->"<path>: no such file or directory") is unchanged. The previous error-string dedup meant twodistinct dirents failing for the same reason collapsed to a single entry;
each failed dirent now appears separately, which is a strict improvement.
2. Paste: precheck dst writability before allocating a task
Problem
PasteMethodreturned a task id (HTTP 200) the instant request parsingsucceeded. If the destination was not writable, the failure surfaced
asynchronously, and the failure mode depended on the backend:
Running -> Failedmid-phaseHandleBatchCopyreturns "permission denied" mid-taskAccessDenied/403 forbidden/insufficient_scopeFrom the UI every variant looked the same: a flaky failure delivered
seconds after the request returned 200.
Fix
A new
precheck.DestinationWritableruns immediately afterPasteParamis fully resolved (right after the existingprecheck.SourceExists):dst.FileTypedrive(master node only by routing), localcache/external/internal/smb/usb/hddWriteTempFile-> create+delete a 1-byte file in the deepest existing ancestor of the dst path<extend>does not match the current node)GET /api/resources?probe=writeagainst the owning files-pod -> the receiving side runs the sameWriteTempFile. New query param, scoped to this one call.syncseahub.CheckFolderPermissionon the deepest existing parent; must return"rw"awss3/tencent/google/dropboxGetFilesListon the bucket root.About(used for free-space) is not supported on S3/Tencent, hence the list probe.share=1swaps inDstOwnerso the probe runs as the share grantor.Probe failure now returns a synchronous
-- same shape and message that the rest of the codebase (middleware share
checks, etc.) already uses. No task is allocated, no phantom Completed
entry. The detailed Go error stays in
klog.Warningf.Task-side translation also aligned
Task.formatJobStatusErroralready collapses rclone's many permissionstrings (
AccessDenied,403 forbidden,insufficient_scope,insufficientFilePermissions,invalid_grant,expired_access_token,permission denied) into a single user-facing error; it now uses thesame
ErrorMessagePermissionDeniedconstant so the synchronous and theasync paths produce identical UI text.
HandleBatchCopyalso requiresdstPerm == "rw"(no longer justnon-empty) so sync-to-sync copy targeting a read-only library is
rejected up front instead of half-failing inside seafile.
3. Paste: complete move src cleanup
Bug A:
cloud -> syncmove was copy-onlytask_paste_sync.go::UploadToSyncranos.RemoveAll(srcUri + Path)fora move. For a cloud src this resolves to a path like
/awss3/<bucket>/...which does not exist on the local filesystem;
os.RemoveAllreturnsniland the task completes "successfully" with the cloud object stillin place.
Fix: in
UploadToSync, branch ont.param.Src.IsCloud():cmd.Clear(t.param.Src)(the same rclone-aware drop usedby every other "cloud src move" site in
task_paste_cloud.go);os.RemoveAll(srcUri + Path).Cloud
cmd.Clearfailures are log-only -- matches the existingtask_paste_cloud.goconvention (the copy already succeeded; a transientnetwork error during the post-copy delete should not fail the user-visible
task).
Bug B: cross-node POSIX move was copy-only
task_paste_download.go::DownloadFromFilesis the phase used when srcand dst live on different nodes (e.g. moving from an
externaldisk onnode A to a
cacheon node B). It had no src deletion at all -- forActionMovethe function returned after the last byte was streamed.Fix: new helper
clearRemoteSrcissuesDELETE /api/resources/<fsType>/<extend><parent>against the src node(located via
IntegrationManager().GetFilesPod). Triggered only after asuccessful copy and only when
t.param.Action == common.ActionMove. Samelog-only failure handling as
cmd.Clearfor parity with the cloud path.The HTTP request runs under
context.Background(), nott.ctx, onpurpose:
cmd.Clear(the equivalent for cloud src) ignores task ctx soa user pause issued right after the copy doesn't leave the src
half-dropped.
streamHTTPClient.ResponseHeaderTimeoutstill caps astuck peer.
Cross-effect with bug #1: with the missing-dirent fix, a duplicate
trigger (e.g. retry after a flaky network) makes
clearRemoteSrcreceive an HTTP 500 with the failed dirents instead of a misleading 200;
the log-only path makes this visible without breaking the task.
Files touched
pkg/drivers/posix/posix/posix.gopkg/drivers/posix/posix/helper.goextractErrMsgpkg/drivers/sync/seahub/batch.goErrEntryNotFound;HandleBatchCopyrequiresdstPerm == "rw"pkg/drivers/sync/sync.goHandleBatchDeletepkg/drivers/clouds/rclone/rclone.gopkg/drivers/precheck/precheck.goDestinationWritable+ per-backend dispatcherspkg/models/query_param.goProbequery parampkg/hertz/biz/handler/api/resources/resources_service.gohandleProbeWritefor cross-node POSIX probepkg/hertz/biz/handler/api/paste/paste_service.goDestinationWritable; align error shapepkg/tasks/task.goformatJobStatusErroruses sharedErrorMessagePermissionDeniedpkg/tasks/task_paste_cloud.goformatJobStatusErroron cloud paste failurespkg/tasks/task_paste_posix.gopkg/tasks/task_paste_sync.goUploadToSynconIsCloud()for move src cleanuppkg/tasks/task_paste_download.goclearRemoteSrcfor cross-node POSIX movepkg/common/constant.goErrorMessageDstNotWritable; reuseErrorMessagePermissionDeniedTest plan
Delete
DELETE /api/resources/<...>for a missing dirent ->500withdata: ["<dirent>"],message: "delete failed paths". Posix-specific reason still in server log.DELETE /api/resources/sync/<repo>/for a missing file ->500withdata: ["<dirent>"],message: "delete failed paths"; same for a missing folder (trailing slash preserved).DELETEfor a missing object ->500with the failed path.HandleDeleteinvoked by share / paste flows) of an already-absent target is still idempotent (no failure surfaced upward).200 OK.Paste -- dst-writable precheck
403 { "error": "Permission denied." }, no task allocated.403, withGET /api/resources?probe=writevisible in the receiving pod's log.403.403(probe failure) or task fails withPermission denied.(async path viaformatJobStatusError).share=1paste to a grantor-owned dst respects the grantor's permission (not the requester's).Paste -- move src cleanup
awss3 -> syncmove of a file -> file appears on sync, removed from S3.awss3 -> syncmove of a directory -> directory tree appears on sync, prefix removed from S3.tencent / google / dropbox -> syncmove -> src removed.external on node A -> cache on node B) -> src removed from node A after copy completes on node B.posix -> posix (same node)(atomicmv),cloud -> posix,sync -> posix,sync -> cloud,posix -> cloud,cloud -> cloud.General
GOOS=linux GOARCH=amd64 go buildclean for all touched packages.gofmt/ lint clean for all touched files.Made with Cursor