- Fix issues with new setup
- Fix arr setup getting thr wrong crendentials - Add file link invalidator - Other minor bug fixes
This commit is contained in:
@@ -5,22 +5,12 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirrobot01/decypharr/internal/utils"
|
||||
"github.com/sirrobot01/decypharr/pkg/debrid/store"
|
||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||
)
|
||||
|
||||
type retryAction int
|
||||
|
||||
const (
|
||||
noRetry retryAction = iota
|
||||
retryWithLimit
|
||||
retryAlways
|
||||
)
|
||||
|
||||
const (
|
||||
MaxNetworkRetries = 3
|
||||
MaxLinkRetries = 10
|
||||
@@ -44,7 +34,6 @@ type File struct {
|
||||
name string
|
||||
torrentName string
|
||||
link string
|
||||
downloadLink types.DownloadLink
|
||||
size int64
|
||||
isDir bool
|
||||
fileId string
|
||||
@@ -70,17 +59,12 @@ func (f *File) Close() error {
|
||||
// This is just to satisfy the os.File interface
|
||||
f.content = nil
|
||||
f.children = nil
|
||||
f.downloadLink = types.DownloadLink{}
|
||||
f.readOffset = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *File) getDownloadLink() (types.DownloadLink, error) {
|
||||
// Check if we already have a final URL cached
|
||||
|
||||
if f.downloadLink.Valid() == nil {
|
||||
return f.downloadLink, nil
|
||||
}
|
||||
downloadLink, err := f.cache.GetDownloadLink(f.torrentName, f.name, f.link)
|
||||
if err != nil {
|
||||
return downloadLink, err
|
||||
@@ -89,7 +73,6 @@ func (f *File) getDownloadLink() (types.DownloadLink, error) {
|
||||
if err != nil {
|
||||
return types.DownloadLink{}, err
|
||||
}
|
||||
f.downloadLink = downloadLink
|
||||
return downloadLink, nil
|
||||
}
|
||||
|
||||
@@ -137,163 +120,44 @@ func (f *File) StreamResponse(w http.ResponseWriter, r *http.Request) error {
|
||||
if f.content != nil {
|
||||
return f.servePreloadedContent(w, r)
|
||||
}
|
||||
_logger := f.cache.Logger()
|
||||
|
||||
return f.streamWithRetry(w, r, 0, 0)
|
||||
start, end := f.getRange(r)
|
||||
|
||||
resp, err := f.cache.Stream(r.Context(), start, end, f.getDownloadLink)
|
||||
if err != nil {
|
||||
_logger.Error().Err(err).Str("file", f.name).Msg("Failed to stream with initial link")
|
||||
return &streamError{Err: err, StatusCode: http.StatusRequestedRangeNotSatisfiable}
|
||||
}
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(resp.Body)
|
||||
return f.handleSuccessfulResponse(w, resp, start, end)
|
||||
}
|
||||
|
||||
func (f *File) streamWithRetry(w http.ResponseWriter, r *http.Request, networkRetries, recoverableRetries int) error {
|
||||
|
||||
_log := f.cache.Logger()
|
||||
|
||||
downloadLink, err := f.getDownloadLink()
|
||||
if err != nil {
|
||||
return &streamError{Err: err, StatusCode: http.StatusPreconditionFailed}
|
||||
}
|
||||
|
||||
upstreamReq, err := http.NewRequest("GET", downloadLink.DownloadLink, nil)
|
||||
if err != nil {
|
||||
return &streamError{Err: err, StatusCode: http.StatusInternalServerError}
|
||||
}
|
||||
|
||||
isRangeRequest := f.handleRangeRequest(upstreamReq, r, w)
|
||||
if isRangeRequest == -1 {
|
||||
return &streamError{Err: fmt.Errorf("invalid range"), StatusCode: http.StatusRequestedRangeNotSatisfiable}
|
||||
}
|
||||
|
||||
resp, err := f.cache.Download(upstreamReq)
|
||||
if err != nil {
|
||||
// Network error - retry with limit
|
||||
if networkRetries < MaxNetworkRetries {
|
||||
_log.Debug().
|
||||
Int("network_retries", networkRetries+1).
|
||||
Err(err).
|
||||
Msg("Network error, retrying")
|
||||
return f.streamWithRetry(w, r, networkRetries+1, recoverableRetries)
|
||||
}
|
||||
return &streamError{Err: err, StatusCode: http.StatusServiceUnavailable}
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||
retryType, retryErr := f.handleUpstreamError(downloadLink, resp)
|
||||
|
||||
switch retryType {
|
||||
case retryAlways:
|
||||
if recoverableRetries >= MaxLinkRetries {
|
||||
return &streamError{
|
||||
Err: fmt.Errorf("max link retries exceeded (%d)", MaxLinkRetries),
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
}
|
||||
|
||||
_log.Debug().
|
||||
Int("recoverable_retries", recoverableRetries+1).
|
||||
Str("file", f.name).
|
||||
Msg("Recoverable error, retrying")
|
||||
return f.streamWithRetry(w, r, 0, recoverableRetries+1) // Reset network retries
|
||||
|
||||
case retryWithLimit:
|
||||
if networkRetries < MaxNetworkRetries {
|
||||
_log.Debug().
|
||||
Int("network_retries", networkRetries+1).
|
||||
Str("file", f.name).
|
||||
Msg("Network error, retrying")
|
||||
return f.streamWithRetry(w, r, networkRetries+1, recoverableRetries)
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case noRetry:
|
||||
if retryErr != nil {
|
||||
return retryErr
|
||||
}
|
||||
return &streamError{
|
||||
Err: fmt.Errorf("non-retryable error: status %d", resp.StatusCode),
|
||||
StatusCode: http.StatusBadGateway,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Success - stream the response
|
||||
func (f *File) handleSuccessfulResponse(w http.ResponseWriter, resp *http.Response, start, end int64) error {
|
||||
statusCode := http.StatusOK
|
||||
if isRangeRequest == 1 {
|
||||
if start > 0 || end > 0 {
|
||||
statusCode = http.StatusPartialContent
|
||||
}
|
||||
|
||||
// Copy relevant headers
|
||||
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
||||
w.Header().Set("Content-Length", contentLength)
|
||||
}
|
||||
|
||||
if contentRange := resp.Header.Get("Content-Range"); contentRange != "" && isRangeRequest == 1 {
|
||||
if contentRange := resp.Header.Get("Content-Range"); contentRange != "" && statusCode == http.StatusPartialContent {
|
||||
w.Header().Set("Content-Range", contentRange)
|
||||
}
|
||||
|
||||
// Copy other important headers
|
||||
if contentType := resp.Header.Get("Content-Type"); contentType != "" {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
|
||||
return f.streamBuffer(w, resp.Body, statusCode)
|
||||
}
|
||||
|
||||
func (f *File) handleUpstreamError(downloadLink types.DownloadLink, resp *http.Response) (retryAction, error) {
|
||||
_log := f.cache.Logger()
|
||||
|
||||
cleanupResp := func(resp *http.Response) {
|
||||
if resp.Body != nil {
|
||||
_, _ = io.Copy(io.Discard, resp.Body)
|
||||
resp.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
switch resp.StatusCode {
|
||||
case http.StatusServiceUnavailable:
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
cleanupResp(resp)
|
||||
|
||||
if readErr != nil {
|
||||
_log.Error().Err(readErr).Msg("Failed to read response body")
|
||||
return retryWithLimit, nil
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
if strings.Contains(bodyStr, "you have exceeded your traffic") {
|
||||
_log.Debug().
|
||||
Str("token", utils.Mask(downloadLink.Token)).
|
||||
Str("file", f.name).
|
||||
Msg("Bandwidth exceeded for account, invalidating link")
|
||||
|
||||
f.cache.MarkDownloadLinkAsInvalid(f.downloadLink, "bandwidth_exceeded")
|
||||
f.downloadLink = types.DownloadLink{}
|
||||
return retryAlways, nil
|
||||
}
|
||||
|
||||
return noRetry, &streamError{
|
||||
Err: fmt.Errorf("service unavailable: %s", bodyStr),
|
||||
StatusCode: http.StatusServiceUnavailable,
|
||||
}
|
||||
|
||||
case http.StatusNotFound:
|
||||
cleanupResp(resp)
|
||||
_log.Debug().
|
||||
Str("file", f.name).
|
||||
Msg("Link not found, invalidating and regenerating")
|
||||
|
||||
f.cache.MarkDownloadLinkAsInvalid(f.downloadLink, "link_not_found")
|
||||
f.downloadLink = types.DownloadLink{}
|
||||
return retryAlways, nil
|
||||
|
||||
default:
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
cleanupResp(resp)
|
||||
|
||||
_log.Error().
|
||||
Int("status_code", resp.StatusCode).
|
||||
Str("file", f.name).
|
||||
Str("response_body", string(body)).
|
||||
Msg("Unexpected upstream error")
|
||||
|
||||
return retryWithLimit, &streamError{
|
||||
Err: fmt.Errorf("upstream error %d: %s", resp.StatusCode, string(body)),
|
||||
StatusCode: http.StatusBadGateway,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *File) streamBuffer(w http.ResponseWriter, src io.Reader, statusCode int) error {
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
@@ -342,21 +206,21 @@ func (f *File) streamBuffer(w http.ResponseWriter, src io.Reader, statusCode int
|
||||
}
|
||||
}
|
||||
|
||||
func (f *File) handleRangeRequest(upstreamReq *http.Request, r *http.Request, w http.ResponseWriter) int {
|
||||
func (f *File) getRange(r *http.Request) (int64, int64) {
|
||||
rangeHeader := r.Header.Get("Range")
|
||||
if rangeHeader == "" {
|
||||
// For video files, apply byte range if exists
|
||||
if byteRange, _ := f.getDownloadByteRange(); byteRange != nil {
|
||||
upstreamReq.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", byteRange[0], byteRange[1]))
|
||||
return byteRange[0], byteRange[1]
|
||||
}
|
||||
return 0 // No range request
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Parse range request
|
||||
ranges, err := parseRange(rangeHeader, f.size)
|
||||
if err != nil || len(ranges) != 1 {
|
||||
w.Header().Set("Content-Range", fmt.Sprintf("bytes */%d", f.size))
|
||||
return -1 // Invalid range
|
||||
// Invalid range, return full content
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
// Apply byte range offset if exists
|
||||
@@ -367,9 +231,7 @@ func (f *File) handleRangeRequest(upstreamReq *http.Request, r *http.Request, w
|
||||
start += byteRange[0]
|
||||
end += byteRange[0]
|
||||
}
|
||||
|
||||
upstreamReq.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
|
||||
return 1 // Valid range request
|
||||
return start, end
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
@@ -101,18 +101,13 @@ func (h *Handler) RemoveAll(ctx context.Context, name string) error {
|
||||
if len(parts) >= 2 {
|
||||
if utils.Contains(h.getParentItems(), parts[0]) {
|
||||
torrentName := parts[1]
|
||||
cached := h.cache.GetTorrentByName(torrentName)
|
||||
if cached != nil && len(parts) >= 3 {
|
||||
filename := filepath.Clean(path.Join(parts[2:]...))
|
||||
if file, ok := cached.GetFile(filename); ok {
|
||||
if err := h.cache.RemoveFile(cached.Id, file.Name); err != nil {
|
||||
h.logger.Error().Err(err).Msgf("Failed to remove file %s from torrent %s", file.Name, torrentName)
|
||||
return err
|
||||
}
|
||||
// If the file was successfully removed, we can return nil
|
||||
return nil
|
||||
}
|
||||
filename := filepath.Clean(path.Join(parts[2:]...))
|
||||
if err := h.cache.RemoveFile(torrentName, filename); err != nil {
|
||||
h.logger.Error().Err(err).Msgf("Failed to remove file %s from torrent %s", filename, torrentName)
|
||||
return err
|
||||
}
|
||||
// If the file was successfully removed, we can return nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -489,7 +484,6 @@ func (h *Handler) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (h *Handler) handleHead(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -128,14 +128,6 @@ func writeXml(w http.ResponseWriter, status int, buf stringbuf.StringBuf) {
|
||||
_, _ = w.Write(buf.Bytes())
|
||||
}
|
||||
|
||||
func hasHeadersWritten(w http.ResponseWriter) bool {
|
||||
// Most ResponseWriter implementations support this
|
||||
if hw, ok := w.(interface{ Written() bool }); ok {
|
||||
return hw.Written()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isClientDisconnection(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
|
||||
@@ -4,10 +4,6 @@ import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/sirrobot01/decypharr/internal/config"
|
||||
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -16,6 +12,12 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/sirrobot01/decypharr/internal/config"
|
||||
"github.com/sirrobot01/decypharr/internal/utils"
|
||||
"github.com/sirrobot01/decypharr/pkg/wire"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
@@ -33,42 +35,8 @@ var (
|
||||
}
|
||||
return strings.Join(segments, "/")
|
||||
},
|
||||
"formatSize": func(bytes int64) string {
|
||||
const (
|
||||
KB = 1024
|
||||
MB = 1024 * KB
|
||||
GB = 1024 * MB
|
||||
TB = 1024 * GB
|
||||
)
|
||||
|
||||
var size float64
|
||||
var unit string
|
||||
|
||||
switch {
|
||||
case bytes >= TB:
|
||||
size = float64(bytes) / TB
|
||||
unit = "TB"
|
||||
case bytes >= GB:
|
||||
size = float64(bytes) / GB
|
||||
unit = "GB"
|
||||
case bytes >= MB:
|
||||
size = float64(bytes) / MB
|
||||
unit = "MB"
|
||||
case bytes >= KB:
|
||||
size = float64(bytes) / KB
|
||||
unit = "KB"
|
||||
default:
|
||||
size = float64(bytes)
|
||||
unit = "bytes"
|
||||
}
|
||||
|
||||
// Format to 2 decimal places for larger units, no decimals for bytes
|
||||
if unit == "bytes" {
|
||||
return fmt.Sprintf("%.0f %s", size, unit)
|
||||
}
|
||||
return fmt.Sprintf("%.2f %s", size, unit)
|
||||
},
|
||||
"hasSuffix": strings.HasSuffix,
|
||||
"formatSize": utils.FormatSize,
|
||||
"hasSuffix": strings.HasSuffix,
|
||||
}
|
||||
tplRoot = template.Must(template.ParseFS(templatesFS, "templates/root.html"))
|
||||
tplDirectory = template.Must(template.New("").Funcs(funcMap).ParseFS(templatesFS, "templates/directory.html"))
|
||||
@@ -108,6 +76,7 @@ func (wd *WebDav) Routes() http.Handler {
|
||||
wr := chi.NewRouter()
|
||||
wr.Use(middleware.StripSlashes)
|
||||
wr.Use(wd.commonMiddleware)
|
||||
// wr.Use(wd.authMiddleware) Disable auth for now
|
||||
|
||||
wd.setupRootHandler(wr)
|
||||
wd.mountHandlers(wr)
|
||||
@@ -178,6 +147,21 @@ func (wd *WebDav) commonMiddleware(next http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func (wd *WebDav) authMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
cfg := config.Get()
|
||||
if cfg.UseAuth && cfg.EnableWebdavAuth {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if !ok || !config.VerifyAuth(username, password) {
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func (wd *WebDav) handleGetRoot() http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
Reference in New Issue
Block a user