284 lines
6.9 KiB
Go
284 lines
6.9 KiB
Go
package rclone
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"fmt"
|
|
"github.com/rs/zerolog"
|
|
"github.com/sirrobot01/debrid-blackhole/internal/config"
|
|
"github.com/sirrobot01/debrid-blackhole/internal/logger"
|
|
"github.com/sirrobot01/debrid-blackhole/pkg/webdav"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type Remote struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Url string `json:"url"`
|
|
MountPoint string `json:"mount_point"`
|
|
Flags map[string]string `json:"flags"`
|
|
}
|
|
|
|
func (rc *Rclone) Config() string {
|
|
var content string
|
|
|
|
for _, remote := range rc.Remotes {
|
|
content += fmt.Sprintf("[%s]\n", remote.Name)
|
|
content += fmt.Sprintf("type = %s\n", remote.Type)
|
|
content += fmt.Sprintf("url = %s\n", remote.Url)
|
|
content += fmt.Sprintf("vendor = other\n")
|
|
|
|
for key, value := range remote.Flags {
|
|
content += fmt.Sprintf("%s = %s\n", key, value)
|
|
}
|
|
content += "\n\n"
|
|
}
|
|
|
|
return content
|
|
}
|
|
|
|
type Rclone struct {
|
|
Remotes map[string]Remote `json:"remotes"`
|
|
logger zerolog.Logger
|
|
cmd *exec.Cmd
|
|
configPath string
|
|
}
|
|
|
|
func New(webdav *webdav.WebDav) (*Rclone, error) {
|
|
// Check if rclone is installed
|
|
cfg := config.GetConfig()
|
|
configPath := fmt.Sprintf("%s/rclone.conf", cfg.Path)
|
|
|
|
if _, err := exec.LookPath("rclone"); err != nil {
|
|
return nil, fmt.Errorf("rclone is not installed: %w", err)
|
|
}
|
|
remotes := make(map[string]Remote)
|
|
for _, handler := range webdav.Handlers {
|
|
url := fmt.Sprintf("http://localhost:%s/webdav/%s/", cfg.QBitTorrent.Port, strings.ToLower(handler.Name))
|
|
rmt := Remote{
|
|
Type: "webdav",
|
|
Name: handler.Name,
|
|
Url: url,
|
|
MountPoint: filepath.Join("/mnt/rclone/", handler.Name),
|
|
Flags: map[string]string{},
|
|
}
|
|
remotes[handler.Name] = rmt
|
|
}
|
|
|
|
rc := &Rclone{
|
|
logger: logger.NewLogger("rclone", "info", os.Stdout),
|
|
Remotes: remotes,
|
|
configPath: configPath,
|
|
}
|
|
if err := rc.WriteConfig(); err != nil {
|
|
return nil, err
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
func (rc *Rclone) WriteConfig() error {
|
|
|
|
// Create config directory if it doesn't exist
|
|
configDir := filepath.Dir(rc.configPath)
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create config directory: %w", err)
|
|
}
|
|
|
|
// Write the config file
|
|
if err := os.WriteFile(rc.configPath, []byte(rc.Config()), 0600); err != nil {
|
|
return fmt.Errorf("failed to write config file: %w", err)
|
|
}
|
|
|
|
rc.logger.Info().Msgf("Wrote rclone config with %d remotes to %s", len(rc.Remotes), rc.configPath)
|
|
return nil
|
|
}
|
|
|
|
func (rc *Rclone) Start(ctx context.Context) error {
|
|
var wg sync.WaitGroup
|
|
errChan := make(chan error)
|
|
for _, remote := range rc.Remotes {
|
|
wg.Add(1)
|
|
go func(remote Remote) {
|
|
defer wg.Done()
|
|
if err := rc.Mount(ctx, &remote); err != nil {
|
|
rc.logger.Error().Err(err).Msgf("failed to mount %s", remote.Name)
|
|
select {
|
|
case errChan <- err:
|
|
default:
|
|
}
|
|
}
|
|
}(remote)
|
|
}
|
|
return <-errChan
|
|
}
|
|
|
|
func (rc *Rclone) testConnection(ctx context.Context, remote *Remote) error {
|
|
testArgs := []string{
|
|
"ls",
|
|
"--config", rc.configPath,
|
|
"--log-level", "DEBUG",
|
|
remote.Name + ":",
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, "rclone", testArgs...)
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
rc.logger.Error().Err(err).Str("output", string(output)).Msg("Connection test failed")
|
|
return fmt.Errorf("connection test failed: %w", err)
|
|
}
|
|
|
|
rc.logger.Info().Msg("Connection test successful")
|
|
return nil
|
|
}
|
|
|
|
func (rc *Rclone) Mount(ctx context.Context, remote *Remote) error {
|
|
// Ensure the mount point directory exists
|
|
if err := os.MkdirAll(remote.MountPoint, 0755); err != nil {
|
|
rc.logger.Info().Err(err).Msgf("failed to create mount point directory: %s", remote.MountPoint)
|
|
return err
|
|
}
|
|
|
|
//if err := rc.testConnection(ctx, remote); err != nil {
|
|
// return err
|
|
//}
|
|
|
|
// Basic arguments
|
|
args := []string{
|
|
"mount",
|
|
remote.Name + ":",
|
|
remote.MountPoint,
|
|
"--config", rc.configPath,
|
|
"--vfs-cache-mode", "full",
|
|
"--log-level", "DEBUG", // Keep this, remove -vv
|
|
"--allow-other", // Keep this
|
|
"--allow-root", // Add this
|
|
"--default-permissions", // Add this
|
|
"--vfs-cache-max-age", "24h",
|
|
"--timeout", "1m",
|
|
"--transfers", "4",
|
|
"--buffer-size", "32M",
|
|
}
|
|
|
|
// Add any additional flags
|
|
for key, value := range remote.Flags {
|
|
args = append(args, "--"+key, value)
|
|
}
|
|
|
|
// Create command
|
|
rc.cmd = exec.CommandContext(ctx, "rclone", args...)
|
|
|
|
// Set up pipes for stdout and stderr
|
|
stdout, err := rc.cmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
stderr, err := rc.cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start the command
|
|
if err := rc.cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Channel to signal mount success
|
|
mountReady := make(chan bool)
|
|
mountError := make(chan error)
|
|
|
|
// Monitor stdout
|
|
go func() {
|
|
scanner := bufio.NewScanner(stdout)
|
|
for scanner.Scan() {
|
|
text := scanner.Text()
|
|
rc.logger.Info().Msg("stdout: " + text)
|
|
if strings.Contains(text, "Mount succeeded") {
|
|
mountReady <- true
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Monitor stderr
|
|
go func() {
|
|
scanner := bufio.NewScanner(stderr)
|
|
for scanner.Scan() {
|
|
text := scanner.Text()
|
|
rc.logger.Info().Msg("stderr: " + text)
|
|
if strings.Contains(text, "error") {
|
|
mountError <- fmt.Errorf("mount error: %s", text)
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Wait for mount with timeout
|
|
select {
|
|
case <-mountReady:
|
|
rc.logger.Info().Msgf("Successfully mounted %s at %s", remote.Name, remote.MountPoint)
|
|
return nil
|
|
case err := <-mountError:
|
|
err = rc.cmd.Process.Kill()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return err
|
|
case <-ctx.Done():
|
|
err := rc.cmd.Process.Kill()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return ctx.Err()
|
|
case <-time.After(30 * time.Second):
|
|
err := rc.cmd.Process.Kill()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return fmt.Errorf("mount timeout after 30 seconds")
|
|
}
|
|
}
|
|
|
|
func (rc *Rclone) Unmount(ctx context.Context, remote *Remote) error {
|
|
if rc.cmd != nil && rc.cmd.Process != nil {
|
|
// First try graceful shutdown
|
|
if err := rc.cmd.Process.Signal(os.Interrupt); err != nil {
|
|
rc.logger.Warn().Err(err).Msg("failed to send interrupt signal")
|
|
}
|
|
|
|
// Wait for a bit to allow graceful shutdown
|
|
done := make(chan error)
|
|
go func() {
|
|
done <- rc.cmd.Wait()
|
|
}()
|
|
|
|
select {
|
|
case err := <-done:
|
|
if err != nil {
|
|
rc.logger.Warn().Err(err).Msg("process exited with error")
|
|
}
|
|
case <-time.After(5 * time.Second):
|
|
// Force kill if it doesn't shut down gracefully
|
|
if err := rc.cmd.Process.Kill(); err != nil {
|
|
rc.logger.Error().Err(err).Msg("failed to kill process")
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use fusermount to ensure the mountpoint is unmounted
|
|
cmd := exec.CommandContext(ctx, "fusermount", "-u", remote.MountPoint)
|
|
if err := cmd.Run(); err != nil {
|
|
rc.logger.Warn().Err(err).Msg("fusermount unmount failed")
|
|
// Don't return error here as the process might already be dead
|
|
}
|
|
|
|
rc.logger.Info().Msgf("Successfully unmounted %s", remote.MountPoint)
|
|
return nil
|
|
}
|