Skip to content

Commit 9737006

Browse files
syscccodex
andauthored
feat(onedrive_sharelink): support direct download redirects (#2591)
fix(onedrive_sharelink): support direct download redirects Resolve SharePoint item metadata to obtain @content.downloadUrl for redirect downloads. Co-authored-by: syscc <syscc@users.noreply.github.com> Co-authored-by: Codex <codex@openai.com>
1 parent 054db9f commit 9737006

3 files changed

Lines changed: 134 additions & 17 deletions

File tree

drivers/onedrive_sharelink/driver.go

Lines changed: 107 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package onedrive_sharelink
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
67
"io"
78
"net/http"
@@ -21,7 +22,10 @@ import (
2122
log "github.com/sirupsen/logrus"
2223
)
2324

24-
const headerTTL = 25 * time.Minute
25+
const (
26+
headerTTL = 25 * time.Minute
27+
directLinkTTL = 20 * time.Minute
28+
)
2529

2630
type OnedriveSharelink struct {
2731
model.Storage
@@ -99,6 +103,18 @@ func (d *OnedriveSharelink) Link(ctx context.Context, file model.Obj, args model
99103
return nil, err
100104
}
101105

106+
if args.Redirect {
107+
directURL, err := d.resolveDirectDownloadURL(ctx, file, url, header)
108+
if err != nil {
109+
return nil, err
110+
}
111+
expiration := directLinkTTL
112+
return &model.Link{
113+
URL: directURL,
114+
Expiration: &expiration,
115+
}, nil
116+
}
117+
102118
return &model.Link{
103119
URL: url,
104120
Header: header,
@@ -194,6 +210,96 @@ func cloneHeader(header http.Header) http.Header {
194210
return header.Clone()
195211
}
196212

213+
func (d *OnedriveSharelink) resolveDirectDownloadURL(ctx context.Context, file model.Obj, rawURL string, header http.Header) (string, error) {
214+
var errs []error
215+
if obj, ok := unwrapObject(file); ok {
216+
if obj.SPItemURL != "" {
217+
directURL, err := d.resolveSPItemDownloadURL(ctx, obj.SPItemURL, header)
218+
if err == nil {
219+
return directURL, nil
220+
}
221+
errs = append(errs, err)
222+
}
223+
if obj.ContentDownloadURL != "" {
224+
return obj.ContentDownloadURL, nil
225+
}
226+
}
227+
228+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
229+
if err != nil {
230+
return "", err
231+
}
232+
req.Header = cloneHeader(header)
233+
if req.Header == nil {
234+
req.Header = http.Header{}
235+
}
236+
237+
resp, err := NewNoRedirectCLient().Do(req)
238+
if err != nil {
239+
return "", err
240+
}
241+
defer resp.Body.Close()
242+
243+
location := resp.Header.Get("Location")
244+
if location == "" {
245+
errs = append(errs, fmt.Errorf("download.aspx returned no redirect location, status code: %d", resp.StatusCode))
246+
return "", fmt.Errorf("onedrive_sharelink: direct download URL unavailable: %v", errs)
247+
}
248+
u, err := req.URL.Parse(location)
249+
if err != nil {
250+
return "", err
251+
}
252+
return u.String(), nil
253+
}
254+
255+
type spItemDownloadResp struct {
256+
ContentDownloadURL string `json:"@content.downloadUrl"`
257+
}
258+
259+
func unwrapObject(obj model.Obj) (*Object, bool) {
260+
for {
261+
switch o := obj.(type) {
262+
case *Object:
263+
return o, true
264+
case model.ObjUnwrap:
265+
obj = o.Unwrap()
266+
default:
267+
return nil, false
268+
}
269+
}
270+
}
271+
272+
func (d *OnedriveSharelink) resolveSPItemDownloadURL(ctx context.Context, spItemURL string, header http.Header) (string, error) {
273+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, spItemURL, nil)
274+
if err != nil {
275+
return "", err
276+
}
277+
req.Header = cloneHeader(header)
278+
if req.Header == nil {
279+
req.Header = http.Header{}
280+
}
281+
req.Header.Set("Accept", "application/json;odata.metadata=minimal")
282+
283+
resp, err := NewNoRedirectCLient().Do(req)
284+
if err != nil {
285+
return "", err
286+
}
287+
defer resp.Body.Close()
288+
289+
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
290+
return "", fmt.Errorf("sp item metadata request failed, status code: %d", resp.StatusCode)
291+
}
292+
293+
var data spItemDownloadResp
294+
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
295+
return "", err
296+
}
297+
if data.ContentDownloadURL == "" {
298+
return "", fmt.Errorf("sp item metadata response missing @content.downloadUrl")
299+
}
300+
return data.ContentDownloadURL, nil
301+
}
302+
197303
func (d *OnedriveSharelink) headerSnapshot() http.Header {
198304
d.headerMu.RLock()
199305
defer d.headerMu.RUnlock()

drivers/onedrive_sharelink/meta.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ type Addition struct {
1919

2020
var config = driver.Config{
2121
Name: "Onedrive Sharelink",
22-
OnlyProxy: true,
2322
NoUpload: true,
2423
DefaultRoot: "/",
2524
}

drivers/onedrive_sharelink/types.go

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,30 +23,42 @@ type FolderResp struct {
2323

2424
// Item represents an individual item in the folder.
2525
type Item struct {
26-
ObjType string `json:"FSObjType"` // ObjType indicates if the item is a file or folder.
27-
Name string `json:"FileLeafRef"` // Name is the name of the item.
28-
ModifiedTime time.Time `json:"Modified."` // ModifiedTime is the last modified time of the item.
29-
Size string `json:"File_x0020_Size"` // Size is the size of the item in string format.
30-
Id string `json:"UniqueId"` // Id is the unique identifier of the item.
26+
ObjType string `json:"FSObjType"` // ObjType indicates if the item is a file or folder.
27+
Name string `json:"FileLeafRef"` // Name is the name of the item.
28+
ModifiedTime time.Time `json:"Modified."` // ModifiedTime is the last modified time of the item.
29+
Size string `json:"File_x0020_Size"` // Size is the size of the item in string format.
30+
Id string `json:"UniqueId"` // Id is the unique identifier of the item.
31+
SPItemURL string `json:".spItemUrl"` // SPItemURL points to the SharePoint item metadata API.
32+
ContentDownloadURL string `json:"@content.downloadUrl"` // ContentDownloadURL is a temporary cookie-free download URL.
3133
}
3234

33-
// fileToObj converts an Item to an ObjThumb.
34-
func fileToObj(f Item) *model.ObjThumb {
35+
type Object struct {
36+
model.ObjThumb
37+
SPItemURL string
38+
ContentDownloadURL string
39+
}
40+
41+
// fileToObj converts an Item to an Object.
42+
func fileToObj(f Item) *Object {
3543
// Convert Size from string to int64.
3644
size, _ := strconv.ParseInt(f.Size, 10, 64)
3745
// Convert ObjType from string to int.
3846
objtype, _ := strconv.Atoi(f.ObjType)
3947

4048
// Create a new ObjThumb with the converted values.
41-
file := &model.ObjThumb{
42-
Object: model.Object{
43-
Name: f.Name,
44-
Modified: f.ModifiedTime,
45-
Size: size,
46-
IsFolder: objtype == 1, // Check if the item is a folder.
47-
ID: f.Id,
49+
file := &Object{
50+
ObjThumb: model.ObjThumb{
51+
Object: model.Object{
52+
Name: f.Name,
53+
Modified: f.ModifiedTime,
54+
Size: size,
55+
IsFolder: objtype == 1, // Check if the item is a folder.
56+
ID: f.Id,
57+
},
58+
Thumbnail: model.Thumbnail{},
4859
},
49-
Thumbnail: model.Thumbnail{},
60+
SPItemURL: f.SPItemURL,
61+
ContentDownloadURL: f.ContentDownloadURL,
5062
}
5163
return file
5264
}

0 commit comments

Comments
 (0)