first commit

This commit is contained in:
Awuqing
2026-03-17 13:29:09 +08:00
commit eadd3f8961
219 changed files with 22394 additions and 0 deletions

View File

@@ -0,0 +1,126 @@
package webdav
import (
"context"
"fmt"
"io"
"os"
"path"
"strings"
"backupx/server/internal/storage"
gowebdav "github.com/studio-b12/gowebdav"
)
type client interface {
ReadDir(path string) ([]os.FileInfo, error)
WriteStream(path string, stream io.Reader, perm os.FileMode) error
ReadStream(path string) (io.ReadCloser, error)
Remove(path string) error
MkdirAll(path string, perm os.FileMode) error
Stat(path string) (os.FileInfo, error)
}
type Provider struct {
client client
basePath string
}
type Factory struct {
newClient func(cfg storage.WebDAVConfig) client
}
func NewFactory() Factory {
return Factory{newClient: func(cfg storage.WebDAVConfig) client {
return gowebdav.NewClient(strings.TrimRight(cfg.Endpoint, "/"), cfg.Username, cfg.Password)
}}
}
func (Factory) Type() storage.ProviderType { return storage.ProviderTypeWebDAV }
func (Factory) SensitiveFields() []string { return []string{"username", "password"} }
func (f Factory) New(_ context.Context, rawConfig map[string]any) (storage.StorageProvider, error) {
cfg, err := storage.DecodeConfig[storage.WebDAVConfig](rawConfig)
if err != nil {
return nil, err
}
if strings.TrimSpace(cfg.Endpoint) == "" {
return nil, fmt.Errorf("webdav endpoint is required")
}
newClient := f.newClient
if newClient == nil {
factory := NewFactory()
newClient = factory.newClient
}
return &Provider{client: newClient(cfg), basePath: normalizeBasePath(cfg.BasePath)}, nil
}
func (p *Provider) Type() storage.ProviderType { return storage.ProviderTypeWebDAV }
func (p *Provider) TestConnection(_ context.Context) error {
if err := p.client.MkdirAll(p.basePath, 0o755); err != nil {
return fmt.Errorf("ensure webdav base path: %w", err)
}
if _, err := p.client.Stat(p.basePath); err != nil {
return fmt.Errorf("stat webdav base path: %w", err)
}
return nil
}
func (p *Provider) Upload(_ context.Context, objectKey string, reader io.Reader, _ int64, _ map[string]string) error {
objectPath := p.resolvePath(objectKey)
if err := p.client.MkdirAll(path.Dir(objectPath), 0o755); err != nil {
return fmt.Errorf("create webdav directories: %w", err)
}
if err := p.client.WriteStream(objectPath, reader, 0o644); err != nil {
return fmt.Errorf("write webdav object: %w", err)
}
return nil
}
func (p *Provider) Download(_ context.Context, objectKey string) (io.ReadCloser, error) {
reader, err := p.client.ReadStream(p.resolvePath(objectKey))
if err != nil {
return nil, fmt.Errorf("read webdav object: %w", err)
}
return reader, nil
}
func (p *Provider) Delete(_ context.Context, objectKey string) error {
if err := p.client.Remove(p.resolvePath(objectKey)); err != nil {
return fmt.Errorf("delete webdav object: %w", err)
}
return nil
}
func (p *Provider) List(_ context.Context, prefix string) ([]storage.ObjectInfo, error) {
entries, err := p.client.ReadDir(p.basePath)
if err != nil {
return nil, fmt.Errorf("list webdav directory: %w", err)
}
items := make([]storage.ObjectInfo, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() {
continue
}
key := strings.TrimPrefix(path.Join(strings.TrimPrefix(p.basePath, "/"), entry.Name()), "/")
if prefix != "" && !strings.HasPrefix(key, prefix) {
continue
}
items = append(items, storage.ObjectInfo{Key: key, Size: entry.Size(), UpdatedAt: entry.ModTime().UTC()})
}
return items, nil
}
func normalizeBasePath(value string) string {
clean := path.Clean("/" + strings.TrimSpace(value))
if clean == "." {
return "/"
}
return clean
}
func (p *Provider) resolvePath(objectKey string) string {
cleanKey := path.Clean("/" + strings.TrimSpace(objectKey))
return path.Clean(path.Join(p.basePath, cleanKey))
}

View File

@@ -0,0 +1,79 @@
package webdav
import (
"context"
"io"
"os"
"strings"
"testing"
"time"
"backupx/server/internal/storage"
)
type fakeFileInfo struct {
name string
size int64
mod time.Time
dir bool
}
func (f fakeFileInfo) Name() string { return f.name }
func (f fakeFileInfo) Size() int64 { return f.size }
func (f fakeFileInfo) Mode() os.FileMode { return 0 }
func (f fakeFileInfo) ModTime() time.Time { return f.mod }
func (f fakeFileInfo) IsDir() bool { return f.dir }
func (f fakeFileInfo) Sys() any { return nil }
type fakeClient struct{ data map[string]string }
func (c *fakeClient) ReadDir(_ string) ([]os.FileInfo, error) {
return []os.FileInfo{fakeFileInfo{name: "backup.tar.gz", size: int64(len(c.data["/storage/backup.tar.gz"])), mod: time.Now().UTC()}}, nil
}
func (c *fakeClient) WriteStream(path string, stream io.Reader, _ os.FileMode) error {
content, _ := io.ReadAll(stream)
c.data[path] = string(content)
return nil
}
func (c *fakeClient) ReadStream(path string) (io.ReadCloser, error) {
return io.NopCloser(strings.NewReader(c.data[path])), nil
}
func (c *fakeClient) Remove(path string) error { delete(c.data, path); return nil }
func (c *fakeClient) MkdirAll(_ string, _ os.FileMode) error { return nil }
func (c *fakeClient) Stat(path string) (os.FileInfo, error) {
return fakeFileInfo{name: path, dir: true}, nil
}
func TestWebDAVProviderCRUD(t *testing.T) {
factory := Factory{newClient: func(storage.WebDAVConfig) client { return &fakeClient{data: make(map[string]string)} }}
providerAny, err := factory.New(context.Background(), map[string]any{"endpoint": "http://dav.example.com", "basePath": "/storage"})
if err != nil {
t.Fatalf("Factory.New returned error: %v", err)
}
provider := providerAny.(*Provider)
if err := provider.TestConnection(context.Background()); err != nil {
t.Fatalf("TestConnection returned error: %v", err)
}
if err := provider.Upload(context.Background(), "backup.tar.gz", strings.NewReader("payload"), 7, nil); err != nil {
t.Fatalf("Upload returned error: %v", err)
}
reader, err := provider.Download(context.Background(), "backup.tar.gz")
if err != nil {
t.Fatalf("Download returned error: %v", err)
}
defer reader.Close()
content, _ := io.ReadAll(reader)
if string(content) != "payload" {
t.Fatalf("unexpected content: %s", string(content))
}
items, err := provider.List(context.Background(), "storage")
if err != nil {
t.Fatalf("List returned error: %v", err)
}
if len(items) != 1 || items[0].Key != "storage/backup.tar.gz" {
t.Fatalf("unexpected list result: %#v", items)
}
if err := provider.Delete(context.Background(), "backup.tar.gz"); err != nil {
t.Fatalf("Delete returned error: %v", err)
}
}