diff --git a/Dockerfile b/Dockerfile index 3035c2e..162e63f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,7 +11,7 @@ FROM golang:1-alpine AS builder WORKDIR /app COPY . . RUN apk add --no-cache bash git go gcc musl-dev curl fuse -RUN go build -ldflags="-s -w -X 'github.com/debridmediamanager/zurg/internal.BuiltAt=${BuiltAt}' -X 'github.com/debridmediamanager/zurg/internal.GoVersion=${GoVersion}' -X 'github.com/debridmediamanager/zurg/internal.GitCommit=${GitCommit}' -X 'github.com/debridmediamanager/zurg/internal.Version=${Version}'" -o zurg cmd/zurg +RUN go build -ldflags="-s -w -X 'github.com/debridmediamanager/zurg/internal.BuiltAt=${BuiltAt}' -X 'github.com/debridmediamanager/zurg/internal.GoVersion=${GoVersion}' -X 'github.com/debridmediamanager/zurg/internal.GitCommit=${GitCommit}' -X 'github.com/debridmediamanager/zurg/internal.Version=${Version}'" -o zurg ./cmd/zurg # Obfuscation stage FROM alpine:3 AS obfuscator diff --git a/bench.py b/bench.py index e324095..ac37270 100644 --- a/bench.py +++ b/bench.py @@ -14,14 +14,14 @@ async def extract_links(url): async def benchmark(url): # This will still block, because subprocess.run is not async - subprocess.run(['hey', '-n', '10000', '-c', '50', url]) + subprocess.run(['hey', '-n', '1000', '-c', '10', url]) url = 'http://localhost:9999/http/' async def main(): async for link in extract_links(url): - print("BENCHMARKING " + link.replace('/http/', '/')) - await benchmark(link.replace('/http/', '/')) + print("BENCHMARKING " + link.replace('/http/', '/dav/')) + await benchmark(link.replace('/http/', '/dav/')) print("BENCHMARKING " + link) await benchmark(link) diff --git a/go.mod b/go.mod index 690d6c1..4de5e7c 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,8 @@ require github.com/orcaman/concurrent-map/v2 v2.0.1 require github.com/panjf2000/ants/v2 v2.8.2 +require github.com/julienschmidt/httprouter v1.3.0 // indirect + require ( github.com/cespare/xxhash/v2 v2.1.1 // indirect github.com/dgraph-io/ristretto v0.1.1 diff --git a/go.sum b/go.sum index a80ce4b..8953f85 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekf github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= github.com/panjf2000/ants/v2 v2.8.2 h1:D1wfANttg8uXhC9149gRt1PDQ+dLVFjNXkCEycMcvQQ= diff --git a/internal/app.go b/internal/app.go index 0e87d48..54615e0 100644 --- a/internal/app.go +++ b/internal/app.go @@ -2,18 +2,19 @@ package internal import ( "fmt" - "net/http" + netHttp "net/http" "os" "github.com/debridmediamanager/zurg/internal/config" - "github.com/debridmediamanager/zurg/internal/net" + "github.com/debridmediamanager/zurg/internal/router" "github.com/debridmediamanager/zurg/internal/torrent" "github.com/debridmediamanager/zurg/internal/universal" - zurghttp "github.com/debridmediamanager/zurg/pkg/http" + "github.com/debridmediamanager/zurg/pkg/http" "github.com/debridmediamanager/zurg/pkg/logutil" "github.com/debridmediamanager/zurg/pkg/realdebrid" "github.com/debridmediamanager/zurg/pkg/utils" "github.com/dgraph-io/ristretto" + "github.com/julienschmidt/httprouter" "github.com/panjf2000/ants/v2" ) @@ -27,7 +28,7 @@ func MainApp(configPath string) { os.Exit(1) } - apiClient := zurghttp.NewHTTPClient(config.GetToken(), config.GetRetriesUntilFailed(), config.GetRealDebridTimeout(), config, log.Named("httpclient")) + apiClient := http.NewHTTPClient(config.GetToken(), config.GetRetriesUntilFailed(), config.GetRealDebridTimeout(), config, log.Named("httpclient")) rd := realdebrid.NewRealDebrid(apiClient, log.Named("realdebrid")) @@ -52,17 +53,17 @@ func MainApp(configPath string) { torrentMgr := torrent.NewTorrentManager(config, rd, p, cache, log.Named("manager")) - downloadClient := zurghttp.NewHTTPClient(config.GetToken(), config.GetRetriesUntilFailed(), 0, config, log.Named("dlclient")) + downloadClient := http.NewHTTPClient(config.GetToken(), config.GetRetriesUntilFailed(), 0, config, log.Named("dlclient")) getfile := universal.NewGetFile(downloadClient) - mux := http.NewServeMux() - net.Router(mux, getfile, config, torrentMgr, log.Named("net")) + handler := httprouter.New() + handler.RedirectTrailingSlash = false + handler.RedirectFixedPath = true + router.ApplyRouteTable(handler, getfile, torrentMgr, config, log.Named("router")) addr := fmt.Sprintf("%s:%s", config.GetHost(), config.GetPort()) - server := &http.Server{Addr: addr, Handler: mux} - zurglog.Infof("Starting server on %s", addr) - if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := netHttp.ListenAndServe(addr, handler); err != nil && err != netHttp.ErrServerClosed { zurglog.Errorf("Failed to start server: %v", err) os.Exit(1) } diff --git a/internal/dav/delete.go b/internal/dav/delete.go index b7846d0..c1e834e 100644 --- a/internal/dav/delete.go +++ b/internal/dav/delete.go @@ -2,88 +2,36 @@ package dav import ( "fmt" - "net/http" - "path" - "strings" "github.com/debridmediamanager/zurg/internal/torrent" - "go.uber.org/zap" ) -func HandleDeleteRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, log *zap.SugaredLogger) { - requestPath := path.Clean(r.URL.Path) - filteredSegments := splitIntoSegments(requestPath) - - var err error - switch { - case len(filteredSegments) == 0: - err = fmt.Errorf("cannot delete root") - case len(filteredSegments) == 1: - err = fmt.Errorf("cannot delete configured directory") - case len(filteredSegments) == 2: - err = handleDeleteTorrent(w, filteredSegments, t) - case len(filteredSegments) >= 3: - err = handleDeleteFile(w, filteredSegments, t) - default: - log.Warnf("Request %s %s not found", r.Method, requestPath) - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - if err != nil { - if strings.Contains(err.Error(), "cannot find") { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - log.Errorf("Error processing request: %v", err) - http.Error(w, "Server error", http.StatusInternalServerError) - return - } - - w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"") -} - -func handleDeleteTorrent(w http.ResponseWriter, segments []string, t *torrent.TorrentManager) error { - directory := segments[0] +func HandleDeleteTorrent(directory, torrentName string, t *torrent.TorrentManager) error { torrents, ok := t.DirectoryMap.Get(directory) if !ok { return fmt.Errorf("cannot find directory %s", directory) } - - accessKey := segments[1] - tor, ok := torrents.Get(accessKey) - if !ok { - return fmt.Errorf("cannot find torrent %s", accessKey) + if !torrents.Has(torrentName) { + return fmt.Errorf("cannot find torrent %s", torrentName) } - - t.Delete(tor.AccessKey) - - w.WriteHeader(http.StatusNoContent) + t.Delete(torrentName) return nil } -func handleDeleteFile(w http.ResponseWriter, segments []string, t *torrent.TorrentManager) error { - directory := segments[0] +func HandleDeleteFile(directory, torrentName, fileName string, t *torrent.TorrentManager) error { torrents, ok := t.DirectoryMap.Get(directory) if !ok { return fmt.Errorf("cannot find directory %s", directory) } - - accessKey := segments[1] - tor, ok := torrents.Get(accessKey) + tor, ok := torrents.Get(torrentName) if !ok { - return fmt.Errorf("cannot find torrent %s", accessKey) + return fmt.Errorf("cannot find torrent %s", torrentName) } - - // set filepath to last segment - filepath := segments[len(segments)-1] - file, ok := tor.SelectedFiles.Get(filepath) + file, ok := tor.SelectedFiles.Get(fileName) if !ok { - return fmt.Errorf("cannot find file %s", filepath) + return fmt.Errorf("cannot find file %s", fileName) } - file.Link = "unselect" t.ScheduleForRefresh() - w.WriteHeader(http.StatusNoContent) return nil } diff --git a/internal/dav/listing.go b/internal/dav/listing.go index ae3f660..2079b5a 100644 --- a/internal/dav/listing.go +++ b/internal/dav/listing.go @@ -2,8 +2,6 @@ package dav import ( "fmt" - "net/http" - "path" "path/filepath" "sort" "strings" @@ -13,44 +11,7 @@ import ( "go.uber.org/zap" ) -func HandlePropfindRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, log *zap.SugaredLogger) { - requestPath := path.Clean(r.URL.Path) - - var output *string - var err error - - filteredSegments := splitIntoSegments(requestPath) - switch { - case len(filteredSegments) == 0: - output, err = handleListDirectories(w, t) - case len(filteredSegments) == 1: - output, err = handleListTorrents(w, requestPath, t, log) - case len(filteredSegments) == 2: - output, err = handleListFiles(w, requestPath, t, log) - default: - log.Warnf("Request %s %s not found", r.Method, requestPath) - http.Error(w, "Not Found", http.StatusNotFound) - return - } - - if err != nil { - if strings.Contains(err.Error(), "cannot find") { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - log.Errorf("Error processing request: %v", err) - http.Error(w, "Server error", http.StatusInternalServerError) - return - } - - if output != nil { - w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, *output) - } -} - -func handleListDirectories(w http.ResponseWriter, t *torrent.TorrentManager) (*string, error) { +func HandleListDirectories(t *torrent.TorrentManager) (*string, error) { davDoc := "" // initial response is the directory itself davDoc += dav.BaseDirectory("", "") @@ -68,8 +29,7 @@ func handleListDirectories(w http.ResponseWriter, t *torrent.TorrentManager) (*s return &davDoc, nil } -func handleListTorrents(w http.ResponseWriter, requestPath string, t *torrent.TorrentManager, log *zap.SugaredLogger) (*string, error) { - directory := path.Base(requestPath) +func HandleListTorrents(directory string, t *torrent.TorrentManager, log *zap.SugaredLogger) (*string, error) { _, ok := t.DirectoryMap.Get(directory) if !ok { return nil, fmt.Errorf("cannot find directory %s", directory) @@ -92,20 +52,18 @@ func handleListTorrents(w http.ResponseWriter, requestPath string, t *torrent.To } } -func handleListFiles(w http.ResponseWriter, requestPath string, t *torrent.TorrentManager, log *zap.SugaredLogger) (*string, error) { - directory := path.Base(path.Dir(requestPath)) +func HandleListFiles(directory, torrentName string, t *torrent.TorrentManager, log *zap.SugaredLogger) (*string, error) { torrents, ok := t.DirectoryMap.Get(directory) if !ok { return nil, fmt.Errorf("cannot find directory %s", directory) } - accessKey := path.Base(requestPath) - tor, ok := torrents.Get(accessKey) + tor, ok := torrents.Get(torrentName) if !ok { - return nil, fmt.Errorf("cannot find torrent %s", accessKey) + return nil, fmt.Errorf("cannot find torrent %s", torrentName) } - if resp, ok := t.ResponseCache.Get(directory + "/" + accessKey + ".dav"); !ok { - log.Debugf("Generating xml for torrent %s", accessKey) + if resp, ok := t.ResponseCache.Get(directory + "/" + torrentName + ".dav"); !ok { + log.Debugf("Generating xml for torrent %s", torrentName) davDoc := "" + dav.BaseDirectory(filepath.Join(directory, tor.AccessKey), tor.LatestAdded) filenames := tor.SelectedFiles.Keys() sort.Strings(filenames) diff --git a/internal/http/listing.go b/internal/http/listing.go index adfde6d..024e86a 100644 --- a/internal/http/listing.go +++ b/internal/http/listing.go @@ -2,9 +2,7 @@ package http import ( "fmt" - "net/http" "net/url" - "path" "path/filepath" "sort" "strings" @@ -13,45 +11,9 @@ import ( "go.uber.org/zap" ) -func HandleDirectoryListing(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, log *zap.SugaredLogger) { - requestPath := path.Clean(r.URL.Path) - - var output *string - var err error - - filteredSegments := removeEmptySegments(strings.Split(requestPath, "/")) - switch { - case len(filteredSegments) == 1: - output, err = handleRoot(t) - case len(filteredSegments) == 2: - output, err = handleListOfTorrents(requestPath, t, log) - case len(filteredSegments) == 3: - output, err = handleSingleTorrent(requestPath, t, log) - default: - log.Warnf("Request %s %s not found", r.Method, requestPath) - http.Error(w, "Not Found", http.StatusNotFound) - return - } - if err != nil { - if strings.Contains(err.Error(), "cannot find") { - http.Error(w, "Not Found", http.StatusNotFound) - return - } - log.Errorf("Error processing request: %v", err) - http.Error(w, "Server error", http.StatusInternalServerError) - return - } - - if output != nil { - w.Header().Set("Content-Type", "text/html; charset=\"utf-8\"") - w.WriteHeader(http.StatusOK) - fmt.Fprint(w, *output) - } -} - -func handleRoot(t *torrent.TorrentManager) (*string, error) { +func HandleListDirectories(torMgr *torrent.TorrentManager) (*string, error) { htmlDoc := "
    " - directories := t.DirectoryMap.Keys() + directories := torMgr.DirectoryMap.Keys() sort.Strings(directories) for _, directory := range directories { if strings.HasPrefix(directory, "int__") { @@ -64,8 +26,7 @@ func handleRoot(t *torrent.TorrentManager) (*string, error) { return &htmlDoc, nil } -func handleListOfTorrents(requestPath string, t *torrent.TorrentManager, log *zap.SugaredLogger) (*string, error) { - directory := path.Base(requestPath) +func HandleListTorrents(directory string, t *torrent.TorrentManager, log *zap.SugaredLogger) (*string, error) { torrents, ok := t.DirectoryMap.Get(directory) if !ok { return nil, fmt.Errorf("cannot find directory %s", directory) @@ -85,7 +46,7 @@ func handleListOfTorrents(requestPath string, t *torrent.TorrentManager, log *za return allTorrents[i].AccessKey < allTorrents[j].AccessKey }) for _, tor := range allTorrents { - htmlDoc = htmlDoc + fmt.Sprintf("
  1. %s
  2. ", filepath.Join(requestPath, url.PathEscape(tor.AccessKey)), tor.AccessKey) + htmlDoc = htmlDoc + fmt.Sprintf("
  3. %s
  4. ", filepath.Join(directory, url.PathEscape(tor.AccessKey)), tor.AccessKey) } return &htmlDoc, nil } else { @@ -94,20 +55,18 @@ func handleListOfTorrents(requestPath string, t *torrent.TorrentManager, log *za } } -func handleSingleTorrent(requestPath string, t *torrent.TorrentManager, log *zap.SugaredLogger) (*string, error) { - directory := path.Base(path.Dir(requestPath)) +func HandleListFiles(directory, torrentName string, t *torrent.TorrentManager, log *zap.SugaredLogger) (*string, error) { torrents, ok := t.DirectoryMap.Get(directory) if !ok { return nil, fmt.Errorf("cannot find directory %s", directory) } - accessKey := path.Base(requestPath) - tor, ok := torrents.Get(accessKey) + tor, ok := torrents.Get(torrentName) if !ok { - return nil, fmt.Errorf("cannot find torrent %s", accessKey) + return nil, fmt.Errorf("cannot find torrent %s", torrentName) } - if resp, ok := t.ResponseCache.Get(directory + "/" + accessKey + ".html"); !ok { - log.Debugf("Generating html for torrent %s", accessKey) + if resp, ok := t.ResponseCache.Get(directory + "/" + torrentName + ".html"); !ok { + log.Debugf("Generating html for torrent %s", torrentName) htmlDoc := "
      " filenames := tor.SelectedFiles.Keys() sort.Strings(filenames) @@ -116,8 +75,8 @@ func handleSingleTorrent(requestPath string, t *torrent.TorrentManager, log *zap if file == nil || !strings.HasPrefix(file.Link, "http") { continue } - filePath := filepath.Join(requestPath, url.PathEscape(filename)) - htmlDoc += fmt.Sprintf("
    1. %s
    2. ", filePath, filename) + filePath := filepath.Join(directory, torrentName, url.PathEscape(filename)) + htmlDoc += fmt.Sprintf("
    3. %s
    4. ", filePath, filename) } return &htmlDoc, nil } else { diff --git a/internal/net/router.go b/internal/net/router.go deleted file mode 100644 index aa41b14..0000000 --- a/internal/net/router.go +++ /dev/null @@ -1,66 +0,0 @@ -package net - -import ( - "net/http" - "path" - "strings" - - "github.com/debridmediamanager/zurg/internal/config" - "github.com/debridmediamanager/zurg/internal/dav" - intHttp "github.com/debridmediamanager/zurg/internal/http" - "github.com/debridmediamanager/zurg/internal/torrent" - "github.com/debridmediamanager/zurg/internal/universal" - "go.uber.org/zap" -) - -// Router creates a WebDAV router -func Router(mux *http.ServeMux, getfile *universal.GetFile, c config.ConfigInterface, t *torrent.TorrentManager, log *zap.SugaredLogger) { - mux.HandleFunc("/http/", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case http.MethodGet: - requestPath := path.Clean(r.URL.Path) - if countNonEmptySegments(strings.Split(requestPath, "/")) > 3 { - getfile.HandleGetRequest(w, r, t, c, log) - } else { - intHttp.HandleDirectoryListing(w, r, t, log) - } - - case http.MethodHead: - universal.HandleHeadRequest(w, r, t, log) - - default: - log.Errorf("Request %s %s not supported yet", r.Method, r.URL.Path) - http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) - } - }) - - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - switch r.Method { - case "PROPFIND": - dav.HandlePropfindRequest(w, r, t, log) - - case http.MethodDelete: - dav.HandleDeleteRequest(w, r, t, log) - - case http.MethodGet: - getfile.HandleGetRequest(w, r, t, c, log) - - case http.MethodOptions: - w.WriteHeader(http.StatusOK) - - default: - log.Errorf("Request %s %s not supported yet", r.Method, r.URL.Path) - http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) - } - }) -} - -func countNonEmptySegments(urlSegments []string) int { - count := 0 - for _, s := range urlSegments { - if s != "" { - count++ - } - } - return count -} diff --git a/internal/router/router.go b/internal/router/router.go new file mode 100644 index 0000000..9e48209 --- /dev/null +++ b/internal/router/router.go @@ -0,0 +1,172 @@ +package router + +import ( + "fmt" + "net/http" + + "github.com/debridmediamanager/zurg/internal/config" + "github.com/debridmediamanager/zurg/internal/dav" + intHttp "github.com/debridmediamanager/zurg/internal/http" + "github.com/debridmediamanager/zurg/internal/torrent" + "github.com/debridmediamanager/zurg/internal/universal" + "github.com/julienschmidt/httprouter" + "go.uber.org/zap" +) + +type ZurgRouter struct { + getfile *universal.GetFile + torMgr *torrent.TorrentManager + cfg config.ConfigInterface + log *zap.SugaredLogger +} + +func ApplyRouteTable(router *httprouter.Router, getfile *universal.GetFile, torMgr *torrent.TorrentManager, cfg config.ConfigInterface, log *zap.SugaredLogger) { + zr := &ZurgRouter{ + getfile: getfile, + torMgr: torMgr, + cfg: cfg, + log: log, + } + + // http router + router.GET("/http/:directory/:torrent/:file", zr.universalDownloadFileHandler) + router.GET("/http/:directory/:torrent/", zr.httpTorrentDirectoryHandler) + router.GET("/http/:directory/", zr.httpDirectoryHandler) + router.GET("/http/", zr.httpRootHandler) + // HEAD route + router.HEAD("/http/:directory/:torrent/:file", zr.headFileHandler) + + // dav router + router.GET("/dav/:directory/:torrent/:file", zr.universalDownloadFileHandler) + router.GET("/dav/:directory/:torrent/", zr.propfindTorrentHandler) + router.GET("/dav/:directory/", zr.propfindDirectoryHandler) + router.GET("/dav/", zr.propfindRootHandler) + // PROPFIND routes + router.Handle("PROPFIND", "/dav/:directory/:torrent/", zr.propfindTorrentHandler) + router.Handle("PROPFIND", "/dav/:directory/", zr.propfindDirectoryHandler) + router.Handle("PROPFIND", "/dav/", zr.propfindRootHandler) + // DELETE routes + router.DELETE("/dav/:directory/:torrent/:file", zr.deleteFileHandler) + router.DELETE("/dav/:directory/:torrent/", zr.deleteTorrentHandler) + + // Global OPTIONS route + router.GlobalOPTIONS = http.HandlerFunc(zr.globalOptionsHandler) + + // root route + router.GET("/", zr.rootHandler) +} + +func (zr *ZurgRouter) httpTorrentDirectoryHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + directory := params.ByName("directory") + torrentName := params.ByName("torrent") + out, err := intHttp.HandleListFiles(directory, torrentName, zr.torMgr, zr.log) + if err != nil { + http.Error(resp, "Not Found", http.StatusNotFound) + return + } + resp.Header().Set("Content-Type", "text/html; charset=\"utf-8\"") + resp.WriteHeader(http.StatusOK) + fmt.Fprint(resp, *out) +} + +func (zr *ZurgRouter) httpDirectoryHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + directory := params.ByName("directory") + out, err := intHttp.HandleListTorrents(directory, zr.torMgr, zr.log) + if err != nil { + http.Error(resp, "Not Found", http.StatusNotFound) + return + } + resp.Header().Set("Content-Type", "text/html; charset=\"utf-8\"") + resp.WriteHeader(http.StatusOK) + fmt.Fprint(resp, *out) +} + +func (zr *ZurgRouter) httpRootHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + out, err := intHttp.HandleListDirectories(zr.torMgr) + if err != nil { + http.Error(resp, "Not Found", http.StatusNotFound) + return + } + resp.Header().Set("Content-Type", "text/html; charset=\"utf-8\"") + resp.WriteHeader(http.StatusOK) + fmt.Fprint(resp, *out) +} + +func (zr *ZurgRouter) rootHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + fmt.Fprint(resp, "

      zurg

      HTTP
      DAV") +} + +func (zr *ZurgRouter) propfindTorrentHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + directory := params.ByName("directory") + torrentName := params.ByName("torrent") + out, err := dav.HandleListFiles(directory, torrentName, zr.torMgr, zr.log) + if err != nil { + http.Error(resp, "Not Found", http.StatusNotFound) + return + } + resp.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"") + resp.WriteHeader(http.StatusOK) + fmt.Fprint(resp, *out) +} + +func (zr *ZurgRouter) propfindDirectoryHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + directory := params.ByName("directory") + out, err := dav.HandleListTorrents(directory, zr.torMgr, zr.log) + if err != nil { + http.Error(resp, "Not Found", http.StatusNotFound) + return + } + resp.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"") + resp.WriteHeader(http.StatusOK) + fmt.Fprint(resp, *out) +} + +func (zr *ZurgRouter) propfindRootHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + out, err := dav.HandleListDirectories(zr.torMgr) + if err != nil { + http.Error(resp, "Not Found", http.StatusNotFound) + return + } + resp.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"") + resp.WriteHeader(http.StatusOK) + fmt.Fprint(resp, *out) +} + +func (zr *ZurgRouter) deleteFileHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + directory := params.ByName("directory") + torrentName := params.ByName("torrent") + fileName := params.ByName("file") + if dav.HandleDeleteFile(directory, torrentName, fileName, zr.torMgr) != nil { + http.Error(resp, "Not Found", http.StatusNotFound) + return + } + resp.WriteHeader(http.StatusNoContent) +} + +func (zr *ZurgRouter) deleteTorrentHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + directory := params.ByName("directory") + torrentName := params.ByName("torrent") + if dav.HandleDeleteTorrent(directory, torrentName, zr.torMgr) != nil { + http.Error(resp, "Not Found", http.StatusNotFound) + return + } + resp.WriteHeader(http.StatusNoContent) +} + +func (zr *ZurgRouter) globalOptionsHandler(resp http.ResponseWriter, req *http.Request) { + resp.WriteHeader(http.StatusOK) +} + +func (zr *ZurgRouter) universalDownloadFileHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + directory := params.ByName("directory") + torrentName := params.ByName("torrent") + fileName := params.ByName("file") + zr.getfile.HandleGetRequest(directory, torrentName, fileName, resp, req, zr.torMgr, zr.cfg, zr.log) +} + +func (zr *ZurgRouter) headFileHandler(resp http.ResponseWriter, req *http.Request, params httprouter.Params) { + directory := params.ByName("directory") + torrentName := params.ByName("torrent") + fileName := params.ByName("file") + universal.HandleHeadRequest(directory, torrentName, fileName, resp, req, zr.torMgr, zr.log) +} diff --git a/internal/torrent/manager.go b/internal/torrent/manager.go index cac60b7..d2791b6 100644 --- a/internal/torrent/manager.go +++ b/internal/torrent/manager.go @@ -67,26 +67,26 @@ func NewTorrentManager(cfg config.ConfigInterface, api *realdebrid.RealDebrid, p // Fetch downloads t.DownloadCache = cmap.New[*realdebrid.Download]() - _ = t.workerPool.Submit(func() { - page := 1 - offset := 0 - for { - downloads, totalDownloads, err := t.Api.GetDownloads(page, offset) - if err != nil { - t.log.Fatalf("Cannot get downloads: %v\n", err) - } - for i := range downloads { - if !t.DownloadCache.Has(downloads[i].Link) { - t.DownloadCache.Set(downloads[i].Link, &downloads[i]) - } - } - offset += len(downloads) - page++ - if offset >= totalDownloads { - break - } - } - }) + // _ = t.workerPool.Submit(func() { + // page := 1 + // offset := 0 + // for { + // downloads, totalDownloads, err := t.Api.GetDownloads(page, offset) + // if err != nil { + // t.log.Fatalf("Cannot get downloads: %v\n", err) + // } + // for i := range downloads { + // if !t.DownloadCache.Has(downloads[i].Link) { + // t.DownloadCache.Set(downloads[i].Link, &downloads[i]) + // } + // } + // offset += len(downloads) + // page++ + // if offset >= totalDownloads { + // break + // } + // } + // }) var newTorrents []realdebrid.Torrent var err error @@ -869,7 +869,7 @@ func (t *TorrentManager) updateDirectoryResponsesCache() { continue } davRet += dav.Directory(tor.AccessKey, tor.LatestAdded) - htmlRet += fmt.Sprintf("
    5. %s
    6. ", directory, tor.AccessKey, tor.AccessKey) + htmlRet += fmt.Sprintf("
    7. %s
    8. ", directory, tor.AccessKey, tor.AccessKey) } } @@ -897,7 +897,7 @@ func (t *TorrentManager) buildTorrentResponses(tor *Torrent) (string, string) { davRet += dav.File(filename, file.Bytes, file.Ended) filePath := filepath.Join("$dir", tor.AccessKey, url.PathEscape(filename)) - htmlRet += fmt.Sprintf("
    9. %s
    10. ", filePath, filename) + htmlRet += fmt.Sprintf("
    11. %s
    12. ", filePath, filename) } davRet += "" diff --git a/internal/universal/get.go b/internal/universal/get.go index 6b8d412..e2a84ad 100644 --- a/internal/universal/get.go +++ b/internal/universal/get.go @@ -5,13 +5,10 @@ import ( "io" "net/http" "net/url" - "path" "path/filepath" "strings" "github.com/debridmediamanager/zurg/internal/config" - "github.com/debridmediamanager/zurg/internal/dav" - intHttp "github.com/debridmediamanager/zurg/internal/http" intTor "github.com/debridmediamanager/zurg/internal/torrent" zurghttp "github.com/debridmediamanager/zurg/pkg/http" "go.uber.org/zap" @@ -26,155 +23,131 @@ func NewGetFile(client *zurghttp.HTTPClient) *GetFile { } // HandleGetRequest handles a GET request universally for both WebDAV and HTTP -func (gf *GetFile) HandleGetRequest(w http.ResponseWriter, r *http.Request, t *intTor.TorrentManager, c config.ConfigInterface, log *zap.SugaredLogger) { - requestPath := path.Clean(r.URL.Path) - isDav := true - if strings.Contains(requestPath, "/http") { - requestPath = strings.Replace(requestPath, "/http", "/", 1) - isDav = false - } - if requestPath == "/favicon.ico" { +func (gf *GetFile) HandleGetRequest(directory, torrentName, fileName string, resp http.ResponseWriter, req *http.Request, torMgr *intTor.TorrentManager, cfg config.ConfigInterface, log *zap.SugaredLogger) { + torrents, ok := torMgr.DirectoryMap.Get(directory) + if !ok { + log.Warnf("Cannot find directory %s", directory) + http.Error(resp, "File not found", http.StatusNotFound) return } - segments := strings.Split(requestPath, "/") - // If there are less than 3 segments, return an error or adjust as needed - if len(segments) <= 3 { - if isDav { - dav.HandlePropfindRequest(w, r, t, log) - } else { - intHttp.HandleDirectoryListing(w, r, t, log) - } + torrent, ok := torrents.Get(torrentName) + if !ok { + log.Warnf("Cannot find torrent %sfrom path %s", torrentName, req.URL.Path) + http.Error(resp, "File not found", http.StatusNotFound) return } - baseDirectory := segments[len(segments)-3] - accessKey := segments[len(segments)-2] - filename := segments[len(segments)-1] - - torrents, ok := t.DirectoryMap.Get(baseDirectory) + file, ok := torrent.SelectedFiles.Get(fileName) if !ok { - log.Warnf("Cannot find directory %s", baseDirectory) - http.Error(w, "File not found", http.StatusNotFound) - return - } - torrent, ok := torrents.Get(accessKey) - if !ok { - log.Warnf("Cannot find torrent %s in the directory %s", accessKey, baseDirectory) - http.Error(w, "File not found", http.StatusNotFound) - return - } - - file, ok := torrent.SelectedFiles.Get(filename) - if !ok { - log.Warnf("Cannot find file from path %s", requestPath) - http.Error(w, "File not found", http.StatusNotFound) + log.Warnf("Cannot find file %s from path %s", fileName, req.URL.Path) + http.Error(resp, "File not found", http.StatusNotFound) return } if !strings.HasPrefix(file.Link, "http") { // This is a dead file, serve an alternate file - log.Warnf("File %s is not available", filename) - http.Error(w, "File is not available", http.StatusNotFound) + log.Warnf("File %s is not available", fileName) + http.Error(resp, "File is not available", http.StatusNotFound) return } link := file.Link - if download, exists := t.DownloadCache.Get(link); exists { - if c.ShouldServeFromRclone() && t.Api.CanFetchFirstByte(download.Download) { - redirect(w, r, download.Download, c) + if download, exists := torMgr.DownloadCache.Get(link); exists { + if cfg.ShouldServeFromRclone() && torMgr.Api.CanFetchFirstByte(download.Download) { + redirect(resp, req, download.Download, cfg) return } else { - err := gf.streamCachedLinkToResponse(download.Download, w, r, t, c, log) + err := gf.streamCachedLinkToResponse(download.Download, resp, req, torMgr, cfg, log) if err == nil { return } } } - resp := t.UnrestrictUntilOk(link) - if resp == nil { + unrestrict := torMgr.UnrestrictUntilOk(link) + if unrestrict == nil { // log.Warnf("File %s is no longer available, link %s", filepath.Base(file.Path), link) file.Link = "repair" - if c.EnableRepair() { + if cfg.EnableRepair() { // log.Debugf("File %s is marked for repair", filepath.Base(file.Path)) - t.ScheduleForRefresh() // force a recheck + torMgr.ScheduleForRefresh() // force a recheck } - http.Error(w, "File is not available", http.StatusNotFound) + http.Error(resp, "File is not available", http.StatusNotFound) return } else { - if resp.Filename != filename { + if unrestrict.Filename != fileName { // this is possible if there's only 1 streamable file in the torrent // and then suddenly it's a rar file - actualExt := filepath.Ext(resp.Filename) - expectedExt := filepath.Ext(filename) - if actualExt != expectedExt && resp.Streamable != 1 { - log.Warnf("File was changed and is not streamable: %s and %s", filename, resp.Filename) - http.Error(w, "File is not available", http.StatusNotFound) + actualExt := filepath.Ext(unrestrict.Filename) + expectedExt := filepath.Ext(fileName) + if actualExt != expectedExt && unrestrict.Streamable != 1 { + log.Warnf("File was changed and is not streamable: %s and %s", fileName, unrestrict.Filename) + http.Error(resp, "File is not available", http.StatusNotFound) return } else { - log.Warnf("Filename mismatch: %s and %s", filename, resp.Filename) + log.Warnf("Filename mismatch: %s and %s", fileName, unrestrict.Filename) } } - t.DownloadCache.Set(link, resp) - if c.ShouldServeFromRclone() { - redirect(w, r, resp.Download, c) + torMgr.DownloadCache.Set(link, unrestrict) + if cfg.ShouldServeFromRclone() { + redirect(resp, req, unrestrict.Download, cfg) } else { - gf.streamFileToResponse(file, resp.Download, w, r, t, c, log) + gf.streamFileToResponse(file, unrestrict.Download, resp, req, torMgr, cfg, log) } return } } -func (gf *GetFile) streamCachedLinkToResponse(url string, w http.ResponseWriter, r *http.Request, torMgr *intTor.TorrentManager, cfg config.ConfigInterface, log *zap.SugaredLogger) error { - // Create a new request for the file download. - req, err := http.NewRequest(http.MethodGet, url, nil) +func (gf *GetFile) streamCachedLinkToResponse(url string, resp http.ResponseWriter, req *http.Request, torMgr *intTor.TorrentManager, cfg config.ConfigInterface, log *zap.SugaredLogger) error { + // Create a new dlReq for the file download. + dlReq, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { return fmt.Errorf("file is not available") } // copy range header if it exists - if r.Header.Get("Range") != "" { - req.Header.Add("Range", r.Header.Get("Range")) + if req.Header.Get("Range") != "" { + dlReq.Header.Add("Range", req.Header.Get("Range")) } - resp, err := gf.client.Do(req) + download, err := gf.client.Do(dlReq) if err != nil { return fmt.Errorf("file is not available") } - defer resp.Body.Close() + defer download.Body.Close() - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { + if download.StatusCode != http.StatusOK && download.StatusCode != http.StatusPartialContent { return fmt.Errorf("file is not available") } - for k, vv := range resp.Header { + for k, vv := range download.Header { for _, v := range vv { - w.Header().Add(k, v) + resp.Header().Add(k, v) } } buf := make([]byte, cfg.GetNetworkBufferSize()) - io.CopyBuffer(w, resp.Body, buf) + io.CopyBuffer(resp, download.Body, buf) return nil } -func (gf *GetFile) streamFileToResponse(file *intTor.File, url string, w http.ResponseWriter, r *http.Request, torMgr *intTor.TorrentManager, cfg config.ConfigInterface, log *zap.SugaredLogger) { +func (gf *GetFile) streamFileToResponse(file *intTor.File, url string, resp http.ResponseWriter, req *http.Request, torMgr *intTor.TorrentManager, cfg config.ConfigInterface, log *zap.SugaredLogger) { // Create a new request for the file download. - req, err := http.NewRequest(http.MethodGet, url, nil) + dlReq, err := http.NewRequest(http.MethodGet, url, nil) if err != nil { if file != nil { log.Errorf("Error creating new request for file %s: %v", file.Path, err) } - http.Error(w, "File is not available", http.StatusNotFound) + http.Error(resp, "File is not available", http.StatusNotFound) return } // copy range header if it exists - if r.Header.Get("Range") != "" { - req.Header.Add("Range", r.Header.Get("Range")) + if req.Header.Get("Range") != "" { + dlReq.Header.Add("Range", req.Header.Get("Range")) } - resp, err := gf.client.Do(req) + download, err := gf.client.Do(dlReq) if err != nil { if file != nil { log.Warnf("Cannot download file %s: %v", file.Path, err) @@ -184,40 +157,40 @@ func (gf *GetFile) streamFileToResponse(file *intTor.File, url string, w http.Re torMgr.ScheduleForRefresh() // force a recheck } } - http.Error(w, "File is not available", http.StatusNotFound) + http.Error(resp, "File is not available", http.StatusNotFound) return } - defer resp.Body.Close() + defer download.Body.Close() - if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { + if download.StatusCode != http.StatusOK && download.StatusCode != http.StatusPartialContent { if file != nil { - log.Warnf("Received a %s status code for file %s", resp.Status, file.Path) + log.Warnf("Received a %s status code for file %s", download.Status, file.Path) file.Link = "repair" if cfg.EnableRepair() { // log.Debugf("File %s is marked for repair", filepath.Base(file.Path)) torMgr.ScheduleForRefresh() // force a recheck } } - http.Error(w, "File is not available", http.StatusNotFound) + http.Error(resp, "File is not available", http.StatusNotFound) return } - for k, vv := range resp.Header { + for k, vv := range download.Header { for _, v := range vv { - w.Header().Add(k, v) + resp.Header().Add(k, v) } } buf := make([]byte, cfg.GetNetworkBufferSize()) - io.CopyBuffer(w, resp.Body, buf) + io.CopyBuffer(resp, download.Body, buf) } -func redirect(w http.ResponseWriter, r *http.Request, url string, c config.ConfigInterface) { - prefHost := c.GetRandomPreferredHost() +func redirect(resp http.ResponseWriter, req *http.Request, url string, cfg config.ConfigInterface) { + prefHost := cfg.GetRandomPreferredHost() if prefHost != "" { url = replaceHostInURL(url, prefHost) } - http.Redirect(w, r, url, http.StatusFound) + http.Redirect(resp, req, url, http.StatusFound) } func replaceHostInURL(inputURL string, newHost string) string { diff --git a/internal/universal/head.go b/internal/universal/head.go index b288fe1..5b34761 100644 --- a/internal/universal/head.go +++ b/internal/universal/head.go @@ -3,7 +3,6 @@ package universal import ( "fmt" "net/http" - "path" "path/filepath" "strings" @@ -11,55 +10,34 @@ import ( "go.uber.org/zap" ) -const ( - SPLIT_TOKEN = "$" -) +func HandleHeadRequest(directory, torrentName, fileName string, w http.ResponseWriter, req *http.Request, t *torrent.TorrentManager, log *zap.SugaredLogger) { -func HandleHeadRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, log *zap.SugaredLogger) { - requestPath := path.Clean(r.URL.Path) - requestPath = strings.Replace(requestPath, "/http", "", 1) - if requestPath == "/favicon.ico" { - return - } - - segments := strings.Split(requestPath, "/") - // If there are less than 3 segments, return an error or adjust as needed - if len(segments) < 4 { - log.Errorf("Request %s %s not supported yet", r.Method, r.URL.Path) - http.Error(w, "Method not implemented", http.StatusMethodNotAllowed) - return - } - - baseDirectory := segments[len(segments)-3] - accessKey := segments[len(segments)-2] - filename := segments[len(segments)-1] - - torrents, ok := t.DirectoryMap.Get(baseDirectory) + torrents, ok := t.DirectoryMap.Get(directory) if !ok { - log.Warnf("Cannot find directory %s", baseDirectory) + log.Warnf("Cannot find directory %s", directory) http.Error(w, "File not found", http.StatusNotFound) return } - torrent, ok := torrents.Get(accessKey) + torrent, ok := torrents.Get(torrentName) if !ok { - log.Warnf("Cannot find torrent %s in the directory %s", accessKey, baseDirectory) + log.Warnf("Cannot find torrent %s from path %s", torrentName, req.URL.Path) http.Error(w, "File not found", http.StatusNotFound) return } - file, _ := torrent.SelectedFiles.Get(filename) + file, _ := torrent.SelectedFiles.Get(fileName) if file == nil { - log.Warnf("Cannot find file from path %s", requestPath) + log.Warnf("Cannot find file %s from path %s", fileName, req.URL.Path) http.Error(w, "Cannot find file", http.StatusNotFound) return } if !strings.HasPrefix(file.Link, "http") { // This is a dead file, serve an alternate file - log.Warnf("File %s is no longer available", filename) + log.Warnf("File %s is no longer available", fileName) http.Error(w, "Cannot find file", http.StatusNotFound) return } - contentType := getContentMimeType(filename) + contentType := getContentMimeType(fileName) contentLength := fmt.Sprintf("%d", file.Bytes) lastModified := file.Ended w.Header().Set("Content-Type", contentType)