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 <BrianDKing@gmail.com>
This commit is contained in:
Nick Craig-Wood
2026-05-26 10:26:24 +01:00
parent c8ceb209fc
commit ddbc886395
2 changed files with 18 additions and 91 deletions

View File

@@ -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"`

View File

@@ -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("<id>|") - 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{