Add support for rclone refresh dirs instead of refreshing everything
This commit is contained in:
@@ -15,9 +15,10 @@ type WebDav struct {
|
|||||||
FolderNaming string `json:"folder_naming,omitempty"`
|
FolderNaming string `json:"folder_naming,omitempty"`
|
||||||
|
|
||||||
// Rclone
|
// Rclone
|
||||||
RcUrl string `json:"rc_url,omitempty"`
|
RcUrl string `json:"rc_url,omitempty"`
|
||||||
RcUser string `json:"rc_user,omitempty"`
|
RcUser string `json:"rc_user,omitempty"`
|
||||||
RcPass string `json:"rc_pass,omitempty"`
|
RcPass string `json:"rc_pass,omitempty"`
|
||||||
|
RcRefreshDirs string `json:"rc_refresh_dirs,omitempty"` // comma separated list of directories to refresh
|
||||||
|
|
||||||
// Directories
|
// Directories
|
||||||
Directories map[string]WebdavDirectories `json:"directories,omitempty"`
|
Directories map[string]WebdavDirectories `json:"directories,omitempty"`
|
||||||
|
|||||||
@@ -138,12 +138,19 @@ func (c *Cache) refreshRclone() error {
|
|||||||
}
|
}
|
||||||
// Create form data
|
// Create form data
|
||||||
data := ""
|
data := ""
|
||||||
for index, dir := range c.GetDirectories() {
|
dirs := strings.FieldsFunc(cfg.RcRefreshDirs, func(r rune) bool {
|
||||||
if dir != "" {
|
return r == ',' || r == '&'
|
||||||
if index == 0 {
|
})
|
||||||
data += "dir=" + dir
|
if len(dirs) == 0 {
|
||||||
} else {
|
data = "dir=__all__"
|
||||||
data += "&dir" + fmt.Sprint(index+1) + "=" + dir
|
} else {
|
||||||
|
for index, dir := range dirs {
|
||||||
|
if dir != "" {
|
||||||
|
if index == 0 {
|
||||||
|
data += "dir=" + dir
|
||||||
|
} else {
|
||||||
|
data += "&dir" + fmt.Sprint(index+1) + "=" + dir
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,13 +372,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row mt-3 webdav d-none">
|
<div class="row mt-3 webdav d-none">
|
||||||
<h6 class="pb-2">Webdav</h6>
|
<h6 class="pb-2">Webdav</h6>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-2 mb-3">
|
||||||
<label class="form-label" for="debrid[${index}].torrents_refresh_interval">Torrents Refresh Interval</label>
|
<label class="form-label" for="debrid[${index}].torrents_refresh_interval">Torrents Refresh Interval</label>
|
||||||
<input type="text" class="form-control webdav-field" name="debrid[${index}].torrents_refresh_interval" id="debrid[${index}].torrents_refresh_interval" placeholder="15s" value="15s">
|
<input type="text" class="form-control webdav-field" name="debrid[${index}].torrents_refresh_interval" id="debrid[${index}].torrents_refresh_interval" placeholder="15s" value="15s">
|
||||||
<small class="form-text text-muted">How often to refresh the torrents list from debrid(instant when using webdav)</small>
|
<small class="form-text text-muted">How often to refresh the torrents list from debrid(instant when using webdav)</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-2 mb-3">
|
||||||
<label class="form-label" for="debrid[${index}].download_links_refresh_interval">Download Links Refresh Interval</label>
|
<label class="form-label" for="debrid[${index}].download_links_refresh_interval">Links Refresh Interval</label>
|
||||||
<input type="text" class="form-control webdav-field" name="debrid[${index}].download_links_refresh_interval" id="debrid[${index}].download_links_refresh_interval" placeholder="40m" value="40m">
|
<input type="text" class="form-control webdav-field" name="debrid[${index}].download_links_refresh_interval" id="debrid[${index}].download_links_refresh_interval" placeholder="40m" value="40m">
|
||||||
<small class="form-text text-muted">How often to refresh the download links list from debrid</small>
|
<small class="form-text text-muted">How often to refresh the download links list from debrid</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,6 +387,11 @@
|
|||||||
<input type="text" class="form-control webdav-field" name="debrid[${index}].auto_expire_links_after" id="debrid[${index}].auto_expire_links_after" placeholder="3d" value="3d">
|
<input type="text" class="form-control webdav-field" name="debrid[${index}].auto_expire_links_after" id="debrid[${index}].auto_expire_links_after" placeholder="3d" value="3d">
|
||||||
<small class="form-text text-muted">How long to keep the links in the webdav before expiring</small>
|
<small class="form-text text-muted">How long to keep the links in the webdav before expiring</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-2 mb-3">
|
||||||
|
<label class="form-label" for="debrid[${index}].workers">Number of Workers</label>
|
||||||
|
<input type="text" class="form-control webdav-field" name="debrid[${index}].workers" id="debrid[${index}].workers" placeholder="e.g., 50" value="50">
|
||||||
|
<small class="form-text text-muted">Number of workers to use for the webdav server(when refreshing)</small>
|
||||||
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<label class="form-label" for="debrid[${index}].folder_naming">Folder Naming Structure</label>
|
<label class="form-label" for="debrid[${index}].folder_naming">Folder Naming Structure</label>
|
||||||
<select class="form-select webdav-field" name="debrid[${index}].folder_naming" id="debrid[${index}].folder_naming">
|
<select class="form-select webdav-field" name="debrid[${index}].folder_naming" id="debrid[${index}].folder_naming">
|
||||||
@@ -399,16 +404,16 @@
|
|||||||
</select>
|
</select>
|
||||||
<small class="form-text text-muted">How to name each torrent directory in the webdav</small>
|
<small class="form-text text-muted">How to name each torrent directory in the webdav</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
|
||||||
<label class="form-label" for="debrid[${index}].workers">Number of Workers</label>
|
|
||||||
<input type="text" class="form-control webdav-field" name="debrid[${index}].workers" id="debrid[${index}].workers" placeholder="e.g., 50" value="50">
|
|
||||||
<small class="form-text text-muted">Number of workers to use for the webdav server(when refreshing)</small>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<label class="form-label" for="debrid[${index}].rc_url">Rclone RC URL</label>
|
<label class="form-label" for="debrid[${index}].rc_url">Rclone RC URL</label>
|
||||||
<input type="text" class="form-control webdav-field" name="debrid[${index}].rc_url" id="debrid[${index}].rc_url" placeholder="e.g., http://localhost:9990">
|
<input type="text" class="form-control webdav-field" name="debrid[${index}].rc_url" id="debrid[${index}].rc_url" placeholder="e.g., http://localhost:9990">
|
||||||
<small class="form-text text-muted">Rclone RC URL for the webdav server(speeds up import significantly)</small>
|
<small class="form-text text-muted">Rclone RC URL for the webdav server(speeds up import significantly)</small>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="col-md-3 mb-3">
|
||||||
|
<label class="form-label" for="debrid[${index}].rc_refresh_dirs">Rclone RC Dirs</label>
|
||||||
|
<input type="text" class="form-control webdav-field" name="debrid[${index}].rc_refresh_dirs" id="debrid[${index}].rc_refresh_dirs">
|
||||||
|
<small class="form-text text-muted">Directories to refresh via RC(comma-seperated e.g. __all__, torrents) </small>
|
||||||
|
</div>
|
||||||
<div class="col-md-3 mb-3">
|
<div class="col-md-3 mb-3">
|
||||||
<label class="form-label" for="debrid[${index}].rc_user">Rclone RC User</label>
|
<label class="form-label" for="debrid[${index}].rc_user">Rclone RC User</label>
|
||||||
<input type="text" class="form-control webdav-field" name="debrid[${index}].rc_user" id="debrid[${index}].rc_user">
|
<input type="text" class="form-control webdav-field" name="debrid[${index}].rc_user" id="debrid[${index}].rc_user">
|
||||||
@@ -1090,6 +1095,7 @@
|
|||||||
debrid.rc_url = document.querySelector(`[name="debrid[${i}].rc_url"]`).value;
|
debrid.rc_url = document.querySelector(`[name="debrid[${i}].rc_url"]`).value;
|
||||||
debrid.rc_user = document.querySelector(`[name="debrid[${i}].rc_user"]`).value;
|
debrid.rc_user = document.querySelector(`[name="debrid[${i}].rc_user"]`).value;
|
||||||
debrid.rc_pass = document.querySelector(`[name="debrid[${i}].rc_pass"]`).value;
|
debrid.rc_pass = document.querySelector(`[name="debrid[${i}].rc_pass"]`).value;
|
||||||
|
debrid.rc_refresh_dirs = document.querySelector(`[name="debrid[${i}].rc_refresh_dirs"]`).value;
|
||||||
|
|
||||||
//custom folders
|
//custom folders
|
||||||
debrid.directories = {};
|
debrid.directories = {};
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package webdav
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
@@ -31,41 +30,6 @@ type Handler struct {
|
|||||||
RootPath string
|
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 {
|
func NewHandler(name string, cache *debrid.Cache, logger zerolog.Logger) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Name: name,
|
Name: name,
|
||||||
@@ -540,102 +504,3 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) {
|
|||||||
w.Header().Set("DAV", "1, 2")
|
w.Header().Set("DAV", "1, 2")
|
||||||
w.WriteHeader(http.StatusOK)
|
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
128
pkg/webdav/propfind.go
Normal 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, "&", "&")
|
||||||
|
s = strings.ReplaceAll(s, "<", "<")
|
||||||
|
s = strings.ReplaceAll(s, ">", ">")
|
||||||
|
s = strings.ReplaceAll(s, "'", "'")
|
||||||
|
s = strings.ReplaceAll(s, "\"", """)
|
||||||
|
s = strings.ReplaceAll(s, "\n", " ")
|
||||||
|
s = strings.ReplaceAll(s, "\r", " ")
|
||||||
|
s = strings.ReplaceAll(s, "\t", "	")
|
||||||
|
return s
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user