personal workspace backend flush

This commit is contained in:
Leon Bösche
2026-01-09 17:32:16 +01:00
parent ebb97f4f39
commit e16b1bb083
3 changed files with 233 additions and 39 deletions

View File

@@ -0,0 +1,113 @@
package storage
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"path"
"strings"
"go.b0esche.cloud/backend/internal/config"
)
type WebDAVClient struct {
baseURL string
user string
pass string
basePrefix string
httpClient *http.Client
}
// NewWebDAVClient returns nil if no Nextcloud URL configured
func NewWebDAVClient(cfg *config.Config) *WebDAVClient {
if cfg == nil || strings.TrimSpace(cfg.NextcloudURL) == "" {
return nil
}
u := strings.TrimRight(cfg.NextcloudURL, "/")
base := cfg.NextcloudBase
if base == "" {
base = "/"
}
return &WebDAVClient{
baseURL: u,
user: cfg.NextcloudUser,
pass: cfg.NextcloudPass,
basePrefix: strings.TrimRight(base, "/"),
httpClient: &http.Client{},
}
}
// ensureParent creates intermediate collections using MKCOL. Ignoring errors when already exists.
func (c *WebDAVClient) ensureParent(ctx context.Context, remotePath string) error {
// build incremental paths
dir := path.Dir(remotePath)
if dir == "." || dir == "/" || dir == "" {
return nil
}
// split and build prefixes
parts := strings.Split(strings.Trim(dir, "/"), "/")
cur := c.basePrefix
for _, p := range parts {
cur = path.Join(cur, p)
mkurl := fmt.Sprintf("%s%s", c.baseURL, cur)
req, _ := http.NewRequestWithContext(ctx, "MKCOL", mkurl, nil)
if c.user != "" {
req.SetBasicAuth(c.user, c.pass)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
resp.Body.Close()
// 201 created, 405 exists — ignore
if resp.StatusCode == 201 || resp.StatusCode == 405 {
continue
}
}
return nil
}
// Upload streams the content to the remotePath using HTTP PUT (WebDAV). remotePath should be absolute under basePrefix.
func (c *WebDAVClient) Upload(ctx context.Context, remotePath string, r io.Reader, size int64) error {
if c == nil {
return fmt.Errorf("no webdav client configured")
}
// Ensure parent collections
if err := c.ensureParent(ctx, remotePath); err != nil {
return err
}
// Construct URL
// remotePath might be like /orgs/<id>/file.txt; ensure it joins to basePrefix
rel := strings.TrimLeft(remotePath, "/")
u := c.basePrefix
if u == "/" || u == "" {
u = "/"
}
full := fmt.Sprintf("%s%s/%s", c.baseURL, u, url.PathEscape(rel))
full = strings.ReplaceAll(full, "%2F", "/")
req, err := http.NewRequestWithContext(ctx, "PUT", full, r)
if err != nil {
return err
}
if size > 0 {
req.ContentLength = size
}
if c.user != "" {
req.SetBasicAuth(c.user, c.pass)
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("webdav upload failed: %d %s", resp.StatusCode, string(body))
}