- Cleaup the code
- Add delete button to webdav ui - Some other bug fixes here and there
This commit is contained in:
@@ -22,7 +22,6 @@ type AllDebrid struct {
|
||||
Host string `json:"host"`
|
||||
APIKey string
|
||||
accounts map[string]types.Account
|
||||
accountsMu sync.RWMutex
|
||||
DownloadUncached bool
|
||||
client *request.Client
|
||||
|
||||
|
||||
@@ -96,16 +96,6 @@ func (tc *torrentCache) set(name string, torrent, newTorrent CachedTorrent) {
|
||||
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 {
|
||||
// Fast path: if we have a sorted list and no changes since last sort
|
||||
if !tc.sortNeeded.Load() {
|
||||
@@ -135,7 +125,7 @@ func (tc *torrentCache) refreshListing() {
|
||||
tc.mu.Lock()
|
||||
all := make([]sortableFile, 0, len(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.mu.Unlock()
|
||||
@@ -279,7 +269,7 @@ func (tc *torrentCache) getIdMaps() map[string]struct{} {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
res := make(map[string]struct{}, len(tc.byID))
|
||||
for id, _ := range tc.byID {
|
||||
for id := range tc.byID {
|
||||
res[id] = struct{}{}
|
||||
}
|
||||
return res
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/sirrobot01/decypharr/internal/utils"
|
||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"net/http"
|
||||
@@ -23,7 +22,6 @@ type DebridLink struct {
|
||||
Host string `json:"host"`
|
||||
APIKey string
|
||||
accounts map[string]types.Account
|
||||
accountsMutex sync.RWMutex
|
||||
DownloadUncached bool
|
||||
client *request.Client
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ type Torbox struct {
|
||||
Host string `json:"host"`
|
||||
APIKey string
|
||||
accounts map[string]types.Account
|
||||
accountsMutex sync.RWMutex
|
||||
DownloadUncached bool
|
||||
client *request.Client
|
||||
|
||||
|
||||
@@ -145,32 +145,32 @@
|
||||
<div class="collapse navbar-collapse" id="navbarNav">
|
||||
<ul class="navbar-nav me-auto">
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
@@ -209,23 +209,15 @@
|
||||
|
||||
window.urlBase = "{{.URLBase}}";
|
||||
|
||||
function joinUrl (base, path) {
|
||||
if (path.substring(0, 1) === "/") {
|
||||
// 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 joinURL(...segments) {
|
||||
return segments.join('/').replace(/[/]+/g, '/').replace(/^(.+):\//, '$1://').replace(/^file:/, 'file:/').replace(/\/(?=[?&#])/g, '').replace(/\?/g, '&');
|
||||
}
|
||||
|
||||
function fetcher(endpoint, options = {}) {
|
||||
// Use the global urlBase or default to empty string
|
||||
let baseUrl = window.urlBase || '';
|
||||
|
||||
let url = joinUrl(baseUrl, endpoint);
|
||||
let url = joinURL(baseUrl, endpoint);
|
||||
|
||||
// Return the regular fetcher with the complete URL
|
||||
return fetch(url, options);
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
createToast(errorText || 'Registration failed', 'error');
|
||||
});
|
||||
} else {
|
||||
window.location.href = joinUrl(window.urlBase, '/');
|
||||
window.location.href = joinURL(window.urlBase, '/');
|
||||
}
|
||||
|
||||
})
|
||||
@@ -78,7 +78,7 @@
|
||||
fetcher('/skip-auth', { method: 'GET' })
|
||||
.then(response => {
|
||||
if (response.ok) {
|
||||
window.location.href = joinUrl(window.urlBase, '/');
|
||||
window.location.href = joinURL(window.urlBase, '/');
|
||||
} else {
|
||||
throw new Error('Failed to skip authentication');
|
||||
}
|
||||
|
||||
@@ -4,10 +4,8 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -27,15 +25,17 @@ type Handler struct {
|
||||
Name string
|
||||
logger zerolog.Logger
|
||||
cache *debrid.Cache
|
||||
URLBase 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{
|
||||
Name: name,
|
||||
cache: cache,
|
||||
logger: logger,
|
||||
RootPath: fmt.Sprintf("/%s", name),
|
||||
URLBase: urlBase,
|
||||
RootPath: path.Join(urlBase, "webdav", name),
|
||||
}
|
||||
return h
|
||||
}
|
||||
@@ -52,7 +52,7 @@ func (h *Handler) RemoveAll(ctx context.Context, name string) error {
|
||||
}
|
||||
name = path.Clean(name)
|
||||
|
||||
rootDir := path.Clean(h.getRootPath())
|
||||
rootDir := path.Clean(h.RootPath)
|
||||
|
||||
if name == rootDir {
|
||||
return os.ErrPermission
|
||||
@@ -74,10 +74,6 @@ func (h *Handler) Rename(ctx context.Context, oldName, newName string) error {
|
||||
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 {
|
||||
return h.cache.GetListing(folder)
|
||||
}
|
||||
@@ -120,7 +116,7 @@ func (h *Handler) getChildren(name string) []os.FileInfo {
|
||||
name = "/" + 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)
|
||||
if name == root {
|
||||
@@ -147,7 +143,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
||||
name = "/" + name
|
||||
}
|
||||
name = utils.PathUnescape(path.Clean(name))
|
||||
rootDir := path.Clean(h.getRootPath())
|
||||
rootDir := path.Clean(h.RootPath)
|
||||
metadataOnly := ctx.Value("metadataOnly") != nil
|
||||
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 {
|
||||
displayName := filepath.Clean(path.Base(name))
|
||||
if name == rootDir {
|
||||
displayName = string(os.PathSeparator)
|
||||
displayName = "/"
|
||||
}
|
||||
return &File{
|
||||
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)
|
||||
parentPath := path.Dir(cleanPath)
|
||||
showParent := cleanPath != "/" && parentPath != "." && parentPath != cleanPath
|
||||
isBadPath := strings.HasSuffix(cleanPath, "__bad__")
|
||||
_, canDelete := h.isParentPath(cleanPath)
|
||||
|
||||
// Prepare template data
|
||||
data := struct {
|
||||
@@ -343,73 +341,21 @@ func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file we
|
||||
ParentPath string
|
||||
ShowParent bool
|
||||
Children []os.FileInfo
|
||||
URLBase string
|
||||
IsBadPath bool
|
||||
CanDelete bool
|
||||
}{
|
||||
Path: cleanPath,
|
||||
ParentPath: parentPath,
|
||||
ShowParent: showParent,
|
||||
Children: children,
|
||||
}
|
||||
|
||||
// Parse and execute template
|
||||
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
|
||||
URLBase: h.URLBase,
|
||||
IsBadPath: isBadPath,
|
||||
CanDelete: canDelete,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
if err := tmpl.Execute(w, data); err != nil {
|
||||
h.logger.Error().Err(err).Msg("Failed to execute directory template")
|
||||
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||
if err := tplDirectory.ExecuteTemplate(w, "directory.html", data); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -423,7 +369,12 @@ func (h *Handler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer fRaw.Close()
|
||||
defer func(fRaw webdav.File) {
|
||||
err := fRaw.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(fRaw)
|
||||
|
||||
fi, err := fRaw.Stat()
|
||||
if err != nil {
|
||||
@@ -483,7 +434,12 @@ func (h *Handler) handleHead(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
defer func(f webdav.File) {
|
||||
err := f.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}(f)
|
||||
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package webdav
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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]
|
||||
}
|
||||
|
||||
func acceptsGzip(r *http.Request) bool {
|
||||
return strings.Contains(r.Header.Get("Accept-Encoding"), "gzip")
|
||||
}
|
||||
|
||||
func isValidURL(str string) bool {
|
||||
u, err := url.Parse(str)
|
||||
// A valid URL should parse without error, and have a non-empty scheme and 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"
|
||||
|
||||
// fastEscapePath returns a percent-encoded path, preserving '/'
|
||||
|
||||
@@ -13,7 +13,11 @@ import (
|
||||
)
|
||||
|
||||
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) {
|
||||
@@ -21,17 +25,10 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
||||
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
|
||||
escHref string // already XML-safe + percent-escaped
|
||||
escName string
|
||||
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>
|
||||
`
|
||||
141
pkg/webdav/templates/directory.html
Normal file
141
pkg/webdav/templates/directory.html
Normal file
@@ -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>
|
||||
71
pkg/webdav/templates/root.html
Normal file
71
pkg/webdav/templates/root.html
Normal file
@@ -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>
|
||||
|
||||
@@ -2,27 +2,90 @@ package webdav
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/sirrobot01/decypharr/internal/config"
|
||||
"github.com/sirrobot01/decypharr/pkg/service"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"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 {
|
||||
Handlers []*Handler
|
||||
ready chan struct{}
|
||||
URLBase string
|
||||
}
|
||||
|
||||
func New() *WebDav {
|
||||
svc := service.GetService()
|
||||
urlBase := config.Get().URLBase
|
||||
w := &WebDav{
|
||||
Handlers: make([]*Handler, 0),
|
||||
ready: make(chan struct{}),
|
||||
URLBase: urlBase,
|
||||
}
|
||||
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)
|
||||
}
|
||||
return w
|
||||
@@ -103,7 +166,7 @@ func (wd *WebDav) Start(ctx context.Context) error {
|
||||
|
||||
func (wd *WebDav) mountHandlers(r chi.Router) {
|
||||
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) {
|
||||
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 {
|
||||
Handlers []*Handler
|
||||
Prefix string
|
||||
URLBase string
|
||||
}{
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user