package universal import ( "fmt" "io" "net/http" "net/url" "path" "path/filepath" "strings" "github.com/debridmediamanager.com/zurg/internal/config" "github.com/debridmediamanager.com/zurg/internal/dav" intHttp "github.com/debridmediamanager.com/zurg/internal/http" intTor "github.com/debridmediamanager.com/zurg/internal/torrent" zurghttp "github.com/debridmediamanager.com/zurg/pkg/http" "go.uber.org/zap" ) type GetFile struct { client *zurghttp.HTTPClient } func NewGetFile(client *zurghttp.HTTPClient) *GetFile { return &GetFile{client: client} } // 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" { 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) } return } baseDirectory := segments[len(segments)-3] accessKey := segments[len(segments)-2] filename := segments[len(segments)-1] torrents, ok := t.DirectoryMap.Get(baseDirectory) 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) 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) 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) } else { err := gf.streamCachedLinkToResponse(download.Download, w, r, t, c, log) if err == nil { return } } } resp := t.UnrestrictUntilOk(link) if resp == nil { // log.Warnf("File %s is no longer available, link %s", filepath.Base(file.Path), link) file.Link = "repair" if c.EnableRepair() { // log.Debugf("File %s is marked for repair", filepath.Base(file.Path)) t.SetChecksum("") // force a recheck } http.Error(w, "File is not available", http.StatusNotFound) return } else { if resp.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) return } else { log.Warnf("Filename mismatch: %s and %s", filename, resp.Filename) } } t.DownloadCache.Set(link, resp) if c.ShouldServeFromRclone() { redirect(w, r, resp.Download, c) } else { gf.streamFileToResponse(file, resp.Download, w, r, t, c, 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) 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")) } resp, err := gf.client.Do(req) if err != nil { return fmt.Errorf("file is not available") } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { return fmt.Errorf("file is not available") } for k, vv := range resp.Header { for _, v := range vv { w.Header().Add(k, v) } } buf := make([]byte, cfg.GetNetworkBufferSize()) io.CopyBuffer(w, resp.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) { // Create a new request for the file download. req, 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) return } // copy range header if it exists if r.Header.Get("Range") != "" { req.Header.Add("Range", r.Header.Get("Range")) } resp, err := gf.client.Do(req) if err != nil { if file != nil { log.Warnf("Cannot download file %s: %v", file.Path, err) file.Link = "repair" if cfg.EnableRepair() { // log.Debugf("File %s is marked for repair", filepath.Base(file.Path)) torMgr.SetChecksum("") // force a recheck } } http.Error(w, "File is not available", http.StatusNotFound) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent { if file != nil { log.Warnf("Received a %s status code for file %s", resp.Status, file.Path) file.Link = "repair" if cfg.EnableRepair() { // log.Debugf("File %s is marked for repair", filepath.Base(file.Path)) torMgr.SetChecksum("") // force a recheck } } http.Error(w, "File is not available", http.StatusNotFound) return } for k, vv := range resp.Header { for _, v := range vv { w.Header().Add(k, v) } } buf := make([]byte, cfg.GetNetworkBufferSize()) io.CopyBuffer(w, resp.Body, buf) } func redirect(w http.ResponseWriter, r *http.Request, url string, c config.ConfigInterface) { prefHost := c.GetRandomPreferredHost() if prefHost != "" { url = replaceHostInURL(url, prefHost) } http.Redirect(w, r, url, http.StatusFound) } func replaceHostInURL(inputURL string, newHost string) string { u, err := url.Parse(inputURL) if err != nil { return "" } u.Host = newHost return u.String() }