From 651b3035094ddc72b314a61b742d6b26a1e3f401 Mon Sep 17 00:00:00 2001 From: xireiki Date: Sun, 26 Jul 2026 08:17:41 +0800 Subject: [PATCH 1/3] 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 a975772ac..7ecf1501f 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 13785939f..9fc864151 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/3] 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 7ecf1501f..6692ce920 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/3] 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 6692ce920..469f302a5 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 000000000..a0763b16e --- /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 9fc864151..364e0761b 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"