From e791f4b743d9660b0ad1decc4c5ed0e864f3b243 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Sun, 28 Jul 2024 16:57:54 -0700 Subject: [PATCH] Fixed: Updating series path from different OS paths Closes #6953 --- .../PathExtensionFixture.cs | 41 +++++-- src/NzbDrone.Common/Disk/OsPath.cs | 104 +++++++++++++++++- .../Extensions/PathExtensions.cs | 43 +++----- src/NzbDrone.Core/Tv/SeriesPathBuilder.cs | 16 ++- 4 files changed, 162 insertions(+), 42 deletions(-) diff --git a/src/NzbDrone.Common.Test/PathExtensionFixture.cs b/src/NzbDrone.Common.Test/PathExtensionFixture.cs index 967126a1c..ddb54c538 100644 --- a/src/NzbDrone.Common.Test/PathExtensionFixture.cs +++ b/src/NzbDrone.Common.Test/PathExtensionFixture.cs @@ -133,11 +133,16 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\test\", @"C:\Test\mydir")] [TestCase(@"C:\test", @"C:\Test\mydir\")] - public void path_should_be_parent_on_windows_only(string parentPath, string childPath) + public void windows_path_should_be_parent(string parentPath, string childPath) { - var expectedResult = OsInfo.IsWindows; + parentPath.IsParentPath(childPath).Should().Be(true); + } - parentPath.IsParentPath(childPath).Should().Be(expectedResult); + [TestCase("/test", "/test/mydir/")] + [TestCase("/test/", "/test/mydir")] + public void posix_path_should_be_parent(string parentPath, string childPath) + { + parentPath.IsParentPath(childPath).Should().Be(true); } [TestCase(@"C:\Test\mydir", @"C:\Test")] @@ -145,39 +150,57 @@ namespace NzbDrone.Common.Test [TestCase(@"C:\", null)] [TestCase(@"\\server\share", null)] [TestCase(@"\\server\share\test", @"\\server\share")] - public void path_should_return_parent_windows(string path, string parentPath) + public void windows_path_should_return_parent(string path, string parentPath) { - WindowsOnly(); path.GetParentPath().Should().Be(parentPath); } [TestCase(@"/", null)] [TestCase(@"/test", "/")] - public void path_should_return_parent_mono(string path, string parentPath) + [TestCase(@"/test/tv", "/test")] + public void unix_path_should_return_parent(string path, string parentPath) { - PosixOnly(); path.GetParentPath().Should().Be(parentPath); } [TestCase(@"C:\Test\mydir", "Test")] [TestCase(@"C:\Test\", @"C:\")] + [TestCase(@"C:\Test", @"C:\")] [TestCase(@"C:\", null)] [TestCase(@"\\server\share", null)] [TestCase(@"\\server\share\test", @"\\server\share")] public void path_should_return_parent_name_windows(string path, string parentPath) { - WindowsOnly(); path.GetParentName().Should().Be(parentPath); } [TestCase(@"/", null)] [TestCase(@"/test", "/")] + [TestCase(@"/test/tv", "test")] public void path_should_return_parent_name_mono(string path, string parentPath) { - PosixOnly(); path.GetParentName().Should().Be(parentPath); } + [TestCase(@"C:\Test\mydir", "mydir")] + [TestCase(@"C:\Test\", "Test")] + [TestCase(@"C:\Test", "Test")] + [TestCase(@"C:\", "C:\\")] + [TestCase(@"\\server\share", @"\\server\share")] + [TestCase(@"\\server\share\test", "test")] + public void path_should_return_directory_name_windows(string path, string parentPath) + { + path.GetDirectoryName().Should().Be(parentPath); + } + + [TestCase(@"/", "/")] + [TestCase(@"/test", "test")] + [TestCase(@"/test/tv", "tv")] + public void path_should_return_directory_name_mono(string path, string parentPath) + { + path.GetDirectoryName().Should().Be(parentPath); + } + [Test] public void path_should_return_parent_for_oversized_path() { diff --git a/src/NzbDrone.Common/Disk/OsPath.cs b/src/NzbDrone.Common/Disk/OsPath.cs index f6f01fccf..45e520761 100644 --- a/src/NzbDrone.Common/Disk/OsPath.cs +++ b/src/NzbDrone.Common/Disk/OsPath.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.RegularExpressions; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk @@ -9,6 +10,8 @@ namespace NzbDrone.Common.Disk private readonly string _path; private readonly OsPathKind _kind; + private static readonly Regex UncPathRegex = new Regex(@"(?^\\\\(?:\?\\UNC\\)?[^\\]+\\[^\\]+)(?:\\|$)", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public OsPath(string path) { if (path == null) @@ -96,6 +99,19 @@ namespace NzbDrone.Common.Disk return path; } + private static string TrimTrailingSlash(string path, OsPathKind kind) + { + switch (kind) + { + case OsPathKind.Windows when !path.EndsWith(":\\"): + return path.TrimEnd('\\'); + case OsPathKind.Unix when path != "/": + return path.TrimEnd('/'); + } + + return path; + } + public OsPathKind Kind => _kind; public bool IsWindowsPath => _kind == OsPathKind.Windows; @@ -130,7 +146,19 @@ namespace NzbDrone.Common.Disk if (index == -1) { - return new OsPath(null); + return Null; + } + + var rootLength = GetRootLength(); + + if (rootLength == _path.Length) + { + return Null; + } + + if (rootLength > index + 1) + { + return new OsPath(_path.Substring(0, rootLength)); } return new OsPath(_path.Substring(0, index), _kind).AsDirectory(); @@ -139,6 +167,8 @@ namespace NzbDrone.Common.Disk public string FullPath => _path; + public string PathWithoutTrailingSlash => TrimTrailingSlash(_path, _kind); + public string FileName { get @@ -161,6 +191,30 @@ namespace NzbDrone.Common.Disk } } + public string Name + { + // Meant to behave similar to DirectoryInfo.Name + + get + { + var index = GetFileNameIndex(); + + if (index == -1) + { + return PathWithoutTrailingSlash; + } + + var rootLength = GetRootLength(); + + if (rootLength > index + 1) + { + return _path.Substring(0, rootLength); + } + + return TrimTrailingSlash(_path.Substring(index).TrimStart('/', '\\'), _kind); + } + } + public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs); private int GetFileNameIndex() @@ -190,11 +244,50 @@ namespace NzbDrone.Common.Disk return index; } + private int GetRootLength() + { + if (!IsRooted) + { + return 0; + } + + if (_kind == OsPathKind.Unix) + { + return 1; + } + + if (_kind == OsPathKind.Windows) + { + if (HasWindowsDriveLetter(_path)) + { + return 3; + } + + var uncMatch = UncPathRegex.Match(_path); + + // \\?\UNC\server\share\ or \\server\share + if (uncMatch.Success) + { + return uncMatch.Groups["unc"].Length; + } + + // \\?\C:\ + if (_path.StartsWith(@"\\?\")) + { + return 7; + } + } + + return 0; + } + private string[] GetFragments() { return _path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); } + public static OsPath Null => new (null); + public override string ToString() { return _path; @@ -267,6 +360,11 @@ namespace NzbDrone.Common.Disk } public bool Equals(OsPath other) + { + return Equals(other, false); + } + + public bool Equals(OsPath other, bool ignoreTrailingSlash) { if (ReferenceEquals(other, null)) { @@ -278,8 +376,8 @@ namespace NzbDrone.Common.Disk return true; } - var left = _path; - var right = other._path; + var left = ignoreTrailingSlash ? PathWithoutTrailingSlash : _path; + var right = ignoreTrailingSlash ? other.PathWithoutTrailingSlash : other._path; if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) { diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index 4585326f1..7dced0c0e 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -92,26 +92,23 @@ namespace NzbDrone.Common.Extensions public static string GetParentPath(this string childPath) { - var cleanPath = childPath.GetCleanPath(); + var path = new OsPath(childPath).Directory; - if (cleanPath.IsNullOrWhiteSpace()) - { - return null; - } - - return Directory.GetParent(cleanPath)?.FullName; + return path == OsPath.Null ? null : path.PathWithoutTrailingSlash; } public static string GetParentName(this string childPath) { - var cleanPath = childPath.GetCleanPath(); + var path = new OsPath(childPath).Directory; - if (cleanPath.IsNullOrWhiteSpace()) - { - return null; - } + return path == OsPath.Null ? null : path.Name; + } - return Directory.GetParent(cleanPath)?.Name; + public static string GetDirectoryName(this string childPath) + { + var path = new OsPath(childPath); + + return path == OsPath.Null ? null : path.Name; } public static string GetCleanPath(this string path) @@ -125,27 +122,17 @@ namespace NzbDrone.Common.Extensions public static bool IsParentPath(this string parentPath, string childPath) { - if (parentPath != "/" && !parentPath.EndsWith(":\\")) - { - parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); - } + var parent = new OsPath(parentPath); + var child = new OsPath(childPath); - if (childPath != "/" && !parentPath.EndsWith(":\\")) + while (child.Directory != OsPath.Null) { - childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); - } - - var parent = new DirectoryInfo(parentPath); - var child = new DirectoryInfo(childPath); - - while (child.Parent != null) - { - if (child.Parent.FullName.Equals(parent.FullName, DiskProviderBase.PathStringComparison)) + if (child.Directory.Equals(parent, true)) { return true; } - child = child.Parent; + child = child.Directory; } return false; diff --git a/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs b/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs index 5a222774f..738569a51 100644 --- a/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs +++ b/src/NzbDrone.Core/Tv/SeriesPathBuilder.cs @@ -1,5 +1,6 @@ using System; using System.IO; +using NLog; using NzbDrone.Common.Extensions; using NzbDrone.Core.Organizer; using NzbDrone.Core.RootFolders; @@ -15,11 +16,13 @@ namespace NzbDrone.Core.Tv { private readonly IBuildFileNames _fileNameBuilder; private readonly IRootFolderService _rootFolderService; + private readonly Logger _logger; - public SeriesPathBuilder(IBuildFileNames fileNameBuilder, IRootFolderService rootFolderService) + public SeriesPathBuilder(IBuildFileNames fileNameBuilder, IRootFolderService rootFolderService, Logger logger) { _fileNameBuilder = fileNameBuilder; _rootFolderService = rootFolderService; + _logger = logger; } public string BuildPath(Series series, bool useExistingRelativeFolder) @@ -42,7 +45,16 @@ namespace NzbDrone.Core.Tv { var rootFolderPath = _rootFolderService.GetBestRootFolderPath(series.Path); - return rootFolderPath.GetRelativePath(series.Path); + if (rootFolderPath.IsParentPath(series.Path)) + { + return rootFolderPath.GetRelativePath(series.Path); + } + + var directoryName = series.Path.GetDirectoryName(); + + _logger.Warn("Unable to get relative path for series path {0}, using series folder name {1}", series.Path, directoryName); + + return directoryName; } } }