diff --git a/drivers/onedrive_sharelink/driver.go b/drivers/onedrive_sharelink/driver.go index a975772ac0..469f302a58 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 { @@ -80,6 +82,17 @@ 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) + } + } + return nil } @@ -87,12 +100,27 @@ 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 + } + 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 +} + 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 +174,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 +217,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 +323,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) { @@ -436,14 +464,49 @@ func (d *OnedriveSharelink) uploadSessionChunk(ctx context.Context, uploadURL st } func (d *OnedriveSharelink) drivePathAPIURL(path string) string { - drivePath := stdpath.Join(d.driveRootPath, path) - drivePath = utils.FixAndCleanPath(drivePath) + 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 != "/" { + base = d.effectiveDriveRootPath() + } + drivePath := utils.FixAndCleanPath(stdpath.Join(base, path)) if drivePath == "/" { return d.DriveURL + "/root" } 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 + } + 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 @@ -561,6 +624,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/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 13785939fb..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" ) @@ -258,6 +259,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 = utils.FixAndCleanPath(d.RootFolderPath) + } log.Debugln("rootFolder:", rootFolder) // Extract the relative path up to and including "Documents" relativePath := strings.Split(rootFolder, "Documents")[0] + "Documents"