Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 71 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,45 @@ 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
}

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)
}
Expand Down Expand Up @@ -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{},
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down
90 changes: 90 additions & 0 deletions drivers/onedrive_sharelink/driver_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
6 changes: 6 additions & 0 deletions drivers/onedrive_sharelink/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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"
Expand Down