194 lines
5.4 KiB
Go
194 lines
5.4 KiB
Go
|
|
package services
|
||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"encoding/xml"
|
||
|
|
"fmt"
|
||
|
|
"io"
|
||
|
|
"net/http"
|
||
|
|
"path"
|
||
|
|
"strings"
|
||
|
|
"time"
|
||
|
|
)
|
||
|
|
|
||
|
|
type webDAVStorageBackend struct {
|
||
|
|
cfg StorageBackendConfig
|
||
|
|
client *http.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) ID() string { return b.cfg.ID }
|
||
|
|
func (b webDAVStorageBackend) Type() string { return StorageBackendWebDAV }
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) Put(ctx context.Context, key string, body io.Reader, _ int64, contentType string) error {
|
||
|
|
if err := b.mkcolParents(ctx, key); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
request, err := b.request(ctx, http.MethodPut, key, body)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if contentType != "" {
|
||
|
|
request.Header.Set("Content-Type", contentType)
|
||
|
|
}
|
||
|
|
response, err := b.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer response.Body.Close()
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
return fmt.Errorf("webdav put failed: %s", response.Status)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) Get(ctx context.Context, key string) (StorageObject, error) {
|
||
|
|
request, err := b.request(ctx, http.MethodGet, key, nil)
|
||
|
|
if err != nil {
|
||
|
|
return StorageObject{}, err
|
||
|
|
}
|
||
|
|
response, err := b.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return StorageObject{}, err
|
||
|
|
}
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
response.Body.Close()
|
||
|
|
return StorageObject{}, fmt.Errorf("webdav get failed: %s", response.Status)
|
||
|
|
}
|
||
|
|
modTime, _ := time.Parse(http.TimeFormat, response.Header.Get("Last-Modified"))
|
||
|
|
return StorageObject{Key: key, Size: response.ContentLength, ContentType: response.Header.Get("Content-Type"), ModTime: modTime, Body: response.Body}, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) Delete(ctx context.Context, key string) error {
|
||
|
|
return b.deletePath(ctx, key)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) DeletePrefix(ctx context.Context, prefix string) error {
|
||
|
|
return b.deletePath(ctx, strings.TrimSuffix(prefix, "/")+"/")
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) Usage(ctx context.Context) (int64, error) {
|
||
|
|
request, err := b.request(ctx, "PROPFIND", "", nil)
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
request.Header.Set("Depth", "infinity")
|
||
|
|
request.Header.Set("Content-Type", "application/xml")
|
||
|
|
response, err := b.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
defer response.Body.Close()
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
return 0, fmt.Errorf("webdav usage failed: %s", response.Status)
|
||
|
|
}
|
||
|
|
var multi webDAVMultiStatus
|
||
|
|
if err := xml.NewDecoder(response.Body).Decode(&multi); err != nil {
|
||
|
|
return 0, err
|
||
|
|
}
|
||
|
|
var total int64
|
||
|
|
for _, item := range multi.Responses {
|
||
|
|
if item.PropStat.Prop.ResourceType.Collection != nil {
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
total += item.PropStat.Prop.ContentLength
|
||
|
|
}
|
||
|
|
return total, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) Test(ctx context.Context) error {
|
||
|
|
key := ".warpbox-storage-test-" + randomID(6)
|
||
|
|
if err := b.Put(ctx, key, strings.NewReader("ok"), 2, "text/plain"); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
return b.Delete(ctx, key)
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) deletePath(ctx context.Context, key string) error {
|
||
|
|
request, err := b.request(ctx, http.MethodDelete, key, nil)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
response, err := b.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
defer response.Body.Close()
|
||
|
|
if response.StatusCode == http.StatusNotFound {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||
|
|
return fmt.Errorf("webdav delete failed: %s", response.Status)
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) mkcolParents(ctx context.Context, key string) error {
|
||
|
|
dir := path.Dir(cleanObjectKey(key))
|
||
|
|
if dir == "." || dir == "/" {
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
parts := strings.Split(strings.Trim(dir, "/"), "/")
|
||
|
|
current := ""
|
||
|
|
for _, part := range parts {
|
||
|
|
current = path.Join(current, part)
|
||
|
|
request, err := b.request(ctx, "MKCOL", strings.TrimSuffix(current, "/")+"/", nil)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
response, err := b.client.Do(request)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
response.Body.Close()
|
||
|
|
if response.StatusCode != http.StatusCreated && response.StatusCode != http.StatusMethodNotAllowed && response.StatusCode != http.StatusConflict {
|
||
|
|
return fmt.Errorf("webdav mkcol failed: %s", response.Status)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func (b webDAVStorageBackend) request(ctx context.Context, method, key string, body io.Reader) (*http.Request, error) {
|
||
|
|
endpoint := strings.TrimRight(b.cfg.Endpoint, "/")
|
||
|
|
if endpoint == "" {
|
||
|
|
return nil, fmt.Errorf("webdav url is required")
|
||
|
|
}
|
||
|
|
remote := path.Join(cleanRemoteRoot(b.cfg.RemotePath), cleanObjectKey(key))
|
||
|
|
if strings.HasSuffix(key, "/") && !strings.HasSuffix(remote, "/") {
|
||
|
|
remote += "/"
|
||
|
|
}
|
||
|
|
target := endpoint + "/" + strings.TrimLeft(remote, "/")
|
||
|
|
request, err := http.NewRequestWithContext(ctx, method, target, body)
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
if b.cfg.Username != "" || b.cfg.Password != "" {
|
||
|
|
request.SetBasicAuth(b.cfg.Username, b.cfg.Password)
|
||
|
|
}
|
||
|
|
return request, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
type webDAVMultiStatus struct {
|
||
|
|
Responses []webDAVResponse `xml:"response"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type webDAVResponse struct {
|
||
|
|
PropStat webDAVPropStat `xml:"propstat"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type webDAVPropStat struct {
|
||
|
|
Prop webDAVProp `xml:"prop"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type webDAVProp struct {
|
||
|
|
ContentLength int64 `xml:"getcontentlength"`
|
||
|
|
ResourceType webDAVResourceType `xml:"resourcetype"`
|
||
|
|
}
|
||
|
|
|
||
|
|
type webDAVResourceType struct {
|
||
|
|
Collection *struct{} `xml:"collection"`
|
||
|
|
}
|
||
|
|
|
||
|
|
func newWebDAVStorageBackend(cfg StorageBackendConfig) webDAVStorageBackend {
|
||
|
|
return webDAVStorageBackend{cfg: cfg, client: http.DefaultClient}
|
||
|
|
}
|