package handlers import ( "encoding/base64" "fmt" "mime" "net/http" "os" "path/filepath" "strings" "sync" "time" "git.elijahkuntz.com/Elijah/drive/config" "git.elijahkuntz.com/Elijah/drive/database" "github.com/gofiber/fiber/v2" "github.com/valyala/fasthttp/fasthttpadaptor" "golang.org/x/net/webdav" ) type WebDAVHandler struct { Handler *webdav.Handler DB *database.DB Config *config.Config authCache map[string]time.Time cacheMu sync.RWMutex } func NewWebDAVHandler(db *database.DB, cfg *config.Config) *WebDAVHandler { fs := webdav.Dir(cfg.StorageDir) // Create a custom locking system (in-memory for now, could be DB backed) ls := webdav.NewMemLS() h := &webdav.Handler{ Prefix: "/webdav", FileSystem: fs, LockSystem: ls, Logger: func(r *http.Request, err error) { if err != nil { fmt.Printf("WebDAV [%s] %s: %v\n", r.Method, r.URL.Path, err) } }, } return &WebDAVHandler{ Handler: h, DB: db, Config: cfg, authCache: make(map[string]time.Time), } } func (h *WebDAVHandler) Handle(c *fiber.Ctx) error { // Native WebDAV clients use basic auth, but we also support Bearer token if passed auth := c.Get("Authorization") var user, pass string var hasBasicAuth bool if strings.HasPrefix(auth, "Basic ") { if payload, err := base64.StdEncoding.DecodeString(auth[6:]); err == nil { parts := strings.SplitN(string(payload), ":", 2) if len(parts) == 2 { user = parts[0] pass = parts[1] hasBasicAuth = true } } } if hasBasicAuth { cacheKey := user + ":" + pass h.cacheMu.RLock() expireTime, ok := h.authCache[cacheKey] h.cacheMu.RUnlock() if !ok || time.Now().After(expireTime) { // Verify basic auth against DB var storedHash string err := h.DB.QueryRow("SELECT password_hash FROM users WHERE username = ?", user).Scan(&storedHash) if err != nil || !verifyPassword(pass, storedHash) { c.Set("WWW-Authenticate", `Basic realm="Drive WebDAV"`) return c.SendStatus(fiber.StatusUnauthorized) } // Cache successful auth for 5 minutes h.cacheMu.Lock() h.authCache[cacheKey] = time.Now().Add(5 * time.Minute) // Simple cache cleanup for k, v := range h.authCache { if time.Now().After(v) { delete(h.authCache, k) } } h.cacheMu.Unlock() } } else if c.Locals("userID") == nil { c.Set("WWW-Authenticate", `Basic realm="Drive WebDAV"`) return c.SendStatus(fiber.StatusUnauthorized) } // Adapt Fiber's FastHTTP to standard net/http for the WebDAV handler fasthttpadaptor.NewFastHTTPHandlerFunc(func(w http.ResponseWriter, r *http.Request) { relPath := strings.TrimPrefix(r.URL.Path, "/webdav") // Native WebDAV handles overwriting automatically h.Handler.ServeHTTP(w, r) // If it was a PUT request, we should sync it with our DB (checksum, size, etc.) if r.Method == "PUT" && r.Response != nil && r.Response.StatusCode >= 200 && r.Response.StatusCode < 300 { h.syncToDB(relPath) } })(c.Context()) return nil } // syncToDB manually synchronizes a file written via WebDAV to the SQLite database func (h *WebDAVHandler) syncToDB(relPath string) { fullPath := filepath.Join(h.Config.StorageDir, filepath.FromSlash(relPath)) info, err := os.Stat(fullPath) if err != nil { return } checksum, _ := generateChecksum(fullPath) // Reusing the helper from files.go cleanPath := filepath.ToSlash(relPath) mimeType := "application/octet-stream" if ext := filepath.Ext(cleanPath); ext != "" { if m := mime.TypeByExtension(ext); m != "" { mimeType = m } } h.DB.Exec(` INSERT INTO files (path, name, size, mime_type, checksum, is_dir) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET size = excluded.size, checksum = excluded.checksum, updated_at = CURRENT_TIMESTAMP `, cleanPath, filepath.Base(cleanPath), info.Size(), mimeType, checksum, info.IsDir()) }