From 651b3035094ddc72b314a61b742d6b26a1e3f401 Mon Sep 17 00:00:00 2001 From: xireiki Date: Sun, 26 Jul 2026 08:17:41 +0800 Subject: [PATCH 1/4] fix(onedrive_sharelink): respect root_folder_path for subdirectory access - Add relativePath() to strip RootFolderPath prefix from virtual paths - Add effectiveDriveRootPath() to compute drive-relative path from RootFolderPath - Override rootFolder in getFiles() when RootFolderPath is configured - Apply relativePath() in List, MakeDir, Put, GetDirectUploadInfo - Store listURL for path computation against document library root Co-authored-by: GitHub Copilot --- drivers/onedrive_sharelink/driver.go | 57 +++++++++++++++++++++++++--- drivers/onedrive_sharelink/util.go | 5 +++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/drivers/onedrive_sharelink/driver.go b/drivers/onedrive_sharelink/driver.go index a975772ac0..7ecf1501f9 100644 --- a/drivers/onedrive_sharelink/driver.go +++ b/drivers/onedrive_sharelink/driver.go @@ -44,6 +44,8 @@ type OnedriveSharelink struct { headerMu sync.RWMutex sg singleflight.Group[http.Header] + + listURL string } func (d *OnedriveSharelink) Config() driver.Config { @@ -87,12 +89,31 @@ func (d *OnedriveSharelink) Drop(ctx context.Context) error { return nil } +// relativePath converts the full virtual path from OpenList to a path relative +// to the shared folder's root. When root_folder_path is configured, OpenList +// passes the full path including that prefix. +func (d *OnedriveSharelink) relativePath(virtualPath string) string { + if d.RootFolderPath == "" || d.RootFolderPath == "/" { + return virtualPath + } + root := utils.FixAndCleanPath(d.RootFolderPath) + vpath := utils.FixAndCleanPath(virtualPath) + if vpath == root { + return "/" + } + if strings.HasPrefix(vpath, root+"/") { + return utils.FixAndCleanPath(strings.TrimPrefix(vpath, root)) + } + return virtualPath +} + func (d *OnedriveSharelink) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { - files, err := d.getFiles(ctx, dir.GetPath()) + relPath := d.relativePath(dir.GetPath()) + files, err := d.getFiles(ctx, relPath) if err != nil { return nil, err } - folderSizes, err := d.driveChildrenFolderSizes(ctx, dir.GetPath()) + folderSizes, err := d.driveChildrenFolderSizes(ctx, relPath) if err != nil { log.Warnf("onedrive_sharelink: failed to get folder sizes for %s: %+v", dir.GetPath(), err) } @@ -146,7 +167,7 @@ func (d *OnedriveSharelink) MakeDir(ctx context.Context, parentDir model.Obj, di if err != nil { return err } - apiURL := injectAccessToken(d.drivePathAPIURL(parentDir.GetPath())+"/children", token) + apiURL := injectAccessToken(d.drivePathAPIURL(d.relativePath(parentDir.GetPath()))+"/children", token) body := map[string]any{ "name": dirName, "folder": map[string]any{}, @@ -189,7 +210,7 @@ func (d *OnedriveSharelink) Remove(ctx context.Context, obj model.Obj) error { } func (d *OnedriveSharelink) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { - info, err := d.createUploadInfo(ctx, stdpath.Join(dstDir.GetPath(), stream.GetName()), stream.GetSize()) + info, err := d.createUploadInfo(ctx, stdpath.Join(d.relativePath(dstDir.GetPath()), stream.GetName()), stream.GetSize()) if err != nil { return err } @@ -295,7 +316,7 @@ func (d *OnedriveSharelink) GetDirectUploadInfo(ctx context.Context, tool string if tool != "HttpDirect" { return nil, errs.NotImplement } - return d.createUploadInfo(ctx, stdpath.Join(dstDir.GetPath(), fileName), fileSize) + return d.createUploadInfo(ctx, stdpath.Join(d.relativePath(dstDir.GetPath()), fileName), fileSize) } func (d *OnedriveSharelink) createUploadInfo(ctx context.Context, path string, fileSize int64) (*model.HttpDirectUploadInfo, error) { @@ -437,6 +458,11 @@ func (d *OnedriveSharelink) uploadSessionChunk(ctx context.Context, uploadURL st func (d *OnedriveSharelink) drivePathAPIURL(path string) string { drivePath := stdpath.Join(d.driveRootPath, path) + // When RootFolderPath is configured, the base drive root needs to take it + // into account so the API targets the correct subfolder. + if d.RootFolderPath != "" && d.RootFolderPath != "/" { + drivePath = stdpath.Join(d.effectiveDriveRootPath(), path) + } drivePath = utils.FixAndCleanPath(drivePath) if drivePath == "/" { return d.DriveURL + "/root" @@ -444,6 +470,26 @@ func (d *OnedriveSharelink) drivePathAPIURL(path string) string { return fmt.Sprintf("%s/root:%s:", d.DriveURL, utils.EncodePath(drivePath, true)) } +// effectiveDriveRootPath computes the drive-relative root path from the +// user-configured RootFolderPath. RootFolderPath is a SharePoint server-relative +// path (e.g. /personal/user/Documents/subfolder); this method extracts the +// portion beyond the document library root (e.g. /subfolder). +func (d *OnedriveSharelink) effectiveDriveRootPath() string { + if d.listURL == "" || d.RootFolderPath == "" || d.RootFolderPath == "/" { + return d.driveRootPath + } + root := strings.TrimRight(d.RootFolderPath, "/") + list := strings.TrimRight(d.listURL, "/") + if root == list { + return "/" + } + prefix := list + "/" + if strings.HasPrefix(root+"/", prefix) { + return utils.FixAndCleanPath(strings.TrimPrefix(root, list)) + } + return d.driveRootPath +} + func injectAccessToken(rawURL, token string) string { if token == "" { return rawURL @@ -561,6 +607,7 @@ func (d *OnedriveSharelink) refreshDriveContextFromRedirect(ctx context.Context, d.DriveAccessToken = ctxInfo.DriveInfo.DriveAccessToken d.DriveTokenTime = time.Now().Unix() d.driveRootPath = rootPath + d.listURL = ctxInfo.ListURL d.headerMu.Unlock() return nil } diff --git a/drivers/onedrive_sharelink/util.go b/drivers/onedrive_sharelink/util.go index 13785939fb..9fc864151e 100644 --- a/drivers/onedrive_sharelink/util.go +++ b/drivers/onedrive_sharelink/util.go @@ -258,6 +258,11 @@ func (d *OnedriveSharelink) getFiles(ctx context.Context, path string) ([]Item, if err != nil { return nil, err } + // If the user configured a root_folder_path, use it as the initial root + // folder so the GraphQL query targets the correct subdirectory. + if d.RootFolderPath != "" && d.RootFolderPath != "/" { + rootFolder = d.RootFolderPath + } log.Debugln("rootFolder:", rootFolder) // Extract the relative path up to and including "Documents" relativePath := strings.Split(rootFolder, "Documents")[0] + "Documents" From 310c9c9ec2d7c89ed27f9203a52673cd145d0361 Mon Sep 17 00:00:00 2001 From: xireiki Date: Wed, 19 Aug 2026 17:52:08 +0800 Subject: [PATCH 2/4] fix(drivers/onedrive_sharelink): Make the `drivers/onedrive_sharelink` path matching more robust. --- drivers/onedrive_sharelink/driver.go | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/drivers/onedrive_sharelink/driver.go b/drivers/onedrive_sharelink/driver.go index 7ecf1501f9..6692ce920f 100644 --- a/drivers/onedrive_sharelink/driver.go +++ b/drivers/onedrive_sharelink/driver.go @@ -82,6 +82,14 @@ func (d *OnedriveSharelink) Init(ctx context.Context) error { } d.storeHeaders(h) + // Validate the RootFolderPath format. + if d.RootFolderPath != "" && d.RootFolderPath != "/" { + cleaned := utils.FixAndCleanPath(d.RootFolderPath) + if !strings.HasPrefix(cleaned, "/") { + return fmt.Errorf("root_folder_path must be an absolute path, got %q", d.RootFolderPath) + } + } + return nil } @@ -101,9 +109,12 @@ func (d *OnedriveSharelink) relativePath(virtualPath string) string { if vpath == root { return "/" } - if strings.HasPrefix(vpath, root+"/") { - return utils.FixAndCleanPath(strings.TrimPrefix(vpath, root)) + // Use `utils.IsSubPath` or an explicit prefix-plus-separator check. + if strings.HasPrefix(vpath+"/", root+"/") { + rel := strings.TrimPrefix(vpath, root) + return utils.FixAndCleanPath(rel) } + log.Warnf("onedrive_sharelink: path %q is outside configured root %q", virtualPath, d.RootFolderPath) return virtualPath } @@ -478,15 +489,15 @@ func (d *OnedriveSharelink) effectiveDriveRootPath() string { if d.listURL == "" || d.RootFolderPath == "" || d.RootFolderPath == "/" { return d.driveRootPath } - root := strings.TrimRight(d.RootFolderPath, "/") - list := strings.TrimRight(d.listURL, "/") + root := utils.FixAndCleanPath(d.RootFolderPath) + list := utils.FixAndCleanPath(d.listURL) if root == list { return "/" } - prefix := list + "/" - if strings.HasPrefix(root+"/", prefix) { + if strings.HasPrefix(root+"/", list+"/") { return utils.FixAndCleanPath(strings.TrimPrefix(root, list)) } + log.Warnf("onedrive_sharelink: RootFolderPath %q is not under listURL %q", d.RootFolderPath, d.listURL) return d.driveRootPath } From 47a030742b0629239d87e2bf5a9103265d10e486 Mon Sep 17 00:00:00 2001 From: xireiki Date: Wed, 19 Aug 2026 18:29:11 +0800 Subject: [PATCH 3/4] refactor(drivers/onedrive_sharelink): harden root_folder_path path handling - Merge vpath==root into a shared stripPrefix helper that matches on the path separator, so /foo no longer matches /foobar - Reuse stripPrefix in effectiveDriveRootPath, removing duplicated FixAndCleanPath/prefix/warning logic - Select the base drive root once in drivePathAPIURL instead of a discarded first Join - Enforce absolute and \"/Documents\"-segment requirements for root_folder_path in Init, and normalize the override in getFiles - Add unit tests for relativePath and effectiveDriveRootPath boundary cases --- drivers/onedrive_sharelink/driver.go | 56 +++++++------- drivers/onedrive_sharelink/driver_test.go | 90 +++++++++++++++++++++++ drivers/onedrive_sharelink/util.go | 3 +- 3 files changed, 123 insertions(+), 26 deletions(-) create mode 100644 drivers/onedrive_sharelink/driver_test.go diff --git a/drivers/onedrive_sharelink/driver.go b/drivers/onedrive_sharelink/driver.go index 6692ce920f..469f302a58 100644 --- a/drivers/onedrive_sharelink/driver.go +++ b/drivers/onedrive_sharelink/driver.go @@ -83,12 +83,15 @@ func (d *OnedriveSharelink) Init(ctx context.Context) error { d.storeHeaders(h) // Validate the RootFolderPath format. - if d.RootFolderPath != "" && d.RootFolderPath != "/" { - cleaned := utils.FixAndCleanPath(d.RootFolderPath) - if !strings.HasPrefix(cleaned, "/") { - return fmt.Errorf("root_folder_path must be an absolute path, got %q", d.RootFolderPath) - } - } + if d.RootFolderPath != "" && d.RootFolderPath != "/" { + if !strings.HasPrefix(d.RootFolderPath, "/") { + return fmt.Errorf("root_folder_path must be an absolute path, got %q", d.RootFolderPath) + } + cleaned := utils.FixAndCleanPath(d.RootFolderPath) + if !strings.Contains(cleaned, "/Documents") { + return fmt.Errorf("root_folder_path must contain the \"/Documents\" segment, got %q", d.RootFolderPath) + } + } return nil } @@ -104,15 +107,8 @@ func (d *OnedriveSharelink) relativePath(virtualPath string) string { if d.RootFolderPath == "" || d.RootFolderPath == "/" { return virtualPath } - root := utils.FixAndCleanPath(d.RootFolderPath) - vpath := utils.FixAndCleanPath(virtualPath) - if vpath == root { - return "/" - } - // Use `utils.IsSubPath` or an explicit prefix-plus-separator check. - if strings.HasPrefix(vpath+"/", root+"/") { - rel := strings.TrimPrefix(vpath, root) - return utils.FixAndCleanPath(rel) + if rel, ok := stripPrefix(virtualPath, d.RootFolderPath); ok { + return rel } log.Warnf("onedrive_sharelink: path %q is outside configured root %q", virtualPath, d.RootFolderPath) return virtualPath @@ -468,13 +464,13 @@ func (d *OnedriveSharelink) uploadSessionChunk(ctx context.Context, uploadURL st } func (d *OnedriveSharelink) drivePathAPIURL(path string) string { - drivePath := stdpath.Join(d.driveRootPath, path) + base := d.driveRootPath // When RootFolderPath is configured, the base drive root needs to take it // into account so the API targets the correct subfolder. if d.RootFolderPath != "" && d.RootFolderPath != "/" { - drivePath = stdpath.Join(d.effectiveDriveRootPath(), path) + base = d.effectiveDriveRootPath() } - drivePath = utils.FixAndCleanPath(drivePath) + drivePath := utils.FixAndCleanPath(stdpath.Join(base, path)) if drivePath == "/" { return d.DriveURL + "/root" } @@ -489,18 +485,28 @@ func (d *OnedriveSharelink) effectiveDriveRootPath() string { if d.listURL == "" || d.RootFolderPath == "" || d.RootFolderPath == "/" { return d.driveRootPath } - root := utils.FixAndCleanPath(d.RootFolderPath) - list := utils.FixAndCleanPath(d.listURL) - if root == list { - return "/" - } - if strings.HasPrefix(root+"/", list+"/") { - return utils.FixAndCleanPath(strings.TrimPrefix(root, list)) + if rel, ok := stripPrefix(d.RootFolderPath, d.listURL); ok { + return rel } log.Warnf("onedrive_sharelink: RootFolderPath %q is not under listURL %q", d.RootFolderPath, d.listURL) return d.driveRootPath } +// stripPrefix removes prefix from path when path equals prefix or is nested +// under it, matching on the path separator to avoid /foo matching /foobar. +// It reports whether the prefix matched. +func stripPrefix(path, prefix string) (string, bool) { + path = utils.FixAndCleanPath(path) + prefix = utils.FixAndCleanPath(prefix) + if path == prefix { + return "/", true + } + if strings.HasPrefix(path, prefix+"/") { + return utils.FixAndCleanPath(path[len(prefix):]), true + } + return path, false +} + func injectAccessToken(rawURL, token string) string { if token == "" { return rawURL diff --git a/drivers/onedrive_sharelink/driver_test.go b/drivers/onedrive_sharelink/driver_test.go new file mode 100644 index 0000000000..a0763b16ee --- /dev/null +++ b/drivers/onedrive_sharelink/driver_test.go @@ -0,0 +1,90 @@ +package onedrive_sharelink + +import "testing" + +func TestRelativePath(t *testing.T) { + root := "/personal/user/Documents/sub" + tests := []struct { + name string + root string + virtualPath string + want string + }{ + {"empty root", "", "/a/b", "/a/b"}, + {"slash root", "/", "/a/b", "/a/b"}, + {"exact root", root, root, "/"}, + {"child", root, root + "/a", "/a"}, + {"grandchild", root, root + "/a/b/c", "/a/b/c"}, + {"dirty child", root, root + "/a/../b", "/b"}, + {"prefix collision", root, root + "A/x", root + "A/x"}, + {"outside root", root, "/other", "/other"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &OnedriveSharelink{} + d.RootFolderPath = tt.root + if got := d.relativePath(tt.virtualPath); got != tt.want { + t.Errorf("relativePath(%q) = %q, want %q", tt.virtualPath, got, tt.want) + } + }) + } +} + +func TestEffectiveDriveRootPath(t *testing.T) { + list := "/personal/user/Documents" + driveRoot := "/" + tests := []struct { + name string + root string + list string + want string + }{ + {"no listURL", list, "", driveRoot}, + {"empty root", "", list, driveRoot}, + {"slash root", "/", list, driveRoot}, + {"exact list", list, list, "/"}, + {"child", list + "/sub", list, "/sub"}, + {"nested child", list + "/sub/a", list, "/sub/a"}, + {"prefix collision", list + "A", list, driveRoot}, + {"outside list", "/other", list, driveRoot}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &OnedriveSharelink{} + d.RootFolderPath = tt.root + d.listURL = tt.list + d.driveRootPath = driveRoot + if got := d.effectiveDriveRootPath(); got != tt.want { + t.Errorf("effectiveDriveRootPath() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestDrivePathAPIURL(t *testing.T) { + const drive = "https://d.example.com" + tests := []struct { + name string + root string + list string + path string + want string + }{ + {"no root, drive root", "", "", "/", drive + "/root"}, + {"no root, child", "", "", "/a", drive + "/root:/a:"}, + {"root equals list", "/personal/user/Documents", "/personal/user/Documents", "/a", drive + "/root:/a:"}, + {"root under list, child", "/personal/user/Documents/sub", "/personal/user/Documents", "/a", drive + "/root:/sub/a:"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &OnedriveSharelink{} + d.DriveURL = drive + d.RootFolderPath = tt.root + d.listURL = tt.list + d.driveRootPath = "/" + if got := d.drivePathAPIURL(tt.path); got != tt.want { + t.Errorf("drivePathAPIURL(%q) = %q, want %q", tt.path, got, tt.want) + } + }) + } +} diff --git a/drivers/onedrive_sharelink/util.go b/drivers/onedrive_sharelink/util.go index 9fc864151e..364e0761b7 100644 --- a/drivers/onedrive_sharelink/util.go +++ b/drivers/onedrive_sharelink/util.go @@ -14,6 +14,7 @@ import ( "github.com/OpenListTeam/OpenList/v4/drivers/base" "github.com/OpenListTeam/OpenList/v4/internal/conf" + "github.com/OpenListTeam/OpenList/v4/pkg/utils" log "github.com/sirupsen/logrus" "golang.org/x/net/html" ) @@ -261,7 +262,7 @@ func (d *OnedriveSharelink) getFiles(ctx context.Context, path string) ([]Item, // If the user configured a root_folder_path, use it as the initial root // folder so the GraphQL query targets the correct subdirectory. if d.RootFolderPath != "" && d.RootFolderPath != "/" { - rootFolder = d.RootFolderPath + rootFolder = utils.FixAndCleanPath(d.RootFolderPath) } log.Debugln("rootFolder:", rootFolder) // Extract the relative path up to and including "Documents" From 79f29530b5140f86f3d1ccb96df2fed45746614d Mon Sep 17 00:00:00 2001 From: xireiki Date: Tue, 1 Sep 2026 23:26:11 +0800 Subject: [PATCH 4/4] fix(drivers/onedrive_sharelink): drop hardcoded Documents path assumption - Derive the document library root from the share link response instead of the configured `root_folder_path`, so non-English sites and custom libraries are no longer rejected - Remove the `/Documents` segment check from `Init`, keeping only the absolute path validation - Return an error from `relativePath` when a path falls outside the configured root instead of silently using the original path - Extract `hasCustomRoot` to replace the repeated `root_folder_path` checks - Cover non-English and custom document libraries plus the out-of-root error in the path conversion tests --- drivers/onedrive_sharelink/driver.go | 60 ++++++++++++++--------- drivers/onedrive_sharelink/driver_test.go | 25 ++++++---- drivers/onedrive_sharelink/util.go | 9 ++-- 3 files changed, 59 insertions(+), 35 deletions(-) diff --git a/drivers/onedrive_sharelink/driver.go b/drivers/onedrive_sharelink/driver.go index 469f302a58..489cd268fc 100644 --- a/drivers/onedrive_sharelink/driver.go +++ b/drivers/onedrive_sharelink/driver.go @@ -82,20 +82,21 @@ func (d *OnedriveSharelink) Init(ctx context.Context) error { } d.storeHeaders(h) - // Validate the RootFolderPath format. - if d.RootFolderPath != "" && d.RootFolderPath != "/" { - if !strings.HasPrefix(d.RootFolderPath, "/") { - return fmt.Errorf("root_folder_path must be an absolute path, got %q", d.RootFolderPath) - } - cleaned := utils.FixAndCleanPath(d.RootFolderPath) - if !strings.Contains(cleaned, "/Documents") { - return fmt.Errorf("root_folder_path must contain the \"/Documents\" segment, got %q", d.RootFolderPath) - } + // Validate the RootFolderPath format. It is a SharePoint server-relative + // path, so it must be absolute; the document library segment is site + // language dependent and therefore not checked here. + if d.hasCustomRoot() && !strings.HasPrefix(d.RootFolderPath, "/") { + return fmt.Errorf("root_folder_path must be an absolute path, got %q", d.RootFolderPath) } return nil } +// hasCustomRoot reports whether the user configured a non-root RootFolderPath. +func (d *OnedriveSharelink) hasCustomRoot() bool { + return d.RootFolderPath != "" && d.RootFolderPath != "/" +} + func (d *OnedriveSharelink) Drop(ctx context.Context) error { return nil } @@ -103,19 +104,22 @@ func (d *OnedriveSharelink) Drop(ctx context.Context) error { // relativePath converts the full virtual path from OpenList to a path relative // to the shared folder's root. When root_folder_path is configured, OpenList // passes the full path including that prefix. -func (d *OnedriveSharelink) relativePath(virtualPath string) string { - if d.RootFolderPath == "" || d.RootFolderPath == "/" { - return virtualPath +func (d *OnedriveSharelink) relativePath(virtualPath string) (string, error) { + if !d.hasCustomRoot() { + return virtualPath, nil } - if rel, ok := stripPrefix(virtualPath, d.RootFolderPath); ok { - return rel + rel, ok := stripPrefix(virtualPath, d.RootFolderPath) + if !ok { + return "", fmt.Errorf("path %q is outside configured root %q", virtualPath, d.RootFolderPath) } - log.Warnf("onedrive_sharelink: path %q is outside configured root %q", virtualPath, d.RootFolderPath) - return virtualPath + return rel, nil } func (d *OnedriveSharelink) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) { - relPath := d.relativePath(dir.GetPath()) + relPath, err := d.relativePath(dir.GetPath()) + if err != nil { + return nil, err + } files, err := d.getFiles(ctx, relPath) if err != nil { return nil, err @@ -170,11 +174,15 @@ func (d *OnedriveSharelink) Link(ctx context.Context, file model.Obj, args model } func (d *OnedriveSharelink) MakeDir(ctx context.Context, parentDir model.Obj, dirName string) error { + relPath, err := d.relativePath(parentDir.GetPath()) + if err != nil { + return err + } token, err := d.getValidDriveAccessToken(ctx) if err != nil { return err } - apiURL := injectAccessToken(d.drivePathAPIURL(d.relativePath(parentDir.GetPath()))+"/children", token) + apiURL := injectAccessToken(d.drivePathAPIURL(relPath)+"/children", token) body := map[string]any{ "name": dirName, "folder": map[string]any{}, @@ -217,7 +225,11 @@ func (d *OnedriveSharelink) Remove(ctx context.Context, obj model.Obj) error { } func (d *OnedriveSharelink) Put(ctx context.Context, dstDir model.Obj, stream model.FileStreamer, up driver.UpdateProgress) error { - info, err := d.createUploadInfo(ctx, stdpath.Join(d.relativePath(dstDir.GetPath()), stream.GetName()), stream.GetSize()) + relPath, err := d.relativePath(dstDir.GetPath()) + if err != nil { + return err + } + info, err := d.createUploadInfo(ctx, stdpath.Join(relPath, stream.GetName()), stream.GetSize()) if err != nil { return err } @@ -323,7 +335,11 @@ func (d *OnedriveSharelink) GetDirectUploadInfo(ctx context.Context, tool string if tool != "HttpDirect" { return nil, errs.NotImplement } - return d.createUploadInfo(ctx, stdpath.Join(d.relativePath(dstDir.GetPath()), fileName), fileSize) + relPath, err := d.relativePath(dstDir.GetPath()) + if err != nil { + return nil, err + } + return d.createUploadInfo(ctx, stdpath.Join(relPath, fileName), fileSize) } func (d *OnedriveSharelink) createUploadInfo(ctx context.Context, path string, fileSize int64) (*model.HttpDirectUploadInfo, error) { @@ -467,7 +483,7 @@ func (d *OnedriveSharelink) drivePathAPIURL(path string) string { base := d.driveRootPath // When RootFolderPath is configured, the base drive root needs to take it // into account so the API targets the correct subfolder. - if d.RootFolderPath != "" && d.RootFolderPath != "/" { + if d.hasCustomRoot() { base = d.effectiveDriveRootPath() } drivePath := utils.FixAndCleanPath(stdpath.Join(base, path)) @@ -482,7 +498,7 @@ func (d *OnedriveSharelink) drivePathAPIURL(path string) string { // path (e.g. /personal/user/Documents/subfolder); this method extracts the // portion beyond the document library root (e.g. /subfolder). func (d *OnedriveSharelink) effectiveDriveRootPath() string { - if d.listURL == "" || d.RootFolderPath == "" || d.RootFolderPath == "/" { + if d.listURL == "" || !d.hasCustomRoot() { return d.driveRootPath } if rel, ok := stripPrefix(d.RootFolderPath, d.listURL); ok { diff --git a/drivers/onedrive_sharelink/driver_test.go b/drivers/onedrive_sharelink/driver_test.go index a0763b16ee..01af1d215f 100644 --- a/drivers/onedrive_sharelink/driver_test.go +++ b/drivers/onedrive_sharelink/driver_test.go @@ -9,21 +9,27 @@ func TestRelativePath(t *testing.T) { root string virtualPath string want string + wantErr bool }{ - {"empty root", "", "/a/b", "/a/b"}, - {"slash root", "/", "/a/b", "/a/b"}, - {"exact root", root, root, "/"}, - {"child", root, root + "/a", "/a"}, - {"grandchild", root, root + "/a/b/c", "/a/b/c"}, - {"dirty child", root, root + "/a/../b", "/b"}, - {"prefix collision", root, root + "A/x", root + "A/x"}, - {"outside root", root, "/other", "/other"}, + {"empty root", "", "/a/b", "/a/b", false}, + {"slash root", "/", "/a/b", "/a/b", false}, + {"exact root", root, root, "/", false}, + {"child", root, root + "/a", "/a", false}, + {"grandchild", root, root + "/a/b/c", "/a/b/c", false}, + {"dirty child", root, root + "/a/../b", "/b", false}, + {"non-english library", "/personal/user/文档/sub", "/personal/user/文档/sub/a", "/a", false}, + {"prefix collision", root, root + "A/x", "", true}, + {"outside root", root, "/other", "", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { d := &OnedriveSharelink{} d.RootFolderPath = tt.root - if got := d.relativePath(tt.virtualPath); got != tt.want { + got, err := d.relativePath(tt.virtualPath) + if (err != nil) != tt.wantErr { + t.Fatalf("relativePath(%q) error = %v, wantErr %v", tt.virtualPath, err, tt.wantErr) + } + if err == nil && got != tt.want { t.Errorf("relativePath(%q) = %q, want %q", tt.virtualPath, got, tt.want) } }) @@ -74,6 +80,7 @@ func TestDrivePathAPIURL(t *testing.T) { {"no root, child", "", "", "/a", drive + "/root:/a:"}, {"root equals list", "/personal/user/Documents", "/personal/user/Documents", "/a", drive + "/root:/a:"}, {"root under list, child", "/personal/user/Documents/sub", "/personal/user/Documents", "/a", drive + "/root:/sub/a:"}, + {"custom library, child", "/sites/x/Shared Documents/sub", "/sites/x/Shared Documents", "/a", drive + "/root:/sub/a:"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/drivers/onedrive_sharelink/util.go b/drivers/onedrive_sharelink/util.go index 364e0761b7..0d68895aab 100644 --- a/drivers/onedrive_sharelink/util.go +++ b/drivers/onedrive_sharelink/util.go @@ -260,13 +260,14 @@ func (d *OnedriveSharelink) getFiles(ctx context.Context, path string) ([]Item, return nil, err } // If the user configured a root_folder_path, use it as the initial root - // folder so the GraphQL query targets the correct subdirectory. - if d.RootFolderPath != "" && d.RootFolderPath != "/" { + // folder so the GraphQL query targets the correct subdirectory. The + // document library root is still derived from the share link itself, so it + // does not depend on the configured path. + relativePath := strings.Split(rootFolder, "Documents")[0] + "Documents" + if d.hasCustomRoot() { rootFolder = utils.FixAndCleanPath(d.RootFolderPath) } log.Debugln("rootFolder:", rootFolder) - // Extract the relative path up to and including "Documents" - relativePath := strings.Split(rootFolder, "Documents")[0] + "Documents" // URL encode the relative path relativeUrl := url.QueryEscape(relativePath)