diff --git a/cmd/zurg/main.go b/cmd/zurg/main.go index 4c57453..babaa79 100644 --- a/cmd/zurg/main.go +++ b/cmd/zurg/main.go @@ -4,10 +4,12 @@ import ( "fmt" "log" "net/http" + "time" "github.com/debridmediamanager.com/zurg/internal/config" "github.com/debridmediamanager.com/zurg/internal/dav" "github.com/debridmediamanager.com/zurg/internal/torrent" + "github.com/hashicorp/golang-lru/v2/expirable" ) func main() { @@ -18,8 +20,11 @@ func main() { t := torrent.NewTorrentManager(c) + cache := expirable.NewLRU[string, string](1e4, nil, time.Hour) + mux := http.NewServeMux() - dav.Router(mux, c, t) + dav.Router(mux, c, t, cache) + addr := fmt.Sprintf(":%s", c.GetPort()) log.Printf("Starting server on %s\n", addr) err := http.ListenAndServe(addr, mux) diff --git a/go.mod b/go.mod index 5b403aa..6c4297d 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/debridmediamanager.com/zurg go 1.21.3 -require gopkg.in/yaml.v3 v3.0.1 +require ( + github.com/hashicorp/golang-lru/v2 v2.0.7 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/go.sum b/go.sum index a62c313..c282774 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/config/v1.go b/internal/config/v1.go index 63dd9f2..fca267f 100644 --- a/internal/config/v1.go +++ b/internal/config/v1.go @@ -41,9 +41,11 @@ func (z *ZurgConfigV1) GetCacheTimeHours() int { } func (z *ZurgConfigV1) GetDirectories() []string { - var rootDirectories []string + rootDirectories := make([]string, len(z.Directories)) + i := 0 for directory := range z.Directories { - rootDirectories = append(rootDirectories, directory) + rootDirectories[i] = directory + i++ } return rootDirectories } diff --git a/internal/dav/getfile.go b/internal/dav/getfile.go index 0aeba0f..35fa408 100644 --- a/internal/dav/getfile.go +++ b/internal/dav/getfile.go @@ -12,22 +12,27 @@ import ( "github.com/debridmediamanager.com/zurg/internal/torrent" "github.com/debridmediamanager.com/zurg/pkg/davextra" "github.com/debridmediamanager.com/zurg/pkg/realdebrid" + "github.com/hashicorp/golang-lru/v2/expirable" ) // HandleGetRequest handles a GET request to a file -func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface) { +func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) { requestPath := path.Clean(r.URL.Path) - + if requestPath == "/favicon.ico" { + return + } segments := strings.Split(requestPath, "/") - fmt.Println(segments, len(segments)) // If there are less than 3 segments, return an error or adjust as needed if len(segments) < 4 { // log.Println("Invalid url", requestPath) // http.Error(w, "Cannot find file", http.StatusNotFound) - HandlePropfindRequest(w, r, t, c) + HandlePropfindRequest(w, r, t, c, cache) + return + } + if data, exists := cache.Get(requestPath); exists { + http.Redirect(w, r, data, http.StatusFound) return } - // Get the last two segments baseDirectory := segments[len(segments)-3] torrentName := segments[len(segments)-2] @@ -77,6 +82,7 @@ func HandleGetRequest(w http.ResponseWriter, r *http.Request, t *torrent.Torrent // If the file extension changed, that means it's a different file log.Println("Filename mismatch", resp.Filename, filenameV2) } + cache.Add(requestPath, resp.Download) http.Redirect(w, r, resp.Download, http.StatusFound) } diff --git a/internal/dav/propfind.go b/internal/dav/propfind.go index 35e0f16..a7067a0 100644 --- a/internal/dav/propfind.go +++ b/internal/dav/propfind.go @@ -11,23 +11,22 @@ import ( "github.com/debridmediamanager.com/zurg/internal/config" "github.com/debridmediamanager.com/zurg/internal/torrent" "github.com/debridmediamanager.com/zurg/pkg/dav" + "github.com/hashicorp/golang-lru/v2/expirable" ) -// HandlePropfindRequest handles a PROPFIND request -func HandlePropfindRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface) { +func HandlePropfindRequest(w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface, cache *expirable.LRU[string, string]) { + requestPath := path.Clean(r.URL.Path) + if data, exists := cache.Get(requestPath); exists { + w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"") + w.WriteHeader(http.StatusMultiStatus) + fmt.Fprint(w, data) + return + } + var output []byte var err error - requestPath := path.Clean(r.URL.Path) - pathSegments := strings.Split(requestPath, "/") - - // Remove empty segments caused by leading or trailing slashes - filteredSegments := pathSegments[:0] - for _, segment := range pathSegments { - if segment != "" { - filteredSegments = append(filteredSegments, segment) - } - } + filteredSegments := strings.Split(strings.Trim(requestPath, "/"), "/") switch len(filteredSegments) { case 0: @@ -37,20 +36,23 @@ func HandlePropfindRequest(w http.ResponseWriter, r *http.Request, t *torrent.To case 2: output, err = handleSingleTorrent(requestPath, w, r, t) default: - http.Error(w, "Not Found", http.StatusNotFound) + writeHTTPError(w, "Not Found", http.StatusNotFound) return } if err != nil { log.Printf("Error processing request: %v\n", err) - http.Error(w, "Server error", http.StatusInternalServerError) + writeHTTPError(w, "Server error", http.StatusInternalServerError) return } if output != nil { + respBody := fmt.Sprintf("\n%s\n", output) + cache.Add(requestPath, respBody) + w.Header().Set("Content-Type", "text/xml; charset=\"utf-8\"") w.WriteHeader(http.StatusMultiStatus) - fmt.Fprintf(w, "\n%s\n", output) + fmt.Fprint(w, respBody) } } @@ -64,47 +66,43 @@ func handleRoot(w http.ResponseWriter, r *http.Request, c config.ConfigInterface XMLNS: "DAV:", Response: responses, } - return xml.MarshalIndent(rootResponse, "", " ") + return xml.Marshal(rootResponse) } func handleListOfTorrents(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager, c config.ConfigInterface) ([]byte, error) { basePath := path.Base(requestPath) - directories := c.GetDirectories() - for _, directory := range directories { + for _, directory := range c.GetDirectories() { if basePath == directory { torrents := t.GetByDirectory(basePath) resp, err := createMultiTorrentResponse("/"+basePath, torrents) if err != nil { - log.Printf("Cannot read directory (%s): %v\n", basePath, err) - http.Error(w, "Cannot read directory", http.StatusInternalServerError) - return nil, nil + return nil, fmt.Errorf("cannot read directory (%s): %w", basePath, err) } - return xml.MarshalIndent(resp, "", " ") + return xml.Marshal(resp) } } - log.Println("Cannot find directory when generating list", requestPath) - http.Error(w, "Cannot find directory", http.StatusNotFound) - return nil, nil + return nil, fmt.Errorf("cannot find directory when generating list: %s", requestPath) } func handleSingleTorrent(requestPath string, w http.ResponseWriter, r *http.Request, t *torrent.TorrentManager) ([]byte, error) { - directory := strings.TrimPrefix(path.Dir(requestPath), "/") + directory := path.Dir(requestPath) torrentName := path.Base(requestPath) sameNameTorrents := findAllTorrentsWithName(t, directory, torrentName) if len(sameNameTorrents) == 0 { - log.Println("Cannot find directory when generating single torrent", requestPath) - http.Error(w, "Cannot find directory", http.StatusNotFound) - return nil, nil + return nil, fmt.Errorf("cannot find directory when generating single torrent: %s", requestPath) } resp, err := createSingleTorrentResponse("/"+directory, sameNameTorrents, t) if err != nil { - log.Printf("Cannot read directory (%s): %v\n", requestPath, err) - http.Error(w, "Cannot read directory", http.StatusInternalServerError) - return nil, nil + return nil, fmt.Errorf("cannot read directory (%s): %w", requestPath, err) } - return xml.MarshalIndent(resp, "", " ") + return xml.Marshal(resp) +} + +func writeHTTPError(w http.ResponseWriter, errorMessage string, statusCode int) { + log.Println(errorMessage) + http.Error(w, errorMessage, statusCode) } diff --git a/internal/dav/response.go b/internal/dav/response.go index d593263..1745acd 100644 --- a/internal/dav/response.go +++ b/internal/dav/response.go @@ -39,6 +39,7 @@ func createMultiTorrentResponse(basePath string, torrents []torrent.Torrent) (*d // but it also handles the case where there are many torrents with the same name func createSingleTorrentResponse(basePath string, torrents []torrent.Torrent, t *torrent.TorrentManager) (*dav.MultiStatus, error) { var responses []dav.Response + // initial response is the directory itself currentPath := filepath.Join(basePath, torrents[0].Name) responses = append(responses, dav.Directory(currentPath)) @@ -47,26 +48,28 @@ func createSingleTorrentResponse(basePath string, torrents []torrent.Torrent, t finalName := make(map[string]bool) var torrentResponses []dav.Response + for _, torrent := range torrents { for _, file := range torrent.SelectedFiles { if file.Link == "" { log.Println("File has no link, skipping", file.Path) - // TODO: trigger a re-add for the file - // It is selected but no link is available - // I think this is handled on the manager side so we just need to refresh - // t.RefreshInfo(torrent.ID) continue } + filename := filepath.Base(file.Path) - if _, exists := nameAndLink[filename+file.Link]; exists { + key := filename + file.Link + + if nameAndLink[key] { continue } - nameAndLink[filename+file.Link] = true - if _, exists := finalName[filename]; exists { + nameAndLink[key] = true + + if finalName[filename] { fragment := davextra.GetLinkFragment(file.Link) filename = davextra.InsertLinkFragment(filename, fragment) } finalName[filename] = true + filePath := filepath.Join(currentPath, filename) torrentResponses = append(torrentResponses, dav.File( filePath, @@ -76,6 +79,7 @@ func createSingleTorrentResponse(basePath string, torrents []torrent.Torrent, t )) } } + responses = append(responses, torrentResponses...) return &dav.MultiStatus{ diff --git a/internal/dav/router.go b/internal/dav/router.go index d4a840e..ac6ecdb 100644 --- a/internal/dav/router.go +++ b/internal/dav/router.go @@ -6,17 +6,18 @@ import ( "github.com/debridmediamanager.com/zurg/internal/config" "github.com/debridmediamanager.com/zurg/internal/torrent" + "github.com/hashicorp/golang-lru/v2/expirable" ) // Router creates a WebDAV router -func Router(mux *http.ServeMux, c config.ConfigInterface, t *torrent.TorrentManager) { +func Router(mux *http.ServeMux, c config.ConfigInterface, t *torrent.TorrentManager, cache *expirable.LRU[string, string]) { mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { switch r.Method { case "PROPFIND": - HandlePropfindRequest(w, r, t, c) + HandlePropfindRequest(w, r, t, c, cache) case http.MethodGet: - HandleGetRequest(w, r, t, c) + HandleGetRequest(w, r, t, c, cache) case http.MethodOptions: w.WriteHeader(http.StatusOK) diff --git a/internal/dav/util.go b/internal/dav/util.go index 294c3a8..aa40a77 100644 --- a/internal/dav/util.go +++ b/internal/dav/util.go @@ -20,14 +20,12 @@ func convertRFC3339toRFC1123(input string) string { // findAllTorrentsWithName finds all torrents in a given directory with a given name func findAllTorrentsWithName(t *torrent.TorrentManager, directory, torrentName string) []torrent.Torrent { - var matchingTorrents []torrent.Torrent - + matchingTorrents := make([]torrent.Torrent, 0, 10) torrents := t.GetByDirectory(directory) for i := range torrents { if torrents[i].Name == torrentName || strings.HasPrefix(torrents[i].Name, torrentName) { matchingTorrents = append(matchingTorrents, torrents[i]) } } - return matchingTorrents }