drime: fix large file uploads landing in drive root instead of configured folder

The /s3/multipart/create and /s3/entries endpoints interpret relativePath
as an absolute path from the drive root, not relative to parent_id. When
root_folder_id was set to a non-root folder, files larger than
upload_cutoff ended up at the user's drive root instead of the configured
folder.

Resolve the absolute path of the Fs root once via GET /folders/{hash}/path
(cached on first OpenChunkWriter call) and use that to build the correct
relativePath.

Fixes #9392
This commit is contained in:
Nick Craig-Wood
2026-05-07 17:38:27 +01:00
parent 15419f90c2
commit 667903dca0
2 changed files with 92 additions and 16 deletions
+8
View File
@@ -238,6 +238,14 @@ 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"`
+84 -16
View File
@@ -15,6 +15,7 @@ should stay under that.
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -199,13 +200,16 @@ 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
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
}
// Object describes a drime object
@@ -330,6 +334,62 @@ 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)
@@ -370,11 +430,12 @@ 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))),
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),
}
f.features = (&fs.Features{
CanHaveEmptyDirectories: true,
@@ -1096,6 +1157,15 @@ 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))
// Temporary Object under construction
o := &Object{
fs: f,
@@ -1129,7 +1199,7 @@ func (f *Fs) OpenChunkWriter(ctx context.Context, remote string, src fs.ObjectIn
Size: createSize,
Extension: strings.TrimPrefix(path.Ext(leaf), `.`),
ParentID: json.Number(directoryID),
RelativePath: f.opt.Enc.FromStandardPath(path.Join(f.root, remote)),
RelativePath: relPath,
WorkspaceID: f.opt.WorkspaceID,
}
@@ -1156,8 +1226,6 @@ func (f *Fs) OpenChunkWriter(ctx context.Context, remote string, src fs.ObjectIn
if ext == "" {
ext = "bin"
}
rel := f.opt.Enc.FromStandardPath(path.Join(f.root, remote))
chunkWriter := &drimeChunkWriter{
uploadID: resp.UploadID,
key: resp.Key,
@@ -1170,7 +1238,7 @@ func (f *Fs) OpenChunkWriter(ctx context.Context, remote string, src fs.ObjectIn
mime: mime,
extension: ext,
parentID: json.Number(directoryID),
relativePath: rel,
relativePath: relPath,
}
info = fs.ChunkWriterInfo{
ChunkSize: int64(chunkSize),