277 lines
6.9 KiB
Go
277 lines
6.9 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, "/")
|
|
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: 10 * time.Minute},
|
|
}
|
|
}
|
|
|
|
// 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 = ""
|
|
}
|
|
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))
|
|
}
|