Downloads fail on Linux containers when filenames contain square
brackets or multiple consecutive spaces. On Linux, .NET's
Path.GetInvalidFileNameChars() only returns NUL and '/', so characters
like [ ] { } that cause issues with shell globbing, URI handling, and
various download clients pass through the existing filter unchanged.
Add FilenameSanitizer that:
- Strips square brackets, curly braces, and control characters
- Collapses multiple consecutive spaces into one
- Trims leading and trailing whitespace
- Preserves parentheses and all other characters
Apply sanitization at filesystem boundaries: DownloadHelper (where the
Real-Debrid filename is turned into a local path), UnpackClient,
Torrent delete / RunOnTorrentComplete, and the qBittorrent / SABnzbd
status paths that *arr reads back. Aria2c and DownloadStation also
sanitize their remote output paths so they match the sanitized local
filePath. Symlink's rclone-mount lookup still uses the original name
since that has to match the real file in the mount.
Add a SanitizeFilenames toggle in DbSettings (default on) so users
can disable the behaviour if they need the exact Real-Debrid name.
170 lines
4.8 KiB
C#
170 lines
4.8 KiB
C#
using System.IO.Abstractions;
|
|
using System.Web;
|
|
using RdtClient.Data.Models.Data;
|
|
|
|
namespace RdtClient.Service.Helpers;
|
|
|
|
public static class DownloadHelper
|
|
{
|
|
public static String? GetDownloadPath(String downloadPath, Torrent torrent, Download download, IFileSystem? fileSystem = null)
|
|
{
|
|
var fileUrl = download.Link;
|
|
|
|
if (String.IsNullOrWhiteSpace(fileUrl) || torrent.RdName == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var originalDirectory = RemoveInvalidPathChars(torrent.RdName);
|
|
var directory = FilenameSanitizer.SanitizeFilenameIfEnabled(originalDirectory);
|
|
|
|
var torrentPath = Path.Combine(downloadPath, directory);
|
|
|
|
var originalFileName = GetOriginalFileName(download);
|
|
|
|
if (originalFileName == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var matchingTorrentFiles = torrent.Files.Where(m => m.Path.EndsWith(originalFileName)).Where(m => !String.IsNullOrWhiteSpace(m.Path)).ToList();
|
|
|
|
if (matchingTorrentFiles.Count > 0)
|
|
{
|
|
var matchingTorrentFile = matchingTorrentFiles[0];
|
|
|
|
var subPath = Path.GetDirectoryName(matchingTorrentFile.Path);
|
|
|
|
if (!String.IsNullOrWhiteSpace(subPath))
|
|
{
|
|
subPath = subPath.Trim('/').Trim('\\');
|
|
|
|
subPath = StripTorrentNamePrefix(subPath, originalDirectory);
|
|
|
|
if (!String.IsNullOrWhiteSpace(subPath))
|
|
{
|
|
subPath = FilenameSanitizer.SanitizePathIfEnabled(subPath);
|
|
|
|
torrentPath = Path.Combine(torrentPath, subPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
fileSystem ??= new FileSystem();
|
|
|
|
if (!fileSystem.Directory.Exists(torrentPath))
|
|
{
|
|
fileSystem.Directory.CreateDirectory(torrentPath);
|
|
}
|
|
|
|
var fileName = FilenameSanitizer.SanitizeFilenameIfEnabled(originalFileName);
|
|
|
|
var filePath = Path.Combine(torrentPath, fileName);
|
|
|
|
return filePath;
|
|
}
|
|
|
|
public static String? GetDownloadPath(Torrent torrent, Download download)
|
|
{
|
|
var fileUrl = download.Link;
|
|
|
|
if (String.IsNullOrWhiteSpace(fileUrl) || torrent.RdName == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var uri = new Uri(fileUrl);
|
|
var torrentPath = RemoveInvalidPathChars(torrent.RdName);
|
|
|
|
var fileName = download.FileName;
|
|
|
|
if (String.IsNullOrWhiteSpace(fileName))
|
|
{
|
|
fileName = uri.Segments.Last();
|
|
|
|
fileName = HttpUtility.UrlDecode(fileName);
|
|
}
|
|
|
|
var matchingTorrentFiles = torrent.Files.Where(m => m.Path.EndsWith(fileName)).Where(m => !String.IsNullOrWhiteSpace(m.Path)).ToList();
|
|
|
|
if (matchingTorrentFiles.Count > 0)
|
|
{
|
|
var matchingTorrentFile = matchingTorrentFiles[0];
|
|
|
|
var subPath = Path.GetDirectoryName(matchingTorrentFile.Path);
|
|
|
|
if (!String.IsNullOrWhiteSpace(subPath))
|
|
{
|
|
subPath = subPath.Trim('/').Trim('\\');
|
|
|
|
subPath = StripTorrentNamePrefix(subPath, torrentPath);
|
|
|
|
if (!String.IsNullOrWhiteSpace(subPath))
|
|
{
|
|
torrentPath = Path.Combine(torrentPath, subPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
var filePath = Path.Combine(torrentPath, fileName);
|
|
|
|
return filePath;
|
|
}
|
|
|
|
public static String? GetFileName(Download download)
|
|
{
|
|
var cleaned = GetOriginalFileName(download);
|
|
|
|
return cleaned == null ? null : FilenameSanitizer.SanitizeFilenameIfEnabled(cleaned);
|
|
}
|
|
|
|
private static String? GetOriginalFileName(Download download)
|
|
{
|
|
if (String.IsNullOrWhiteSpace(download.Link))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var fileName = download.FileName;
|
|
|
|
if (String.IsNullOrWhiteSpace(fileName))
|
|
{
|
|
fileName = HttpUtility.UrlDecode(new Uri(download.Link).Segments.Last());
|
|
}
|
|
|
|
return FileHelper.RemoveInvalidFileNameChars(fileName);
|
|
}
|
|
|
|
public static String RemoveInvalidPathChars(String path)
|
|
{
|
|
return String.Concat(path.Split(Path.GetInvalidPathChars()));
|
|
}
|
|
|
|
private static String StripTorrentNamePrefix(String subPath, String torrentName)
|
|
{
|
|
var separatorIndex = subPath.IndexOfAny(['/', '\\']);
|
|
|
|
String firstComponent;
|
|
String remainder;
|
|
|
|
if (separatorIndex < 0)
|
|
{
|
|
firstComponent = subPath;
|
|
remainder = String.Empty;
|
|
}
|
|
else
|
|
{
|
|
firstComponent = subPath[..separatorIndex];
|
|
remainder = subPath[(separatorIndex + 1)..];
|
|
}
|
|
|
|
var normalizedFirst = RemoveInvalidPathChars(firstComponent);
|
|
|
|
if (normalizedFirst.Equals(torrentName.TrimEnd('/', '\\'), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return remainder;
|
|
}
|
|
|
|
return subPath;
|
|
}
|
|
}
|