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} }