- Cleaup the code
- Add delete button to webdav ui - Some other bug fixes here and there
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -327,9 +328,12 @@ func (c *Config) setDefaults() {
|
|||||||
c.URLBase = "/"
|
c.URLBase = "/"
|
||||||
}
|
}
|
||||||
// validate url base starts with /
|
// validate url base starts with /
|
||||||
if c.URLBase[0] != '/' {
|
if !strings.HasPrefix(c.URLBase, "/") {
|
||||||
c.URLBase = "/" + c.URLBase
|
c.URLBase = "/" + c.URLBase
|
||||||
}
|
}
|
||||||
|
if !strings.HasSuffix(c.URLBase, "/") {
|
||||||
|
c.URLBase += "/"
|
||||||
|
}
|
||||||
|
|
||||||
// Load the auth file
|
// Load the auth file
|
||||||
c.Auth = c.GetAuth()
|
c.Auth = c.GetAuth()
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ type Debouncer[T any] struct {
|
|||||||
timer *time.Timer
|
timer *time.Timer
|
||||||
interval time.Duration
|
interval time.Duration
|
||||||
caller func(arg T)
|
caller func(arg T)
|
||||||
arg T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDebouncer[T any](interval time.Duration, caller func(arg T)) *Debouncer[T] {
|
func NewDebouncer[T any](interval time.Duration, caller func(arg T)) *Debouncer[T] {
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
videoRegex = regexp.MustCompile(videoMatch)
|
|
||||||
musicRegex = regexp.MustCompile(musicMatch)
|
|
||||||
mediaRegex = regexp.MustCompile(videoMatch + "|" + musicMatch)
|
mediaRegex = regexp.MustCompile(videoMatch + "|" + musicMatch)
|
||||||
sampleRegex = regexp.MustCompile(sampleMatch)
|
sampleRegex = regexp.MustCompile(sampleMatch)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ type AllDebrid struct {
|
|||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
APIKey string
|
APIKey string
|
||||||
accounts map[string]types.Account
|
accounts map[string]types.Account
|
||||||
accountsMu sync.RWMutex
|
|
||||||
DownloadUncached bool
|
DownloadUncached bool
|
||||||
client *request.Client
|
client *request.Client
|
||||||
|
|
||||||
|
|||||||
@@ -96,16 +96,6 @@ func (tc *torrentCache) set(name string, torrent, newTorrent CachedTorrent) {
|
|||||||
tc.sortNeeded.Store(true)
|
tc.sortNeeded.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *torrentCache) setMany(torrents map[string]CachedTorrent) {
|
|
||||||
tc.mu.Lock()
|
|
||||||
defer tc.mu.Unlock()
|
|
||||||
for id, torrent := range torrents {
|
|
||||||
tc.byID[id] = torrent
|
|
||||||
tc.byName[torrent.Name] = torrent
|
|
||||||
}
|
|
||||||
tc.sortNeeded.Store(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (tc *torrentCache) getListing() []os.FileInfo {
|
func (tc *torrentCache) getListing() []os.FileInfo {
|
||||||
// Fast path: if we have a sorted list and no changes since last sort
|
// Fast path: if we have a sorted list and no changes since last sort
|
||||||
if !tc.sortNeeded.Load() {
|
if !tc.sortNeeded.Load() {
|
||||||
@@ -135,7 +125,7 @@ func (tc *torrentCache) refreshListing() {
|
|||||||
tc.mu.Lock()
|
tc.mu.Lock()
|
||||||
all := make([]sortableFile, 0, len(tc.byName))
|
all := make([]sortableFile, 0, len(tc.byName))
|
||||||
for name, t := range tc.byName {
|
for name, t := range tc.byName {
|
||||||
all = append(all, sortableFile{t.Id, name, t.AddedOn, t.Size, t.Bad})
|
all = append(all, sortableFile{t.Id, name, t.AddedOn, t.Bytes, t.Bad})
|
||||||
}
|
}
|
||||||
tc.sortNeeded.Store(false)
|
tc.sortNeeded.Store(false)
|
||||||
tc.mu.Unlock()
|
tc.mu.Unlock()
|
||||||
@@ -279,7 +269,7 @@ func (tc *torrentCache) getIdMaps() map[string]struct{} {
|
|||||||
tc.mu.Lock()
|
tc.mu.Lock()
|
||||||
defer tc.mu.Unlock()
|
defer tc.mu.Unlock()
|
||||||
res := make(map[string]struct{}, len(tc.byID))
|
res := make(map[string]struct{}, len(tc.byID))
|
||||||
for id, _ := range tc.byID {
|
for id := range tc.byID {
|
||||||
res[id] = struct{}{}
|
res[id] = struct{}{}
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/sirrobot01/decypharr/internal/utils"
|
"github.com/sirrobot01/decypharr/internal/utils"
|
||||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -23,7 +22,6 @@ type DebridLink struct {
|
|||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
APIKey string
|
APIKey string
|
||||||
accounts map[string]types.Account
|
accounts map[string]types.Account
|
||||||
accountsMutex sync.RWMutex
|
|
||||||
DownloadUncached bool
|
DownloadUncached bool
|
||||||
client *request.Client
|
client *request.Client
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ type Torbox struct {
|
|||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
APIKey string
|
APIKey string
|
||||||
accounts map[string]types.Account
|
accounts map[string]types.Account
|
||||||
accountsMutex sync.RWMutex
|
|
||||||
DownloadUncached bool
|
DownloadUncached bool
|
||||||
client *request.Client
|
client *request.Client
|
||||||
|
|
||||||
|
|||||||
@@ -145,32 +145,32 @@
|
|||||||
<div class="collapse navbar-collapse" id="navbarNav">
|
<div class="collapse navbar-collapse" id="navbarNav">
|
||||||
<ul class="navbar-nav me-auto">
|
<ul class="navbar-nav me-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{if eq .Page "index"}}active{{end}}" href="/">
|
<a class="nav-link {{if eq .Page "index"}}active{{end}}" href="{{.URLBase}}">
|
||||||
<i class="bi bi-table me-1"></i>Torrents
|
<i class="bi bi-table me-1"></i>Torrents
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{if eq .Page "download"}}active{{end}}" href="/download">
|
<a class="nav-link {{if eq .Page "download"}}active{{end}}" href="{{.URLBase}}download">
|
||||||
<i class="bi bi-cloud-download me-1"></i>Download
|
<i class="bi bi-cloud-download me-1"></i>Download
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{if eq .Page "repair"}}active{{end}}" href="/repair">
|
<a class="nav-link {{if eq .Page "repair"}}active{{end}}" href="{{.URLBase}}repair">
|
||||||
<i class="bi bi-tools me-1"></i>Repair
|
<i class="bi bi-tools me-1"></i>Repair
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link {{if eq .Page "config"}}active{{end}}" href="/config">
|
<a class="nav-link {{if eq .Page "config"}}active{{end}}" href="{{.URLBase}}config">
|
||||||
<i class="bi bi-gear me-1"></i>Settings
|
<i class="bi bi-gear me-1"></i>Settings
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/webdav" target="_blank">
|
<a class="nav-link" href="{{.URLBase}}webdav" target="_blank">
|
||||||
<i class="bi bi-cloud me-1"></i>WebDAV
|
<i class="bi bi-cloud me-1"></i>WebDAV
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/logs" target="_blank">
|
<a class="nav-link" href="{{.URLBase}}logs" target="_blank">
|
||||||
<i class="bi bi-journal me-1"></i>Logs
|
<i class="bi bi-journal me-1"></i>Logs
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -209,23 +209,15 @@
|
|||||||
|
|
||||||
window.urlBase = "{{.URLBase}}";
|
window.urlBase = "{{.URLBase}}";
|
||||||
|
|
||||||
function joinUrl (base, path) {
|
function joinURL(...segments) {
|
||||||
if (path.substring(0, 1) === "/") {
|
return segments.join('/').replace(/[/]+/g, '/').replace(/^(.+):\//, '$1://').replace(/^file:/, 'file:/').replace(/\/(?=[?&#])/g, '').replace(/\?/g, '&');
|
||||||
// path starts with `/`. Thus it is absolute.
|
|
||||||
return path;
|
|
||||||
}
|
|
||||||
if (base.substring(base.length-1) === "/") {
|
|
||||||
// base ends with `/`
|
|
||||||
return base + path;
|
|
||||||
}
|
|
||||||
return base + "/" + path;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function fetcher(endpoint, options = {}) {
|
function fetcher(endpoint, options = {}) {
|
||||||
// Use the global urlBase or default to empty string
|
// Use the global urlBase or default to empty string
|
||||||
let baseUrl = window.urlBase || '';
|
let baseUrl = window.urlBase || '';
|
||||||
|
|
||||||
let url = joinUrl(baseUrl, endpoint);
|
let url = joinURL(baseUrl, endpoint);
|
||||||
|
|
||||||
// Return the regular fetcher with the complete URL
|
// Return the regular fetcher with the complete URL
|
||||||
return fetch(url, options);
|
return fetch(url, options);
|
||||||
|
|||||||
@@ -64,7 +64,7 @@
|
|||||||
createToast(errorText || 'Registration failed', 'error');
|
createToast(errorText || 'Registration failed', 'error');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
window.location.href = joinUrl(window.urlBase, '/');
|
window.location.href = joinURL(window.urlBase, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
@@ -78,7 +78,7 @@
|
|||||||
fetcher('/skip-auth', { method: 'GET' })
|
fetcher('/skip-auth', { method: 'GET' })
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
window.location.href = joinUrl(window.urlBase, '/');
|
window.location.href = joinURL(window.urlBase, '/');
|
||||||
} else {
|
} else {
|
||||||
throw new Error('Failed to skip authentication');
|
throw new Error('Failed to skip authentication');
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-73
@@ -4,10 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -27,15 +25,17 @@ type Handler struct {
|
|||||||
Name string
|
Name string
|
||||||
logger zerolog.Logger
|
logger zerolog.Logger
|
||||||
cache *debrid.Cache
|
cache *debrid.Cache
|
||||||
|
URLBase string
|
||||||
RootPath string
|
RootPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(name string, cache *debrid.Cache, logger zerolog.Logger) *Handler {
|
func NewHandler(name, urlBase string, cache *debrid.Cache, logger zerolog.Logger) *Handler {
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
Name: name,
|
Name: name,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
RootPath: fmt.Sprintf("/%s", name),
|
URLBase: urlBase,
|
||||||
|
RootPath: path.Join(urlBase, "webdav", name),
|
||||||
}
|
}
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,7 @@ func (h *Handler) RemoveAll(ctx context.Context, name string) error {
|
|||||||
}
|
}
|
||||||
name = path.Clean(name)
|
name = path.Clean(name)
|
||||||
|
|
||||||
rootDir := path.Clean(h.getRootPath())
|
rootDir := path.Clean(h.RootPath)
|
||||||
|
|
||||||
if name == rootDir {
|
if name == rootDir {
|
||||||
return os.ErrPermission
|
return os.ErrPermission
|
||||||
@@ -74,10 +74,6 @@ func (h *Handler) Rename(ctx context.Context, oldName, newName string) error {
|
|||||||
return os.ErrPermission // Read-only filesystem
|
return os.ErrPermission // Read-only filesystem
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) getRootPath() string {
|
|
||||||
return fmt.Sprintf(path.Join("/", "webdav", "%s"), h.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) getTorrentsFolders(folder string) []os.FileInfo {
|
func (h *Handler) getTorrentsFolders(folder string) []os.FileInfo {
|
||||||
return h.cache.GetListing(folder)
|
return h.cache.GetListing(folder)
|
||||||
}
|
}
|
||||||
@@ -120,7 +116,7 @@ func (h *Handler) getChildren(name string) []os.FileInfo {
|
|||||||
name = "/" + name
|
name = "/" + name
|
||||||
}
|
}
|
||||||
name = utils.PathUnescape(path.Clean(name))
|
name = utils.PathUnescape(path.Clean(name))
|
||||||
root := path.Clean(h.getRootPath())
|
root := path.Clean(h.RootPath)
|
||||||
|
|
||||||
// top‐level “parents” (e.g. __all__, torrents etc)
|
// top‐level “parents” (e.g. __all__, torrents etc)
|
||||||
if name == root {
|
if name == root {
|
||||||
@@ -147,7 +143,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
name = "/" + name
|
name = "/" + name
|
||||||
}
|
}
|
||||||
name = utils.PathUnescape(path.Clean(name))
|
name = utils.PathUnescape(path.Clean(name))
|
||||||
rootDir := path.Clean(h.getRootPath())
|
rootDir := path.Clean(h.RootPath)
|
||||||
metadataOnly := ctx.Value("metadataOnly") != nil
|
metadataOnly := ctx.Value("metadataOnly") != nil
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
@@ -169,7 +165,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
if children := h.getChildren(name); children != nil {
|
if children := h.getChildren(name); children != nil {
|
||||||
displayName := filepath.Clean(path.Base(name))
|
displayName := filepath.Clean(path.Base(name))
|
||||||
if name == rootDir {
|
if name == rootDir {
|
||||||
displayName = string(os.PathSeparator)
|
displayName = "/"
|
||||||
}
|
}
|
||||||
return &File{
|
return &File{
|
||||||
cache: h.cache,
|
cache: h.cache,
|
||||||
@@ -336,6 +332,8 @@ func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file we
|
|||||||
cleanPath := path.Clean(r.URL.Path)
|
cleanPath := path.Clean(r.URL.Path)
|
||||||
parentPath := path.Dir(cleanPath)
|
parentPath := path.Dir(cleanPath)
|
||||||
showParent := cleanPath != "/" && parentPath != "." && parentPath != cleanPath
|
showParent := cleanPath != "/" && parentPath != "." && parentPath != cleanPath
|
||||||
|
isBadPath := strings.HasSuffix(cleanPath, "__bad__")
|
||||||
|
_, canDelete := h.isParentPath(cleanPath)
|
||||||
|
|
||||||
// Prepare template data
|
// Prepare template data
|
||||||
data := struct {
|
data := struct {
|
||||||
@@ -343,73 +341,21 @@ func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file we
|
|||||||
ParentPath string
|
ParentPath string
|
||||||
ShowParent bool
|
ShowParent bool
|
||||||
Children []os.FileInfo
|
Children []os.FileInfo
|
||||||
|
URLBase string
|
||||||
|
IsBadPath bool
|
||||||
|
CanDelete bool
|
||||||
}{
|
}{
|
||||||
Path: cleanPath,
|
Path: cleanPath,
|
||||||
ParentPath: parentPath,
|
ParentPath: parentPath,
|
||||||
ShowParent: showParent,
|
ShowParent: showParent,
|
||||||
Children: children,
|
Children: children,
|
||||||
}
|
URLBase: h.URLBase,
|
||||||
|
IsBadPath: isBadPath,
|
||||||
// Parse and execute template
|
CanDelete: canDelete,
|
||||||
funcMap := template.FuncMap{
|
|
||||||
"add": func(a, b int) int {
|
|
||||||
return a + b
|
|
||||||
},
|
|
||||||
"urlpath": func(p string) string {
|
|
||||||
segments := strings.Split(p, "/")
|
|
||||||
for i, segment := range segments {
|
|
||||||
segments[i] = url.PathEscape(segment)
|
|
||||||
}
|
|
||||||
return strings.Join(segments, "/")
|
|
||||||
},
|
|
||||||
"formatSize": func(bytes int64) string {
|
|
||||||
const (
|
|
||||||
KB = 1024
|
|
||||||
MB = 1024 * KB
|
|
||||||
GB = 1024 * MB
|
|
||||||
TB = 1024 * GB
|
|
||||||
)
|
|
||||||
|
|
||||||
var size float64
|
|
||||||
var unit string
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case bytes >= TB:
|
|
||||||
size = float64(bytes) / TB
|
|
||||||
unit = "TB"
|
|
||||||
case bytes >= GB:
|
|
||||||
size = float64(bytes) / GB
|
|
||||||
unit = "GB"
|
|
||||||
case bytes >= MB:
|
|
||||||
size = float64(bytes) / MB
|
|
||||||
unit = "MB"
|
|
||||||
case bytes >= KB:
|
|
||||||
size = float64(bytes) / KB
|
|
||||||
unit = "KB"
|
|
||||||
default:
|
|
||||||
size = float64(bytes)
|
|
||||||
unit = "bytes"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format to 2 decimal places for larger units, no decimals for bytes
|
|
||||||
if unit == "bytes" {
|
|
||||||
return fmt.Sprintf("%.0f %s", size, unit)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%.2f %s", size, unit)
|
|
||||||
},
|
|
||||||
"hasSuffix": strings.HasSuffix,
|
|
||||||
}
|
|
||||||
tmpl, err := template.New("directory").Funcs(funcMap).Parse(directoryTemplate)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error().Err(err).Msg("Failed to parse directory template")
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tplDirectory.ExecuteTemplate(w, "directory.html", data); err != nil {
|
||||||
h.logger.Error().Err(err).Msg("Failed to execute directory template")
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -423,7 +369,12 @@ func (h *Handler) handleGet(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer fRaw.Close()
|
defer func(fRaw webdav.File) {
|
||||||
|
err := fRaw.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(fRaw)
|
||||||
|
|
||||||
fi, err := fRaw.Stat()
|
fi, err := fRaw.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -483,7 +434,12 @@ func (h *Handler) handleHead(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.NotFound(w, r)
|
http.NotFound(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer func(f webdav.File) {
|
||||||
|
err := f.Close()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}(f)
|
||||||
|
|
||||||
fi, err := f.Stat()
|
fi, err := f.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package webdav
|
package webdav
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// getName: Returns the torrent name and filename from the path
|
// getName: Returns the torrent name and filename from the path
|
||||||
@@ -18,27 +16,12 @@ func getName(rootDir, path string) (string, string) {
|
|||||||
return parts[1], strings.Join(parts[2:], string(os.PathSeparator)) // Note the change from [0] to [1]
|
return parts[1], strings.Join(parts[2:], string(os.PathSeparator)) // Note the change from [0] to [1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func acceptsGzip(r *http.Request) bool {
|
|
||||||
return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidURL(str string) bool {
|
func isValidURL(str string) bool {
|
||||||
u, err := url.Parse(str)
|
u, err := url.Parse(str)
|
||||||
// A valid URL should parse without error, and have a non-empty scheme and host.
|
// A valid URL should parse without error, and have a non-empty scheme and host.
|
||||||
return err == nil && u.Scheme != "" && u.Host != ""
|
return err == nil && u.Scheme != "" && u.Host != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine TTL based on the requested folder:
|
|
||||||
// - If the path is exactly the parent folder (which changes frequently),
|
|
||||||
// use a short TTL.
|
|
||||||
// - Otherwise, for deeper (torrent folder) paths, use a longer TTL.
|
|
||||||
func (h *Handler) getCacheTTL(urlPath string) time.Duration {
|
|
||||||
if _, ok := h.isParentPath(urlPath); ok {
|
|
||||||
return 30 * time.Second // Short TTL for parent folders
|
|
||||||
}
|
|
||||||
return 2 * time.Minute // Longer TTL for other paths
|
|
||||||
}
|
|
||||||
|
|
||||||
var pctHex = "0123456789ABCDEF"
|
var pctHex = "0123456789ABCDEF"
|
||||||
|
|
||||||
// fastEscapePath returns a percent-encoded path, preserving '/'
|
// fastEscapePath returns a percent-encoded path, preserving '/'
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var builderPool = sync.Pool{
|
var builderPool = sync.Pool{
|
||||||
New: func() interface{} { return stringbuf.New("") },
|
|
||||||
|
New: func() interface{} {
|
||||||
|
buf := stringbuf.New("")
|
||||||
|
return buf
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -21,17 +25,10 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
|||||||
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
||||||
r = r.WithContext(ctx)
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
// Determine depth (default "1")
|
|
||||||
depth := r.Header.Get("Depth")
|
|
||||||
if depth == "" {
|
|
||||||
depth = "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
cleanPath := path.Clean(r.URL.Path)
|
cleanPath := path.Clean(r.URL.Path)
|
||||||
|
|
||||||
// Build the list of entries
|
// Build the list of entries
|
||||||
type entry struct {
|
type entry struct {
|
||||||
href string
|
|
||||||
escHref string // already XML-safe + percent-escaped
|
escHref string // already XML-safe + percent-escaped
|
||||||
escName string
|
escName string
|
||||||
size int64
|
size int64
|
||||||
|
|||||||
@@ -1,149 +0,0 @@
|
|||||||
package webdav
|
|
||||||
|
|
||||||
const rootTemplate = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>WebDAV Shares</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #3498db;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 10px;
|
|
||||||
display: block;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
background-color: #f7f9fa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Available WebDAV Shares</h1>
|
|
||||||
<ul>
|
|
||||||
{{range .Handlers}}
|
|
||||||
<li><a href="{{$.Prefix}}{{.RootPath}}">{{$.Prefix}}{{.RootPath}}</a></li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
|
|
||||||
const directoryTemplate = `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Index of {{.Path}}</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
color: #2c3e50;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #3498db;
|
|
||||||
text-decoration: none;
|
|
||||||
padding: 10px;
|
|
||||||
display: block;
|
|
||||||
border: 1px solid #eee;
|
|
||||||
border-radius: 4px;
|
|
||||||
position: relative;
|
|
||||||
padding-left: 50px; /* Make room for the number */
|
|
||||||
}
|
|
||||||
a:hover {
|
|
||||||
background-color: #f7f9fa;
|
|
||||||
}
|
|
||||||
.file-info {
|
|
||||||
color: #666;
|
|
||||||
font-size: 0.9em;
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.parent-dir {
|
|
||||||
background-color: #f8f9fa;
|
|
||||||
}
|
|
||||||
.file-number {
|
|
||||||
position: absolute;
|
|
||||||
left: 10px;
|
|
||||||
top: 10px;
|
|
||||||
width: 30px;
|
|
||||||
color: #777;
|
|
||||||
font-weight: bold;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
.file-name {
|
|
||||||
display: inline-block;
|
|
||||||
max-width: 70%;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
.disabled {
|
|
||||||
color: #999;
|
|
||||||
pointer-events: none;
|
|
||||||
border-color: #f0f0f0;
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
.disabled .file-number {
|
|
||||||
color: #bbb;
|
|
||||||
}
|
|
||||||
.disabled .file-info {
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Index of {{.Path}}</h1>
|
|
||||||
<ul>
|
|
||||||
{{if .ShowParent}}
|
|
||||||
<li><a href="{{urlpath .ParentPath}}" class="parent-dir"><span class="file-number"></span>Parent Directory</a></li>
|
|
||||||
{{end}}
|
|
||||||
{{$isBadPath := hasSuffix .Path "__bad__"}}
|
|
||||||
{{range $index, $file := .Children}}
|
|
||||||
<li>
|
|
||||||
{{if $isBadPath}}
|
|
||||||
<a class="disabled">
|
|
||||||
{{else}}
|
|
||||||
<a href="{{urlpath (printf "%s/%s" $.Path $file.Name)}}">
|
|
||||||
{{end}}
|
|
||||||
<span class="file-number">{{add $index 1}}.</span>
|
|
||||||
<span class="file-name">{{$file.Name}}{{if $file.IsDir}}/{{end}}</span>
|
|
||||||
<span class="file-info">
|
|
||||||
{{if not $file.IsDir}}
|
|
||||||
{{formatSize $file.Size}}
|
|
||||||
{{end}}
|
|
||||||
{{$file.ModTime.Format "2006-01-02 15:04:05"}}
|
|
||||||
</span>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{{end}}
|
|
||||||
</ul>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`
|
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="">
|
||||||
|
<head>
|
||||||
|
<title>Index of {{.Path}}</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #2c3e50;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
li {
|
||||||
|
margin: 10px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 10px;
|
||||||
|
flex: 1;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 50px; /* room for number */
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
background-color: #f7f9fa;
|
||||||
|
}
|
||||||
|
.file-info {
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.9em;
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
.parent-dir {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.file-number {
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 10px;
|
||||||
|
width: 30px;
|
||||||
|
color: #777;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.file-name {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 70%;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.delete-btn {
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: #c00;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9em;
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #e8e8e8;
|
||||||
|
}
|
||||||
|
.delete-btn:disabled {
|
||||||
|
color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.disabled a {
|
||||||
|
color: #999;
|
||||||
|
pointer-events: none;
|
||||||
|
border-color: #f0f0f0;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="{{.URLBase}}" class="btn">← Home</a>
|
||||||
|
</nav>
|
||||||
|
<h3>Index of {{.Path}}</h3>
|
||||||
|
<ul>
|
||||||
|
{{- if .ShowParent}}
|
||||||
|
<li>
|
||||||
|
<a href="{{urlpath .ParentPath}}" class="parent-dir">
|
||||||
|
<span class="file-number"></span> Parent Directory
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{{- end}}
|
||||||
|
{{$isBadPath := hasSuffix .Path "__bad__"}}
|
||||||
|
{{- range $i, $file := .Children}}
|
||||||
|
<li class="{{if $isBadPath}}disabled{{end}}">
|
||||||
|
<a {{ if not $isBadPath}}href="{{urlpath (printf "%s/%s" $.Path $file.Name)}}"{{end}}>
|
||||||
|
<span class="file-number">{{add $i 1}}.</span>
|
||||||
|
<span class="file-name">{{$file.Name}}{{if $file.IsDir}}/{{end}}</span>
|
||||||
|
<span class="file-info">
|
||||||
|
{{formatSize $file.Size}} ||
|
||||||
|
{{$file.ModTime.Format "2006-01-02 15:04:05"}}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
{{- if and $.CanDelete }}
|
||||||
|
<button
|
||||||
|
class="delete-btn"
|
||||||
|
data-path="{{printf "%s/%s" $.Path $file.Name}}">
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
{{- end}}
|
||||||
|
</li>
|
||||||
|
{{- end}}
|
||||||
|
</ul>
|
||||||
|
<script>
|
||||||
|
document.querySelectorAll('.delete-btn').forEach(btn=>{
|
||||||
|
btn.addEventListener('click', ()=>{
|
||||||
|
let p = btn.getAttribute('data-path');
|
||||||
|
let name = p.split('/').pop();
|
||||||
|
if(!confirm('Delete '+name+'?')) return;
|
||||||
|
fetch(p, { method: 'DELETE' })
|
||||||
|
.then(_=>location.reload());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>Available WebDAV Shares</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
nav {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.9em;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 4px;
|
||||||
|
color: #333;
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #e8e8e8;
|
||||||
|
}
|
||||||
|
.list-group {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.list-group-item {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
.list-group-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
.share-link {
|
||||||
|
color: #3498db;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.share-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="{{.URLBase}}" class="btn">← Home</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<h1>Available WebDAV Shares</h1>
|
||||||
|
<ul class="list-group">
|
||||||
|
{{- range .Handlers }}
|
||||||
|
<li class="list-group-item">
|
||||||
|
<a href="{{.RootPath}}" class="share-link">{{.Name}}</a>
|
||||||
|
</li>
|
||||||
|
{{- end }}
|
||||||
|
</ul>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
+68
-11
@@ -2,27 +2,90 @@ package webdav
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
"github.com/sirrobot01/decypharr/pkg/service"
|
"github.com/sirrobot01/decypharr/pkg/service"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed templates/*
|
||||||
|
var templatesFS embed.FS
|
||||||
|
|
||||||
|
var (
|
||||||
|
funcMap = template.FuncMap{
|
||||||
|
"add": func(a, b int) int {
|
||||||
|
return a + b
|
||||||
|
},
|
||||||
|
"urlpath": func(p string) string {
|
||||||
|
segments := strings.Split(p, "/")
|
||||||
|
for i, segment := range segments {
|
||||||
|
segments[i] = url.PathEscape(segment)
|
||||||
|
}
|
||||||
|
return strings.Join(segments, "/")
|
||||||
|
},
|
||||||
|
"formatSize": func(bytes int64) string {
|
||||||
|
const (
|
||||||
|
KB = 1024
|
||||||
|
MB = 1024 * KB
|
||||||
|
GB = 1024 * MB
|
||||||
|
TB = 1024 * GB
|
||||||
|
)
|
||||||
|
|
||||||
|
var size float64
|
||||||
|
var unit string
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case bytes >= TB:
|
||||||
|
size = float64(bytes) / TB
|
||||||
|
unit = "TB"
|
||||||
|
case bytes >= GB:
|
||||||
|
size = float64(bytes) / GB
|
||||||
|
unit = "GB"
|
||||||
|
case bytes >= MB:
|
||||||
|
size = float64(bytes) / MB
|
||||||
|
unit = "MB"
|
||||||
|
case bytes >= KB:
|
||||||
|
size = float64(bytes) / KB
|
||||||
|
unit = "KB"
|
||||||
|
default:
|
||||||
|
size = float64(bytes)
|
||||||
|
unit = "bytes"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format to 2 decimal places for larger units, no decimals for bytes
|
||||||
|
if unit == "bytes" {
|
||||||
|
return fmt.Sprintf("%.0f %s", size, unit)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%.2f %s", size, unit)
|
||||||
|
},
|
||||||
|
"hasSuffix": strings.HasSuffix,
|
||||||
|
}
|
||||||
|
tplRoot = template.Must(template.ParseFS(templatesFS, "templates/root.html"))
|
||||||
|
tplDirectory = template.Must(template.New("").Funcs(funcMap).ParseFS(templatesFS, "templates/directory.html"))
|
||||||
|
)
|
||||||
|
|
||||||
type WebDav struct {
|
type WebDav struct {
|
||||||
Handlers []*Handler
|
Handlers []*Handler
|
||||||
ready chan struct{}
|
ready chan struct{}
|
||||||
|
URLBase string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New() *WebDav {
|
func New() *WebDav {
|
||||||
svc := service.GetService()
|
svc := service.GetService()
|
||||||
|
urlBase := config.Get().URLBase
|
||||||
w := &WebDav{
|
w := &WebDav{
|
||||||
Handlers: make([]*Handler, 0),
|
Handlers: make([]*Handler, 0),
|
||||||
ready: make(chan struct{}),
|
ready: make(chan struct{}),
|
||||||
|
URLBase: urlBase,
|
||||||
}
|
}
|
||||||
for name, c := range svc.Debrid.Caches {
|
for name, c := range svc.Debrid.Caches {
|
||||||
h := NewHandler(name, c, c.GetLogger())
|
h := NewHandler(name, urlBase, c, c.GetLogger())
|
||||||
w.Handlers = append(w.Handlers, h)
|
w.Handlers = append(w.Handlers, h)
|
||||||
}
|
}
|
||||||
return w
|
return w
|
||||||
@@ -103,7 +166,7 @@ func (wd *WebDav) Start(ctx context.Context) error {
|
|||||||
|
|
||||||
func (wd *WebDav) mountHandlers(r chi.Router) {
|
func (wd *WebDav) mountHandlers(r chi.Router) {
|
||||||
for _, h := range wd.Handlers {
|
for _, h := range wd.Handlers {
|
||||||
r.Mount(h.RootPath, h)
|
r.Mount("/"+h.Name, h) // Mount to /name since router is already prefixed with /webdav
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,20 +190,14 @@ func (wd *WebDav) handleRoot() http.HandlerFunc {
|
|||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
|
|
||||||
tmpl, err := template.New("root").Parse(rootTemplate)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
data := struct {
|
data := struct {
|
||||||
Handlers []*Handler
|
Handlers []*Handler
|
||||||
Prefix string
|
URLBase string
|
||||||
}{
|
}{
|
||||||
Handlers: wd.Handlers,
|
Handlers: wd.Handlers,
|
||||||
Prefix: "/webdav",
|
URLBase: wd.URLBase,
|
||||||
}
|
}
|
||||||
if err := tmpl.Execute(w, data); err != nil {
|
if err := tplRoot.Execute(w, data); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user