- Hotfixes;
- Speed improvements
This commit is contained in:
@@ -58,12 +58,6 @@ type RepairRequest struct {
|
|||||||
FileName string
|
FileName string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PropfindResponse struct {
|
|
||||||
Data []byte
|
|
||||||
GzippedData []byte
|
|
||||||
Ts time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
type Cache struct {
|
type Cache struct {
|
||||||
dir string
|
dir string
|
||||||
client types.Client
|
client types.Client
|
||||||
@@ -72,7 +66,6 @@ type Cache struct {
|
|||||||
torrents *torrentCache
|
torrents *torrentCache
|
||||||
downloadLinks *xsync.Map[string, linkCache]
|
downloadLinks *xsync.Map[string, linkCache]
|
||||||
invalidDownloadLinks sync.Map
|
invalidDownloadLinks sync.Map
|
||||||
PropfindResp *xsync.Map[string, PropfindResponse]
|
|
||||||
folderNaming WebDavFolderNaming
|
folderNaming WebDavFolderNaming
|
||||||
|
|
||||||
listingDebouncer *utils.Debouncer[bool]
|
listingDebouncer *utils.Debouncer[bool]
|
||||||
@@ -134,7 +127,6 @@ func New(dc config.Debrid, client types.Client) *Cache {
|
|||||||
dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files
|
dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files
|
||||||
|
|
||||||
torrents: newTorrentCache(dirFilters),
|
torrents: newTorrentCache(dirFilters),
|
||||||
PropfindResp: xsync.NewMap[string, PropfindResponse](),
|
|
||||||
client: client,
|
client: client,
|
||||||
logger: logger.New(fmt.Sprintf("%s-webdav", client.GetName())),
|
logger: logger.New(fmt.Sprintf("%s-webdav", client.GetName())),
|
||||||
workers: dc.Workers,
|
workers: dc.Workers,
|
||||||
@@ -338,7 +330,7 @@ func (c *Cache) Sync() error {
|
|||||||
// Write these torrents to the cache
|
// Write these torrents to the cache
|
||||||
c.setTorrents(cachedTorrents, func() {
|
c.setTorrents(cachedTorrents, func() {
|
||||||
c.listingDebouncer.Call(false)
|
c.listingDebouncer.Call(false)
|
||||||
}) // This is set to false, cos it's likely rclone hs not started yet.
|
}) // Initial calls
|
||||||
c.logger.Info().Msgf("Loaded %d torrents from cache", len(cachedTorrents))
|
c.logger.Info().Msgf("Loaded %d torrents from cache", len(cachedTorrents))
|
||||||
|
|
||||||
if len(newTorrents) > 0 {
|
if len(newTorrents) > 0 {
|
||||||
@@ -630,7 +622,9 @@ func (c *Cache) ProcessTorrent(t *types.Torrent) error {
|
|||||||
IsComplete: len(t.Files) > 0,
|
IsComplete: len(t.Files) > 0,
|
||||||
AddedOn: addedOn,
|
AddedOn: addedOn,
|
||||||
}
|
}
|
||||||
c.setTorrent(ct, nil)
|
c.setTorrent(ct, func(tor *CachedTorrent) {
|
||||||
|
c.listingDebouncer.Call(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -651,7 +645,7 @@ func (c *Cache) AddTorrent(t *types.Torrent) error {
|
|||||||
AddedOn: addedOn,
|
AddedOn: addedOn,
|
||||||
}
|
}
|
||||||
c.setTorrent(ct, func(tor *CachedTorrent) {
|
c.setTorrent(ct, func(tor *CachedTorrent) {
|
||||||
c.listingDebouncer.Call(true)
|
c.RefreshListings(true)
|
||||||
})
|
})
|
||||||
go c.GenerateDownloadLinks(ct)
|
go c.GenerateDownloadLinks(ct)
|
||||||
return nil
|
return nil
|
||||||
@@ -722,7 +716,9 @@ func (c *Cache) deleteTorrent(id string, removeFromDebrid bool) bool {
|
|||||||
t.Files = newFiles
|
t.Files = newFiles
|
||||||
newId = cmp.Or(newId, t.Id)
|
newId = cmp.Or(newId, t.Id)
|
||||||
t.Id = newId
|
t.Id = newId
|
||||||
c.setTorrent(t, nil)
|
c.setTorrent(t, func(tor *CachedTorrent) {
|
||||||
|
c.RefreshListings(false)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ func (c *Cache) GetDownloadLink(torrentName, filename, fileLink string) (string,
|
|||||||
|
|
||||||
if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight {
|
if req, inFlight := c.downloadLinkRequests.Load(fileLink); inFlight {
|
||||||
// Wait for the other request to complete and use its result
|
// Wait for the other request to complete and use its result
|
||||||
fmt.Println("Waiting for existing request to complete")
|
|
||||||
result := req.(*downloadLinkRequest)
|
result := req.(*downloadLinkRequest)
|
||||||
return result.Wait()
|
return result.Wait()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,10 +31,6 @@ func (c *Cache) RefreshListings(refreshRclone bool) {
|
|||||||
// Copy the torrents to a string|time map
|
// Copy the torrents to a string|time map
|
||||||
c.torrents.refreshListing() // refresh torrent listings
|
c.torrents.refreshListing() // refresh torrent listings
|
||||||
|
|
||||||
if err := c.refreshParentXml(); err != nil {
|
|
||||||
c.logger.Debug().Err(err).Msg("Failed to refresh XML")
|
|
||||||
}
|
|
||||||
|
|
||||||
if refreshRclone {
|
if refreshRclone {
|
||||||
if err := c.refreshRclone(); err != nil {
|
if err := c.refreshRclone(); err != nil {
|
||||||
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
|
c.logger.Trace().Err(err).Msg("Failed to refresh rclone") // silent error
|
||||||
@@ -115,7 +111,7 @@ func (c *Cache) refreshTorrents() {
|
|||||||
close(workChan)
|
close(workChan)
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
|
|
||||||
c.listingDebouncer.Call(true)
|
c.listingDebouncer.Call(false)
|
||||||
|
|
||||||
c.logger.Debug().Msgf("Processed %d new torrents", counter)
|
c.logger.Debug().Msgf("Processed %d new torrents", counter)
|
||||||
}
|
}
|
||||||
@@ -140,13 +136,14 @@ func (c *Cache) refreshRclone() error {
|
|||||||
MaxIdleConnsPerHost: 5,
|
MaxIdleConnsPerHost: 5,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// Create form data
|
||||||
data := ""
|
data := ""
|
||||||
for index, dir := range c.GetDirectories() {
|
for index, dir := range c.GetDirectories() {
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
if index == 0 {
|
if index == 0 {
|
||||||
data += "dir=" + dir
|
data += "dir=" + dir
|
||||||
} else {
|
} else {
|
||||||
data += "&dir" + fmt.Sprint(index) + "=" + dir
|
data += "&dir" + fmt.Sprint(index+1) + "=" + dir
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +200,7 @@ func (c *Cache) refreshTorrent(torrentId string) *CachedTorrent {
|
|||||||
IsComplete: len(torrent.Files) > 0,
|
IsComplete: len(torrent.Files) > 0,
|
||||||
}
|
}
|
||||||
c.setTorrent(ct, func(torrent *CachedTorrent) {
|
c.setTorrent(ct, func(torrent *CachedTorrent) {
|
||||||
c.listingDebouncer.Call(true)
|
go c.listingDebouncer.Call(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
return ct
|
return ct
|
||||||
|
|||||||
@@ -17,13 +17,13 @@ func (c *Cache) StartSchedule() error {
|
|||||||
c.logger.Trace().Msgf("Next download link refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
c.logger.Trace().Msgf("Next download link refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
||||||
}
|
}
|
||||||
|
|
||||||
torrentJob, err := utils.ScheduleJob(ctx, c.torrentRefreshInterval, nil, c.refreshTorrents)
|
//torrentJob, err := utils.ScheduleJob(ctx, c.torrentRefreshInterval, nil, c.refreshTorrents)
|
||||||
if err != nil {
|
//if err != nil {
|
||||||
c.logger.Error().Err(err).Msg("Failed to add torrent refresh job")
|
// c.logger.Error().Err(err).Msg("Failed to add torrent refresh job")
|
||||||
}
|
//}
|
||||||
if t, err := torrentJob.NextRun(); err == nil {
|
//if t, err := torrentJob.NextRun(); err == nil {
|
||||||
c.logger.Trace().Msgf("Next torrent refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
// c.logger.Trace().Msgf("Next torrent refresh job: %s", t.Format("2006-01-02 15:04:05"))
|
||||||
}
|
//}
|
||||||
|
|
||||||
// Schedule the reset invalid links job
|
// Schedule the reset invalid links job
|
||||||
// This job will run every 24 hours
|
// This job will run every 24 hours
|
||||||
|
|||||||
@@ -1,230 +1 @@
|
|||||||
package debrid
|
package debrid
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/xml"
|
|
||||||
"fmt"
|
|
||||||
"github.com/beevik/etree"
|
|
||||||
"github.com/sirrobot01/decypharr/internal/request"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
DavNS = "DAV:"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Multistatus XML types for WebDAV response
|
|
||||||
type Multistatus struct {
|
|
||||||
XMLName xml.Name `xml:"D:multistatus"`
|
|
||||||
Namespace string `xml:"xmlns:D,attr"`
|
|
||||||
Responses []Response `xml:"D:response"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
Href string `xml:"D:href"`
|
|
||||||
Propstat Propstat `xml:"D:propstat"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Propstat struct {
|
|
||||||
Prop Prop `xml:"D:prop"`
|
|
||||||
Status string `xml:"D:status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Prop struct {
|
|
||||||
ResourceType ResourceType `xml:"D:resourcetype"`
|
|
||||||
DisplayName string `xml:"D:displayname"`
|
|
||||||
LastModified string `xml:"D:getlastmodified"`
|
|
||||||
ContentType string `xml:"D:getcontenttype"`
|
|
||||||
ContentLength string `xml:"D:getcontentlength"`
|
|
||||||
SupportedLock SupportedLock `xml:"D:supportedlock"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ResourceType struct {
|
|
||||||
Collection *struct{} `xml:"D:collection,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SupportedLock struct {
|
|
||||||
LockEntry LockEntry `xml:"D:lockentry"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LockEntry struct {
|
|
||||||
LockScope LockScope `xml:"D:lockscope"`
|
|
||||||
LockType LockType `xml:"D:locktype"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LockScope struct {
|
|
||||||
Exclusive *struct{} `xml:"D:exclusive"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LockType struct {
|
|
||||||
Write *struct{} `xml:"D:write"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) refreshParentXml() error {
|
|
||||||
// Refresh the defaults first
|
|
||||||
parents := []string{"__all__", "torrents"}
|
|
||||||
torrents := c.GetListing("__all__")
|
|
||||||
clientName := c.client.GetName()
|
|
||||||
customFolders := c.GetCustomFolders()
|
|
||||||
wg := sync.WaitGroup{}
|
|
||||||
totalFolders := len(parents) + len(customFolders)
|
|
||||||
wg.Add(totalFolders)
|
|
||||||
errCh := make(chan error, totalFolders)
|
|
||||||
for _, parent := range parents {
|
|
||||||
parent := parent
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
if err := c.refreshFolderXml(torrents, clientName, parent); err != nil {
|
|
||||||
errCh <- fmt.Errorf("failed to refresh folder %s: %v", parent, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
// refresh custom folders
|
|
||||||
for _, folder := range customFolders {
|
|
||||||
go func() {
|
|
||||||
folder := folder
|
|
||||||
defer wg.Done()
|
|
||||||
listing := c.GetListing(folder)
|
|
||||||
if err := c.refreshFolderXml(listing, clientName, folder); err != nil {
|
|
||||||
errCh <- fmt.Errorf("failed to refresh folder %s: %v", folder, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
close(errCh)
|
|
||||||
// if any errors, return the first
|
|
||||||
if err := <-errCh; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Cache) refreshFolderXml(torrents []os.FileInfo, clientName, parent string) error {
|
|
||||||
// Get the current timestamp in RFC1123 format
|
|
||||||
currentTime := time.Now().UTC().Format(http.TimeFormat)
|
|
||||||
|
|
||||||
// Create the multistatus response structure
|
|
||||||
ms := Multistatus{
|
|
||||||
Namespace: DavNS,
|
|
||||||
Responses: make([]Response, 0, len(torrents)+1), // Pre-allocate for parent + torrents
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the parent directory
|
|
||||||
baseUrl := path.Join("webdav", clientName, parent)
|
|
||||||
|
|
||||||
// Add parent response
|
|
||||||
ms.Responses = append(ms.Responses, createDirectoryResponse(baseUrl, parent, currentTime))
|
|
||||||
|
|
||||||
// Add torrents to the response
|
|
||||||
for _, torrent := range torrents {
|
|
||||||
name := torrent.Name()
|
|
||||||
torrentPath := path.Join("/webdav", clientName, parent, name) + "/"
|
|
||||||
ms.Responses = append(ms.Responses, createDirectoryResponse(torrentPath, name, currentTime))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a buffer and encode the XML
|
|
||||||
xmlData, err := xml.MarshalIndent(ms, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to generate XML: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add XML declaration
|
|
||||||
xmlHeader := []byte(xml.Header)
|
|
||||||
xmlOutput := append(xmlHeader, xmlData...)
|
|
||||||
|
|
||||||
// Cache the result
|
|
||||||
cacheKey := fmt.Sprintf("%s:1", baseUrl)
|
|
||||||
|
|
||||||
// Assume Gzip function exists elsewhere
|
|
||||||
gzippedData := request.Gzip(xmlOutput) // Replace with your actual gzip function
|
|
||||||
|
|
||||||
c.PropfindResp.Store(cacheKey, PropfindResponse{
|
|
||||||
Data: xmlOutput,
|
|
||||||
GzippedData: gzippedData,
|
|
||||||
Ts: time.Now(),
|
|
||||||
})
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func createDirectoryResponse(href, displayName, modTime string) Response {
|
|
||||||
return Response{
|
|
||||||
Href: href,
|
|
||||||
Propstat: Propstat{
|
|
||||||
Prop: Prop{
|
|
||||||
ResourceType: ResourceType{
|
|
||||||
Collection: &struct{}{},
|
|
||||||
},
|
|
||||||
DisplayName: displayName,
|
|
||||||
LastModified: modTime,
|
|
||||||
ContentType: "httpd/unix-directory",
|
|
||||||
ContentLength: "0",
|
|
||||||
SupportedLock: SupportedLock{
|
|
||||||
LockEntry: LockEntry{
|
|
||||||
LockScope: LockScope{
|
|
||||||
Exclusive: &struct{}{},
|
|
||||||
},
|
|
||||||
LockType: LockType{
|
|
||||||
Write: &struct{}{},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
Status: "HTTP/1.1 200 OK",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addDirectoryResponse(multistatus *etree.Element, href, displayName, modTime string) *etree.Element {
|
|
||||||
responseElem := multistatus.CreateElement("D:response")
|
|
||||||
|
|
||||||
// Add href - ensure it's properly formatted
|
|
||||||
hrefElem := responseElem.CreateElement("D:href")
|
|
||||||
hrefElem.SetText(href)
|
|
||||||
|
|
||||||
// Add propstat
|
|
||||||
propstatElem := responseElem.CreateElement("D:propstat")
|
|
||||||
|
|
||||||
// Add prop
|
|
||||||
propElem := propstatElem.CreateElement("D:prop")
|
|
||||||
|
|
||||||
// Add resource type (collection = directory)
|
|
||||||
resourceTypeElem := propElem.CreateElement("D:resourcetype")
|
|
||||||
resourceTypeElem.CreateElement("D:collection")
|
|
||||||
|
|
||||||
// Add display name
|
|
||||||
displayNameElem := propElem.CreateElement("D:displayname")
|
|
||||||
displayNameElem.SetText(displayName)
|
|
||||||
|
|
||||||
// Add last modified time
|
|
||||||
lastModElem := propElem.CreateElement("D:getlastmodified")
|
|
||||||
lastModElem.SetText(modTime)
|
|
||||||
|
|
||||||
// Add content type for directories
|
|
||||||
contentTypeElem := propElem.CreateElement("D:getcontenttype")
|
|
||||||
contentTypeElem.SetText("httpd/unix-directory")
|
|
||||||
|
|
||||||
// Add length (size) - directories typically have zero size
|
|
||||||
contentLengthElem := propElem.CreateElement("D:getcontentlength")
|
|
||||||
contentLengthElem.SetText("0")
|
|
||||||
|
|
||||||
// Add supported lock
|
|
||||||
lockElem := propElem.CreateElement("D:supportedlock")
|
|
||||||
lockEntryElem := lockElem.CreateElement("D:lockentry")
|
|
||||||
|
|
||||||
lockScopeElem := lockEntryElem.CreateElement("D:lockscope")
|
|
||||||
lockScopeElem.CreateElement("D:exclusive")
|
|
||||||
|
|
||||||
lockTypeElem := lockEntryElem.CreateElement("D:locktype")
|
|
||||||
lockTypeElem.CreateElement("D:write")
|
|
||||||
|
|
||||||
// Add status
|
|
||||||
statusElem := propstatElem.CreateElement("D:status")
|
|
||||||
statusElem.SetText("HTTP/1.1 200 OK")
|
|
||||||
|
|
||||||
return responseElem
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ package webdav
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -17,7 +17,6 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/sirrobot01/decypharr/internal/request"
|
|
||||||
"github.com/sirrobot01/decypharr/internal/utils"
|
"github.com/sirrobot01/decypharr/internal/utils"
|
||||||
"github.com/sirrobot01/decypharr/pkg/debrid/debrid"
|
"github.com/sirrobot01/decypharr/pkg/debrid/debrid"
|
||||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||||
@@ -32,6 +31,41 @@ 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,
|
||||||
@@ -87,12 +121,8 @@ func (h *Handler) getTorrentsFolders(folder string) []os.FileInfo {
|
|||||||
func (h *Handler) getParentItems() []string {
|
func (h *Handler) getParentItems() []string {
|
||||||
parents := []string{"__all__", "torrents"}
|
parents := []string{"__all__", "torrents"}
|
||||||
|
|
||||||
// Add user-defined parent items
|
// Add custom folders
|
||||||
for _, dir := range h.cache.GetCustomFolders() {
|
parents = append(parents, h.cache.GetCustomFolders()...)
|
||||||
if dir != "" {
|
|
||||||
parents = append(parents, dir)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// version.txt
|
// version.txt
|
||||||
parents = append(parents, "version.txt")
|
parents = append(parents, "version.txt")
|
||||||
@@ -119,29 +149,47 @@ func (h *Handler) getParentFiles() []os.FileInfo {
|
|||||||
return rootFiles
|
return rootFiles
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
// returns the os.FileInfo slice for “depth-1” children of cleanPath
|
||||||
|
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.UnescapePath(path.Clean(name))
|
||||||
|
root := path.Clean(h.getRootPath())
|
||||||
|
|
||||||
|
// top‐level “parents” (e.g. __all__, torrents)
|
||||||
|
if name == root {
|
||||||
|
return h.getParentFiles()
|
||||||
|
}
|
||||||
|
// one level down (e.g. /root/parentFolder)
|
||||||
|
if parent, ok := h.isParentPath(name); ok {
|
||||||
|
return h.getTorrentsFolders(parent)
|
||||||
|
}
|
||||||
|
// torrent-folder level (e.g. /root/parentFolder/torrentName)
|
||||||
|
rel := strings.TrimPrefix(name, root+string(os.PathSeparator))
|
||||||
|
parts := strings.Split(rel, string(os.PathSeparator))
|
||||||
|
parent, _ := url.PathUnescape(parts[0])
|
||||||
|
if len(parts) == 2 && utils.Contains(h.getParentItems(), parent) {
|
||||||
|
torrentName := utils.UnescapePath(parts[1])
|
||||||
|
if t := h.cache.GetTorrentByName(torrentName); t != nil {
|
||||||
|
return h.getFileInfos(t.Torrent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||||
|
if !strings.HasPrefix(name, "/") {
|
||||||
|
name = "/" + name
|
||||||
|
}
|
||||||
|
name = utils.UnescapePath(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()
|
||||||
|
|
||||||
// Fast path optimization with a map lookup instead of string comparisons
|
// 1) special case version.txt
|
||||||
switch name {
|
if name == filepath.Join(rootDir, "version.txt") {
|
||||||
case rootDir:
|
|
||||||
return &File{
|
|
||||||
cache: h.cache,
|
|
||||||
isDir: true,
|
|
||||||
children: h.getParentFiles(),
|
|
||||||
name: string(os.PathSeparator),
|
|
||||||
metadataOnly: true,
|
|
||||||
modTime: now,
|
|
||||||
}, nil
|
|
||||||
case filepath.Join(rootDir, "version.txt"):
|
|
||||||
versionInfo := version.GetInfo().String()
|
versionInfo := version.GetInfo().String()
|
||||||
return &File{
|
return &File{
|
||||||
cache: h.cache,
|
cache: h.cache,
|
||||||
@@ -154,55 +202,35 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single check for top-level folders
|
// 2) directory case: ask getChildren
|
||||||
if parent, ok := h.isParentPath(name); ok {
|
if children := h.getChildren(name); children != nil {
|
||||||
folderName := strings.TrimPrefix(name, rootDir)
|
displayName := path.Base(name)
|
||||||
folderName = strings.TrimPrefix(folderName, string(os.PathSeparator))
|
if name == rootDir {
|
||||||
|
displayName = string(os.PathSeparator)
|
||||||
// Only fetcher the torrent folders once
|
}
|
||||||
children := h.getTorrentsFolders(parent)
|
|
||||||
|
|
||||||
return &File{
|
return &File{
|
||||||
cache: h.cache,
|
cache: h.cache,
|
||||||
isDir: true,
|
isDir: true,
|
||||||
children: children,
|
children: children,
|
||||||
name: folderName,
|
name: displayName,
|
||||||
size: 0,
|
size: 0,
|
||||||
metadataOnly: metadataOnly,
|
metadataOnly: metadataOnly,
|
||||||
modTime: now,
|
modTime: now,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
_path := strings.TrimPrefix(name, rootDir)
|
// 3) file‐within‐torrent case
|
||||||
parts := strings.Split(strings.TrimPrefix(_path, string(os.PathSeparator)), string(os.PathSeparator))
|
// everything else must be a file under a torrent folder
|
||||||
parentFolder, _ := url.QueryUnescape(parts[0])
|
rel := strings.TrimPrefix(name, rootDir+string(os.PathSeparator))
|
||||||
|
parts := strings.Split(rel, string(os.PathSeparator))
|
||||||
if len(parts) >= 2 && (utils.Contains(h.getParentItems(), parentFolder)) {
|
if len(parts) >= 2 {
|
||||||
|
parent, _ := url.PathUnescape(parts[0])
|
||||||
torrentName := parts[1]
|
if utils.Contains(h.getParentItems(), parent) {
|
||||||
cachedTorrent := h.cache.GetTorrentByName(torrentName)
|
torrentName := utils.UnescapePath(parts[1])
|
||||||
if cachedTorrent == nil {
|
cached := h.cache.GetTorrentByName(torrentName)
|
||||||
h.logger.Debug().Msgf("Torrent not found: %s", torrentName)
|
if cached != nil && len(parts) >= 3 {
|
||||||
return nil, os.ErrNotExist
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) == 2 {
|
|
||||||
// Torrent folder level
|
|
||||||
return &File{
|
|
||||||
cache: h.cache,
|
|
||||||
torrentName: torrentName,
|
|
||||||
isDir: true,
|
|
||||||
children: h.getFileInfos(cachedTorrent.Torrent),
|
|
||||||
name: cachedTorrent.Name,
|
|
||||||
size: cachedTorrent.Size,
|
|
||||||
metadataOnly: metadataOnly,
|
|
||||||
modTime: cachedTorrent.AddedOn,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Torrent file level
|
|
||||||
filename := filepath.Join(parts[2:]...)
|
filename := filepath.Join(parts[2:]...)
|
||||||
if file, ok := cachedTorrent.Files[filename]; ok {
|
if file, ok := cached.Files[filename]; ok {
|
||||||
return &File{
|
return &File{
|
||||||
cache: h.cache,
|
cache: h.cache,
|
||||||
torrentName: torrentName,
|
torrentName: torrentName,
|
||||||
@@ -212,10 +240,12 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
|
|||||||
size: file.Size,
|
size: file.Size,
|
||||||
link: file.Link,
|
link: file.Link,
|
||||||
metadataOnly: metadataOnly,
|
metadataOnly: metadataOnly,
|
||||||
modTime: cachedTorrent.AddedOn,
|
modTime: cached.AddedOn,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
h.logger.Info().Msgf("File not found: %s", name)
|
h.logger.Info().Msgf("File not found: %s", name)
|
||||||
return nil, os.ErrNotExist
|
return nil, os.ErrNotExist
|
||||||
@@ -257,163 +287,21 @@ func (h *Handler) getFileInfos(torrent *types.Torrent) []os.FileInfo {
|
|||||||
|
|
||||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// Handle OPTIONS
|
switch r.Method {
|
||||||
if r.Method == "OPTIONS" {
|
case "GET":
|
||||||
w.WriteHeader(http.StatusOK)
|
h.handleGet(w, r)
|
||||||
return
|
return
|
||||||
}
|
case "HEAD":
|
||||||
|
h.handleHead(w, r)
|
||||||
// Cache PROPFIND responses for a short time to reduce load.
|
|
||||||
if r.Method == "PROPFIND" {
|
|
||||||
// Determine the Depth; default to "1" if not provided.
|
|
||||||
// Set metadata only
|
|
||||||
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
|
||||||
r = r.WithContext(ctx)
|
|
||||||
cleanPath := path.Clean(r.URL.Path)
|
|
||||||
if r.Header.Get("Depth") == "" {
|
|
||||||
r.Header.Set("Depth", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reject "infinity" depth
|
|
||||||
if r.Header.Get("Depth") == "infinity" {
|
|
||||||
r.Header.Set("Depth", "1")
|
|
||||||
}
|
|
||||||
|
|
||||||
cacheKey := fmt.Sprintf("%s:%s", cleanPath, r.Header.Get("Depth"))
|
|
||||||
|
|
||||||
if served := h.serveFromCacheIfValid(w, r, cacheKey); served {
|
|
||||||
return
|
return
|
||||||
}
|
case "OPTIONS":
|
||||||
|
h.handleOptions(w, r)
|
||||||
// No valid cache entry; process the PROPFIND request.
|
return
|
||||||
responseRecorder := httptest.NewRecorder()
|
case "PROPFIND":
|
||||||
handler := &webdav.Handler{
|
h.handlePropfind(w, r)
|
||||||
FileSystem: h,
|
|
||||||
LockSystem: webdav.NewMemLS(),
|
|
||||||
Logger: func(r *http.Request, err error) {
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error().Err(err).Msg("WebDAV error")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
handler.ServeHTTP(responseRecorder, r)
|
|
||||||
responseData := responseRecorder.Body.Bytes()
|
|
||||||
gzippedData := request.Gzip(responseData)
|
|
||||||
|
|
||||||
// Create compressed version
|
|
||||||
|
|
||||||
h.cache.PropfindResp.Store(cacheKey, debrid.PropfindResponse{
|
|
||||||
Data: responseData,
|
|
||||||
GzippedData: gzippedData,
|
|
||||||
Ts: time.Now(),
|
|
||||||
})
|
|
||||||
|
|
||||||
// Forward the captured response to the client.
|
|
||||||
for k, v := range responseRecorder.Header() {
|
|
||||||
w.Header()[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
if acceptsGzip(r) {
|
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
|
||||||
w.Header().Set("Vary", "Accept-Encoding")
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(gzippedData)))
|
|
||||||
w.WriteHeader(responseRecorder.Code)
|
|
||||||
_, _ = w.Write(gzippedData)
|
|
||||||
} else {
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(responseData)))
|
|
||||||
w.WriteHeader(responseRecorder.Code)
|
|
||||||
_, _ = w.Write(responseData)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
|
||||||
|
|
||||||
// Handle GET requests for file/directory content
|
|
||||||
if r.Method == "GET" {
|
|
||||||
fRaw, err := h.OpenFile(r.Context(), r.URL.Path, os.O_RDONLY, 0)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error().Err(err).
|
|
||||||
Str("path", r.URL.Path).
|
|
||||||
Msg("Failed to open file")
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer fRaw.Close()
|
|
||||||
|
|
||||||
fi, err := fRaw.Stat()
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error().Err(err).Msg("Failed to stat file")
|
|
||||||
http.Error(w, "Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the target is a directory, use your directory listing logic.
|
|
||||||
if fi.IsDir() {
|
|
||||||
h.serveDirectory(w, r, fRaw)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Checks if the file is a torrent file
|
|
||||||
// .content is nil if the file is a torrent file
|
|
||||||
// .content means file is preloaded, e.g version.txt
|
|
||||||
if file, ok := fRaw.(*File); ok && file.content == nil {
|
|
||||||
link, err := file.getDownloadLink()
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Trace().
|
|
||||||
Err(err).
|
|
||||||
Str("path", r.URL.Path).
|
|
||||||
Msg("Could not fetch download link")
|
|
||||||
http.Error(w, "Could not fetch download link", http.StatusPreconditionFailed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if link == "" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
file.downloadLink = link
|
|
||||||
}
|
|
||||||
|
|
||||||
rs, ok := fRaw.(io.ReadSeeker)
|
|
||||||
if !ok {
|
|
||||||
// If not, read the entire file into memory as a fallback.
|
|
||||||
buf, err := io.ReadAll(fRaw)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error().Err(err).Msg("Failed to read file content")
|
|
||||||
http.Error(w, "Server Error", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rs = bytes.NewReader(buf)
|
|
||||||
}
|
|
||||||
fileName := fi.Name()
|
|
||||||
contentType := getContentType(fileName)
|
|
||||||
w.Header().Set("Content-Type", contentType)
|
|
||||||
// http.ServeContent automatically handles Range requests.
|
|
||||||
http.ServeContent(w, r, fileName, fi.ModTime(), rs)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Method == "HEAD" {
|
|
||||||
f, err := h.OpenFile(r.Context(), r.URL.Path, os.O_RDONLY, 0)
|
|
||||||
if err != nil {
|
|
||||||
h.logger.Error().Err(err).Str("path", r.URL.Path).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
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", getContentType(fi.Name()))
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
|
|
||||||
w.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
|
|
||||||
w.Header().Set("Accept-Ranges", "bytes")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
default:
|
||||||
handler := &webdav.Handler{
|
handler := &webdav.Handler{
|
||||||
FileSystem: h,
|
FileSystem: h,
|
||||||
LockSystem: webdav.NewMemLS(),
|
LockSystem: webdav.NewMemLS(),
|
||||||
@@ -428,6 +316,9 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
handler.ServeHTTP(w, r)
|
handler.ServeHTTP(w, r)
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getContentType(fileName string) string {
|
func getContentType(fileName string) string {
|
||||||
@@ -466,34 +357,6 @@ func (h *Handler) isParentPath(urlPath string) (string, bool) {
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) serveFromCacheIfValid(w http.ResponseWriter, r *http.Request, key string) bool {
|
|
||||||
respCache, ok := h.cache.PropfindResp.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
ttl := h.getCacheTTL(r.URL.Path)
|
|
||||||
|
|
||||||
if time.Since(respCache.Ts) >= ttl {
|
|
||||||
h.cache.PropfindResp.Delete(key)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
|
||||||
|
|
||||||
if acceptsGzip(r) && len(respCache.GzippedData) > 0 {
|
|
||||||
w.Header().Set("Content-Encoding", "gzip")
|
|
||||||
w.Header().Set("Vary", "Accept-Encoding")
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(respCache.GzippedData)))
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write(respCache.GzippedData)
|
|
||||||
} else {
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(respCache.Data)))
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_, _ = w.Write(respCache.Data)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file webdav.File) {
|
func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file webdav.File) {
|
||||||
var children []os.FileInfo
|
var children []os.FileInfo
|
||||||
if f, ok := file.(*File); ok {
|
if f, ok := file.(*File); ok {
|
||||||
@@ -587,3 +450,192 @@ func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file we
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
fRaw, err := h.OpenFile(r.Context(), r.URL.Path, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error().Err(err).
|
||||||
|
Str("path", r.URL.Path).
|
||||||
|
Msg("Failed to open file")
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer fRaw.Close()
|
||||||
|
|
||||||
|
fi, err := fRaw.Stat()
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error().Err(err).Msg("Failed to stat file")
|
||||||
|
http.Error(w, "Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the target is a directory, use your directory listing logic.
|
||||||
|
if fi.IsDir() {
|
||||||
|
h.serveDirectory(w, r, fRaw)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the file is a torrent file
|
||||||
|
// .content is nil if the file is a torrent file
|
||||||
|
// .content means file is preloaded, e.g version.txt
|
||||||
|
if file, ok := fRaw.(*File); ok && file.content == nil {
|
||||||
|
link, err := file.getDownloadLink()
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Trace().
|
||||||
|
Err(err).
|
||||||
|
Str("path", r.URL.Path).
|
||||||
|
Msg("Could not fetch download link")
|
||||||
|
http.Error(w, "Could not fetch download link", http.StatusPreconditionFailed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if link == "" {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
file.downloadLink = link
|
||||||
|
}
|
||||||
|
|
||||||
|
rs, ok := fRaw.(io.ReadSeeker)
|
||||||
|
if !ok {
|
||||||
|
// If not, read the entire file into memory as a fallback.
|
||||||
|
buf, err := io.ReadAll(fRaw)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error().Err(err).Msg("Failed to read file content")
|
||||||
|
http.Error(w, "Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rs = bytes.NewReader(buf)
|
||||||
|
}
|
||||||
|
fileName := fi.Name()
|
||||||
|
contentType := getContentType(fileName)
|
||||||
|
w.Header().Set("Content-Type", contentType)
|
||||||
|
// http.ServeContent automatically handles Range requests.
|
||||||
|
http.ServeContent(w, r, fileName, fi.ModTime(), rs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleHead(w http.ResponseWriter, r *http.Request) {
|
||||||
|
f, err := h.OpenFile(r.Context(), r.URL.Path, os.O_RDONLY, 0)
|
||||||
|
if err != nil {
|
||||||
|
h.logger.Error().Err(err).Str("path", r.URL.Path).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
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", getContentType(fi.Name()))
|
||||||
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
|
||||||
|
w.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
|
||||||
|
w.Header().Set("Accept-Ranges", "bytes")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Allow", "OPTIONS, GET, HEAD, PUT, DELETE, MKCOL, COPY, MOVE, PROPFIND")
|
||||||
|
w.Header().Set("DAV", "1, 2")
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Setup context for metadata only
|
||||||
|
ctx := context.WithValue(r.Context(), "metadataOnly", true)
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
// Determine depth (default "1")
|
||||||
|
depth := r.Header.Get("Depth")
|
||||||
|
if depth == "" {
|
||||||
|
depth = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanPath := path.Clean(r.URL.Path)
|
||||||
|
|
||||||
|
// Build the list of entries
|
||||||
|
type entry struct {
|
||||||
|
href string
|
||||||
|
fi os.FileInfo
|
||||||
|
}
|
||||||
|
var entries []entry
|
||||||
|
|
||||||
|
// Always include the resource itself
|
||||||
|
f, err := h.OpenFile(r.Context(), cleanPath, os.O_RDONLY, 0)
|
||||||
|
if err == nil {
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if fi, err2 := f.Stat(); err2 == nil {
|
||||||
|
entries = append(entries, entry{
|
||||||
|
href: cleanPath,
|
||||||
|
fi: fi,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add children if directory and depth isn't 0
|
||||||
|
if fi.IsDir() {
|
||||||
|
children := h.getChildren(cleanPath)
|
||||||
|
for _, child := range children {
|
||||||
|
entries = append(entries, entry{
|
||||||
|
href: path.Join("/", cleanPath, child.Name()) + "/",
|
||||||
|
fi: child,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create MultiStatus response
|
||||||
|
multiStatus := MultiStatus{
|
||||||
|
Namespace: "DAV:",
|
||||||
|
Responses: []DAVResponse{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add responses for each entry
|
||||||
|
for _, e := range entries {
|
||||||
|
var resourceType *ResourceType
|
||||||
|
var contentLength int64
|
||||||
|
|
||||||
|
if e.fi.IsDir() {
|
||||||
|
resourceType = &ResourceType{
|
||||||
|
Collection: &Collection{},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
contentLength = e.fi.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format href path properly
|
||||||
|
raw := e.href
|
||||||
|
u := &url.URL{Path: raw}
|
||||||
|
escaped := u.EscapedPath()
|
||||||
|
|
||||||
|
response := DAVResponse{
|
||||||
|
Href: escaped,
|
||||||
|
PropStat: PropStat{
|
||||||
|
Prop: Prop{
|
||||||
|
ResourceType: resourceType,
|
||||||
|
LastModified: e.fi.ModTime().Format("2006-01-02T15:04:05.000-07:00"),
|
||||||
|
ContentLength: contentLength,
|
||||||
|
DisplayName: e.fi.Name(),
|
||||||
|
},
|
||||||
|
Status: "HTTP/1.1 200 OK",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
multiStatus.Responses = append(multiStatus.Responses, response)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Marshal to XML
|
||||||
|
body, err := xml.Marshal(multiStatus)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "application/xml; charset=utf-8")
|
||||||
|
w.Header().Set("Vary", "Accept-Encoding")
|
||||||
|
|
||||||
|
// Set status code
|
||||||
|
w.WriteHeader(207) // MultiStatus
|
||||||
|
_, _ = w.Write([]byte(xml.Header))
|
||||||
|
_, _ = w.Write(body)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user