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..ddfd144 100644 --- a/server/RdtClient.Service/Services/DownloadClient.cs +++ b/server/RdtClient.Service/Services/DownloadClient.cs @@ -58,6 +58,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), _ => 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..5bf3830 --- /dev/null +++ b/server/RdtClient.Service/Services/Downloaders/SymlinkDownloader.cs @@ -0,0 +1,138 @@ +using System.Diagnostics; +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 CancellationTokenSource _cancellationToken = new(); + + private readonly ILogger _logger; + + public SymlinkDownloader(String uri, String filePath) + { + _logger = Log.ForContext(); + + _uri = uri; + _filePath = filePath; + } + + public async Task Download() + { + _logger.Debug($"Starting download of {_uri}, writing to path: {_filePath}"); + + var fileName = Path.GetFileName(_filePath); + + DownloadProgress?.Invoke(this, new DownloadProgressEventArgs + { + BytesDone = 0, + BytesTotal = 0, + Speed = 0 + }); + + var retryCount = 1; + + while (retryCount < RetryCount) + { + _logger.Debug($"Searching {Settings.Get.DownloadClient.RcloneMountPath} for {fileName} ({retryCount}/{RetryCount}) "); + + // Recursively search for the fileName in the rclone mount location. + var foundFiles = Directory.GetFiles(Settings.Get.DownloadClient.RcloneMountPath, fileName, SearchOption.AllDirectories).ToList(); + + if (foundFiles.Any()) + { + if (foundFiles.Count > 1) + { + _logger.Warning($"Found {foundFiles.Count} files named {fileName}"); + } + + // Assume first matching filename is the one we want. + var actualFilePath = foundFiles.First(); + + var result = TryCreateSymbolicLink(actualFilePath, _filePath); + + if (result) + { + 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."); + + DownloadComplete?.Invoke(this, new DownloadCompleteEventArgs + { + Error = $"File '{fileName}' not found after {RetryCount} attempts." + }); + + return null; + } + + 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; + } + + private Boolean 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(); + + var errors = process.StandardError.ReadToEnd(); + + process.WaitForExit(); + + if (process.ExitCode == 0) + { + _logger.Information($"Created symbolic link from {sourcePath} to {symlinkPath}"); + + return true; + } + + _logger.Error($"Failed to create symbolic link: {process.ExitCode} - {errors}"); + + return false; + } + catch (Exception ex) + { + _logger.Error($"Error creating symbolic link from {sourcePath} to {symlinkPath}: {ex.Message}"); + + return false; + } + } +}