Files
b0esche_cloud/go_cloud/internal/storage/webdav.go

290 lines
7.3 KiB
Go

package storage
import (
"context"
"fmt"
"io"
"log"
"net/http"
"net/url"
"path"
"strings"
"time"
"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) == "" {
log.Printf("[WEBDAV] No Nextcloud URL configured, WebDAV client is nil\n")
return nil
}
u := strings.TrimRight(cfg.NextcloudURL, "/")
if !strings.Contains(u, "/remote.php") {
u += "/remote.php/dav/files/" + cfg.NextcloudUser
}
base := cfg.NextcloudBase
if base == "" {
base = "/"
}
log.Printf("[WEBDAV] Initializing WebDAV client - URL: %s, User: %s, BasePath: %s\n", u, cfg.NextcloudUser, base)
return &WebDAVClient{
BaseURL: u,
user: cfg.NextcloudUser,
pass: cfg.NextcloudPass,
basePrefix: strings.TrimRight(base, "/"),
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
// 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)
var mkurl string
if cur == "" || cur == "/" {
mkurl = fmt.Sprintf("%s/%s", c.BaseURL, url.PathEscape(p))
} else {
// Ensure there's a "/" between baseURL and cur
sep := ""
if !strings.HasSuffix(c.BaseURL, "/") && !strings.HasPrefix(cur, "/") {
sep = "/"
}
mkurl = fmt.Sprintf("%s%s%s", c.BaseURL, sep, strings.TrimPrefix(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 = ""
}
u = strings.TrimRight(u, "/")
var full string
if u == "" {
full = fmt.Sprintf("%s/%s", c.BaseURL, url.PathEscape(rel))
} else {
full = fmt.Sprintf("%s%s/%s", c.BaseURL, u, url.PathEscape(rel))
}
full = strings.ReplaceAll(full, "%2F", "/")
fmt.Printf("[WEBDAV-UPLOAD] BaseURL: %s, BasePrefix: %s, RemotePath: %s, Full URL: %s\n", c.BaseURL, c.basePrefix, remotePath, full)
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))
}
// Download retrieves a file from the remotePath using HTTP GET (WebDAV).
func (c *WebDAVClient) Download(ctx context.Context, remotePath string, rangeHeader string) (*http.Response, error) {
if c == nil {
return nil, fmt.Errorf("no webdav client configured")
}
rel := strings.TrimLeft(remotePath, "/")
u := c.basePrefix
if u == "/" || u == "" {
u = ""
}
u = strings.TrimRight(u, "/")
var full string
if u == "" {
full = fmt.Sprintf("%s/%s", c.BaseURL, url.PathEscape(rel))
} else {
full = fmt.Sprintf("%s%s/%s", c.BaseURL, u, url.PathEscape(rel))
}
full = strings.ReplaceAll(full, "%2F", "/")
req, err := http.NewRequestWithContext(ctx, "GET", full, nil)
if err != nil {
return nil, err
}
if c.user != "" {
req.SetBasicAuth(c.user, c.pass)
}
if rangeHeader != "" {
req.Header.Set("Range", rangeHeader)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return resp, nil
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
return nil, fmt.Errorf("webdav download failed: %d %s", resp.StatusCode, string(body))
}
// Delete removes a file or collection from the remotePath using HTTP DELETE (WebDAV).
func (c *WebDAVClient) Delete(ctx context.Context, remotePath string) error {
if c == nil {
return fmt.Errorf("no webdav client configured")
}
rel := strings.TrimLeft(remotePath, "/")
u := c.basePrefix
if u == "/" || u == "" {
u = ""
}
u = strings.TrimRight(u, "/")
var full string
if u == "" {
full = fmt.Sprintf("%s/%s", c.BaseURL, url.PathEscape(rel))
} else {
full = fmt.Sprintf("%s%s/%s", c.BaseURL, u, url.PathEscape(rel))
}
full = strings.ReplaceAll(full, "%2F", "/")
req, err := http.NewRequestWithContext(ctx, "DELETE", full, nil)
if err != nil {
return err
}
if c.user != "" {
req.SetBasicAuth(c.user, c.pass)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return nil
}
// 404 means already deleted, consider it success
if resp.StatusCode == 404 {
return nil
}
body, _ := io.ReadAll(resp.Body)
return fmt.Errorf("webdav delete failed: %d %s", resp.StatusCode, string(body))
}
// Move moves/renames a file using WebDAV MOVE method
func (c *WebDAVClient) Move(ctx context.Context, sourcePath, targetPath string) error {
if c == nil {
return fmt.Errorf("no webdav client configured")
}
sourceRel := strings.TrimLeft(sourcePath, "/")
targetRel := strings.TrimLeft(targetPath, "/")
u := c.basePrefix
if u == "/" || u == "" {
u = ""
}
u = strings.TrimRight(u, "/")
// Build source URL
var sourceURL string
if u == "" {
sourceURL = fmt.Sprintf("%s/%s", c.BaseURL, url.PathEscape(sourceRel))
} else {
sourceURL = fmt.Sprintf("%s%s/%s", c.BaseURL, u, url.PathEscape(sourceRel))
}
sourceURL = strings.ReplaceAll(sourceURL, "%2F", "/")
// Build target URL
var targetURL string
if u == "" {
targetURL = fmt.Sprintf("%s/%s", c.BaseURL, url.PathEscape(targetRel))
} else {
targetURL = fmt.Sprintf("%s%s/%s", c.BaseURL, u, url.PathEscape(targetRel))
}
targetURL = strings.ReplaceAll(targetURL, "%2F", "/")
req, err := http.NewRequestWithContext(ctx, "MOVE", sourceURL, nil)
if err != nil {
return err
}
req.Header.Set("Destination", targetURL)
if c.user != "" {
req.SetBasicAuth(c.user, c.pass)
}
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 move failed: %d %s", resp.StatusCode, string(body))
}