From ddbc886395ea52386bb6aef7dfbe6262346ecde4 Mon Sep 17 00:00:00 2001 From: Nick Craig-Wood Date: Tue, 26 May 2026 10:26:24 +0100 Subject: [PATCH] drime: fix files being uploaded to the wrong directory Large files (sent as multipart uploads) were placed in the wrong folder for two reasons: - the parent folder was sent as "parent_id", but the API ignores that and expects "parentId", so the parent was never honoured - relativePath was sent as the full path from the drive root, which made the server build folders from it and silently drop any "0" path segment (e.g. ".../data/0/file" lost the "0") Send the parent as "parentId" and use just the leaf as relativePath, matching the working single-part upload. This also lets us remove the now-unneeded absolute-path resolution code. Fixes #9392 Co-authored-by: Brian King --- backend/drime/api/types.go | 12 +---- backend/drime/drime.go | 97 +++++++------------------------------- 2 files changed, 18 insertions(+), 91 deletions(-) diff --git a/backend/drime/api/types.go b/backend/drime/api/types.go index 9f70bf696..adb2862d2 100644 --- a/backend/drime/api/types.go +++ b/backend/drime/api/types.go @@ -171,7 +171,7 @@ type MultiPartCreateRequest struct { Mime string `json:"mime"` Size int64 `json:"size"` Extension string `json:"extension"` - ParentID json.Number `json:"parent_id"` + ParentID json.Number `json:"parentId"` RelativePath string `json:"relativePath"` WorkspaceID string `json:"workspaceId,omitempty"` } @@ -222,7 +222,7 @@ type MultiPartEntriesRequest struct { Filename string `json:"filename"` Size int64 `json:"size"` ClientExtension string `json:"clientExtension"` - ParentID json.Number `json:"parent_id"` + ParentID json.Number `json:"parentId"` RelativePath string `json:"relativePath"` WorkspaceID string `json:"workspaceId,omitempty"` } @@ -238,14 +238,6 @@ type MultiPartAbort struct { Key string `json:"key"` } -// FolderPathResponse is returned by GET /folders/{hash}/path -// -// Path is the breadcrumb from the drive root down to the requested folder. -type FolderPathResponse struct { - Status string `json:"status"` - Path []Item `json:"path"` -} - // SpaceUsageResponse is returned by GET /user/space-usage type SpaceUsageResponse struct { Used int64 `json:"used"` diff --git a/backend/drime/drime.go b/backend/drime/drime.go index 22d63487f..ba71c5fa4 100644 --- a/backend/drime/drime.go +++ b/backend/drime/drime.go @@ -15,7 +15,6 @@ should stay under that. import ( "context" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -200,16 +199,13 @@ type Options struct { // Fs represents a remote drime type Fs struct { - name string // name of this remote - root string // the path we are working on - opt Options // parsed options - features *fs.Features // optional features - srv *rest.Client // the connection to the server - dirCache *dircache.DirCache // Map of directory path to directory id - pacer *fs.Pacer // pacer for API calls - absRootOnce *sync.Once // protects absRoot computation - absRoot string // absolute path of f.root from drive root - absRootErr error // error from computing absRoot, if any + name string // name of this remote + root string // the path we are working on + opt Options // parsed options + features *fs.Features // optional features + srv *rest.Client // the connection to the server + dirCache *dircache.DirCache // Map of directory path to directory id + pacer *fs.Pacer // pacer for API calls } // Object describes a drime object @@ -339,62 +335,6 @@ func (f *Fs) getItem(ctx context.Context, id string, dirID string, leaf string) return info, err } -// idToAbsolutePath returns the absolute path (from the user's drive root) of -// the folder with the given ID. -// -// Drime exposes GET /folders/{hash}/path which returns the breadcrumb from -// drive root down to the folder. The hash format is undocumented but -// observed to be base64("|") - every item drime returns has a `hash` -// field of that shape (e.g. id 704791396 => "NzA0NzkxMzk2fA"). The docs -// example "MTExMzQ0fHBhZA" decodes to "111344|pad", so a non-empty suffix -// after the pipe is also accepted; if drime ever starts requiring a -// specific suffix this will break. -// -// Returns "" for an empty/zero ID (drive root). -func (f *Fs) idToAbsolutePath(ctx context.Context, id string) (string, error) { - if id == "" || id == "0" { - return "", nil - } - hash := base64.StdEncoding.EncodeToString([]byte(id + "|")) - opts := rest.Opts{ - Method: "GET", - Path: "/folders/" + hash + "/path", - Parameters: url.Values{}, - } - if f.opt.WorkspaceID != "" { - opts.Parameters.Set("workspaceId", f.opt.WorkspaceID) - } - var result api.FolderPathResponse - var resp *http.Response - err := f.pacer.Call(func() (bool, error) { - var err error - resp, err = f.srv.CallJSON(ctx, &opts, nil, &result) - return shouldRetry(ctx, resp, err) - }) - if err != nil { - return "", fmt.Errorf("failed to get folder path for %q: %w", id, err) - } - parts := make([]string, 0, len(result.Path)) - for _, item := range result.Path { - parts = append(parts, f.opt.Enc.ToStandardName(item.Name)) - } - return path.Join(parts...), nil -} - -// absoluteRoot returns the absolute path from the drive root to the Fs root. -// Computed once and cached. -func (f *Fs) absoluteRoot(ctx context.Context) (string, error) { - f.absRootOnce.Do(func() { - rootID, err := f.dirCache.RootID(ctx, false) - if err != nil { - f.absRootErr = err - return - } - f.absRoot, f.absRootErr = f.idToAbsolutePath(ctx, rootID) - }) - return f.absRoot, f.absRootErr -} - // errorHandler parses a non 2xx error response into an error func errorHandler(resp *http.Response) error { body, err := rest.ReadBody(resp) @@ -435,12 +375,11 @@ func NewFs(ctx context.Context, name, root string, m configmap.Mapper) (fs.Fs, e client := fshttp.NewClient(ctx) f := &Fs{ - name: name, - root: root, - opt: *opt, - srv: rest.NewClient(client).SetRoot(rootURL), - pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), - absRootOnce: new(sync.Once), + name: name, + root: root, + opt: *opt, + srv: rest.NewClient(client).SetRoot(rootURL), + pacer: fs.NewPacer(ctx, pacer.NewDefault(pacer.MinSleep(minSleep), pacer.MaxSleep(maxSleep), pacer.DecayConstant(decayConstant))), } f.features = (&fs.Features{ CanHaveEmptyDirectories: true, @@ -1173,14 +1112,10 @@ func (f *Fs) OpenChunkWriter(ctx context.Context, remote string, src fs.ObjectIn return info, nil, err } - // The /s3/multipart/create and /s3/entries endpoints interpret - // relativePath as an absolute path from the drive root, not relative to - // parent_id. Resolve our root's absolute path so we can build it. - absRoot, err := f.absoluteRoot(ctx) - if err != nil { - return info, nil, fmt.Errorf("failed to resolve absolute path of root: %w", err) - } - relPath := f.opt.Enc.FromStandardPath(path.Join(absRoot, remote)) + // Send just the leaf as relativePath, matching the single-part /uploads + // convention. The file is placed by parentId; sending an absolute path + // here makes the server build folders from it and drop "0" path segments. + relPath := f.opt.Enc.FromStandardName(leaf) // Temporary Object under construction o := &Object{