- Add mounting support
- Fix minor issues
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/sirrobot01/decypharr/pkg/debrid/providers/torbox"
|
||||
"github.com/sirrobot01/decypharr/pkg/debrid/store"
|
||||
"github.com/sirrobot01/decypharr/pkg/debrid/types"
|
||||
"github.com/sirrobot01/decypharr/pkg/mount"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -30,6 +31,12 @@ func (de *Debrid) Cache() *store.Cache {
|
||||
return de.cache
|
||||
}
|
||||
|
||||
func (de *Debrid) Reset() {
|
||||
if de.cache != nil {
|
||||
de.cache.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
debrids map[string]*Debrid
|
||||
mu sync.RWMutex
|
||||
@@ -43,16 +50,28 @@ func NewStorage() *Storage {
|
||||
|
||||
debrids := make(map[string]*Debrid)
|
||||
|
||||
bindAddress := cfg.BindAddress
|
||||
if bindAddress == "" {
|
||||
bindAddress = "localhost"
|
||||
}
|
||||
webdavUrl := fmt.Sprintf("http://%s:%s%s/webdav", bindAddress, cfg.Port, cfg.URLBase)
|
||||
|
||||
for _, dc := range cfg.Debrids {
|
||||
client, err := createDebridClient(dc)
|
||||
if err != nil {
|
||||
_logger.Error().Err(err).Str("Debrid", dc.Name).Msg("failed to connect to debrid client")
|
||||
continue
|
||||
}
|
||||
var cache *store.Cache
|
||||
var (
|
||||
cache *store.Cache
|
||||
mounter *mount.Mount
|
||||
)
|
||||
_log := client.Logger()
|
||||
if dc.UseWebDav {
|
||||
cache = store.NewDebridCache(dc, client)
|
||||
if cfg.Rclone.Enabled {
|
||||
mounter = mount.NewMount(dc.Name, webdavUrl)
|
||||
}
|
||||
cache = store.NewDebridCache(dc, client, mounter)
|
||||
_log.Info().Msg("Debrid Service started with WebDAV")
|
||||
} else {
|
||||
_log.Info().Msg("Debrid Service started")
|
||||
@@ -102,8 +121,17 @@ func (d *Storage) Client(name string) types.Client {
|
||||
|
||||
func (d *Storage) Reset() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
// Reset all debrid clients and caches
|
||||
for _, debrid := range d.debrids {
|
||||
if debrid != nil {
|
||||
debrid.Reset()
|
||||
}
|
||||
}
|
||||
|
||||
// Reinitialize the debrids map
|
||||
d.debrids = make(map[string]*Debrid)
|
||||
d.mu.Unlock()
|
||||
d.lastUsed = ""
|
||||
}
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ type RealDebrid struct {
|
||||
addSamples bool
|
||||
Profile *types.Profile
|
||||
minimumFreeSlot int // Minimum number of active pots to maintain (used for cached stuffs, etc.)
|
||||
|
||||
limit int
|
||||
}
|
||||
|
||||
func New(dc config.Debrid) (*RealDebrid, error) {
|
||||
@@ -101,6 +101,7 @@ func New(dc config.Debrid) (*RealDebrid, error) {
|
||||
checkCached: dc.CheckCached,
|
||||
addSamples: dc.AddSamples,
|
||||
minimumFreeSlot: dc.MinimumFreeSlot,
|
||||
limit: dc.Limit,
|
||||
}
|
||||
|
||||
if _, err := r.GetProfile(); err != nil {
|
||||
@@ -794,6 +795,10 @@ func (r *RealDebrid) getTorrents(offset int, limit int) (int, []*types.Torrent,
|
||||
|
||||
func (r *RealDebrid) GetTorrents() ([]*types.Torrent, error) {
|
||||
limit := 5000
|
||||
if r.limit != 0 {
|
||||
limit = r.limit
|
||||
}
|
||||
hardLimit := r.limit
|
||||
|
||||
// Get first batch and total count
|
||||
allTorrents := make([]*types.Torrent, 0)
|
||||
@@ -812,6 +817,10 @@ func (r *RealDebrid) GetTorrents() ([]*types.Torrent, error) {
|
||||
}
|
||||
allTorrents = append(allTorrents, torrents...)
|
||||
offset += totalTorrents
|
||||
if hardLimit != 0 && len(allTorrents) >= hardLimit {
|
||||
// If hard limit is set, stop fetching more torrents
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if fetchError != nil {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/sirrobot01/decypharr/pkg/mount"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -106,9 +107,10 @@ type Cache struct {
|
||||
|
||||
config config.Debrid
|
||||
customFolders []string
|
||||
mounter *mount.Mount
|
||||
}
|
||||
|
||||
func NewDebridCache(dc config.Debrid, client types.Client) *Cache {
|
||||
func NewDebridCache(dc config.Debrid, client types.Client, mounter *mount.Mount) *Cache {
|
||||
cfg := config.Get()
|
||||
cet, err := time.LoadLocation("CET")
|
||||
if err != nil {
|
||||
@@ -163,6 +165,7 @@ func NewDebridCache(dc config.Debrid, client types.Client) *Cache {
|
||||
|
||||
config: dc,
|
||||
customFolders: customFolders,
|
||||
mounter: mounter,
|
||||
|
||||
ready: make(chan struct{}),
|
||||
}
|
||||
@@ -186,6 +189,15 @@ func (c *Cache) StreamWithRclone() bool {
|
||||
// and before you discard the instance on a restart.
|
||||
func (c *Cache) Reset() {
|
||||
|
||||
// Unmount first
|
||||
if c.mounter != nil && c.mounter.IsMounted() {
|
||||
if err := c.mounter.Unmount(); err != nil {
|
||||
c.logger.Error().Err(err).Msgf("Failed to unmount %s", c.config.Name)
|
||||
} else {
|
||||
c.logger.Info().Msgf("Unmounted %s", c.config.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if err := c.scheduler.StopJobs(); err != nil {
|
||||
c.logger.Error().Err(err).Msg("Failed to stop scheduler jobs")
|
||||
}
|
||||
@@ -198,7 +210,9 @@ func (c *Cache) Reset() {
|
||||
c.listingDebouncer.Stop()
|
||||
|
||||
// Close the repair channel
|
||||
close(c.repairChan)
|
||||
if c.repairChan != nil {
|
||||
close(c.repairChan)
|
||||
}
|
||||
|
||||
// 1. Reset torrent storage
|
||||
c.torrents.reset()
|
||||
@@ -219,6 +233,9 @@ func (c *Cache) Reset() {
|
||||
|
||||
// 6. Reset repair channel so the next Start() can spin it up
|
||||
c.repairChan = make(chan RepairRequest, 100)
|
||||
|
||||
// Reset the ready channel
|
||||
c.ready = make(chan struct{})
|
||||
}
|
||||
|
||||
func (c *Cache) Start(ctx context.Context) error {
|
||||
@@ -250,10 +267,15 @@ func (c *Cache) Start(ctx context.Context) error {
|
||||
addr := cfg.BindAddress + ":" + cfg.Port + cfg.URLBase + "webdav/" + name + "/"
|
||||
c.logger.Info().Msgf("%s WebDav server running at %s", name, addr)
|
||||
|
||||
<-ctx.Done()
|
||||
c.logger.Info().Msgf("Stopping %s WebDav server", name)
|
||||
c.Reset()
|
||||
|
||||
if c.mounter != nil {
|
||||
if err := c.mounter.Mount(ctx); err != nil {
|
||||
c.logger.Error().Err(err).Msgf("Failed to mount %s", c.config.Name)
|
||||
} else {
|
||||
c.logger.Info().Msgf("Mounted %s at %s", c.config.Name, c.mounter.LocalPath)
|
||||
}
|
||||
} else {
|
||||
c.logger.Warn().Msgf("Mounting is disabled for %s", c.config.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -881,3 +903,7 @@ func (c *Cache) RemoveFile(torrentId string, filename string) error {
|
||||
func (c *Cache) Logger() zerolog.Logger {
|
||||
return c.logger
|
||||
}
|
||||
|
||||
func (c *Cache) GetConfig() config.Debrid {
|
||||
return c.config
|
||||
}
|
||||
|
||||
@@ -127,6 +127,21 @@ func (c *Cache) refreshTorrents(ctx context.Context) {
|
||||
|
||||
func (c *Cache) refreshRclone() error {
|
||||
cfg := c.config
|
||||
dirs := strings.FieldsFunc(cfg.RcRefreshDirs, func(r rune) bool {
|
||||
return r == ',' || r == '&'
|
||||
})
|
||||
if len(dirs) == 0 {
|
||||
dirs = []string{"__all__"}
|
||||
}
|
||||
if c.mounter != nil {
|
||||
return c.mounter.Refresh(dirs)
|
||||
} else {
|
||||
return c.refreshRcloneWithRC(dirs)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) refreshRcloneWithRC(dirs []string) error {
|
||||
cfg := c.config
|
||||
|
||||
if cfg.RcUrl == "" {
|
||||
return nil
|
||||
@@ -138,7 +153,7 @@ func (c *Cache) refreshRclone() error {
|
||||
|
||||
client := http.DefaultClient
|
||||
// Create form data
|
||||
data := c.buildRcloneRequestData()
|
||||
data := c.buildRcloneRequestData(dirs)
|
||||
|
||||
if err := c.sendRcloneRequest(client, "vfs/forget", data); err != nil {
|
||||
c.logger.Error().Err(err).Msg("Failed to send rclone vfs/forget request")
|
||||
@@ -151,16 +166,7 @@ func (c *Cache) refreshRclone() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) buildRcloneRequestData() string {
|
||||
cfg := c.config
|
||||
dirs := strings.FieldsFunc(cfg.RcRefreshDirs, func(r rune) bool {
|
||||
return r == ',' || r == '&'
|
||||
})
|
||||
|
||||
if len(dirs) == 0 {
|
||||
return "dir=__all__"
|
||||
}
|
||||
|
||||
func (c *Cache) buildRcloneRequestData(dirs []string) string {
|
||||
var data strings.Builder
|
||||
for index, dir := range dirs {
|
||||
if dir != "" {
|
||||
|
||||
445
pkg/mount/mount.go
Normal file
445
pkg/mount/mount.go
Normal file
@@ -0,0 +1,445 @@
|
||||
package mount
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/sirrobot01/decypharr/internal/logger"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rclone/rclone/cmd/mountlib"
|
||||
"github.com/rclone/rclone/fs"
|
||||
"github.com/rclone/rclone/fs/config"
|
||||
"github.com/rclone/rclone/vfs"
|
||||
"github.com/rclone/rclone/vfs/vfscommon"
|
||||
|
||||
_ "github.com/rclone/rclone/backend/local" // Local backend (required for VFS cache)
|
||||
_ "github.com/rclone/rclone/backend/webdav" // WebDAV backend
|
||||
_ "github.com/rclone/rclone/cmd/cmount" // Custom mount for macOS
|
||||
_ "github.com/rclone/rclone/cmd/mount" // Standard FUSE mount
|
||||
// Try to import available mount backends for macOS
|
||||
// These are conditional imports that may or may not work depending on system setup
|
||||
_ "github.com/rclone/rclone/cmd/mount2" // Alternative FUSE mount
|
||||
|
||||
configPkg "github.com/sirrobot01/decypharr/internal/config"
|
||||
)
|
||||
|
||||
func getMountFn() (mountlib.MountFn, error) {
|
||||
// Try mount methods in order of preference
|
||||
for _, method := range []string{"", "mount", "cmount", "mount2"} {
|
||||
_, mountFn := mountlib.ResolveMountMethod(method)
|
||||
if mountFn != nil {
|
||||
return mountFn, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("no suitable mount function found")
|
||||
}
|
||||
|
||||
type Mount struct {
|
||||
Provider string
|
||||
LocalPath string
|
||||
WebDAVURL string
|
||||
mountPoint *mountlib.MountPoint
|
||||
vfs *vfs.VFS
|
||||
cancel context.CancelFunc
|
||||
mounted atomic.Bool
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func NewMount(provider, webdavURL string) *Mount {
|
||||
cfg := configPkg.Get()
|
||||
_logger := logger.New("mount-" + provider)
|
||||
mountPath := filepath.Join(cfg.Rclone.MountPath, provider)
|
||||
_url, err := url.JoinPath(webdavURL, provider)
|
||||
if err != nil {
|
||||
_url = fmt.Sprintf("%s/%s", webdavURL, provider)
|
||||
}
|
||||
|
||||
// Get mount function to validate if FUSE is available
|
||||
mountFn, err := getMountFn()
|
||||
if err != nil || mountFn == nil {
|
||||
_logger.Warn().Err(err).Msgf("Mount function not available for %s, using WebDAV URL %s", provider, _url)
|
||||
return nil
|
||||
}
|
||||
return &Mount{
|
||||
Provider: provider,
|
||||
LocalPath: mountPath,
|
||||
WebDAVURL: _url,
|
||||
logger: _logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mount) Mount(ctx context.Context) error {
|
||||
if m.mounted.Load() {
|
||||
m.logger.Info().Msgf("Mount %s is already mounted at %s", m.Provider, m.LocalPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(m.LocalPath, 0755); err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("failed to create mount directory %s: %w", m.LocalPath, err)
|
||||
}
|
||||
|
||||
// Check if the mount point is already busy/mounted
|
||||
if m.isMountBusy() {
|
||||
m.logger.Warn().Msgf("Mount point %s appears to be busy, attempting cleanup", m.LocalPath)
|
||||
if err := m.forceUnmount(); err != nil {
|
||||
m.logger.Error().Err(err).Msgf("Failed to cleanup busy mount point %s", m.LocalPath)
|
||||
return fmt.Errorf("mount point %s is busy and cleanup failed: %w", m.LocalPath, err)
|
||||
}
|
||||
m.logger.Info().Msgf("Successfully cleaned up busy mount point %s", m.LocalPath)
|
||||
}
|
||||
|
||||
// Create mount context
|
||||
mountCtx, cancel := context.WithCancel(ctx)
|
||||
m.cancel = cancel
|
||||
|
||||
configName := fmt.Sprintf("decypharr_%s", m.Provider)
|
||||
if err := setRcloneConfig(configName, m.WebDAVURL); err != nil {
|
||||
return fmt.Errorf("failed to set rclone config: %w", err)
|
||||
}
|
||||
|
||||
// Check if fusermount3 is available
|
||||
if _, err := exec.LookPath("fusermount3"); err != nil {
|
||||
m.logger.Info().Msgf("FUSE mounting not available (fusermount3 not found). Files accessible via WebDAV at %s", m.WebDAVURL)
|
||||
m.mounted.Store(true) // Mark as "mounted" for WebDAV access
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get the mount function - try different mount methods
|
||||
mountFn, err := getMountFn()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get mount function for %s: %w", m.Provider, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
if err := m.performMount(mountCtx, mountFn, configName); err != nil {
|
||||
m.logger.Error().Err(err).Msgf("Failed to mount %s at %s", m.Provider, m.LocalPath)
|
||||
return
|
||||
}
|
||||
m.mounted.Store(true)
|
||||
m.logger.Info().Msgf("Successfully mounted %s WebDAV at %s", m.Provider, m.LocalPath)
|
||||
|
||||
// Wait for context cancellation
|
||||
<-mountCtx.Done()
|
||||
}()
|
||||
m.logger.Info().Msgf("Mount process started for %s at %s", m.Provider, m.LocalPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func setRcloneConfig(configName, webdavURL string) error {
|
||||
// Set configuration in rclone's config system using FileSetValue
|
||||
config.FileSetValue(configName, "type", "webdav")
|
||||
config.FileSetValue(configName, "url", webdavURL)
|
||||
config.FileSetValue(configName, "vendor", "other")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mount) performMount(ctx context.Context, mountfn mountlib.MountFn, configName string) error {
|
||||
// Create filesystem from config
|
||||
fsrc, err := fs.NewFs(ctx, fmt.Sprintf("%s:", configName))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create filesystem: %w", err)
|
||||
}
|
||||
|
||||
// Get global rclone config
|
||||
cfg := configPkg.Get()
|
||||
rcloneOpt := &cfg.Rclone
|
||||
|
||||
// Parse duration strings
|
||||
dirCacheTime, _ := time.ParseDuration(rcloneOpt.DirCacheTime)
|
||||
attrTimeout, _ := time.ParseDuration(rcloneOpt.AttrTimeout)
|
||||
|
||||
// Parse cache mode
|
||||
var cacheMode vfscommon.CacheMode
|
||||
switch rcloneOpt.VfsCacheMode {
|
||||
case "off":
|
||||
cacheMode = vfscommon.CacheModeOff
|
||||
case "minimal":
|
||||
cacheMode = vfscommon.CacheModeMinimal
|
||||
case "writes":
|
||||
cacheMode = vfscommon.CacheModeWrites
|
||||
case "full":
|
||||
cacheMode = vfscommon.CacheModeFull
|
||||
default:
|
||||
cacheMode = vfscommon.CacheModeOff
|
||||
}
|
||||
|
||||
vfsOpt := &vfscommon.Options{
|
||||
NoModTime: rcloneOpt.NoModTime,
|
||||
NoChecksum: rcloneOpt.NoChecksum,
|
||||
DirCacheTime: fs.Duration(dirCacheTime),
|
||||
PollInterval: 0, // Polling is disabled for webdav
|
||||
CacheMode: cacheMode,
|
||||
UID: rcloneOpt.UID,
|
||||
GID: rcloneOpt.GID,
|
||||
}
|
||||
|
||||
if rcloneOpt.VfsReadChunkSizeLimit != "" {
|
||||
var chunkSizeLimit fs.SizeSuffix
|
||||
if err := chunkSizeLimit.Set(rcloneOpt.VfsReadChunkSizeLimit); err == nil {
|
||||
vfsOpt.ChunkSizeLimit = chunkSizeLimit
|
||||
}
|
||||
}
|
||||
|
||||
if rcloneOpt.VfsReadAhead != "" {
|
||||
var readAhead fs.SizeSuffix
|
||||
if err := readAhead.Set(rcloneOpt.VfsReadAhead); err == nil {
|
||||
vfsOpt.ReadAhead = readAhead
|
||||
}
|
||||
}
|
||||
|
||||
if rcloneOpt.VfsReadChunkSize != "" {
|
||||
var chunkSize fs.SizeSuffix
|
||||
if err := chunkSize.Set(rcloneOpt.VfsReadChunkSize); err == nil {
|
||||
vfsOpt.ChunkSize = chunkSize
|
||||
}
|
||||
}
|
||||
|
||||
// Parse and set buffer size globally for rclone
|
||||
if rcloneOpt.BufferSize != "" {
|
||||
var bufferSize fs.SizeSuffix
|
||||
if err := bufferSize.Set(rcloneOpt.BufferSize); err == nil {
|
||||
fs.GetConfig(ctx).BufferSize = bufferSize
|
||||
}
|
||||
}
|
||||
|
||||
// Create mount options using global config
|
||||
mountOpt := &mountlib.Options{
|
||||
DebugFUSE: false,
|
||||
AllowNonEmpty: true,
|
||||
AllowOther: true,
|
||||
Daemon: false,
|
||||
AttrTimeout: fs.Duration(attrTimeout),
|
||||
DeviceName: fmt.Sprintf("decypharr-%s", configName),
|
||||
VolumeName: fmt.Sprintf("decypharr-%s", configName),
|
||||
}
|
||||
|
||||
// Set cache dir
|
||||
if rcloneOpt.CacheDir != "" {
|
||||
cacheDir := filepath.Join(rcloneOpt.CacheDir, configName)
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
// Log error but continue
|
||||
m.logger.Error().Err(err).Msgf("Failed to create cache directory %s, using default cache", cacheDir)
|
||||
}
|
||||
if err := config.SetCacheDir(cacheDir); err != nil {
|
||||
// Log error but continue
|
||||
m.logger.Error().Err(err).Msgf("Failed to set cache directory %s, using default cache", cacheDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Create VFS instance
|
||||
vfsInstance := vfs.New(fsrc, vfsOpt)
|
||||
m.vfs = vfsInstance
|
||||
|
||||
// Create mount point using rclone's internal mounting
|
||||
mountPoint := mountlib.NewMountPoint(mountfn, m.LocalPath, fsrc, mountOpt, vfsOpt)
|
||||
m.mountPoint = mountPoint
|
||||
|
||||
// Start the mount
|
||||
_, err = mountPoint.Mount()
|
||||
if err != nil {
|
||||
// Cleanup mount point if it failed
|
||||
if mountPoint != nil && mountPoint.UnmountFn != nil {
|
||||
if unmountErr := m.Unmount(); unmountErr != nil {
|
||||
m.logger.Error().Err(unmountErr).Msgf("Failed to cleanup mount point %s after mount failure", m.LocalPath)
|
||||
} else {
|
||||
m.logger.Info().Msgf("Successfully cleaned up mount point %s after mount failure", m.LocalPath)
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("failed to mount %s at %s: %w", m.Provider, m.LocalPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mount) Unmount() error {
|
||||
if !m.mounted.Load() {
|
||||
m.logger.Info().Msgf("Mount %s is not mounted, skipping unmount", m.Provider)
|
||||
return nil
|
||||
}
|
||||
|
||||
m.mounted.Store(false)
|
||||
|
||||
if m.vfs != nil {
|
||||
m.logger.Debug().Msgf("Shutting down VFS for provider %s", m.Provider)
|
||||
m.vfs.Shutdown()
|
||||
} else {
|
||||
m.logger.Warn().Msgf("VFS instance for provider %s is nil, skipping shutdown", m.Provider)
|
||||
}
|
||||
if m.mountPoint == nil || m.mountPoint.UnmountFn == nil {
|
||||
m.logger.Warn().Msgf("Mount point for provider %s is nil or unmount function is not set, skipping unmount", m.Provider)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.mountPoint.Unmount(); err != nil {
|
||||
// Try to force unmount if normal unmount fails
|
||||
if err := m.forceUnmount(); err != nil {
|
||||
m.logger.Error().Err(err).Msgf("Failed to force unmount %s at %s", m.Provider, m.LocalPath)
|
||||
return fmt.Errorf("failed to unmount %s at %s: %w", m.Provider, m.LocalPath, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mount) forceUnmount() error {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin", "freebsd", "openbsd":
|
||||
if err := m.tryUnmount("umount", m.LocalPath); err == nil {
|
||||
m.logger.Info().Msgf("Successfully unmounted %s", m.LocalPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try lazy unmount
|
||||
if err := m.tryUnmount("umount", "-l", m.LocalPath); err == nil {
|
||||
m.logger.Info().Msgf("Successfully lazy unmounted %s", m.LocalPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.tryUnmount("fusermount", "-uz", m.LocalPath); err == nil {
|
||||
m.logger.Info().Msgf("Successfully unmounted %s using fusermount3", m.LocalPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := m.tryUnmount("fusermount3", "-uz", m.LocalPath); err == nil {
|
||||
m.logger.Info().Msgf("Successfully unmounted %s using fusermount3", m.LocalPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("all unmount attempts failed for %s", m.LocalPath)
|
||||
default:
|
||||
return fmt.Errorf("force unmount not supported on %s", runtime.GOOS)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mount) tryUnmount(command string, args ...string) error {
|
||||
cmd := exec.Command(command, args...)
|
||||
return cmd.Run()
|
||||
}
|
||||
|
||||
func (m *Mount) isMountBusy() bool {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin", "freebsd", "openbsd":
|
||||
// Check if the mount point is listed in /proc/mounts or mount output
|
||||
cmd := exec.Command("mount")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(output), m.LocalPath)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Mount) IsMounted() bool {
|
||||
return m.mounted.Load() && m.mountPoint != nil
|
||||
}
|
||||
|
||||
func (m *Mount) Refresh(dirs []string) error {
|
||||
|
||||
if !m.mounted.Load() || m.vfs == nil {
|
||||
return fmt.Errorf("provider %s not properly mounted", m.Provider)
|
||||
}
|
||||
// Forget the directories first
|
||||
if err := m.ForgetVFS(dirs); err != nil {
|
||||
return fmt.Errorf("failed to forget VFS directories for %s: %w", m.Provider, err)
|
||||
}
|
||||
//Then refresh the directories
|
||||
if err := m.RefreshVFS(dirs); err != nil {
|
||||
return fmt.Errorf("failed to refresh VFS directories for %s: %w", m.Provider, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mount) RefreshVFS(dirs []string) error {
|
||||
|
||||
root, err := m.vfs.Root()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get VFS root for %s: %w", m.Provider, err)
|
||||
}
|
||||
|
||||
getDir := func(path string) (*vfs.Dir, error) {
|
||||
path = strings.Trim(path, "/")
|
||||
segments := strings.Split(path, "/")
|
||||
var node vfs.Node = root
|
||||
for _, s := range segments {
|
||||
if dir, ok := node.(*vfs.Dir); ok {
|
||||
node, err = dir.Stat(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if dir, ok := node.(*vfs.Dir); ok {
|
||||
return dir, nil
|
||||
}
|
||||
return nil, vfs.EINVAL
|
||||
}
|
||||
|
||||
// If no specific directories provided, refresh root
|
||||
if len(dirs) == 0 {
|
||||
|
||||
if _, err := root.ReadDirAll(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(dirs) == 1 {
|
||||
vfsDir, err := getDir(dirs[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find directory '%s' for refresh in %s: %w", dirs[0], m.Provider, err)
|
||||
}
|
||||
if _, err := vfsDir.ReadDirAll(); err != nil {
|
||||
return fmt.Errorf("failed to refresh directory '%s' in %s: %w", dirs[0], m.Provider, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var errs []error
|
||||
// Refresh specific directories
|
||||
for _, dir := range dirs {
|
||||
if dir != "" {
|
||||
// Clean the directory path
|
||||
vfsDir, err := getDir(dir)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to find directory '%s' for refresh in %s: %w", dir, m.Provider, err))
|
||||
}
|
||||
if _, err := vfsDir.ReadDirAll(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("failed to refresh directory '%s' in %s: %w", dir, m.Provider, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return errors.Join(errs...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Mount) ForgetVFS(dirs []string) error {
|
||||
// Get root directory
|
||||
root, err := m.vfs.Root()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get VFS root for %s: %w", m.Provider, err)
|
||||
}
|
||||
|
||||
// Forget specific directories
|
||||
for _, dir := range dirs {
|
||||
if dir != "" {
|
||||
// Clean the directory path
|
||||
dir = strings.Trim(dir, "/")
|
||||
// Forget the directory from cache
|
||||
root.ForgetPath(dir, fs.EntryDirectory)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -69,8 +69,10 @@ func Reset() {
|
||||
if instance.importsQueue != nil {
|
||||
instance.importsQueue.Close()
|
||||
}
|
||||
|
||||
close(instance.downloadSemaphore)
|
||||
if instance.downloadSemaphore != nil {
|
||||
// Close the semaphore channel to
|
||||
close(instance.downloadSemaphore)
|
||||
}
|
||||
}
|
||||
once = sync.Once{}
|
||||
instance = nil
|
||||
|
||||
@@ -247,6 +247,7 @@ func (wb *Web) handleUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// Update Repair config
|
||||
currentConfig.Repair = updatedConfig.Repair
|
||||
currentConfig.Rclone = updatedConfig.Rclone
|
||||
|
||||
// Update Debrids
|
||||
if len(updatedConfig.Debrids) > 0 {
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -84,6 +84,9 @@ class ConfigManager {
|
||||
|
||||
// Load repair config
|
||||
this.populateRepairSettings(config.repair);
|
||||
|
||||
// Load rclone config
|
||||
this.populateRcloneSettings(config.rclone);
|
||||
}
|
||||
|
||||
populateGeneralSettings(config) {
|
||||
@@ -139,6 +142,28 @@ class ConfigManager {
|
||||
});
|
||||
}
|
||||
|
||||
populateRcloneSettings(rcloneConfig) {
|
||||
if (!rcloneConfig) return;
|
||||
|
||||
const fields = [
|
||||
'enabled', 'mount_path', 'cache_dir', 'vfs_cache_mode', 'vfs_cache_max_size', 'vfs_cache_max_age',
|
||||
'vfs_cache_poll_interval', 'vfs_read_chunk_size', 'vfs_read_chunk_size_limit', 'buffer_size',
|
||||
'uid', 'gid', 'vfs_read_ahead', 'attr_timeout', 'dir_cache_time', 'poll_interval',
|
||||
'no_modtime', 'no_checksum'
|
||||
];
|
||||
|
||||
fields.forEach(field => {
|
||||
const element = document.querySelector(`[name="rclone.${field}"]`);
|
||||
if (element && rcloneConfig[field] !== undefined) {
|
||||
if (element.type === 'checkbox') {
|
||||
element.checked = rcloneConfig[field];
|
||||
} else {
|
||||
element.value = rcloneConfig[field];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
addDebridConfig(data = {}) {
|
||||
const debridHtml = this.getDebridTemplate(this.debridCount, data);
|
||||
this.refs.debridConfigs.insertAdjacentHTML('beforeend', debridHtml);
|
||||
@@ -262,6 +287,7 @@ class ConfigManager {
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="form-control">
|
||||
<label class="label" for="debrid[${index}].folder">
|
||||
<span class="label-text font-medium">Mount/Rclone Folder</span>
|
||||
@@ -273,7 +299,6 @@ class ConfigManager {
|
||||
<span class="label-text-alt">Path where debrid files are mounted</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="debrid[${index}].rate_limit">
|
||||
<span class="label-text font-medium">Rate Limit</span>
|
||||
@@ -285,6 +310,19 @@ class ConfigManager {
|
||||
<span class="label-text-alt">API rate limit for this service</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="debrid[${index}].proxy">
|
||||
<span class="label-text font-medium">Proxy</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered"
|
||||
name="debrid[${index}].proxy" id="debrid[${index}].proxy"
|
||||
placeholder="socks4, socks5, https proxy">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">This proxy is used for this debrid account</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -997,6 +1035,10 @@ class ConfigManager {
|
||||
}
|
||||
}
|
||||
|
||||
if (config.rclone.enabled && config.rclone.mount_path === '') {
|
||||
errors.push('Rclone mount path is required when Rclone is enabled');
|
||||
}
|
||||
|
||||
return {
|
||||
valid: errors.length === 0,
|
||||
errors
|
||||
@@ -1036,7 +1078,10 @@ class ConfigManager {
|
||||
arrs: this.collectArrConfigs(),
|
||||
|
||||
// Repair configuration
|
||||
repair: this.collectRepairConfig()
|
||||
repair: this.collectRepairConfig(),
|
||||
|
||||
// Rclone configuration
|
||||
rclone: this.collectRcloneConfig()
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1052,6 +1097,7 @@ class ConfigManager {
|
||||
api_key: document.querySelector(`[name="debrid[${i}].api_key"]`).value,
|
||||
folder: document.querySelector(`[name="debrid[${i}].folder"]`).value,
|
||||
rate_limit: document.querySelector(`[name="debrid[${i}].rate_limit"]`).value,
|
||||
proxy: document.querySelector(`[name="debrid[${i}].proxy"]`).value,
|
||||
download_uncached: document.querySelector(`[name="debrid[${i}].download_uncached"]`).checked,
|
||||
unpack_rar: document.querySelector(`[name="debrid[${i}].unpack_rar"]`).checked,
|
||||
add_samples: document.querySelector(`[name="debrid[${i}].add_samples"]`).checked,
|
||||
@@ -1162,6 +1208,42 @@ class ConfigManager {
|
||||
};
|
||||
}
|
||||
|
||||
collectRcloneConfig() {
|
||||
const getElementValue = (name, defaultValue = '') => {
|
||||
const element = document.querySelector(`[name="rclone.${name}"]`);
|
||||
if (!element) return defaultValue;
|
||||
|
||||
if (element.type === 'checkbox') {
|
||||
return element.checked;
|
||||
} else if (element.type === 'number') {
|
||||
const val = parseInt(element.value);
|
||||
return isNaN(val) ? 0 : val;
|
||||
} else {
|
||||
return element.value || defaultValue;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
enabled: getElementValue('enabled', false),
|
||||
mount_path: getElementValue('mount_path'),
|
||||
buffer_size: getElementValue('buffer_size'),
|
||||
cache_dir: getElementValue('cache_dir'),
|
||||
vfs_cache_mode: getElementValue('vfs_cache_mode', 'off'),
|
||||
vfs_cache_max_age: getElementValue('vfs_cache_max_age', '1h'),
|
||||
vfs_cache_max_size: getElementValue('vfs_cache_max_size'),
|
||||
vfs_cache_poll_interval: getElementValue('vfs_cache_poll_interval', '1m'),
|
||||
vfs_read_chunk_size: getElementValue('vfs_read_chunk_size', '128M'),
|
||||
vfs_read_chunk_size_limit: getElementValue('vfs_read_chunk_size_limit', 'off'),
|
||||
uid: getElementValue('uid', 0),
|
||||
gid: getElementValue('gid', 0),
|
||||
vfs_read_ahead: getElementValue('vfs_read_ahead', '128k'),
|
||||
attr_timeout: getElementValue('attr_timeout', '1s'),
|
||||
dir_cache_time: getElementValue('dir_cache_time', '5m'),
|
||||
no_modtime: getElementValue('no_modtime', false),
|
||||
no_checksum: getElementValue('no_checksum', false),
|
||||
};
|
||||
}
|
||||
|
||||
setupMagnetHandler() {
|
||||
window.registerMagnetLinkHandler = () => {
|
||||
if ('registerProtocolHandler' in navigator) {
|
||||
@@ -1198,4 +1280,6 @@ class ConfigManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -28,6 +28,10 @@
|
||||
<i class="bi bi-wrench text-lg"></i>
|
||||
<span class="hidden sm:inline">Repair</span>
|
||||
</button>
|
||||
<button type="button" class="tab-button flex items-center gap-2 py-3 px-1 border-b-2 border-transparent text-base-content/70 hover:text-base-content hover:border-base-300 font-medium text-sm transition-colors" data-tab="rclone">
|
||||
<i class="bi bi-hdd-stack text-lg"></i>
|
||||
<span class="hidden sm:inline">Rclone</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
@@ -328,6 +332,198 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Rclone Tab Content -->
|
||||
<div class="tab-content hidden" data-tab-content="rclone">
|
||||
<div class="space-y-6">
|
||||
<h2 class="text-2xl font-bold flex items-center mb-6">
|
||||
<i class="bi bi-hdd-stack mr-3 text-info"></i>Rclone Mount Settings
|
||||
</h2>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox checkbox-lg" name="rclone.enabled" id="rclone.enabled">
|
||||
<div>
|
||||
<span class="label-text font-medium text-lg">Enable Mount</span>
|
||||
<div class="label-text-alt">Automatically mount your debrid items</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<!-- Mount Path Section -->
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
||||
<i class="bi bi-folder mr-2"></i>Mount Configuration
|
||||
</h3>
|
||||
<div class="grid grid-cols-3 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.mount_path">
|
||||
<span class="label-text font-medium">Global Mount Path</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.mount_path" id="rclone.mount_path" placeholder="/mnt/decypharr">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Base directory where all providers will be mounted (e.g., /mnt/decypharr/realdebrid)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.uid">
|
||||
<span class="label-text font-medium">User ID (PUID)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered" name="rclone.uid" id="rclone.uid" placeholder="1000" min="0">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">User ID for mounted files (0 = current user)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.gid">
|
||||
<span class="label-text font-medium">Group ID (PGID)</span>
|
||||
</label>
|
||||
<input type="number" class="input input-bordered" name="rclone.gid" id="rclone.gid" placeholder="1000" min="0">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Group ID for mounted files (0 = current group)</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.buffer_size">
|
||||
<span class="label-text font-medium">Buffer Size</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.buffer_size" id="rclone.buffer_size" placeholder="10M" min="0">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Buffer Size(This caches to memory, be wary!!)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
||||
<i class="bi bi-speedometer2 mr-2"></i>VFS Cache Settings
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.cache_dir">
|
||||
<span class="label-text font-medium">Cache Directory</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.cache_dir" id="rclone.cache_dir" placeholder="/tmp/rclone">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Directory for rclone cache files (leave empty for system default)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.vfs_cache_mode">
|
||||
<span class="label-text font-medium">VFS Cache Mode</span>
|
||||
</label>
|
||||
<select class="select select-bordered" name="rclone.vfs_cache_mode" id="rclone.vfs_cache_mode">
|
||||
<option value="off">Off - No caching</option>
|
||||
<option value="minimal">Minimal - Cache file structure only</option>
|
||||
<option value="writes">Writes - Cache writes for better performance</option>
|
||||
<option value="full">Full - Cache reads and writes</option>
|
||||
</select>
|
||||
<div class="label">
|
||||
<span class="label-text-alt">VFS caching mode for performance optimization</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.vfs_cache_max_size">
|
||||
<span class="label-text font-medium">VFS Cache Max Size</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.vfs_cache_max_size" id="rclone.vfs_cache_max_size" placeholder="1G">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Maximum cache size (e.g., 1G, 500M, leave empty for unlimited)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.vfs_cache_max_age">
|
||||
<span class="label-text font-medium">VFS Cache Max Age</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.vfs_cache_max_age" id="rclone.vfs_cache_max_age" placeholder="1h">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Maximum age of cache entries (e.g., 1h, 30m)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.vfs_read_chunk_size">
|
||||
<span class="label-text font-medium">Read Chunk Size</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.vfs_read_chunk_size" id="rclone.vfs_read_chunk_size" placeholder="128M">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Size of data chunks to read (e.g., 128M, 64M)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.vfs_read_chunk_size_limit">
|
||||
<span class="label-text font-medium">Read Chunk Size Limit</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.vfs_read_chunk_size_limit" id="rclone.vfs_read_chunk_size_limit" placeholder="128M">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Limit Read Chunk Size</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.vfs_read_ahead">
|
||||
<span class="label-text font-medium">Read Ahead</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.vfs_read_ahead" id="rclone.vfs_read_ahead" placeholder="128k">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">Read ahead buffer size (e.g., 128k, 256k)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label" for="rclone.dir_cache_time">
|
||||
<span class="label-text font-medium">Directory Cache Time</span>
|
||||
</label>
|
||||
<input type="text" class="input input-bordered" name="rclone.dir_cache_time" id="rclone.dir_cache_time" placeholder="5m">
|
||||
<div class="label">
|
||||
<span class="label-text-alt">How long to cache directory listings (e.g., 5m, 10m)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Advanced Settings Section -->
|
||||
<div class="card bg-base-200">
|
||||
<div class="card-body">
|
||||
<h3 class="text-lg font-semibold mb-4 flex items-center">
|
||||
<i class="bi bi-gear mr-2"></i>Advanced Settings
|
||||
</h3>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox" name="rclone.no_modtime" id="rclone.no_modtime">
|
||||
<div>
|
||||
<span class="label-text font-medium">No Modification Time</span>
|
||||
<div class="label-text-alt">Don't read/write modification times</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label class="label cursor-pointer justify-start gap-3">
|
||||
<input type="checkbox" class="checkbox" name="rclone.no_checksum" id="rclone.no_checksum">
|
||||
<div>
|
||||
<span class="label-text font-medium">No Checksum</span>
|
||||
<div class="label-text-alt">Don't checksum files on upload</div>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- End tab-content-container -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,6 @@
|
||||
<body class="min-h-screen bg-base-200 flex flex-col">
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container fixed bottom-4 right-4 z-50 space-y-2">
|
||||
<!-- Toast messages will be created dynamically here -->
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
@@ -72,7 +71,7 @@
|
||||
<a class="btn btn-ghost text-xl font-bold text-primary group" href="{{.URLBase}}">
|
||||
<!-- Logo -->
|
||||
<img src="{{.URLBase}}images/logo.svg" alt="Decypharr Logo" class="w-8 h-8 inline-block mr-2">
|
||||
<span class="hidden sm:inline bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent">Decypharr</span>
|
||||
<span class="hidden sm:inline bg-clip-text text-transparent">Decypharr</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -90,7 +90,9 @@ type WebDav struct {
|
||||
}
|
||||
|
||||
func New() *WebDav {
|
||||
urlBase := config.Get().URLBase
|
||||
cfg := config.Get()
|
||||
urlBase := cfg.URLBase
|
||||
|
||||
w := &WebDav{
|
||||
Handlers: make([]*Handler, 0),
|
||||
URLBase: urlBase,
|
||||
@@ -155,7 +157,7 @@ func (wd *WebDav) mountHandlers(r chi.Router) {
|
||||
r.Route("/"+h.Name, func(r chi.Router) {
|
||||
r.Use(h.readinessMiddleware)
|
||||
r.Mount("/", h)
|
||||
}) // Mount to /name since router is already prefixed with /webdav
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user