From 1dd7ab9c8efa668f9486bca3357f0b3f11892fac Mon Sep 17 00:00:00 2001 From: Alexandre Vassard Date: Sun, 8 Mar 2026 21:15:34 +0100 Subject: [PATCH 1/2] fix(symlink): strip duplicate torrent name from subpath torrent.Files[].Path already includes the torrent name as its first component (e.g. "Hijack/Saison 1/file.mkv"), but GetDownloadPath was also initialising torrentPath with torrent.RdName, causing the name to appear twice (e.g. "Hijack/Hijack/Saison 1"). Strip the torrent name prefix from subPath before combining so the resolved path matches the actual rclone mount layout. --- .../Helpers/DownloadHelper.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/server/RdtClient.Service/Helpers/DownloadHelper.cs b/server/RdtClient.Service/Helpers/DownloadHelper.cs index 36c20cd..39f8635 100644 --- a/server/RdtClient.Service/Helpers/DownloadHelper.cs +++ b/server/RdtClient.Service/Helpers/DownloadHelper.cs @@ -38,7 +38,12 @@ public static class DownloadHelper { subPath = subPath.Trim('/').Trim('\\'); - torrentPath = Path.Combine(torrentPath, subPath); + subPath = StripTorrentNamePrefix(subPath, directory); + + if (!String.IsNullOrWhiteSpace(subPath)) + { + torrentPath = Path.Combine(torrentPath, subPath); + } } } @@ -87,7 +92,12 @@ public static class DownloadHelper { subPath = subPath.Trim('/').Trim('\\'); - torrentPath = Path.Combine(torrentPath, subPath); + subPath = StripTorrentNamePrefix(subPath, torrentPath); + + if (!String.IsNullOrWhiteSpace(subPath)) + { + torrentPath = Path.Combine(torrentPath, subPath); + } } } @@ -117,4 +127,22 @@ public static class DownloadHelper { return String.Concat(path.Split(Path.GetInvalidPathChars())); } + + private static String StripTorrentNamePrefix(String subPath, String torrentName) + { + var prefix = torrentName.TrimEnd('/', '\\'); + + if (subPath.Equals(prefix, StringComparison.OrdinalIgnoreCase)) + { + return String.Empty; + } + + if (subPath.StartsWith(prefix + "/", StringComparison.OrdinalIgnoreCase) || + subPath.StartsWith(prefix + "\\", StringComparison.OrdinalIgnoreCase)) + { + return subPath[(prefix.Length + 1)..]; + } + + return subPath; + } } From 6743bd472e670232e94415e14f5057659a2b8fd6 Mon Sep 17 00:00:00 2001 From: Alexandre Vassard Date: Sun, 8 Mar 2026 21:29:33 +0100 Subject: [PATCH 2/2] fix(symlink): normalize subpath first component before comparing torrent name prefix The previous fix compared subPath directly against the sanitized torrent name, which fails when RdName contains characters removed by RemoveInvalidPathChars (e.g. ':' on Windows). Extract and normalize the first component of subPath before comparing so both sides go through the same sanitization. Add tests covering the duplicate-prefix stripping case. --- .../Helpers/DownloadHelperTest.cs | 89 +++++++++++++++++++ .../Helpers/DownloadHelper.cs | 22 +++-- 2 files changed, 105 insertions(+), 6 deletions(-) diff --git a/server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs b/server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs index 1fac48f..90ba064 100644 --- a/server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs +++ b/server/RdtClient.Service.Test/Helpers/DownloadHelperTest.cs @@ -259,6 +259,95 @@ public class DownloadHelperTest Assert.Equal(expectedPath, path); } + [Fact] + public void GetDownloadPath_WithPath_WhenFilePathStartsWithTorrentName_StripsPrefix() + { + // Arrange + var download = new Download + { + Link = "https://fake.url/file.txt", + FileName = "file.txt" + }; + + String fileRelativePath; + + if (OSHelper.IsLinux) + { + fileRelativePath = "Torrent Name/Saison 1/file.txt"; + } + else + { + fileRelativePath = @"Torrent Name\Saison 1\file.txt"; + } + + IList files = + [ + new() + { + Path = fileRelativePath + } + ]; + + var torrent = new Torrent + { + RdName = "Torrent Name", + RdFiles = JsonSerializer.Serialize(files) + }; + + var fileSystem = new MockFileSystem(); + + // Act + var path = DownloadHelper.GetDownloadPath("/data/downloads", torrent, download, fileSystem); + + // Assert + // The torrent name prefix in the file path should not duplicate the torrent name in the base dir + var expectedPath = Path.Combine("/data/downloads", "Torrent Name", "Saison 1", "file.txt"); + Assert.Equal(expectedPath, path); + } + + [Fact] + public void GetDownloadPath_WithoutPath_WhenFilePathStartsWithTorrentName_StripsPrefix() + { + // Arrange + var download = new Download + { + Link = "https://fake.url/file.txt", + FileName = "file.txt" + }; + + String fileRelativePath; + + if (OSHelper.IsLinux) + { + fileRelativePath = "Torrent Name/Saison 1/file.txt"; + } + else + { + fileRelativePath = @"Torrent Name\Saison 1\file.txt"; + } + + IList files = + [ + new() + { + Path = fileRelativePath + } + ]; + + var torrent = new Torrent + { + RdName = "Torrent Name", + RdFiles = JsonSerializer.Serialize(files) + }; + + // Act + var path = DownloadHelper.GetDownloadPath(torrent, download); + + // Assert + var expectedPath = Path.Combine("Torrent Name", "Saison 1", "file.txt"); + Assert.Equal(expectedPath, path); + } + // This is probably a bug [Fact] public void GetDownloadPath_WithPath_WhenNoUriSegmentsOrFileName_ReturnsTorrentDirectory() diff --git a/server/RdtClient.Service/Helpers/DownloadHelper.cs b/server/RdtClient.Service/Helpers/DownloadHelper.cs index 39f8635..bc5b578 100644 --- a/server/RdtClient.Service/Helpers/DownloadHelper.cs +++ b/server/RdtClient.Service/Helpers/DownloadHelper.cs @@ -130,17 +130,27 @@ public static class DownloadHelper private static String StripTorrentNamePrefix(String subPath, String torrentName) { - var prefix = torrentName.TrimEnd('/', '\\'); + var separatorIndex = subPath.IndexOfAny(['/', '\\']); - if (subPath.Equals(prefix, StringComparison.OrdinalIgnoreCase)) + String firstComponent; + String remainder; + + if (separatorIndex < 0) { - return String.Empty; + firstComponent = subPath; + remainder = String.Empty; + } + else + { + firstComponent = subPath[..separatorIndex]; + remainder = subPath[(separatorIndex + 1)..]; } - if (subPath.StartsWith(prefix + "/", StringComparison.OrdinalIgnoreCase) || - subPath.StartsWith(prefix + "\\", StringComparison.OrdinalIgnoreCase)) + var normalizedFirst = RemoveInvalidPathChars(firstComponent); + + if (normalizedFirst.Equals(torrentName.TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase)) { - return subPath[(prefix.Length + 1)..]; + return remainder; } return subPath;