Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
alist-org
GitHub Repository: alist-org/alist
Path: blob/main/pkg/gowebdav/client.go
1560 views
1
package gowebdav
2
3
import (
4
"bytes"
5
"encoding/xml"
6
"fmt"
7
"github.com/alist-org/alist/v3/pkg/utils"
8
"io"
9
"net/http"
10
"net/url"
11
"os"
12
pathpkg "path"
13
"strings"
14
"sync"
15
"time"
16
)
17
18
// Client defines our structure
19
type Client struct {
20
root string
21
headers http.Header
22
interceptor func(method string, rq *http.Request)
23
c *http.Client
24
25
authMutex sync.Mutex
26
auth Authenticator
27
}
28
29
// Authenticator stub
30
type Authenticator interface {
31
Type() string
32
User() string
33
Pass() string
34
Authorize(*http.Request, string, string)
35
}
36
37
// NoAuth structure holds our credentials
38
type NoAuth struct {
39
user string
40
pw string
41
}
42
43
// Type identifies the authenticator
44
func (n *NoAuth) Type() string {
45
return "NoAuth"
46
}
47
48
// User returns the current user
49
func (n *NoAuth) User() string {
50
return n.user
51
}
52
53
// Pass returns the current password
54
func (n *NoAuth) Pass() string {
55
return n.pw
56
}
57
58
// Authorize the current request
59
func (n *NoAuth) Authorize(req *http.Request, method string, path string) {
60
}
61
62
// NewClient creates a new instance of client
63
func NewClient(uri, user, pw string) *Client {
64
return &Client{FixSlash(uri), make(http.Header), nil, &http.Client{}, sync.Mutex{}, &NoAuth{user, pw}}
65
}
66
67
// SetHeader lets us set arbitrary headers for a given client
68
func (c *Client) SetHeader(key, value string) {
69
c.headers.Add(key, value)
70
}
71
72
// SetInterceptor lets us set an arbitrary interceptor for a given client
73
func (c *Client) SetInterceptor(interceptor func(method string, rq *http.Request)) {
74
c.interceptor = interceptor
75
}
76
77
// SetTimeout exposes the ability to set a time limit for requests
78
func (c *Client) SetTimeout(timeout time.Duration) {
79
c.c.Timeout = timeout
80
}
81
82
// SetTransport exposes the ability to define custom transports
83
func (c *Client) SetTransport(transport http.RoundTripper) {
84
c.c.Transport = transport
85
}
86
87
// SetJar exposes the ability to set a cookie jar to the client.
88
func (c *Client) SetJar(jar http.CookieJar) {
89
c.c.Jar = jar
90
}
91
92
// Connect connects to our dav server
93
func (c *Client) Connect() error {
94
rs, err := c.options("/")
95
if err != nil {
96
return err
97
}
98
99
err = rs.Body.Close()
100
if err != nil {
101
return err
102
}
103
104
if rs.StatusCode != 200 {
105
return newPathError("Connect", c.root, rs.StatusCode)
106
}
107
108
return nil
109
}
110
111
type props struct {
112
Status string `xml:"DAV: status"`
113
Name string `xml:"DAV: prop>displayname,omitempty"`
114
Type xml.Name `xml:"DAV: prop>resourcetype>collection,omitempty"`
115
Size string `xml:"DAV: prop>getcontentlength,omitempty"`
116
ContentType string `xml:"DAV: prop>getcontenttype,omitempty"`
117
ETag string `xml:"DAV: prop>getetag,omitempty"`
118
Modified string `xml:"DAV: prop>getlastmodified,omitempty"`
119
}
120
121
type response struct {
122
Href string `xml:"DAV: href"`
123
Props []props `xml:"DAV: propstat"`
124
}
125
126
func getProps(r *response, status string) *props {
127
for _, prop := range r.Props {
128
if strings.Contains(prop.Status, status) {
129
return &prop
130
}
131
}
132
return nil
133
}
134
135
// ReadDir reads the contents of a remote directory
136
func (c *Client) ReadDir(path string) ([]os.FileInfo, error) {
137
path = FixSlashes(path)
138
files := make([]os.FileInfo, 0)
139
skipSelf := true
140
parse := func(resp interface{}) error {
141
r := resp.(*response)
142
143
if skipSelf {
144
skipSelf = false
145
if p := getProps(r, "200"); p != nil && p.Type.Local == "collection" {
146
r.Props = nil
147
return nil
148
}
149
return newPathError("ReadDir", path, 405)
150
}
151
152
if p := getProps(r, "200"); p != nil {
153
f := new(File)
154
if ps, err := url.PathUnescape(r.Href); err == nil {
155
f.name = pathpkg.Base(ps)
156
} else {
157
f.name = p.Name
158
}
159
f.path = path + f.name
160
f.modified = parseModified(&p.Modified)
161
f.etag = p.ETag
162
f.contentType = p.ContentType
163
164
if p.Type.Local == "collection" {
165
f.path += "/"
166
f.size = 0
167
f.isdir = true
168
} else {
169
f.size = parseInt64(&p.Size)
170
f.isdir = false
171
}
172
173
files = append(files, *f)
174
}
175
176
r.Props = nil
177
return nil
178
}
179
180
err := c.propfind(path, false,
181
`<d:propfind xmlns:d='DAV:'>
182
<d:prop>
183
<d:displayname/>
184
<d:resourcetype/>
185
<d:getcontentlength/>
186
<d:getcontenttype/>
187
<d:getetag/>
188
<d:getlastmodified/>
189
</d:prop>
190
</d:propfind>`,
191
&response{},
192
parse)
193
194
if err != nil {
195
if _, ok := err.(*os.PathError); !ok {
196
err = newPathErrorErr("ReadDir", path, err)
197
}
198
}
199
return files, err
200
}
201
202
// Stat returns the file stats for a specified path
203
func (c *Client) Stat(path string) (os.FileInfo, error) {
204
var f *File
205
parse := func(resp interface{}) error {
206
r := resp.(*response)
207
if p := getProps(r, "200"); p != nil && f == nil {
208
f = new(File)
209
f.name = p.Name
210
f.path = path
211
f.etag = p.ETag
212
f.contentType = p.ContentType
213
214
if p.Type.Local == "collection" {
215
if !strings.HasSuffix(f.path, "/") {
216
f.path += "/"
217
}
218
f.size = 0
219
f.modified = time.Unix(0, 0)
220
f.isdir = true
221
} else {
222
f.size = parseInt64(&p.Size)
223
f.modified = parseModified(&p.Modified)
224
f.isdir = false
225
}
226
}
227
228
r.Props = nil
229
return nil
230
}
231
232
err := c.propfind(path, true,
233
`<d:propfind xmlns:d='DAV:'>
234
<d:prop>
235
<d:displayname/>
236
<d:resourcetype/>
237
<d:getcontentlength/>
238
<d:getcontenttype/>
239
<d:getetag/>
240
<d:getlastmodified/>
241
</d:prop>
242
</d:propfind>`,
243
&response{},
244
parse)
245
246
if err != nil {
247
if _, ok := err.(*os.PathError); !ok {
248
err = newPathErrorErr("ReadDir", path, err)
249
}
250
}
251
return f, err
252
}
253
254
// Remove removes a remote file
255
func (c *Client) Remove(path string) error {
256
return c.RemoveAll(path)
257
}
258
259
// RemoveAll removes remote files
260
func (c *Client) RemoveAll(path string) error {
261
rs, err := c.req("DELETE", path, nil, nil)
262
if err != nil {
263
return newPathError("Remove", path, 400)
264
}
265
err = rs.Body.Close()
266
if err != nil {
267
return err
268
}
269
270
if rs.StatusCode == 200 || rs.StatusCode == 204 || rs.StatusCode == 404 {
271
return nil
272
}
273
274
return newPathError("Remove", path, rs.StatusCode)
275
}
276
277
// Mkdir makes a directory
278
func (c *Client) Mkdir(path string, _ os.FileMode) (err error) {
279
path = FixSlashes(path)
280
status, err := c.mkcol(path)
281
if err != nil {
282
return
283
}
284
if status == 201 {
285
return nil
286
}
287
288
return newPathError("Mkdir", path, status)
289
}
290
291
// MkdirAll like mkdir -p, but for webdav
292
func (c *Client) MkdirAll(path string, _ os.FileMode) (err error) {
293
path = FixSlashes(path)
294
status, err := c.mkcol(path)
295
if err != nil {
296
return
297
}
298
if status == 201 {
299
return nil
300
}
301
if status == 409 {
302
paths := strings.Split(path, "/")
303
sub := "/"
304
for _, e := range paths {
305
if e == "" {
306
continue
307
}
308
sub += e + "/"
309
status, err = c.mkcol(sub)
310
if err != nil {
311
return
312
}
313
if status != 201 {
314
return newPathError("MkdirAll", sub, status)
315
}
316
}
317
return nil
318
}
319
320
return newPathError("MkdirAll", path, status)
321
}
322
323
// Rename moves a file from A to B
324
func (c *Client) Rename(oldpath, newpath string, overwrite bool) error {
325
return c.copymove("MOVE", oldpath, newpath, overwrite)
326
}
327
328
// Copy copies a file from A to B
329
func (c *Client) Copy(oldpath, newpath string, overwrite bool) error {
330
return c.copymove("COPY", oldpath, newpath, overwrite)
331
}
332
333
// Read reads the contents of a remote file
334
func (c *Client) Read(path string) ([]byte, error) {
335
var stream io.ReadCloser
336
var err error
337
338
if stream, _, err = c.ReadStream(path, nil); err != nil {
339
return nil, err
340
}
341
defer stream.Close()
342
343
buf := new(bytes.Buffer)
344
_, err = buf.ReadFrom(stream)
345
if err != nil {
346
return nil, err
347
}
348
return buf.Bytes(), nil
349
}
350
351
func (c *Client) Link(path string) (string, http.Header, error) {
352
method := "GET"
353
u := PathEscape(Join(c.root, path))
354
r, err := http.NewRequest(method, u, nil)
355
356
if err != nil {
357
return "", nil, newPathErrorErr("Link", path, err)
358
}
359
360
if c.c.Jar != nil {
361
for _, cookie := range c.c.Jar.Cookies(r.URL) {
362
r.AddCookie(cookie)
363
}
364
}
365
for k, vals := range c.headers {
366
for _, v := range vals {
367
r.Header.Add(k, v)
368
}
369
}
370
371
c.authMutex.Lock()
372
auth := c.auth
373
c.authMutex.Unlock()
374
375
auth.Authorize(r, method, path)
376
377
if c.interceptor != nil {
378
c.interceptor(method, r)
379
}
380
return r.URL.String(), r.Header, nil
381
}
382
383
// ReadStream reads the stream for a given path
384
func (c *Client) ReadStream(path string, callback func(rq *http.Request)) (io.ReadCloser, http.Header, error) {
385
rs, err := c.req("GET", path, nil, callback)
386
if err != nil {
387
return nil, nil, newPathErrorErr("ReadStream", path, err)
388
}
389
390
if rs.StatusCode < 400 {
391
return rs.Body, rs.Header, nil
392
}
393
394
rs.Body.Close()
395
return nil, nil, newPathError("ReadStream", path, rs.StatusCode)
396
}
397
398
// ReadStreamRange reads the stream representing a subset of bytes for a given path,
399
// utilizing HTTP Range Requests if the server supports it.
400
// The range is expressed as offset from the start of the file and length, for example
401
// offset=10, length=10 will return bytes 10 through 19.
402
//
403
// If the server does not support partial content requests and returns full content instead,
404
// this function will emulate the behavior by skipping `offset` bytes and limiting the result
405
// to `length`.
406
func (c *Client) ReadStreamRange(path string, offset, length int64) (io.ReadCloser, error) {
407
rs, err := c.req("GET", path, nil, func(r *http.Request) {
408
r.Header.Add("Range", fmt.Sprintf("bytes=%v-%v", offset, offset+length-1))
409
})
410
if err != nil {
411
return nil, newPathErrorErr("ReadStreamRange", path, err)
412
}
413
414
if rs.StatusCode == http.StatusPartialContent {
415
// server supported partial content, return as-is.
416
return rs.Body, nil
417
}
418
419
// server returned success, but did not support partial content, so we have the whole
420
// stream in rs.Body
421
if rs.StatusCode == 200 {
422
// discard first 'offset' bytes.
423
if _, err := utils.CopyWithBuffer(io.Discard, io.LimitReader(rs.Body, offset)); err != nil {
424
return nil, newPathErrorErr("ReadStreamRange", path, err)
425
}
426
427
// return a io.ReadCloser that is limited to `length` bytes.
428
return &limitedReadCloser{rs.Body, int(length)}, nil
429
}
430
431
rs.Body.Close()
432
return nil, newPathError("ReadStream", path, rs.StatusCode)
433
}
434
435
// Write writes data to a given path
436
func (c *Client) Write(path string, data []byte, _ os.FileMode) (err error) {
437
s, err := c.put(path, bytes.NewReader(data), nil)
438
if err != nil {
439
return
440
}
441
442
switch s {
443
444
case 200, 201, 204:
445
return nil
446
447
case 409:
448
err = c.createParentCollection(path)
449
if err != nil {
450
return
451
}
452
453
s, err = c.put(path, bytes.NewReader(data), nil)
454
if err != nil {
455
return
456
}
457
if s == 200 || s == 201 || s == 204 {
458
return
459
}
460
}
461
462
return newPathError("Write", path, s)
463
}
464
465
// WriteStream writes a stream
466
func (c *Client) WriteStream(path string, stream io.Reader, _ os.FileMode, callback func(r *http.Request)) (err error) {
467
468
err = c.createParentCollection(path)
469
if err != nil {
470
return err
471
}
472
473
s, err := c.put(path, stream, callback)
474
if err != nil {
475
return err
476
}
477
478
switch s {
479
case 200, 201, 204:
480
return nil
481
482
default:
483
return newPathError("WriteStream", path, s)
484
}
485
}
486
487