package storage import ( "context" "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" dcfg "git.misaka.ren/M1saka/docker_backup/internal/config" ) // WebDAVBackend uploads to a WebDAV server. type WebDAVBackend struct { cfg *dcfg.WebDAVConfig client *http.Client } // NewWebDAVBackend creates a configured WebDAV backend. func NewWebDAVBackend(cfg *dcfg.WebDAVConfig) *WebDAVBackend { return &WebDAVBackend{ cfg: cfg, client: &http.Client{ Timeout: 30 * time.Minute, }, } } // Upload implements Backend. func (w *WebDAVBackend) Upload(ctx context.Context, projectName, localPath string) error { baseURL := strings.TrimRight(strings.TrimSpace(w.cfg.URL), "/") if baseURL == "" { return fmt.Errorf("webdav url is required") } file, err := os.Open(localPath) if err != nil { return fmt.Errorf("open: %w", err) } defer file.Close() stat, err := file.Stat() if err != nil { return fmt.Errorf("stat: %w", err) } parentURL := baseURL + "/" + projectName if err := w.mkcol(ctx, parentURL); err != nil { return fmt.Errorf("webdav mkcol %s: %w", parentURL, err) } remoteURL := parentURL + "/" + filepath.Base(localPath) req, err := http.NewRequestWithContext(ctx, http.MethodPut, remoteURL, file) if err != nil { return fmt.Errorf("new request: %w", err) } req.ContentLength = stat.Size() req.Header.Set("Content-Type", "application/gzip") if w.cfg.Username != "" || w.cfg.Password != "" { req.SetBasicAuth(w.cfg.Username, w.cfg.Password) } resp, err := w.client.Do(req) if err != nil { return fmt.Errorf("webdav put: %w", err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { body, _ := io.ReadAll(resp.Body) return fmt.Errorf("webdav put failed: %s (%d): %s", remoteURL, resp.StatusCode, string(body)) } return nil } // mkcol creates a WebDAV collection (directory). Existing collections are OK. func (w *WebDAVBackend) mkcol(ctx context.Context, url string) error { req, err := http.NewRequestWithContext(ctx, "MKCOL", url, nil) if err != nil { return err } if w.cfg.Username != "" || w.cfg.Password != "" { req.SetBasicAuth(w.cfg.Username, w.cfg.Password) } resp, err := w.client.Do(req) if err != nil { return err } defer resp.Body.Close() // 201 Created is success. Many servers return 405 if collection already exists. if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusMethodNotAllowed { return nil } body, _ := io.ReadAll(resp.Body) return fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body)) }