Add support for rclone refresh dirs instead of refreshing everything

This commit is contained in:
Mukhtar Akere
2025-05-11 15:20:06 +01:00
parent 0f56badb45
commit ffb1745bf6
5 changed files with 159 additions and 152 deletions

View File

@@ -3,7 +3,6 @@ package webdav
import (
"bytes"
"context"
"encoding/xml"
"fmt"
"html/template"
"io"
@@ -31,41 +30,6 @@ type Handler struct {
RootPath string
}
type DAVResponse struct {
XMLName xml.Name `xml:"d:response"`
Href string `xml:"d:href"`
PropStat PropStat `xml:"d:propstat"`
}
type PropStat struct {
XMLName xml.Name `xml:"d:propstat"`
Prop Prop `xml:"d:prop"`
Status string `xml:"d:status"`
}
type Prop struct {
XMLName xml.Name `xml:"d:prop"`
ResourceType *ResourceType `xml:"d:resourcetype,omitempty"`
LastModified string `xml:"d:getlastmodified,omitempty"`
ContentLength int64 `xml:"d:getcontentlength,omitempty"`
DisplayName string `xml:"d:displayname,omitempty"`
}
type ResourceType struct {
XMLName xml.Name `xml:"d:resourcetype"`
Collection *Collection `xml:"d:collection,omitempty"`
}
type Collection struct {
XMLName xml.Name `xml:"d:collection"`
}
type MultiStatus struct {
XMLName xml.Name `xml:"d:multistatus"`
Namespace string `xml:"xmlns:d,attr"`
Responses []DAVResponse `xml:"d:response"`
}
func NewHandler(name string, cache *debrid.Cache, logger zerolog.Logger) *Handler {
h := &Handler{
Name: name,
@@ -540,102 +504,3 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) {
w.Header().Set("DAV", "1, 2")
w.WriteHeader(http.StatusOK)
}
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
// Setup context for metadata only
ctx := context.WithValue(r.Context(), "metadataOnly", true)
r = r.WithContext(ctx)
// Determine depth (default "1")
depth := r.Header.Get("Depth")
if depth == "" {
depth = "1"
}
cleanPath := path.Clean(r.URL.Path)
// Build the list of entries
type entry struct {
href string
fi os.FileInfo
}
var entries []entry
// Always include the resource itself
f, err := h.OpenFile(r.Context(), cleanPath, os.O_RDONLY, 0)
if err == nil {
defer f.Close()
if fi, err2 := f.Stat(); err2 == nil {
entries = append(entries, entry{
href: cleanPath,
fi: fi,
})
// Add children if directory and depth isn't 0
if fi.IsDir() {
children := h.getChildren(cleanPath)
for _, child := range children {
entries = append(entries, entry{
href: path.Join("/", cleanPath, child.Name()) + "/",
fi: child,
})
}
}
}
}
// Create MultiStatus response
multiStatus := MultiStatus{
Namespace: "DAV:",
Responses: []DAVResponse{},
}
// Add responses for each entry
for _, e := range entries {
var resourceType *ResourceType
var contentLength int64
if e.fi.IsDir() {
resourceType = &ResourceType{
Collection: &Collection{},
}
} else {
contentLength = e.fi.Size()
}
// Format href path properly
raw := e.href
u := &url.URL{Path: raw}
escaped := u.EscapedPath()
response := DAVResponse{
Href: escaped,
PropStat: PropStat{
Prop: Prop{
ResourceType: resourceType,
LastModified: e.fi.ModTime().Format("2006-01-02T15:04:05.000-07:00"),
ContentLength: contentLength,
DisplayName: e.fi.Name(),
},
Status: "HTTP/1.1 200 OK",
},
}
multiStatus.Responses = append(multiStatus.Responses, response)
}
// Marshal to XML
body, err := xml.Marshal(multiStatus)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.Header().Set("Vary", "Accept-Encoding")
// Set status code
w.WriteHeader(207) // MultiStatus
_, _ = w.Write([]byte(xml.Header))
_, _ = w.Write(body)
}

128
pkg/webdav/propfind.go Normal file
View File

@@ -0,0 +1,128 @@
package webdav
import (
"context"
"fmt"
"net/http"
"net/url"
"os"
"path"
"strings"
)
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
// Setup context for metadata only
ctx := context.WithValue(r.Context(), "metadataOnly", true)
r = r.WithContext(ctx)
// Determine depth (default "1")
depth := r.Header.Get("Depth")
if depth == "" {
depth = "1"
}
cleanPath := path.Clean(r.URL.Path)
// Build the list of entries
type entry struct {
href string
fi os.FileInfo
}
// Always include the resource itself
f, err := h.OpenFile(r.Context(), cleanPath, os.O_RDONLY, 0)
if err != nil {
h.logger.Error().Err(err).Str("path", cleanPath).Msg("Failed to open file")
http.NotFound(w, r)
return
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
h.logger.Error().Err(err).Msg("Failed to stat file")
http.Error(w, "Server Error", http.StatusInternalServerError)
return
}
// Collect children if a directory and depth allows
children := make([]os.FileInfo, 0)
if fi.IsDir() && depth != "0" {
children = h.getChildren(cleanPath)
}
entries := make([]entry, 0, 1+len(children))
entries = append(entries, entry{href: cleanPath, fi: fi})
for _, child := range children {
childHref := path.Join("/", cleanPath, child.Name())
if child.IsDir() {
childHref += "/"
}
entries = append(entries, entry{href: childHref, fi: child})
}
// Use a string builder for creating XML
var sb strings.Builder
// XML header and main element
sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>`)
sb.WriteString(`<d:multistatus xmlns:d="DAV:">`)
// Format time once
timeFormat := "2006-01-02T15:04:05.000-07:00"
// Add responses for each entry
for _, e := range entries {
// Format href path properly
u := &url.URL{Path: e.href}
escaped := u.EscapedPath()
sb.WriteString(`<d:response>`)
sb.WriteString(fmt.Sprintf(`<d:href>%s</d:href>`, xmlEscape(escaped)))
sb.WriteString(`<d:propstat>`)
sb.WriteString(`<d:prop>`)
// Resource type differs based on directory vs file
if e.fi.IsDir() {
sb.WriteString(`<d:resourcetype><d:collection/></d:resourcetype>`)
} else {
sb.WriteString(`<d:resourcetype/>`)
sb.WriteString(fmt.Sprintf(`<d:getcontentlength>%d</d:getcontentlength>`, e.fi.Size()))
}
// Always add lastmodified and displayname
lastModified := e.fi.ModTime().Format(timeFormat)
sb.WriteString(fmt.Sprintf(`<d:getlastmodified>%s</d:getlastmodified>`, xmlEscape(lastModified)))
sb.WriteString(fmt.Sprintf(`<d:displayname>%s</d:displayname>`, xmlEscape(e.fi.Name())))
sb.WriteString(`</d:prop>`)
sb.WriteString(`<d:status>HTTP/1.1 200 OK</d:status>`)
sb.WriteString(`</d:propstat>`)
sb.WriteString(`</d:response>`)
}
// Close root element
sb.WriteString(`</d:multistatus>`)
// Set headers
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
w.Header().Set("Vary", "Accept-Encoding")
// Set status code and write response
w.WriteHeader(http.StatusMultiStatus) // 207 MultiStatus
_, _ = w.Write([]byte(sb.String()))
}
// Basic XML escaping function
func xmlEscape(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "'", "&apos;")
s = strings.ReplaceAll(s, "\"", "&quot;")
s = strings.ReplaceAll(s, "\n", "&#10;")
s = strings.ReplaceAll(s, "\r", "&#13;")
s = strings.ReplaceAll(s, "\t", "&#9;")
return s
}