Fix issues with repair, move to a different streaming option

This commit is contained in:
Mukhtar Akere
2025-06-18 10:42:44 +01:00
parent 5661b05ec1
commit b2e99585f7
6 changed files with 507 additions and 275 deletions

View File

@@ -2,6 +2,7 @@ package webdav
import (
"context"
"errors"
"fmt"
"github.com/sirrobot01/decypharr/pkg/debrid/types"
"golang.org/x/net/webdav"
@@ -415,104 +416,90 @@ func (h *Handler) serveDirectory(w http.ResponseWriter, r *http.Request, file we
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 func(fRaw webdav.File) {
err := fRaw.Close()
if err != nil {
h.logger.Error().Err(err).Msg("Failed to close file")
return
}
}(fRaw)
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.Debug().
Err(err).
Str("link", file.link).
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
// If the torrent file is not a RAR file and users enabled proxy streaming
if !file.isRar && h.cache.StreamWithRclone() {
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
w.Header().Set("Pragma", "no-cache")
w.Header().Set("Expires", "0")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", 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.Header().Set("X-Accel-Redirect", file.downloadLink)
w.Header().Set("X-Accel-Buffering", "no")
http.Redirect(w, r, file.downloadLink, http.StatusFound)
return
}
}
// ETags
// Set common headers
etag := fmt.Sprintf("\"%x-%x\"", fi.ModTime().Unix(), fi.Size())
w.Header().Set("ETag", etag)
w.Header().Set("Last-Modified", fi.ModTime().UTC().Format(http.TimeFormat))
// 7. Content-Type by extension
ext := filepath.Ext(fi.Name())
contentType := mime.TypeByExtension(ext)
if contentType == "" {
contentType = "application/octet-stream"
if contentType := mime.TypeByExtension(ext); contentType != "" {
w.Header().Set("Content-Type", contentType)
} else {
w.Header().Set("Content-Type", "application/octet-stream")
}
w.Header().Set("Content-Type", contentType)
rs, ok := fRaw.(io.ReadSeeker)
if !ok {
if r.Header.Get("Range") != "" {
http.Error(w, "Range not supported", http.StatusRequestedRangeNotSatisfiable)
// Handle File struct with direct streaming
if file, ok := fRaw.(*File); ok {
// Handle nginx proxy (X-Accel-Redirect)
if file.content == nil && !file.isRar && h.cache.StreamWithRclone() {
link, err := file.getDownloadLink()
if err != nil || link == "" {
http.Error(w, "Could not fetch download link", http.StatusPreconditionFailed)
return
}
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", fi.Name()))
w.Header().Set("X-Accel-Redirect", link)
w.Header().Set("X-Accel-Buffering", "no")
http.Redirect(w, r, link, http.StatusFound)
return
}
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")
ctx := r.Context()
done := make(chan struct{})
go func() {
defer close(done)
_, _ = io.Copy(w, fRaw)
}()
select {
case <-ctx.Done():
h.logger.Debug().Msg("Client cancelled download")
return
case <-done:
if err := file.StreamResponse(w, r); err != nil {
var streamErr *streamError
if errors.As(err, &streamErr) {
// Handle client disconnections silently (just debug log)
if errors.Is(streamErr.Err, context.Canceled) || errors.Is(streamErr.Err, context.DeadlineExceeded) || streamErr.IsClientDisconnection {
return // Don't log as error or try to write response
}
if streamErr.StatusCode > 0 && !hasHeadersWritten(w) {
http.Error(w, streamErr.Error(), streamErr.StatusCode)
} else {
h.logger.Error().
Err(streamErr.Err).
Str("path", r.URL.Path).
Msg("Stream error")
}
} else {
// Generic error
if !hasHeadersWritten(w) {
http.Error(w, "Stream error", http.StatusInternalServerError)
} else {
h.logger.Error().
Err(err).
Str("path", r.URL.Path).
Msg("Stream error after headers written")
}
}
}
return
}
http.ServeContent(w, r, fi.Name(), fi.ModTime(), rs)
// Fallback to ServeContent for other webdav.File implementations
if rs, ok := fRaw.(io.ReadSeeker); ok {
http.ServeContent(w, r, fi.Name(), fi.ModTime(), rs)
} else {
w.Header().Set("Content-Length", fmt.Sprintf("%d", fi.Size()))
w.WriteHeader(http.StatusOK)
_, _ = io.Copy(w, fRaw)
}
}
func (h *Handler) handleHead(w http.ResponseWriter, r *http.Request) {