From 75f8098f4c2566914a297e57c8121a3c42b960f0 Mon Sep 17 00:00:00 2001 From: pettytreebroker Date: Mon, 4 Sep 2023 22:02:49 +0000 Subject: [PATCH] Add SymlinkDownloader --- server/RdtClient.Data/Enums/DownloadClient.cs | 5 +- .../Models/Internal/DbSettings.cs | 4 + .../Services/DownloadClient.cs | 2 + .../Services/Downloaders/SymlinkDownloader.cs | 126 ++++++++++++++++++ 4 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs diff --git a/server/RdtClient.Data/Enums/DownloadClient.cs b/server/RdtClient.Data/Enums/DownloadClient.cs index a4bfa42..dcdd14e 100644 --- a/server/RdtClient.Data/Enums/DownloadClient.cs +++ b/server/RdtClient.Data/Enums/DownloadClient.cs @@ -8,5 +8,8 @@ public enum DownloadClient Internal, [Description("Aria2c")] - Aria2c + Aria2c, + + [Description("Symlink Downloader")] + Symlink, } \ No newline at end of file diff --git a/server/RdtClient.Data/Models/Internal/DbSettings.cs b/server/RdtClient.Data/Models/Internal/DbSettings.cs index 24ae0a2..09d9360 100644 --- a/server/RdtClient.Data/Models/Internal/DbSettings.cs +++ b/server/RdtClient.Data/Models/Internal/DbSettings.cs @@ -111,6 +111,10 @@ http://127.0.0.1:6800/jsonrpc.")] [DisplayName("Aria2c Secret (only used for the Aria2c Downloader)")] [Description("The secret of your Aria2c instance. Optional.")] public String Aria2cSecret { get; set; } = "mysecret123"; + + [DisplayName("Rclone mount path (only used for the Symlink Downloader)")] + [Description("Path where Rclone is mounted. Required for Symlink Downloader.")] + public String RcloneMountPath { get; set; } = "/mnt/rd/"; } public class DbSettingsProvider diff --git a/server/RdtClient.Service/Services/DownloadClient.cs b/server/RdtClient.Service/Services/DownloadClient.cs index 4747065..f50b4d5 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -44,6 +44,7 @@ public class DownloadClient } var filePath = DownloadHelper.GetDownloadPath(_destinationPath, _torrent, _download); + var rcloneMountPath = Settings.Get.DownloadClient.RcloneMountPath; if (filePath == null) { @@ -58,6 +59,7 @@ public class DownloadClient { Data.Enums.DownloadClient.Internal => new InternalDownloader(_download.Link, filePath), Data.Enums.DownloadClient.Aria2c => new Aria2cDownloader(_download.RemoteId, _download.Link, filePath), + Data.Enums.DownloadClient.Symlink => new SymlinkDownloader(_download.Link, filePath, rcloneMountPath), _ => throw new Exception($"Unknown download client {Settings.Get.DownloadClient}") }; diff --git a/server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs b/server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs new file mode 100644 index 0000000..d65292e --- /dev/null +++ b/server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading.Tasks; +using Serilog; + +namespace RdtClient.Service.Services.Downloaders +{ + public class SymlinkDownloader : IDownloader + { + public event EventHandler? DownloadComplete; + public event EventHandler? DownloadProgress; + + private const Int32 RetryCount = 5; + private readonly string _filePath; + private readonly string _uri; + private readonly string _rcloneMountPath; + private readonly ILogger _logger; + private readonly CancellationTokenSource _cancellationToken = new(); + private bool _completed; + + public SymlinkDownloader(string uri, string filePath, string rcloneMountPath) + { + _logger = Log.ForContext(); + + _uri = uri; + _filePath = filePath; + _rcloneMountPath = rcloneMountPath; + } + + public async Task Download() + { + _logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}"); + + string fileName = Path.GetFileName(_filePath); + _completed = false; + + // Sometimes the rclone mount doesn't immediately reflect the new files, + // so try for up to 5 minutes (10 attempts, 30 seconds between attempts). + // Would be better to fail properly and have RDT auto retry, but unsure how to do that. + + var retryCount = 0; + while (retryCount < RetryCount && !_completed) + { + _logger.Debug($"(Attempt {retryCount}/{RetryCount}) Searching {_rcloneMountPath} for {fileName}"); + // Recursively search for the fileName in rclone mount location + string[] foundFiles = Directory.GetFiles(_rcloneMountPath, fileName, SearchOption.AllDirectories); + + if (foundFiles.Length > 0) + { + if (foundFiles.Length > 1) + { + _logger.Warning($"Found {foundFiles.Length} files named {fileName}"); + } + // Assume first matching filename is the one we want + string actualFilePath = foundFiles[0]; + + bool result = TryCreateSymbolicLink(actualFilePath, _filePath).Result; + if (result) + { + _completed = true; + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs{}); + return actualFilePath; + } + } + + await Task.Delay(TimeSpan.FromSeconds(30), _cancellationToken.Token); + + retryCount++; + } + + _logger.Error($"File '{fileName}' not found after {RetryCount} attempts."); + return null; + } + + private Task TryCreateSymbolicLink(string sourcePath, string symlinkPath) + { + try + { + var process = new Process(); + process.StartInfo.FileName = "ln"; + process.StartInfo.Arguments = $"-s \"{sourcePath}\" \"{symlinkPath}\""; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardError = true; + + process.Start(); + process.WaitForExit(); + + if (process.ExitCode == 0) + { + _logger.Information($"Created symbolic link from {sourcePath} to {symlinkPath}"); + return Task.FromResult(true); + } + else + { + _logger.Error($"Failed to create symbolic link: {process.ExitCode}"); + return Task.FromResult(false); + } + } + catch (Exception ex) + { + _logger.Error($"Error creating symbolic link from {sourcePath} to {symlinkPath}: {ex.Message}"); + return Task.FromResult(false); + } + } + + public Task Cancel() + { + _logger.Debug($"Cancelling download {_uri}"); + + _cancellationToken.Cancel(false); + + return Task.CompletedTask; + } + + public Task Pause() + { + return Task.CompletedTask; + } + + public Task Resume() + { + return Task.CompletedTask; + } + } +}