- Improve propfind handler
- remove path escapes in fileinfo - other minor fixes
This commit is contained in:
4
go.mod
4
go.mod
@@ -1,12 +1,11 @@
|
|||||||
module github.com/sirrobot01/decypharr
|
module github.com/sirrobot01/decypharr
|
||||||
|
|
||||||
go 1.24
|
go 1.24.0
|
||||||
|
|
||||||
toolchain go1.24.3
|
toolchain go1.24.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/torrent v1.55.0
|
github.com/anacrolix/torrent v1.55.0
|
||||||
github.com/beevik/etree v1.5.1
|
|
||||||
github.com/cavaliergopher/grab/v3 v3.0.1
|
github.com/cavaliergopher/grab/v3 v3.0.1
|
||||||
github.com/go-chi/chi/v5 v5.1.0
|
github.com/go-chi/chi/v5 v5.1.0
|
||||||
github.com/go-co-op/gocron/v2 v2.16.1
|
github.com/go-co-op/gocron/v2 v2.16.1
|
||||||
@@ -16,6 +15,7 @@ require (
|
|||||||
github.com/puzpuzpuz/xsync/v4 v4.1.0
|
github.com/puzpuzpuz/xsync/v4 v4.1.0
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/rs/zerolog v1.33.0
|
||||||
|
github.com/stanNthe5/stringbuf v0.0.3
|
||||||
golang.org/x/crypto v0.33.0
|
golang.org/x/crypto v0.33.0
|
||||||
golang.org/x/net v0.35.0
|
golang.org/x/net v0.35.0
|
||||||
golang.org/x/sync v0.12.0
|
golang.org/x/sync v0.12.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -36,8 +36,6 @@ github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CM
|
|||||||
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
github.com/anacrolix/torrent v1.55.0 h1:s9yh/YGdPmbN9dTa+0Inh2dLdrLQRvEAj1jdFW/Hdd8=
|
||||||
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
github.com/anacrolix/torrent v1.55.0/go.mod h1:sBdZHBSZNj4de0m+EbYg7vvs/G/STubxu/GzzNbojsE=
|
||||||
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
|
||||||
github.com/beevik/etree v1.5.1 h1:TC3zyxYp+81wAmbsi8SWUpZCurbxa6S8RITYRSkNRwo=
|
|
||||||
github.com/beevik/etree v1.5.1/go.mod h1:gPNJNaBGVZ9AwsidazFZyygnd+0pAU38N4D+WemwKNs=
|
|
||||||
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||||
@@ -204,6 +202,8 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
|
|||||||
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
|
||||||
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
|
github.com/smartystreets/goconvey v0.0.0-20190306220146-200a235640ff/go.mod h1:KSQcGKpxUMHk3nbYzs/tIBAM2iDooCn0BmttHOJEbLs=
|
||||||
|
github.com/stanNthe5/stringbuf v0.0.3 h1:3ChRipDckEY6FykaQ1Dowy3B+ZQa72EDBCasvT5+D1w=
|
||||||
|
github.com/stanNthe5/stringbuf v0.0.3/go.mod h1:hii5Vr+mucoWkNJlIYQVp8YvuPtq45fFnJEAhcPf2cQ=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
func EscapePath(path string) string {
|
func PathUnescape(path string) string {
|
||||||
// escape %
|
|
||||||
escapedPath := strings.ReplaceAll(path, "%", "%25")
|
|
||||||
|
|
||||||
// add others
|
// try to use url.PathUnescape
|
||||||
|
if unescaped, err := url.PathUnescape(path); err == nil {
|
||||||
|
return unescaped
|
||||||
|
}
|
||||||
|
|
||||||
return escapedPath
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnescapePath(path string) string {
|
|
||||||
// unescape %
|
// unescape %
|
||||||
unescapedPath := strings.ReplaceAll(path, "%25", "%")
|
unescapedPath := strings.ReplaceAll(path, "%25", "%")
|
||||||
|
|
||||||
|
|||||||
12
main.go
12
main.go
@@ -5,7 +5,10 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"github.com/sirrobot01/decypharr/cmd/decypharr"
|
"github.com/sirrobot01/decypharr/cmd/decypharr"
|
||||||
"github.com/sirrobot01/decypharr/internal/config"
|
"github.com/sirrobot01/decypharr/internal/config"
|
||||||
|
"github.com/sirrobot01/decypharr/pkg/version"
|
||||||
"log"
|
"log"
|
||||||
|
"net/http"
|
||||||
|
_ "net/http/pprof" // registers pprof handlers
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
@@ -19,6 +22,15 @@ func main() {
|
|||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
if version.GetInfo().Channel == "dev" {
|
||||||
|
log.Println("Running in dev mode")
|
||||||
|
go func() {
|
||||||
|
if err := http.ListenAndServe(":6060", nil); err != nil {
|
||||||
|
log.Fatalf("pprof server failed: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
var configPath string
|
var configPath string
|
||||||
flag.StringVar(&configPath, "config", "/data", "path to the data folder")
|
flag.StringVar(&configPath, "config", "/data", "path to the data folder")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package debrid
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sirrobot01/decypharr/internal/utils"
|
|
||||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -20,7 +19,7 @@ type fileInfo struct {
|
|||||||
isDir bool
|
isDir bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fi *fileInfo) Name() string { return utils.EscapePath(fi.name) }
|
func (fi *fileInfo) Name() string { return fi.name }
|
||||||
func (fi *fileInfo) Size() int64 { return fi.size }
|
func (fi *fileInfo) Size() int64 { return fi.size }
|
||||||
func (fi *fileInfo) Mode() os.FileMode { return fi.mode }
|
func (fi *fileInfo) Mode() os.FileMode { return fi.mode }
|
||||||
func (fi *fileInfo) ModTime() time.Time { return fi.modTime }
|
func (fi *fileInfo) ModTime() time.Time { return fi.modTime }
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const (
|
|||||||
|
|
||||||
filterBySizeGT string = "size_gt"
|
filterBySizeGT string = "size_gt"
|
||||||
filterBySizeLT string = "size_lt"
|
filterBySizeLT string = "size_lt"
|
||||||
|
|
||||||
filterBLastAdded string = "last_added"
|
filterBLastAdded string = "last_added"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ type torrentCache struct {
|
|||||||
folderListing map[string][]os.FileInfo
|
folderListing map[string][]os.FileInfo
|
||||||
folderListingMu sync.RWMutex
|
folderListingMu sync.RWMutex
|
||||||
directoriesFilters map[string][]directoryFilter
|
directoriesFilters map[string][]directoryFilter
|
||||||
sortNeeded bool
|
sortNeeded atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type sortableFile struct {
|
type sortableFile struct {
|
||||||
@@ -62,10 +62,10 @@ func newTorrentCache(dirFilters map[string][]directoryFilter) *torrentCache {
|
|||||||
byID: make(map[string]string),
|
byID: make(map[string]string),
|
||||||
byName: make(map[string]*CachedTorrent),
|
byName: make(map[string]*CachedTorrent),
|
||||||
folderListing: make(map[string][]os.FileInfo),
|
folderListing: make(map[string][]os.FileInfo),
|
||||||
sortNeeded: false,
|
|
||||||
directoriesFilters: dirFilters,
|
directoriesFilters: dirFilters,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tc.sortNeeded.Store(false)
|
||||||
tc.listing.Store(make([]os.FileInfo, 0))
|
tc.listing.Store(make([]os.FileInfo, 0))
|
||||||
return tc
|
return tc
|
||||||
}
|
}
|
||||||
@@ -100,12 +100,12 @@ func (tc *torrentCache) set(id, name string, torrent *CachedTorrent) {
|
|||||||
defer tc.mu.Unlock()
|
defer tc.mu.Unlock()
|
||||||
tc.byID[id] = name
|
tc.byID[id] = name
|
||||||
tc.byName[name] = torrent
|
tc.byName[name] = torrent
|
||||||
tc.sortNeeded = true
|
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 {
|
if !tc.sortNeeded.Load() {
|
||||||
return tc.listing.Load().([]os.FileInfo)
|
return tc.listing.Load().([]os.FileInfo)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,7 +134,7 @@ func (tc *torrentCache) refreshListing() {
|
|||||||
for name, t := range tc.byName {
|
for name, t := range tc.byName {
|
||||||
all = append(all, sortableFile{name, t.AddedOn, t.Size})
|
all = append(all, sortableFile{name, t.AddedOn, t.Size})
|
||||||
}
|
}
|
||||||
tc.sortNeeded = false
|
tc.sortNeeded.Store(false)
|
||||||
tc.mu.Unlock()
|
tc.mu.Unlock()
|
||||||
|
|
||||||
sort.Slice(all, func(i, j int) bool {
|
sort.Slice(all, func(i, j int) bool {
|
||||||
@@ -261,12 +261,12 @@ func (tc *torrentCache) removeId(id string) {
|
|||||||
tc.mu.Lock()
|
tc.mu.Lock()
|
||||||
defer tc.mu.Unlock()
|
defer tc.mu.Unlock()
|
||||||
delete(tc.byID, id)
|
delete(tc.byID, id)
|
||||||
tc.sortNeeded = true
|
tc.sortNeeded.Store(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tc *torrentCache) remove(name string) {
|
func (tc *torrentCache) remove(name string) {
|
||||||
tc.mu.Lock()
|
tc.mu.Lock()
|
||||||
defer tc.mu.Unlock()
|
defer tc.mu.Unlock()
|
||||||
delete(tc.byName, name)
|
delete(tc.byName, name)
|
||||||
tc.sortNeeded = true
|
tc.sortNeeded.Store(true)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ func (q *QBit) createSymlinksWebdav(debridTorrent *debridTypes.Torrent, rclonePa
|
|||||||
|
|
||||||
remainingFiles := make(map[string]debridTypes.File)
|
remainingFiles := make(map[string]debridTypes.File)
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
remainingFiles[utils.EscapePath(file.Name)] = file
|
remainingFiles[file.Name] = file
|
||||||
}
|
}
|
||||||
|
|
||||||
ticker := time.NewTicker(100 * time.Millisecond)
|
ticker := time.NewTicker(100 * time.Millisecond)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package webdav
|
package webdav
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/sirrobot01/decypharr/internal/utils"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -15,7 +14,7 @@ type FileInfo struct {
|
|||||||
isDir bool
|
isDir bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fi *FileInfo) Name() string { return utils.EscapePath(fi.name) } // uses minimal escaping
|
func (fi *FileInfo) Name() string { return fi.name } // uses minimal escaping
|
||||||
func (fi *FileInfo) Size() int64 { return fi.size }
|
func (fi *FileInfo) Size() int64 { return fi.size }
|
||||||
func (fi *FileInfo) Mode() os.FileMode { return fi.mode }
|
func (fi *FileInfo) Mode() os.FileMode { return fi.mode }
|
||||||
func (fi *FileInfo) ModTime() time.Time { return fi.modTime }
|
func (fi *FileInfo) ModTime() time.Time { return fi.modTime }
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ func (h *Handler) getChildren(name string) []os.FileInfo {
|
|||||||
if name[0] != '/' {
|
if name[0] != '/' {
|
||||||
name = "/" + name
|
name = "/" + name
|
||||||
}
|
}
|
||||||
name = utils.UnescapePath(path.Clean(name))
|
name = utils.PathUnescape(path.Clean(name))
|
||||||
root := path.Clean(h.getRootPath())
|
root := path.Clean(h.getRootPath())
|
||||||
|
|
||||||
// top‐level “parents” (e.g. __all__, torrents)
|
// top‐level “parents” (e.g. __all__, torrents)
|
||||||
@@ -133,9 +133,8 @@ func (h *Handler) getChildren(name string) []os.FileInfo {
|
|||||||
// torrent-folder level (e.g. /root/parentFolder/torrentName)
|
// torrent-folder level (e.g. /root/parentFolder/torrentName)
|
||||||
rel := strings.TrimPrefix(name, root+string(os.PathSeparator))
|
rel := strings.TrimPrefix(name, root+string(os.PathSeparator))
|
||||||
parts := strings.Split(rel, string(os.PathSeparator))
|
parts := strings.Split(rel, string(os.PathSeparator))
|
||||||
parent, _ := url.PathUnescape(parts[0])
|
if len(parts) == 2 && utils.Contains(h.getParentItems(), parts[0]) {
|
||||||
if len(parts) == 2 && utils.Contains(h.getParentItems(), parent) {
|
torrentName := parts[1]
|
||||||
torrentName := utils.UnescapePath(parts[1])
|
|
||||||
if t := h.cache.GetTorrentByName(torrentName); t != nil {
|
if t := h.cache.GetTorrentByName(torrentName); t != nil {
|
||||||
return h.getFileInfos(t.Torrent)
|
return h.getFileInfos(t.Torrent)
|
||||||
}
|
}
|
||||||
@@ -147,7 +146,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
if !strings.HasPrefix(name, "/") {
|
if !strings.HasPrefix(name, "/") {
|
||||||
name = "/" + name
|
name = "/" + name
|
||||||
}
|
}
|
||||||
name = utils.UnescapePath(path.Clean(name))
|
name = utils.PathUnescape(path.Clean(name))
|
||||||
rootDir := path.Clean(h.getRootPath())
|
rootDir := path.Clean(h.getRootPath())
|
||||||
metadataOnly := ctx.Value("metadataOnly") != nil
|
metadataOnly := ctx.Value("metadataOnly") != nil
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -188,9 +187,8 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
rel := strings.TrimPrefix(name, rootDir+string(os.PathSeparator))
|
rel := strings.TrimPrefix(name, rootDir+string(os.PathSeparator))
|
||||||
parts := strings.Split(rel, string(os.PathSeparator))
|
parts := strings.Split(rel, string(os.PathSeparator))
|
||||||
if len(parts) >= 2 {
|
if len(parts) >= 2 {
|
||||||
parent, _ := url.PathUnescape(parts[0])
|
if utils.Contains(h.getParentItems(), parts[0]) {
|
||||||
if utils.Contains(h.getParentItems(), parent) {
|
torrentName := parts[1]
|
||||||
torrentName := utils.UnescapePath(parts[1])
|
|
||||||
cached := h.cache.GetTorrentByName(torrentName)
|
cached := h.cache.GetTorrentByName(torrentName)
|
||||||
if cached != nil && len(parts) >= 3 {
|
if cached != nil && len(parts) >= 3 {
|
||||||
filename := filepath.Join(parts[2:]...)
|
filename := filepath.Join(parts[2:]...)
|
||||||
|
|||||||
@@ -38,3 +38,31 @@ func (h *Handler) getCacheTTL(urlPath string) time.Duration {
|
|||||||
}
|
}
|
||||||
return 2 * time.Minute // Longer TTL for other paths
|
return 2 * time.Minute // Longer TTL for other paths
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var pctHex = "0123456789ABCDEF"
|
||||||
|
|
||||||
|
// fastEscapePath returns a percent-encoded path, preserving '/'
|
||||||
|
// and only encoding bytes outside the unreserved set:
|
||||||
|
//
|
||||||
|
// ALPHA / DIGIT / '-' / '_' / '.' / '~' / '/'
|
||||||
|
func fastEscapePath(p string) string {
|
||||||
|
var b strings.Builder
|
||||||
|
|
||||||
|
for i := 0; i < len(p); i++ {
|
||||||
|
c := p[i]
|
||||||
|
// unreserved (plus '/')
|
||||||
|
if (c >= 'a' && c <= 'z') ||
|
||||||
|
(c >= 'A' && c <= 'Z') ||
|
||||||
|
(c >= '0' && c <= '9') ||
|
||||||
|
c == '-' || c == '_' ||
|
||||||
|
c == '.' || c == '~' ||
|
||||||
|
c == '/' {
|
||||||
|
b.WriteByte(c)
|
||||||
|
} else {
|
||||||
|
b.WriteByte('%')
|
||||||
|
b.WriteByte(pctHex[c>>4])
|
||||||
|
b.WriteByte(pctHex[c&0xF])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,14 +2,20 @@ package webdav
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"github.com/stanNthe5/stringbuf"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var builderPool = sync.Pool{
|
||||||
|
New: func() interface{} { return stringbuf.New("") },
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
||||||
// Setup context for metadata only
|
// Setup context for metadata only
|
||||||
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
||||||
@@ -25,8 +31,12 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Build the list of entries
|
// Build the list of entries
|
||||||
type entry struct {
|
type entry struct {
|
||||||
href string
|
href string
|
||||||
fi os.FileInfo
|
escHref string // already XML-safe + percent-escaped
|
||||||
|
escName string
|
||||||
|
size int64
|
||||||
|
isDir bool
|
||||||
|
modTime string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always include the resource itself
|
// Always include the resource itself
|
||||||
@@ -45,65 +55,81 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect children if a directory and depth allows
|
var rawEntries []os.FileInfo
|
||||||
children := make([]os.FileInfo, 0)
|
if fi.IsDir() {
|
||||||
if fi.IsDir() && depth != "0" {
|
rawEntries = append(rawEntries, h.getChildren(cleanPath)...)
|
||||||
children = h.getChildren(cleanPath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entries := make([]entry, 0, 1+len(children))
|
now := time.Now().UTC().Format("2006-01-02T15:04:05.000-07:00")
|
||||||
entries = append(entries, entry{href: cleanPath, fi: fi})
|
entries := make([]entry, 0, len(rawEntries)+1)
|
||||||
|
// Add the current file itself
|
||||||
|
entries = append(entries, entry{
|
||||||
|
escHref: xmlEscape(fastEscapePath(cleanPath)),
|
||||||
|
escName: xmlEscape(fi.Name()),
|
||||||
|
isDir: fi.IsDir(),
|
||||||
|
size: fi.Size(),
|
||||||
|
modTime: fi.ModTime().Format("2006-01-02T15:04:05.000-07:00"),
|
||||||
|
})
|
||||||
|
for _, info := range rawEntries {
|
||||||
|
|
||||||
for _, child := range children {
|
nm := info.Name()
|
||||||
childHref := path.Join("/", cleanPath, child.Name())
|
// build raw href
|
||||||
if child.IsDir() {
|
href := path.Join("/", cleanPath, nm)
|
||||||
childHref += "/"
|
if info.IsDir() {
|
||||||
|
href += "/"
|
||||||
}
|
}
|
||||||
entries = append(entries, entry{href: childHref, fi: child})
|
|
||||||
|
entries = append(entries, entry{
|
||||||
|
escHref: xmlEscape(fastEscapePath(href)),
|
||||||
|
escName: xmlEscape(nm),
|
||||||
|
isDir: info.IsDir(),
|
||||||
|
size: info.Size(),
|
||||||
|
modTime: info.ModTime().Format("2006-01-02T15:04:05.000-07:00"),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use a string builder for creating XML
|
sb := builderPool.Get().(stringbuf.StringBuf)
|
||||||
var sb strings.Builder
|
sb.Reset()
|
||||||
|
defer builderPool.Put(sb)
|
||||||
|
|
||||||
// XML header and main element
|
// XML header and main element
|
||||||
sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>`)
|
_, _ = sb.WriteString(`<?xml version="1.0" encoding="UTF-8"?>`)
|
||||||
sb.WriteString(`<d:multistatus xmlns:d="DAV:">`)
|
_, _ = sb.WriteString(`<d:multistatus xmlns:d="DAV:">`)
|
||||||
|
|
||||||
// Format time once
|
|
||||||
timeFormat := "2006-01-02T15:04:05.000-07:00"
|
|
||||||
|
|
||||||
// Add responses for each entry
|
// Add responses for each entry
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
// Format href path properly
|
_, _ = sb.WriteString(`<d:response>`)
|
||||||
u := &url.URL{Path: e.href}
|
_, _ = sb.WriteString(`<d:href>`)
|
||||||
escaped := u.EscapedPath()
|
_, _ = sb.WriteString(e.escHref)
|
||||||
|
_, _ = sb.WriteString(`</d:href>`)
|
||||||
|
_, _ = sb.WriteString(`<d:propstat>`)
|
||||||
|
_, _ = sb.WriteString(`<d:prop>`)
|
||||||
|
|
||||||
sb.WriteString(`<d:response>`)
|
if e.isDir {
|
||||||
sb.WriteString(fmt.Sprintf(`<d:href>%s</d:href>`, xmlEscape(escaped)))
|
_, _ = sb.WriteString(`<d:resourcetype><d:collection/></d:resourcetype>`)
|
||||||
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 {
|
} else {
|
||||||
sb.WriteString(`<d:resourcetype/>`)
|
_, _ = sb.WriteString(`<d:resourcetype/>`)
|
||||||
sb.WriteString(fmt.Sprintf(`<d:getcontentlength>%d</d:getcontentlength>`, e.fi.Size()))
|
_, _ = sb.WriteString(`<d:getcontentlength>`)
|
||||||
|
_, _ = sb.WriteString(strconv.FormatInt(e.size, 10))
|
||||||
|
_, _ = sb.WriteString(`</d:getcontentlength>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always add lastmodified and displayname
|
_, _ = sb.WriteString(`<d:getlastmodified>`)
|
||||||
lastModified := e.fi.ModTime().Format(timeFormat)
|
_, _ = sb.WriteString(now)
|
||||||
sb.WriteString(fmt.Sprintf(`<d:getlastmodified>%s</d:getlastmodified>`, xmlEscape(lastModified)))
|
_, _ = sb.WriteString(`</d:getlastmodified>`)
|
||||||
sb.WriteString(fmt.Sprintf(`<d:displayname>%s</d:displayname>`, xmlEscape(e.fi.Name())))
|
|
||||||
|
|
||||||
sb.WriteString(`</d:prop>`)
|
_, _ = sb.WriteString(`<d:displayname>`)
|
||||||
sb.WriteString(`<d:status>HTTP/1.1 200 OK</d:status>`)
|
_, _ = sb.WriteString(e.escName)
|
||||||
sb.WriteString(`</d:propstat>`)
|
_, _ = sb.WriteString(`</d:displayname>`)
|
||||||
sb.WriteString(`</d:response>`)
|
|
||||||
|
_, _ = 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
|
// Close root element
|
||||||
sb.WriteString(`</d:multistatus>`)
|
_, _ = sb.WriteString(`</d:multistatus>`)
|
||||||
|
|
||||||
// Set headers
|
// Set headers
|
||||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
@@ -111,18 +137,28 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Set status code and write response
|
// Set status code and write response
|
||||||
w.WriteHeader(http.StatusMultiStatus) // 207 MultiStatus
|
w.WriteHeader(http.StatusMultiStatus) // 207 MultiStatus
|
||||||
_, _ = w.Write([]byte(sb.String()))
|
_, _ = w.Write(sb.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic XML escaping function
|
// Basic XML escaping function
|
||||||
func xmlEscape(s string) string {
|
func xmlEscape(s string) string {
|
||||||
s = strings.ReplaceAll(s, "&", "&")
|
var b strings.Builder
|
||||||
s = strings.ReplaceAll(s, "<", "<")
|
b.Grow(len(s))
|
||||||
s = strings.ReplaceAll(s, ">", ">")
|
for _, r := range s {
|
||||||
s = strings.ReplaceAll(s, "'", "'")
|
switch r {
|
||||||
s = strings.ReplaceAll(s, "\"", """)
|
case '&':
|
||||||
s = strings.ReplaceAll(s, "\n", " ")
|
b.WriteString("&")
|
||||||
s = strings.ReplaceAll(s, "\r", " ")
|
case '<':
|
||||||
s = strings.ReplaceAll(s, "\t", "	")
|
b.WriteString("<")
|
||||||
return s
|
case '>':
|
||||||
|
b.WriteString(">")
|
||||||
|
case '"':
|
||||||
|
b.WriteString(""")
|
||||||
|
case '\'':
|
||||||
|
b.WriteString("'")
|
||||||
|
default:
|
||||||
|
b.WriteRune(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user