Files
docker-compose-backup/internal/storage/webdav.go
T
2026-07-04 17:20:28 +08:00

101 lines
2.5 KiB
Go

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