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
17 changes: 16 additions & 1 deletion drivers/onedrive_app/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,22 @@ func (d *OnedriveAPP) Init(ctx context.Context) error {
if d.ChunkSize < 1 {
d.ChunkSize = 5
}
return d.accessToken()
if d.IsSharepoint && d.SiteID == "" {
return fmt.Errorf("site_id is required when is_sharepoint is enabled")
}
if !d.IsSharepoint && d.Email == "" {
return fmt.Errorf("email is required when is_sharepoint is disabled")
}
if d.IsSharepoint {
d.SiteID = NormalizeSiteID(d.SiteID)
}
if err := d.accessToken(); err != nil {
return err
}
if d.IsSharepoint {
return d.resolveSiteID()
}
return nil
}

func (d *OnedriveAPP) Drop(ctx context.Context) error {
Expand Down
2 changes: 2 additions & 0 deletions drivers/onedrive_app/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type Addition struct {
ClientSecret string `json:"client_secret" required:"true"`
TenantID string `json:"tenant_id"`
Email string `json:"email"`
IsSharepoint bool `json:"is_sharepoint" default:"false" help:"Mount a SharePoint site's default document library instead of a user's OneDrive"`
SiteID string `json:"site_id" help:"One of: <host>,<siteGuid>,<webGuid>; <host>:/sites/<name>; or a full site URL. Point it at the site that owns the library — a bare host resolves to the empty root-site library"`
ChunkSize int64 `json:"chunk_size" type:"number" default:"5"`
CustomHost string `json:"custom_host" help:"Custom host for onedrive download link"`
DisableDiskUsage bool `json:"disable_disk_usage" default:"false"`
Expand Down
84 changes: 81 additions & 3 deletions drivers/onedrive_app/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ import (
"fmt"
"io"
"net/http"
"net/url"
stdpath "path"
"strings"
"time"

"github.com/OpenListTeam/OpenList/v4/drivers/base"
Expand Down Expand Up @@ -40,16 +42,87 @@ var onedriveHostMap = map[string]Host{
},
}

// NormalizeSiteID converts any accepted SharePoint site identifier into the form
// expected by the /sites/{site-id} segment.
//
// Accepted inputs:
//
// contoso.sharepoint.com,<siteGuid>,<webGuid>
// contoso.sharepoint.com:/sites/<name> [/sites/<name>:]
// https://contoso.sharepoint.com/sites/<name>
// https://contoso.sharepoint.com (site collection root)
//
// Graph has two addressing forms: /sites/{hostname} for the root site of a host, and
// /sites/{hostname}:/{path}: for a site below it. Only the path form carries a colon,
// so an empty path must not produce one.
// A path-form identifier still has to be resolved to the comma form before it can
// be followed by /drive; see resolveSiteID.
func NormalizeSiteID(siteID string) string {
siteID = strings.TrimSpace(siteID)
if siteID == "" {
return ""
}
var host, path string
switch {
case strings.HasPrefix(siteID, "https://"), strings.HasPrefix(siteID, "http://"):
u, err := url.Parse(siteID)
if err != nil || u.Host == "" {
return siteID
}
host, path = u.Host, u.Path
case strings.Contains(siteID, ":/"):
host, path, _ = strings.Cut(siteID, ":/")
default:
// 已解析的复合 ID 或纯主机名
return strings.TrimSuffix(siteID, "/")
}
if path = strings.Trim(path, "/:"); path != "" {
return host + ":/" + path + ":"
}
return host
}

// resolveSiteID turns a site path identifier into the opaque comma-separated site
// id, since Graph only accepts /sites/{path}:/ as a terminal segment — it cannot be
// followed by /drive. Already-resolved ids are left untouched.
func (d *OnedriveAPP) resolveSiteID() error {
siteID := NormalizeSiteID(d.SiteID)
if strings.Contains(siteID, ",") {
d.SiteID = siteID
return nil
}
host := onedriveHostMap[d.Region]
var resp struct {
Id string `json:"id"`
}
_, err := d.Request(fmt.Sprintf("%s/v1.0/sites/%s?$select=id", host.Api, siteID), http.MethodGet, nil, &resp)
if err != nil {
return err
}
if resp.Id == "" {
return fmt.Errorf("failed to resolve sharepoint site %q", d.SiteID)
}
d.SiteID = resp.Id
op.MustSaveDriverStorage(d)
return nil
}

func (d *OnedriveAPP) GetMetaUrl(auth bool, path string) string {
host := onedriveHostMap[d.Region]
path = utils.EncodePath(path, true)
if auth {
return host.Oauth
}
var base string
if d.IsSharepoint {
base = fmt.Sprintf("%s/v1.0/sites/%s/drive/root", host.Api, NormalizeSiteID(d.SiteID))
} else {
base = fmt.Sprintf("%s/v1.0/users/%s/drive/root", host.Api, d.Email)
}
if path == "/" || path == "\\" {
return fmt.Sprintf("%s/v1.0/users/%s/drive/root", host.Api, d.Email)
return base
}
return fmt.Sprintf("%s/v1.0/users/%s/drive/root:%s:", host.Api, d.Email, path)
return base + ":" + path + ":"
}

func (d *OnedriveAPP) accessToken() error {
Expand Down Expand Up @@ -213,7 +286,12 @@ func (d *OnedriveAPP) upBig(ctx context.Context, dstDir model.Obj, stream model.

func (d *OnedriveAPP) getDrive(ctx context.Context) (*DriveResp, error) {
host, _ := onedriveHostMap[d.Region]
api := fmt.Sprintf("%s/v1.0/users/%s/drive", host.Api, d.Email)
var api string
if d.IsSharepoint {
api = fmt.Sprintf("%s/v1.0/sites/%s/drive", host.Api, NormalizeSiteID(d.SiteID))
} else {
api = fmt.Sprintf("%s/v1.0/users/%s/drive", host.Api, d.Email)
}
var resp DriveResp
_, err := d.Request(api, http.MethodGet, func(req *resty.Request) {
req.SetContext(ctx)
Expand Down
112 changes: 112 additions & 0 deletions drivers/onedrive_app/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package onedrive_app

import "testing"

func TestNormalizeSiteID(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{
name: "already resolved",
in: "contoso.sharepoint.com,11111111-2222-3333-4444-555555555555,66666666-7777-8888-9999-aaaaaaaaaaaa",
want: "contoso.sharepoint.com,11111111-2222-3333-4444-555555555555,66666666-7777-8888-9999-aaaaaaaaaaaa",
},
{
name: "web url",
in: "https://contoso.sharepoint.com/sites/team",
want: "contoso.sharepoint.com:/sites/team:",
},
{
name: "web url with trailing slash",
in: "https://contoso.sharepoint.com/sites/team/",
want: "contoso.sharepoint.com:/sites/team:",
},
{
name: "web url at site collection root",
in: "https://contoso.sharepoint.com/",
want: "contoso.sharepoint.com",
},
{
name: "web url without trailing slash at root",
in: "https://contoso.sharepoint.com",
want: "contoso.sharepoint.com",
},
{
name: "bare hostname",
in: "contoso.sharepoint.com",
want: "contoso.sharepoint.com",
},
{
name: "hostname with trailing slash",
in: "contoso.sharepoint.com/",
want: "contoso.sharepoint.com",
},
{
name: "path form without colon",
in: "contoso.sharepoint.com:/sites/team",
want: "contoso.sharepoint.com:/sites/team:",
},
{
name: "path form with colon and slash",
in: "contoso.sharepoint.com:/sites/team/:",
want: "contoso.sharepoint.com:/sites/team:",
},
{
name: "path form of host root",
in: "contoso.sharepoint.com:/",
want: "contoso.sharepoint.com",
},
{
name: "surrounding whitespace",
in: " contoso.sharepoint.com:/sites/team: ",
want: "contoso.sharepoint.com:/sites/team:",
},
{
name: "empty",
in: "",
want: "",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := NormalizeSiteID(c.in); got != c.want {
t.Fatalf("NormalizeSiteID(%q) = %q, want %q", c.in, got, c.want)
}
})
}
}

func TestGetMetaUrlSharepoint(t *testing.T) {
d := &OnedriveAPP{
Addition: Addition{
Region: "global",
IsSharepoint: true,
SiteID: "contoso.sharepoint.com,11111111-2222-3333-4444-555555555555,66666666-7777-8888-9999-aaaaaaaaaaaa",
},
}
const wantRoot = "https://graph.microsoft.com/v1.0/sites/contoso.sharepoint.com,11111111-2222-3333-4444-555555555555,66666666-7777-8888-9999-aaaaaaaaaaaa/drive/root"
if got := d.GetMetaUrl(false, "/"); got != wantRoot {
t.Fatalf("root url = %q, want %q", got, wantRoot)
}
if got, want := d.GetMetaUrl(false, "/a b.txt"), wantRoot+":/a%20b.txt:"; got != want {
t.Fatalf("file url = %q, want %q", got, want)
}
}

func TestGetMetaUrlUserDrive(t *testing.T) {
d := &OnedriveAPP{
Addition: Addition{
Region: "global",
Email: "user@contoso.com",
},
}
const wantRoot = "https://graph.microsoft.com/v1.0/users/user@contoso.com/drive/root"
if got := d.GetMetaUrl(false, "/"); got != wantRoot {
t.Fatalf("root url = %q, want %q", got, wantRoot)
}
if got, want := d.GetMetaUrl(false, "/dir"), wantRoot+":/dir:"; got != want {
t.Fatalf("dir url = %q, want %q", got, want)
}
}
Loading