Skip to content
94 changes: 87 additions & 7 deletions drivers/onedrive_sharelink/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type OnedriveSharelink struct {

headerMu sync.RWMutex
sg singleflight.Group[http.Header]

listURL string
}

func (d *OnedriveSharelink) Config() driver.Config {
Expand Down Expand Up @@ -80,19 +82,49 @@ func (d *OnedriveSharelink) Init(ctx context.Context) error {
}
d.storeHeaders(h)

// 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
}

// 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, error) {
if !d.hasCustomRoot() {
return virtualPath, nil
}
rel, ok := stripPrefix(virtualPath, d.RootFolderPath)
if !ok {
return "", fmt.Errorf("path %q is outside configured root %q", virtualPath, d.RootFolderPath)
}
return rel, nil
}

func (d *OnedriveSharelink) List(ctx context.Context, dir model.Obj, args model.ListArgs) ([]model.Obj, error) {
files, err := d.getFiles(ctx, 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
}
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)
}
Expand Down Expand Up @@ -142,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(parentDir.GetPath())+"/children", token)
apiURL := injectAccessToken(d.drivePathAPIURL(relPath)+"/children", token)
body := map[string]any{
"name": dirName,
"folder": map[string]any{},
Expand Down Expand Up @@ -189,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(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
}
Expand Down Expand Up @@ -295,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(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) {
Expand Down Expand Up @@ -436,14 +480,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.hasCustomRoot() {
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.hasCustomRoot() {
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
Expand Down Expand Up @@ -561,6 +640,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
}
Expand Down
97 changes: 97 additions & 0 deletions drivers/onedrive_sharelink/driver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
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
wantErr bool
}{
{"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
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)
}
})
}
}

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:"},
{"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) {
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)
}
})
}
}
11 changes: 9 additions & 2 deletions drivers/onedrive_sharelink/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/OpenListTeam/OpenList/v4/drivers/base"
internalNet "github.com/OpenListTeam/OpenList/v4/internal/net"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
log "github.com/sirupsen/logrus"
"golang.org/x/net/html"
)
Expand Down Expand Up @@ -260,9 +261,15 @@ func (d *OnedriveSharelink) getFiles(ctx context.Context, path string) ([]Item,
if err != nil {
return nil, err
}
log.Debugln("rootFolder:", rootFolder)
// Extract the relative path up to and including "Documents"
// If the user configured a root_folder_path, use it as the initial root
// 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)

// URL encode the relative path
relativeUrl := url.QueryEscape(relativePath)
Expand Down