updated filepaths for multiplatform support (#56)

- Migrate from Go's path to path/filepath to support multi-OS support
This commit is contained in:
somesuchnonsense
2025-04-26 14:59:23 -05:00
committed by GitHub
parent bce51ecd4f
commit 6e2d1e1a7f
5 changed files with 74 additions and 61 deletions

View File

@@ -5,6 +5,14 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/go-co-op/gocron/v2" "github.com/go-co-op/gocron/v2"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"github.com/puzpuzpuz/xsync/v3" "github.com/puzpuzpuz/xsync/v3"
@@ -13,13 +21,6 @@ import (
"github.com/sirrobot01/decypharr/internal/logger" "github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/internal/utils"
"github.com/sirrobot01/decypharr/pkg/debrid/types" "github.com/sirrobot01/decypharr/pkg/debrid/types"
"os"
path "path/filepath"
"strconv"
"strings"
"sync"
"sync/atomic"
"time"
) )
type WebDavFolderNaming string type WebDavFolderNaming string
@@ -115,8 +116,8 @@ func New(dc config.Debrid, client types.Client) *Cache {
autoExpiresLinksAfter = 48 * time.Hour autoExpiresLinksAfter = 48 * time.Hour
} }
return &Cache{ return &Cache{
dir: path.Join(cfg.Path, "cache", dc.Name), // path to save cache files dir: filepath.Join(cfg.Path, "cache", dc.Name), // path to save cache files
torrents: xsync.NewMapOf[string, string](), torrents: xsync.NewMapOf[string, *CachedTorrent](),
torrentsNames: xsync.NewMapOf[string, *CachedTorrent](), torrentsNames: xsync.NewMapOf[string, *CachedTorrent](),
invalidDownloadLinks: xsync.NewMapOf[string, string](), invalidDownloadLinks: xsync.NewMapOf[string, string](),
client: client, client: client,
@@ -178,7 +179,7 @@ func (c *Cache) load() (map[string]*CachedTorrent, error) {
// Get only json files // Get only json files
var jsonFiles []os.DirEntry var jsonFiles []os.DirEntry
for _, file := range files { for _, file := range files {
if !file.IsDir() && path.Ext(file.Name()) == ".json" { if !file.IsDir() && filepath.Ext(file.Name()) == ".json" {
jsonFiles = append(jsonFiles, file) jsonFiles = append(jsonFiles, file)
} }
} }
@@ -206,7 +207,7 @@ func (c *Cache) load() (map[string]*CachedTorrent, error) {
} }
fileName := file.Name() fileName := file.Name()
filePath := path.Join(c.dir, fileName) filePath := filepath.Join(c.dir, fileName)
data, err := os.ReadFile(filePath) data, err := os.ReadFile(filePath)
if err != nil { if err != nil {
c.logger.Error().Err(err).Msgf("Failed to read file: %s", filePath) c.logger.Error().Err(err).Msgf("Failed to read file: %s", filePath)
@@ -238,8 +239,8 @@ func (c *Cache) load() (map[string]*CachedTorrent, error) {
ct.AddedOn = addedOn ct.AddedOn = addedOn
} }
ct.IsComplete = true ct.IsComplete = true
ct.Name = path.Clean(ct.Name)
ct.Files = fs ct.Files = fs
ct.Name = filepath.Clean(ct.Name)
results.Store(ct.Id, &ct) results.Store(ct.Id, &ct)
} }
} }
@@ -393,19 +394,19 @@ func (c *Cache) sync(torrents []*types.Torrent) error {
func (c *Cache) GetTorrentFolder(torrent *types.Torrent) string { func (c *Cache) GetTorrentFolder(torrent *types.Torrent) string {
switch c.folderNaming { switch c.folderNaming {
case WebDavUseFileName: case WebDavUseFileName:
return path.Clean(torrent.Filename) return filepath.Clean(torrent.Filename)
case WebDavUseOriginalName: case WebDavUseOriginalName:
return path.Clean(torrent.OriginalFilename) return filepath.Clean(torrent.OriginalFilename)
case WebDavUseFileNameNoExt: case WebDavUseFileNameNoExt:
return path.Clean(utils.RemoveExtension(torrent.Filename)) return filepath.Clean(utils.RemoveExtension(torrent.Filename))
case WebDavUseOriginalNameNoExt: case WebDavUseOriginalNameNoExt:
return path.Clean(utils.RemoveExtension(torrent.OriginalFilename)) return filepath.Clean(utils.RemoveExtension(torrent.OriginalFilename))
case WebDavUseID: case WebDavUseID:
return torrent.Id return torrent.Id
case WebdavUseHash: case WebdavUseHash:
return strings.ToLower(torrent.InfoHash) return strings.ToLower(torrent.InfoHash)
default: default:
return path.Clean(torrent.Filename) return filepath.Clean(torrent.Filename)
} }
} }
@@ -514,7 +515,7 @@ func (c *Cache) SaveTorrent(ct *CachedTorrent) {
func (c *Cache) saveTorrent(id string, data []byte) { func (c *Cache) saveTorrent(id string, data []byte) {
fileName := id + ".json" fileName := id + ".json"
filePath := path.Join(c.dir, fileName) filePath := filepath.Join(c.dir, fileName)
// Use a unique temporary filename for concurrent safety // Use a unique temporary filename for concurrent safety
tmpFile := filePath + ".tmp." + strconv.FormatInt(time.Now().UnixNano(), 10) tmpFile := filePath + ".tmp." + strconv.FormatInt(time.Now().UnixNano(), 10)
@@ -675,7 +676,7 @@ func (c *Cache) DeleteTorrents(ids []string) {
func (c *Cache) removeFromDB(torrentId string) { func (c *Cache) removeFromDB(torrentId string) {
// Moves the torrent file to the trash // Moves the torrent file to the trash
filePath := path.Join(c.dir, torrentId+".json") filePath := filepath.Join(c.dir, torrentId+".json")
// Check if the file exists // Check if the file exists
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
@@ -683,8 +684,8 @@ func (c *Cache) removeFromDB(torrentId string) {
} }
// Move the file to the trash // Move the file to the trash
trashPath := path.Join(c.dir, "trash", torrentId+".json") trashPath := filepath.Join(c.dir, "trash", torrentId+".json")
if err := os.MkdirAll(path.Dir(trashPath), 0755); err != nil { if err := os.MkdirAll(filepath.Dir(trashPath), 0755); err != nil {
return return
} }
if err := os.Rename(filePath, trashPath); err != nil { if err := os.Rename(filePath, trashPath); err != nil {

View File

@@ -2,12 +2,14 @@ package debrid
import ( import (
"fmt" "fmt"
"github.com/beevik/etree"
"github.com/sirrobot01/decypharr/internal/request"
"net/http" "net/http"
"os" "os"
path "path/filepath" "path"
"path/filepath"
"time" "time"
"github.com/beevik/etree"
"github.com/sirrobot01/decypharr/internal/request"
) )
// resetPropfindResponse resets the propfind response cache for the specified parent directories. // resetPropfindResponse resets the propfind response cache for the specified parent directories.
@@ -64,15 +66,15 @@ func (c *Cache) refreshParentXml(torrents []os.FileInfo, clientName, parent stri
currentTime := time.Now().UTC().Format(http.TimeFormat) currentTime := time.Now().UTC().Format(http.TimeFormat)
// Add the parent directory // Add the parent directory
baseUrl := path.Clean(fmt.Sprintf("/webdav/%s/%s", clientName, parent)) baseUrl := path.Clean(path.Join("webdav", clientName, parent))
parentPath := fmt.Sprintf("%s/", baseUrl) parentPath := path.Join(baseUrl)
addDirectoryResponse(multistatus, parentPath, parent, currentTime) addDirectoryResponse(multistatus, parentPath, parent, currentTime)
// Add torrents to the XML // Add torrents to the XML
for _, torrent := range torrents { for _, torrent := range torrents {
name := torrent.Name() name := torrent.Name()
// Note the path structure change - parent first, then torrent name // Note the path structure change - parent first, then torrent name
torrentPath := fmt.Sprintf("/webdav/%s/%s/%s/", torrentPath := filepath.Join("webdav",
clientName, clientName,
parent, parent,
name, name,

View File

@@ -4,19 +4,20 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"io"
"net/http"
"net/url"
"os"
"os/signal"
"runtime"
"syscall"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware" "github.com/go-chi/chi/v5/middleware"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/config" "github.com/sirrobot01/decypharr/internal/config"
"github.com/sirrobot01/decypharr/internal/logger" "github.com/sirrobot01/decypharr/internal/logger"
"github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/request"
"io"
"net/http"
"os"
"os/signal"
path "path/filepath"
"runtime"
"syscall"
) )
type Server struct { type Server struct {
@@ -34,9 +35,9 @@ func New(handlers map[string]http.Handler) *Server {
s := &Server{ s := &Server{
logger: l, logger: l,
} }
staticPath, _ := url.JoinPath(cfg.URLBase, "static")
r.Handle(path.Join(cfg.URLBase, "static")+"/*", r.Handle(staticPath+"/*",
http.StripPrefix(path.Join(cfg.URLBase, "static"), http.FileServer(http.Dir("static"))), http.StripPrefix(staticPath, http.FileServer(http.Dir("static"))),
) )
r.Route(cfg.URLBase, func(r chi.Router) { r.Route(cfg.URLBase, func(r chi.Router) {

View File

@@ -4,6 +4,18 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"html/template"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path"
"path/filepath"
"slices"
"strings"
"time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"github.com/sirrobot01/decypharr/internal/request" "github.com/sirrobot01/decypharr/internal/request"
"github.com/sirrobot01/decypharr/internal/utils" "github.com/sirrobot01/decypharr/internal/utils"
@@ -11,16 +23,6 @@ import (
"github.com/sirrobot01/decypharr/pkg/debrid/types" "github.com/sirrobot01/decypharr/pkg/debrid/types"
"github.com/sirrobot01/decypharr/pkg/version" "github.com/sirrobot01/decypharr/pkg/version"
"golang.org/x/net/webdav" "golang.org/x/net/webdav"
"html/template"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
path "path/filepath"
"slices"
"strings"
"time"
) )
type Handler struct { type Handler struct {
@@ -47,9 +49,12 @@ func (h *Handler) Mkdir(ctx context.Context, name string, perm os.FileMode) erro
// RemoveAll implements webdav.FileSystem // RemoveAll implements webdav.FileSystem
func (h *Handler) RemoveAll(ctx context.Context, name string) error { func (h *Handler) RemoveAll(ctx context.Context, name string) error {
name = path.Clean("/" + name) if name[0] != '/' {
name = "/" + name
}
name = filepath.Clean(name)
rootDir := h.getRootPath() rootDir := filepath.Clean(h.getRootPath())
if name == rootDir { if name == rootDir {
return os.ErrPermission return os.ErrPermission
@@ -72,7 +77,7 @@ func (h *Handler) Rename(ctx context.Context, oldName, newName string) error {
} }
func (h *Handler) getRootPath() string { func (h *Handler) getRootPath() string {
return fmt.Sprintf("/webdav/%s", h.Name) return fmt.Sprintf(filepath.Join(string(os.PathSeparator), "webdav", "%s"), h.Name)
} }
func (h *Handler) getTorrentsFolders() []os.FileInfo { func (h *Handler) getTorrentsFolders() []os.FileInfo {
@@ -104,8 +109,11 @@ func (h *Handler) getParentFiles() []os.FileInfo {
} }
func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) { func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
name = utils.UnescapePath(path.Clean("/" + name)) if name[0] != '/' {
rootDir := h.getRootPath() name = "/" + name
}
name = utils.UnescapePath(filepath.Clean(name))
rootDir := filepath.Clean(h.getRootPath())
metadataOnly := ctx.Value("metadataOnly") != nil metadataOnly := ctx.Value("metadataOnly") != nil
@@ -118,11 +126,11 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
cache: h.cache, cache: h.cache,
isDir: true, isDir: true,
children: h.getParentFiles(), children: h.getParentFiles(),
name: "/", name: string(os.PathSeparator),
metadataOnly: true, metadataOnly: true,
modTime: now, modTime: now,
}, nil }, nil
case path.Join(rootDir, "version.txt"): case filepath.Join(rootDir, "version.txt"):
versionInfo := version.GetInfo().String() versionInfo := version.GetInfo().String()
return &File{ return &File{
cache: h.cache, cache: h.cache,
@@ -138,7 +146,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
// Single check for top-level folders // Single check for top-level folders
if h.isParentPath(name) { if h.isParentPath(name) {
folderName := strings.TrimPrefix(name, rootDir) folderName := strings.TrimPrefix(name, rootDir)
folderName = strings.TrimPrefix(folderName, "/") folderName = strings.TrimPrefix(folderName, string(os.PathSeparator))
// Only fetcher the torrent folders once // Only fetcher the torrent folders once
children := h.getTorrentsFolders() children := h.getTorrentsFolders()
@@ -155,7 +163,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
} }
_path := strings.TrimPrefix(name, rootDir) _path := strings.TrimPrefix(name, rootDir)
parts := strings.Split(strings.TrimPrefix(_path, "/"), "/") parts := strings.Split(strings.TrimPrefix(_path, string(os.PathSeparator)), string(os.PathSeparator))
if len(parts) >= 2 && (slices.Contains(h.getParentItems(), parts[0])) { if len(parts) >= 2 && (slices.Contains(h.getParentItems(), parts[0])) {
@@ -181,7 +189,7 @@ func (h *Handler) OpenFile(ctx context.Context, name string, flag int, perm os.F
} }
// Torrent file level // Torrent file level
filename := strings.Join(parts[2:], "/") filename := filepath.Join(parts[2:]...)
if file, ok := cachedTorrent.Files[filename]; ok { if file, ok := cachedTorrent.Files[filename]; ok {
return &File{ return &File{
cache: h.cache, cache: h.cache,
@@ -239,7 +247,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Set metadata only // Set metadata only
ctx := context.WithValue(r.Context(), "metadataOnly", true) ctx := context.WithValue(r.Context(), "metadataOnly", true)
r = r.WithContext(ctx) r = r.WithContext(ctx)
cleanPath := path.Clean(r.URL.Path) cleanPath := filepath.Clean(r.URL.Path)
if r.Header.Get("Depth") == "" { if r.Header.Get("Depth") == "" {
r.Header.Set("Depth", "1") r.Header.Set("Depth", "1")
} }
@@ -399,7 +407,7 @@ func (h *Handler) isParentPath(_path string) bool {
rootPath := h.getRootPath() rootPath := h.getRootPath()
parents := h.getParentItems() parents := h.getParentItems()
for _, p := range parents { for _, p := range parents {
if path.Clean(_path) == path.Clean(path.Join(rootPath, p)) { if filepath.Clean(_path) == filepath.Clean(filepath.Join(rootPath, p)) {
return true return true
} }
} }

View File

@@ -3,6 +3,7 @@ package webdav
import ( import (
"net/http" "net/http"
"net/url" "net/url"
"os"
"strings" "strings"
) )
@@ -10,11 +11,11 @@ import (
// /webdav/alldebrid/__all__/TorrentName // /webdav/alldebrid/__all__/TorrentName
func getName(rootDir, path string) (string, string) { func getName(rootDir, path string) (string, string) {
path = strings.TrimPrefix(path, rootDir) path = strings.TrimPrefix(path, rootDir)
parts := strings.Split(strings.TrimPrefix(path, "/"), "/") parts := strings.Split(strings.TrimPrefix(path, string(os.PathSeparator)), string(os.PathSeparator))
if len(parts) < 2 { if len(parts) < 2 {
return "", "" return "", ""
} }
return parts[1], strings.Join(parts[2:], "/") // Note the change from [0] to [1] return parts[1], strings.Join(parts[2:], string(os.PathSeparator)) // Note the change from [0] to [1]
} }
func acceptsGzip(r *http.Request) bool { func acceptsGzip(r *http.Request) bool {