rdt-client/server/RdtClient.Service/Helpers/FilenameSanitizer.cs
Claude 1098bac18f
feat: strip problematic characters from downloaded filenames
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.
2026-04-23 06:19:25 +00:00

99 lines
2.9 KiB
C#

using System.Text;
using System.Text.RegularExpressions;
using RdtClient.Service.Services;
namespace RdtClient.Service.Helpers;
/// <summary>
/// Sanitizes filenames to prevent issues with special characters on Linux containers.
/// Path.GetInvalidFileNameChars() on Linux only returns NUL and '/', so characters
/// like [ ] { } that cause problems with shell globbing, URI handling, and various
/// download clients pass through the built-in filter unchanged.
/// </summary>
public static partial class FilenameSanitizer
{
[GeneratedRegex(@" {2,}")]
private static partial Regex CreateMultipleSpacesRegex();
private static readonly Regex MultipleSpaces = CreateMultipleSpacesRegex();
/// <summary>
/// Returns whether sanitization is enabled in the current settings.
/// </summary>
public static Boolean IsEnabled => Settings.Get.DownloadClient.SanitizeFilenames;
/// <summary>
/// Sanitizes a filename if enabled in settings; otherwise returns it unchanged.
/// </summary>
public static String SanitizeFilenameIfEnabled(String filename)
{
return IsEnabled ? SanitizeFilename(filename) : filename;
}
/// <summary>
/// Sanitizes a full path if enabled in settings; otherwise returns it unchanged.
/// </summary>
public static String SanitizePathIfEnabled(String filePath)
{
return IsEnabled ? SanitizePath(filePath) : filePath;
}
/// <summary>
/// Sanitizes a filename by stripping problematic characters and normalizing whitespace.
/// Does NOT touch directory separators — only a single filename segment.
/// </summary>
public static String SanitizeFilename(String filename)
{
if (String.IsNullOrEmpty(filename))
{
return filename;
}
var sb = new StringBuilder(filename.Length);
foreach (var c in filename)
{
if (Char.IsControl(c))
{
continue;
}
if (c is '[' or ']' or '{' or '}')
{
continue;
}
sb.Append(c);
}
var result = sb.ToString();
result = MultipleSpaces.Replace(result, " ");
result = result.Trim();
return result;
}
/// <summary>
/// Sanitizes each segment of a full file path, preserving directory separators.
/// </summary>
public static String SanitizePath(String filePath)
{
if (String.IsNullOrEmpty(filePath))
{
return filePath;
}
var separator = Path.DirectorySeparatorChar;
var segments = filePath.Split(separator);
for (var i = 0; i < segments.Length; i++)
{
if (!String.IsNullOrEmpty(segments[i]))
{
segments[i] = SanitizeFilename(segments[i]);
}
}
return String.Join(separator, segments);
}
}