diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index f2e2d3429..a4199d64b 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -80,6 +80,7 @@ + diff --git a/src/NzbDrone.Common.Test/OsPathFixture.cs b/src/NzbDrone.Common.Test/OsPathFixture.cs new file mode 100644 index 000000000..1e58b7eb3 --- /dev/null +++ b/src/NzbDrone.Common.Test/OsPathFixture.cs @@ -0,0 +1,237 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NUnit.Framework; +using NzbDrone.Common.Disk; +using NzbDrone.Test.Common; +using FluentAssertions; + +namespace NzbDrone.Common.Test +{ + public class OsPathFixture : TestBase + { + [TestCase(@"C:\rooted\windows\path\", OsPathKind.Windows)] + [TestCase(@"C:\rooted\windows\path", OsPathKind.Windows)] + [TestCase(@"C:\", OsPathKind.Windows)] + [TestCase(@"C:", OsPathKind.Windows)] + [TestCase(@"\\rooted\unc\path\", OsPathKind.Windows)] + [TestCase(@"\\rooted\unc\path", OsPathKind.Windows)] + [TestCase(@"\relative\windows\path\", OsPathKind.Windows)] + [TestCase(@"\relative\windows\path", OsPathKind.Windows)] + [TestCase(@"relative\windows\path\", OsPathKind.Windows)] + [TestCase(@"relative\windows\path", OsPathKind.Windows)] + [TestCase(@"relative\", OsPathKind.Windows)] + [TestCase(@"relative", OsPathKind.Unknown)] + [TestCase("/rooted/linux/path/", OsPathKind.Unix)] + [TestCase("/rooted/linux/path", OsPathKind.Unix)] + [TestCase("/", OsPathKind.Unix)] + [TestCase("linux/path", OsPathKind.Unix)] + public void should_auto_detect_kind(String path, OsPathKind kind) + { + var result = new OsPath(path); + + result.Kind.Should().Be(kind); + + if (kind == OsPathKind.Windows) + { + result.IsWindowsPath.Should().BeTrue(); + result.IsUnixPath.Should().BeFalse(); + } + else if (kind == OsPathKind.Unix) + { + result.IsWindowsPath.Should().BeFalse(); + result.IsUnixPath.Should().BeTrue(); + } + else + { + result.IsWindowsPath.Should().BeFalse(); + result.IsUnixPath.Should().BeFalse(); + } + } + + [Test] + public void should_add_directory_slash() + { + var osPath = new OsPath(@"C:\rooted\windows\path\"); + + osPath.Directory.Should().NotBeNull(); + osPath.Directory.ToString().Should().Be(@"C:\rooted\windows\"); + } + + [TestCase(@"C:\rooted\windows\path", @"C:\rooted\windows\")] + [TestCase(@"C:\rooted", @"C:\")] + [TestCase(@"C:", null)] + [TestCase("/rooted/linux/path", "/rooted/linux/")] + [TestCase("/rooted", "/")] + [TestCase("/", null)] + public void should_return_parent_directory(String path, String expectedParent) + { + var osPath = new OsPath(path); + + osPath.Directory.Should().NotBeNull(); + osPath.Directory.Should().Be(new OsPath(expectedParent)); + } + + [Test] + public void should_return_empty_as_parent_of_root_unc() + { + var osPath = new OsPath(@"\\unc"); + + osPath.Directory.IsEmpty.Should().BeTrue(); + } + + [TestCase(@"C:\rooted\windows\path")] + [TestCase(@"C:")] + [TestCase(@"\\blaat")] + [TestCase("/rooted/linux/path")] + [TestCase("/")] + public void should_detect_rooted_ospaths(String path) + { + var osPath = new OsPath(path); + + osPath.IsRooted.Should().BeTrue(); + } + + [TestCase(@"\rooted\windows\path")] + [TestCase(@"rooted\windows\path")] + [TestCase(@"path")] + [TestCase("linux/path")] + public void should_detect_unrooted_ospaths(String path) + { + var osPath = new OsPath(path); + + osPath.IsRooted.Should().BeFalse(); + } + + [TestCase(@"C:\rooted\windows\path", "path")] + [TestCase(@"C:", "C:")] + [TestCase(@"\\blaat", "blaat")] + [TestCase("/rooted/linux/path", "path")] + [TestCase("/", null)] + [TestCase(@"\rooted\windows\path\", "path")] + [TestCase(@"rooted\windows\path", "path")] + [TestCase(@"path", "path")] + [TestCase("linux/path", "path")] + public void should_return_filename(String path, String expectedFilePath) + { + var osPath = new OsPath(path); + + osPath.FileName.Should().Be(expectedFilePath); + } + + [Test] + public void should_compare_windows_ospathkind_case_insensitive() + { + var left = new OsPath(@"C:\rooted\Windows\path"); + var right = new OsPath(@"C:\rooted\windows\path"); + + left.Should().Be(right); + } + + [Test] + public void should_compare_unix_ospathkind_case_sensitive() + { + var left = new OsPath(@"/rooted/Linux/path"); + var right = new OsPath(@"/rooted/linux/path"); + + left.Should().NotBe(right); + } + + [Test] + public void should_not_ignore_trailing_slash_during_compare() + { + var left = new OsPath(@"/rooted/linux/path/"); + var right = new OsPath(@"/rooted/linux/path"); + + left.Should().NotBe(right); + } + + [TestCase(@"C:\Test", @"sub", @"C:\Test\sub")] + [TestCase(@"C:\Test", @"sub\test", @"C:\Test\sub\test")] + [TestCase(@"C:\Test\", @"\sub", @"C:\Test\sub")] + [TestCase(@"C:\Test", @"sub\", @"C:\Test\sub\")] + [TestCase(@"C:\Test", @"C:\Test2\sub", @"C:\Test2\sub")] + [TestCase(@"/Test", @"sub", @"/Test/sub")] + [TestCase(@"/Test", @"sub/", @"/Test/sub/")] + [TestCase(@"/Test", @"sub/", @"/Test/sub/")] + [TestCase(@"/Test/", @"sub/test/", @"/Test/sub/test/")] + [TestCase(@"/Test/", @"/Test2/", @"/Test2/")] + [TestCase(@"C:\Test", "", @"C:\Test")] + public void should_combine_path(String left, String right, String expectedResult) + { + var osPathLeft = new OsPath(left); + var osPathRight = new OsPath(right); + + var result = osPathLeft + osPathRight; + + result.FullPath.Should().Be(expectedResult); + } + + [Test] + public void should_fix_slashes_windows() + { + var osPath = new OsPath(@"C:/on/windows/transmission\uses/forward/slashes"); + + osPath.Kind.Should().Be(OsPathKind.Windows); + osPath.FullPath.Should().Be(@"C:\on\windows\transmission\uses\forward\slashes"); + } + + [Test] + public void should_fix_slashes_unix() + { + var osPath = new OsPath(@"/just/a/test\to\verify the/slashes\"); + + osPath.Kind.Should().Be(OsPathKind.Unix); + osPath.FullPath.Should().Be(@"/just/a/test/to/verify the/slashes/"); + } + + [Test] + public void should_combine_mixed_slashes() + { + var left = new OsPath(@"C:/on/windows/transmission"); + var right = new OsPath(@"uses/forward/slashes", OsPathKind.Unknown); + + var osPath = left + right; + + osPath.Kind.Should().Be(OsPathKind.Windows); + osPath.FullPath.Should().Be(@"C:\on\windows\transmission\uses\forward\slashes"); + } + + [TestCase(@"C:\Test\Data\", @"C:\Test\Data\Sub\Folder", @"Sub\Folder")] + [TestCase(@"C:\Test\Data\", @"C:\Test\Data2\Sub\Folder", @"..\Data2\Sub\Folder")] + [TestCase(@"/parent/folder", @"/parent/folder/Sub/Folder", @"Sub/Folder")] + public void should_create_relative_path(String parent, String child, String expected) + { + var left = new OsPath(child); + var right = new OsPath(parent); + + var osPath = left - right; + + osPath.Kind.Should().Be(OsPathKind.Unknown); + osPath.FullPath.Should().Be(expected); + } + + [Test] + public void should_parse_null_as_empty() + { + var result = new OsPath(null); + + result.FullPath.Should().BeEmpty(); + result.IsEmpty.Should().BeTrue(); + } + + [TestCase(@"C:\Test\", @"C:\Test", true)] + [TestCase(@"C:\Test\", @"C:\Test\Contains\", true)] + [TestCase(@"C:\Test\", @"C:\Other\", false)] + public void should_evaluate_contains(String parent, String child, Boolean expectedResult) + { + var left = new OsPath(parent); + var right = new OsPath(child); + + var result = left.Contains(right); + + result.Should().Be(expectedResult); + } + } +} diff --git a/src/NzbDrone.Common/Disk/OsPath.cs b/src/NzbDrone.Common/Disk/OsPath.cs new file mode 100644 index 000000000..540f70c25 --- /dev/null +++ b/src/NzbDrone.Common/Disk/OsPath.cs @@ -0,0 +1,406 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Exceptions; + +namespace NzbDrone.Common.Disk +{ + public struct OsPath : IEquatable + { + private readonly String _path; + private readonly OsPathKind _kind; + + public OsPath(String path) + { + if (path == null) + { + _kind = OsPathKind.Unknown; + _path = String.Empty; + } + else + { + _kind = DetectPathKind(path); + _path = FixSlashes(path, _kind); + } + } + + public OsPath(String path, OsPathKind kind) + { + if (path == null) + { + _kind = kind; + _path = String.Empty; + } + else + { + _kind = kind; + _path = FixSlashes(path, kind); + } + } + + private static OsPathKind DetectPathKind(String path) + { + if (path.StartsWith("/")) + { + return OsPathKind.Unix; + } + if (path.Contains(':') || path.Contains('\\')) + { + return OsPathKind.Windows; + } + else if (path.Contains('/')) + { + return OsPathKind.Unix; + } + else + { + return OsPathKind.Unknown; + } + } + + private static String FixSlashes(String path, OsPathKind kind) + { + if (kind == OsPathKind.Windows) + { + return path.Replace('/', '\\'); + } + else if (kind == OsPathKind.Unix) + { + return path.Replace('\\', '/'); + } + + return path; + } + + public OsPathKind Kind + { + get { return _kind; } + } + + public Boolean IsWindowsPath + { + get { return _kind == OsPathKind.Windows; } + } + + public Boolean IsUnixPath + { + get { return _kind == OsPathKind.Unix; } + } + + public Boolean IsEmpty + { + get + { + return _path.IsNullOrWhiteSpace(); + } + } + + public Boolean IsRooted + { + get + { + if (IsWindowsPath) + { + return _path.StartsWith(@"\\") || _path.Contains(':'); + } + else if (IsUnixPath) + { + return _path.StartsWith("/"); + } + else + { + return false; + } + } + } + + public OsPath Directory + { + get + { + var index = GetFileNameIndex(); + + if (index == -1) + { + return new OsPath(null); + } + else + { + return new OsPath(_path.Substring(0, index), _kind).AsDirectory(); + } + } + } + + public String FullPath + { + get + { + return _path; + } + } + + public String FileName + { + get + { + var index = GetFileNameIndex(); + + if (index == -1) + { + var path = _path.Trim('\\', '/'); + + if (path.Length == 0) + { + return null; + } + + return path; + } + else + { + return _path.Substring(index).Trim('\\', '/'); + } + } + } + + private Int32 GetFileNameIndex() + { + if (_path.Length < 2) + { + return -1; + } + + var index = _path.LastIndexOfAny(new[] { '/', '\\' }, _path.Length - 2); + + if (index == -1) + { + return -1; + } + + if (_path.StartsWith(@"\\") && index < 2) + { + return -1; + } + + if (_path.StartsWith("/") && index == 0) + { + index++; + } + + return index; + } + + private String[] GetFragments() + { + return _path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); + } + + public override String ToString() + { + return _path; + } + + public override Int32 GetHashCode() + { + return _path.ToLowerInvariant().GetHashCode(); + } + + public override Boolean Equals(Object obj) + { + if (obj is OsPath) + { + return Equals((OsPath)obj); + } + else if (obj is String) + { + return Equals(new OsPath(obj as String)); + } + else + { + return false; + } + } + + public OsPath AsDirectory() + { + if (IsEmpty) + { + return this; + } + + if (Kind == OsPathKind.Windows) + { + return new OsPath(_path.TrimEnd('\\') + "\\", _kind); + } + else if (Kind == OsPathKind.Unix) + { + return new OsPath(_path.TrimEnd('/') + "/", _kind); + } + else + { + return this; + } + } + + public Boolean Contains(OsPath other) + { + if (!IsRooted || !other.IsRooted) + { + return false; + } + + var leftFragments = GetFragments(); + var rightFragments = other.GetFragments(); + + if (rightFragments.Length < leftFragments.Length) + { + return false; + } + + var stringComparison = (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; + + for (int i = 0; i < leftFragments.Length; i++) + { + if (!String.Equals(leftFragments[i], rightFragments[i], stringComparison)) + { + return false; + } + } + + return true; + } + + public Boolean Equals(OsPath other) + { + if (ReferenceEquals(other, null)) return false; + + if (_path == other._path) + { + return true; + } + + var left = _path; + var right = other._path; + + if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) + { + return String.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); + } + else + { + return String.Equals(left, right, StringComparison.InvariantCulture); + } + } + + public static Boolean operator ==(OsPath left, OsPath right) + { + if (ReferenceEquals(left, null)) return ReferenceEquals(right, null); + + return left.Equals(right); + } + + public static Boolean operator !=(OsPath left, OsPath right) + { + if (ReferenceEquals(left, null)) return !ReferenceEquals(right, null); + + return !left.Equals(right); + } + + public static OsPath operator +(OsPath left, OsPath right) + { + if (left.Kind != right.Kind && right.Kind != OsPathKind.Unknown) + { + throw new Exception(String.Format("Cannot combine OsPaths of different platforms ('{0}' + '{1}')", left, right)); + } + + if (right.IsEmpty) + { + return left; + } + + if (right.IsRooted) + { + return right; + } + + if (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows) + { + return new OsPath(String.Join("\\", left._path.TrimEnd('\\'), right._path.TrimStart('\\')), OsPathKind.Windows); + } + else if (left.Kind == OsPathKind.Unix || right.Kind == OsPathKind.Unix) + { + return new OsPath(String.Join("/", left._path.TrimEnd('/'), right._path), OsPathKind.Unix); + } + else + { + return new OsPath(String.Join("/", left._path, right._path), OsPathKind.Unknown); + } + } + + public static OsPath operator +(OsPath left, String right) + { + return left + new OsPath(right); + } + + public static OsPath operator -(OsPath left, OsPath right) + { + if (!left.IsRooted || !right.IsRooted) + { + throw new ArgumentException("Cannot determine relative path for unrooted paths."); + } + + var leftFragments = left.GetFragments(); + var rightFragments = right.GetFragments(); + + var stringComparison = (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; + + int i; + for (i = 0; i < leftFragments.Length && i < rightFragments.Length; i++) + { + if (!String.Equals(leftFragments[i], rightFragments[i], stringComparison)) + { + break; + } + } + + if (i == 0) + { + return right; + } + + var newFragments = new List(); + + for (int j = i; j < rightFragments.Length; j++) + { + newFragments.Add(".."); + } + + for (int j = i; j < leftFragments.Length; j++) + { + newFragments.Add(leftFragments[j]); + } + + if (left.FullPath.EndsWith("\\") || left.FullPath.EndsWith("/")) + { + newFragments.Add(String.Empty); + } + + if (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows) + { + return new OsPath(String.Join("\\", newFragments), OsPathKind.Unknown); + } + else + { + return new OsPath(String.Join("/", newFragments), OsPathKind.Unknown); + } + } + } + + public enum OsPathKind + { + Unknown, + Windows, + Unix + } +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index adb025be3..1933d084c 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -71,6 +71,7 @@ + diff --git a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs index 51f81076e..116ac2874 100644 --- a/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs +++ b/src/NzbDrone.Core.Test/Download/CompletedDownloadServiceFixture.cs @@ -31,7 +31,7 @@ namespace NzbDrone.Core.Test.Download _completed = Builder.CreateListOfSize(1) .All() .With(h => h.Status = DownloadItemStatus.Completed) - .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic())) .With(h => h.Title = "Drone.S01E01.HDTV") .Build() .ToList(); @@ -325,7 +325,7 @@ namespace NzbDrone.Core.Test.Download _completed.AddRange(Builder.CreateListOfSize(2) .All() .With(h => h.Status = DownloadItemStatus.Completed) - .With(h => h.OutputPath = @"C:\DropFolder\MyDownload".AsOsAgnostic()) + .With(h => h.OutputPath = new OsPath(@"C:\DropFolder\MyDownload".AsOsAgnostic())) .With(h => h.Title = "Drone.S01E01.HDTV") .Build()); diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs index 9186072a1..0a7272e1b 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/DownloadClientFixtureBase.cs @@ -39,8 +39,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests .Returns(r => new HttpResponse(r, new HttpHeader(), new Byte[0])); Mocker.GetMock() - .Setup(v => v.RemapRemoteToLocal(It.IsAny(), It.IsAny())) - .Returns((h,r) => r); + .Setup(v => v.RemapRemoteToLocal(It.IsAny(), It.IsAny())) + .Returns((h, r) => r); } protected virtual RemoteEpisode CreateRemoteEpisode() diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs index 87d193506..a6b0e1cfc 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/NzbgetTests/NzbgetFixture.cs @@ -14,6 +14,7 @@ using NzbDrone.Core.Parser.Model; using NzbDrone.Test.Common; using NzbDrone.Core.Test.Framework; using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Common.Disk; namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests { @@ -300,8 +301,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests public void should_return_status_with_mounted_outputdir() { Mocker.GetMock() - .Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/tv")) - .Returns(@"O:\mymount".AsOsAgnostic()); + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny())) + .Returns(new OsPath(@"O:\mymount".AsOsAgnostic())); var result = Subject.GetStatus(); @@ -314,8 +315,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.NzbgetTests public void should_remap_storage_if_mounted() { Mocker.GetMock() - .Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/tv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE")) - .Returns(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny())) + .Returns(new OsPath(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic())); GivenQueue(null); GivenHistory(_completed); diff --git a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs index 7a5549bbf..686d6b75d 100644 --- a/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs +++ b/src/NzbDrone.Core.Test/Download/DownloadClientTests/SabnzbdTests/SabnzbdFixture.cs @@ -11,6 +11,7 @@ using NzbDrone.Core.Download.Clients.Sabnzbd.Responses; using NzbDrone.Core.Tv; using NzbDrone.Test.Common; using NzbDrone.Core.RemotePathMappings; +using NzbDrone.Common.Disk; namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests { @@ -303,15 +304,15 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests var result = Subject.GetItems().Single(); - result.OutputPath.Should().Be((@"C:\sorted\" + title).AsOsAgnostic()); + result.OutputPath.Should().Be(new OsPath((@"C:\sorted\" + title).AsOsAgnostic()).AsDirectory()); } [Test] public void should_remap_storage_if_mounted() { Mocker.GetMock() - .Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv/Droned.S01E01.Pilot.1080p.WEB-DL-DRONE")) - .Returns(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic()); + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny())) + .Returns(new OsPath(@"O:\mymount\Droned.S01E01.Pilot.1080p.WEB-DL-DRONE".AsOsAgnostic())); GivenQueue(null); GivenHistory(_completed); @@ -370,8 +371,8 @@ namespace NzbDrone.Core.Test.Download.DownloadClientTests.SabnzbdTests public void should_return_status_with_mounted_outputdir() { Mocker.GetMock() - .Setup(v => v.RemapRemoteToLocal("127.0.0.1", "/remote/mount/vv")) - .Returns(@"O:\mymount".AsOsAgnostic()); + .Setup(v => v.RemapRemoteToLocal("127.0.0.1", It.IsAny())) + .Returns(new OsPath(@"O:\mymount".AsOsAgnostic())); GivenQueue(null); diff --git a/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs index 4e0724e84..8260c03ee 100644 --- a/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs +++ b/src/NzbDrone.Core.Test/HealthCheck/Checks/ImportMechanismCheckFixture.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks .With(v => v.State == TrackedDownloadState.Downloading) .With(v => v.DownloadItem = new DownloadClientItem()) .With(v => v.DownloadItem.Status = DownloadItemStatus.Completed) - .With(v => v.DownloadItem.OutputPath = @"C:\Test\DropFolder\myfile.mkv".AsOsAgnostic()) + .With(v => v.DownloadItem.OutputPath = new OsPath(@"C:\Test\DropFolder\myfile.mkv".AsOsAgnostic())) .Build(); Mocker.GetMock() @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Test.HealthCheck.Checks GivenCompletedDownloadHandling(true); GivenDroneFactoryFolder(true); - _completed.First().DownloadItem.OutputPath = (DRONE_FACTORY_FOLDER + @"\myfile.mkv").AsOsAgnostic(); + _completed.First().DownloadItem.OutputPath = new OsPath((DRONE_FACTORY_FOLDER + @"\myfile.mkv").AsOsAgnostic()); Subject.Check().ShouldBeWarning(); } diff --git a/src/NzbDrone.Core.Test/RemotePathMappingsTests/RemotePathMappingServiceFixture.cs b/src/NzbDrone.Core.Test/RemotePathMappingsTests/RemotePathMappingServiceFixture.cs index dea074bce..872ca995b 100644 --- a/src/NzbDrone.Core.Test/RemotePathMappingsTests/RemotePathMappingServiceFixture.cs +++ b/src/NzbDrone.Core.Test/RemotePathMappingsTests/RemotePathMappingServiceFixture.cs @@ -97,13 +97,13 @@ namespace NzbDrone.Core.Test.RemotePathMappingsTests GivenMapping(); - var result = Subject.RemapRemoteToLocal(host, remotePath); + var result = Subject.RemapRemoteToLocal(host, new OsPath(remotePath)); result.Should().Be(expectedLocalPath); } [TestCase("my-server.localdomain", "/mnt/storage/downloads/tv", @"D:\mountedstorage\downloads\tv")] - [TestCase("my-server.localdomain", "/mnt/storage", @"D:\mountedstorage")] + [TestCase("my-server.localdomain", "/mnt/storage/", @"D:\mountedstorage")] [TestCase("my-2server.localdomain", "/mnt/storage/downloads/tv", "/mnt/storage/downloads/tv")] [TestCase("my-server.localdomain", "/mnt/storageabc/downloads/tv", "/mnt/storageabc/downloads/tv")] public void should_remap_local_to_remote(String host, String expectedRemotePath, String localPath) @@ -112,7 +112,7 @@ namespace NzbDrone.Core.Test.RemotePathMappingsTests GivenMapping(); - var result = Subject.RemapLocalToRemote(host, localPath); + var result = Subject.RemapLocalToRemote(host, new OsPath(localPath)); result.Should().Be(expectedRemotePath); } diff --git a/src/NzbDrone.Core/Datastore/Converters/OsPathConverter.cs b/src/NzbDrone.Core/Datastore/Converters/OsPathConverter.cs new file mode 100644 index 000000000..9e460300c --- /dev/null +++ b/src/NzbDrone.Core/Datastore/Converters/OsPathConverter.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Marr.Data.Converters; +using Marr.Data.Mapping; +using Newtonsoft.Json; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.Datastore.Converters +{ + public class OsPathConverter : IConverter + { + public Object FromDB(ConverterContext context) + { + if (context.DbValue == DBNull.Value) + { + return DBNull.Value; + } + + var value = (String)context.DbValue; + + return new OsPath(value); + } + + public Object FromDB(ColumnMap map, Object dbValue) + { + return FromDB(new ConverterContext { ColumnMap = map, DbValue = dbValue }); + } + + public Object ToDB(Object clrValue) + { + var value = (OsPath)clrValue; + + return value.FullPath; + } + + public Type DbType + { + get { return typeof(String); } + } + } +} diff --git a/src/NzbDrone.Core/Datastore/TableMapping.cs b/src/NzbDrone.Core/Datastore/TableMapping.cs index 875ff2420..f6f90609f 100644 --- a/src/NzbDrone.Core/Datastore/TableMapping.cs +++ b/src/NzbDrone.Core/Datastore/TableMapping.cs @@ -28,6 +28,7 @@ using NzbDrone.Core.SeriesStats; using NzbDrone.Core.Tags; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Tv; +using NzbDrone.Common.Disk; namespace NzbDrone.Core.Datastore { @@ -114,6 +115,7 @@ namespace NzbDrone.Core.Datastore MapRepository.Instance.RegisterTypeConverter(typeof(ParsedEpisodeInfo), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(ReleaseInfo), new EmbeddedDocumentConverter()); MapRepository.Instance.RegisterTypeConverter(typeof(HashSet), new EmbeddedDocumentConverter()); + MapRepository.Instance.RegisterTypeConverter(typeof(OsPath), new OsPathConverter()); } private static void RegisterProviderSettingConverter() diff --git a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs index 90cdb1703..aa20fafa0 100644 --- a/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs +++ b/src/NzbDrone.Core/Download/Clients/Deluge/Deluge.cs @@ -115,8 +115,8 @@ namespace NzbDrone.Core.Download.Clients.Deluge item.DownloadClient = Definition.Name; item.DownloadTime = TimeSpan.FromSeconds(torrent.SecondsDownloading); - var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, torrent.DownloadPath); - item.OutputPath = Path.Combine(outputPath, torrent.Name); + var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadPath)); + item.OutputPath = outputPath + torrent.Name; item.RemainingSize = torrent.Size - torrent.BytesDownloaded; item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta); item.TotalSize = torrent.Size; @@ -172,11 +172,11 @@ namespace NzbDrone.Core.Download.Clients.Deluge { var config = _proxy.GetConfig(Settings); - var destDir = config.GetValueOrDefault("download_location") as string; + var destDir = new OsPath(config.GetValueOrDefault("download_location") as string); if (config.GetValueOrDefault("move_completed", false).ToString() == "True") { - destDir = config.GetValueOrDefault("move_completed_path") as string; + destDir = new OsPath(config.GetValueOrDefault("move_completed_path") as string); } var status = new DownloadClientStatus @@ -184,9 +184,9 @@ namespace NzbDrone.Core.Download.Clients.Deluge IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; - if (destDir != null) + if (!destDir.IsEmpty) { - status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }; + status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }; } return status; diff --git a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs index 75e57c55b..6330c417f 100644 --- a/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs +++ b/src/NzbDrone.Core/Download/Clients/Nzbget/Nzbget.cs @@ -131,7 +131,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget historyItem.DownloadClientId = droneParameter == null ? item.Id.ToString() : droneParameter.Value.ToString(); historyItem.Title = item.Name; historyItem.TotalSize = MakeInt64(item.FileSizeHi, item.FileSizeLo); - historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, item.DestDir); + historyItem.OutputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(item.DestDir)); historyItem.Category = item.Category; historyItem.Message = String.Format("PAR Status: {0} - Unpack Status: {1} - Move Status: {2} - Script Status: {3} - Delete Status: {4} - Mark Status: {5}", item.ParStatus, item.UnpackStatus, item.MoveStatus, item.ScriptStatus, item.DeleteStatus, item.MarkStatus); historyItem.Status = DownloadItemStatus.Completed; @@ -215,7 +215,7 @@ namespace NzbDrone.Core.Download.Clients.Nzbget if (category != null) { - status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.DestDir) }; + status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(category.DestDir)) }; } return status; @@ -321,10 +321,10 @@ namespace NzbDrone.Core.Download.Clients.Nzbget if (category != null) { - var localPath = Settings.TvCategoryLocalPath; + var localPath = new OsPath(Settings.TvCategoryLocalPath); Settings.TvCategoryLocalPath = null; - _remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, category.DestDir, localPath); + _remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, new OsPath(category.DestDir), localPath); _logger.Info("Discovered Local Category Path for {0}, the setting was automatically moved to the Remote Path Mapping table.", Definition.Name); } diff --git a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs index de8182094..11bd0fce1 100644 --- a/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs +++ b/src/NzbDrone.Core/Download/Clients/Pneumatic/Pneumatic.cs @@ -89,7 +89,7 @@ namespace NzbDrone.Core.Download.Clients.Pneumatic TotalSize = _diskProvider.GetFileSize(file), - OutputPath = file + OutputPath = new OsPath(file) }; if (_diskProvider.IsFileLocked(file)) diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs index 150fe24cc..9b59132d4 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/Sabnzbd.cs @@ -156,20 +156,20 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd historyItem.Status = DownloadItemStatus.Downloading; } - var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, sabHistoryItem.Storage); + var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(sabHistoryItem.Storage)); - if (!outputPath.IsNullOrWhiteSpace()) + if (!outputPath.IsEmpty) { historyItem.OutputPath = outputPath; - var parent = outputPath.GetParentPath(); - while (parent != null) + var parent = outputPath.Directory; + while (!parent.IsEmpty) { - if (Path.GetFileName(parent) == sabHistoryItem.Title) + if (parent.FileName == sabHistoryItem.Title) { historyItem.OutputPath = parent; } - parent = parent.GetParentPath(); + parent = parent.Directory; } } @@ -259,52 +259,21 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd protected IEnumerable GetCategories(SabnzbdConfig config) { - var completeDir = config.Misc.complete_dir.TrimEnd('\\', '/'); + var completeDir = new OsPath(config.Misc.complete_dir); - if (!completeDir.StartsWith("/") && !completeDir.StartsWith("\\") && !completeDir.Contains(':')) + if (!completeDir.IsRooted) { var queue = _proxy.GetQueue(0, 1, Settings); + var defaultRootFolder = new OsPath(queue.DefaultRootFolder); - if (queue.DefaultRootFolder.StartsWith("/")) - { - completeDir = queue.DefaultRootFolder + "/" + completeDir; - } - else - { - completeDir = queue.DefaultRootFolder + "\\" + completeDir; - } + completeDir = defaultRootFolder + completeDir; } foreach (var category in config.Categories) { - var relativeDir = category.Dir.TrimEnd('*'); + var relativeDir = new OsPath(category.Dir.TrimEnd('*')); - if (relativeDir.IsNullOrWhiteSpace()) - { - category.FullPath = completeDir; - } - else if (completeDir.StartsWith("/")) - { // Process remote Linux paths irrespective of our own OS. - if (relativeDir.StartsWith("/")) - { - category.FullPath = relativeDir; - } - else - { - category.FullPath = completeDir + "/" + relativeDir; - } - } - else - { // Process remote Windows paths irrespective of our own OS. - if (relativeDir.StartsWith("\\") || relativeDir.Contains(':')) - { - category.FullPath = relativeDir; - } - else - { - category.FullPath = completeDir + "\\" + relativeDir; - } - } + category.FullPath = completeDir + relativeDir; yield return category; } @@ -329,7 +298,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (category != null) { - status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) }; + status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, category.FullPath) }; } return status; @@ -454,7 +423,7 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd if (category != null) { - var localPath = Settings.TvCategoryLocalPath; + var localPath = new OsPath(Settings.TvCategoryLocalPath); Settings.TvCategoryLocalPath = null; _remotePathMappingService.MigrateLocalCategoryPath(Definition.Id, Settings, Settings.Host, category.FullPath, localPath); diff --git a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs index 2068cd0a8..18f72d271 100644 --- a/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs +++ b/src/NzbDrone.Core/Download/Clients/Sabnzbd/SabnzbdCategory.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Common.Disk; namespace NzbDrone.Core.Download.Clients.Sabnzbd { @@ -30,6 +31,6 @@ namespace NzbDrone.Core.Download.Clients.Sabnzbd public String Script { get; set; } public String Dir { get; set; } - public String FullPath { get; set; } + public OsPath FullPath { get; set; } } } diff --git a/src/NzbDrone.Core/Download/Clients/TorrentBlackhole/TorrentBlackhole.cs b/src/NzbDrone.Core/Download/Clients/TorrentBlackhole/TorrentBlackhole.cs index a4f1b4f69..cb011f82f 100644 --- a/src/NzbDrone.Core/Download/Clients/TorrentBlackhole/TorrentBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/TorrentBlackhole/TorrentBlackhole.cs @@ -78,7 +78,7 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole TotalSize = files.Select(_diskProvider.GetFileSize).Sum(), - OutputPath = folder + OutputPath = new OsPath(folder) }; if (files.Any(_diskProvider.IsFileLocked)) @@ -108,7 +108,7 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole TotalSize = _diskProvider.GetFileSize(videoFile), - OutputPath = videoFile + OutputPath = new OsPath(videoFile) }; if (_diskProvider.IsFileLocked(videoFile)) @@ -140,7 +140,7 @@ namespace NzbDrone.Core.Download.Clients.TorrentBlackhole return new DownloadClientStatus { IsLocalhost = true, - OutputRootFolders = new List { Settings.WatchFolder } + OutputRootFolders = new List { new OsPath(Settings.WatchFolder) } }; } diff --git a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs index 4d040ebbc..ea52abe9d 100644 --- a/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs +++ b/src/NzbDrone.Core/Download/Clients/Transmission/Transmission.cs @@ -94,19 +94,11 @@ namespace NzbDrone.Core.Download.Clients.Transmission foreach (var torrent in torrents) { - var outputPath = torrent.DownloadDir; - - // Transmission always returns path with forward slashes, even on windows. - if (outputPath.IsNotNullOrWhiteSpace() && (outputPath.StartsWith(@"\\") || outputPath.Contains(':'))) - { - outputPath = outputPath.Replace('/', '\\'); - } - - outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, outputPath); + var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.DownloadDir)); if (Settings.TvCategory.IsNotNullOrWhiteSpace()) { - var directories = outputPath.Split('\\', '/'); + var directories = outputPath.FullPath.Split('\\', '/'); if (!directories.Contains(String.Format(".{0}", Settings.TvCategory))) continue; } @@ -119,7 +111,7 @@ namespace NzbDrone.Core.Download.Clients.Transmission item.DownloadTime = TimeSpan.FromSeconds(torrent.SecondsDownloading); item.Message = torrent.ErrorString; - item.OutputPath = Path.Combine(outputPath, torrent.Name); + item.OutputPath = outputPath + torrent.Name; item.RemainingSize = torrent.LeftUntilDone; item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta); item.TotalSize = torrent.TotalSize; @@ -173,16 +165,10 @@ namespace NzbDrone.Core.Download.Clients.Transmission destDir = String.Format("{0}/.{1}", destDir, Settings.TvCategory); } - // Transmission always returns path with forward slashes, even on windows. - if (destDir.StartsWith(@"\\") || destDir.Contains(':')) - { - destDir = destDir.Replace('/', '\\'); - } - return new DownloadClientStatus { IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost", - OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) } + OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(destDir)) } }; } diff --git a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs index 02ddc5640..333c2598c 100644 --- a/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs +++ b/src/NzbDrone.Core/Download/Clients/UsenetBlackhole/UsenetBlackhole.cs @@ -75,7 +75,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole TotalSize = files.Select(_diskProvider.GetFileSize).Sum(), - OutputPath = folder + OutputPath = new OsPath(folder) }; if (files.Any(_diskProvider.IsFileLocked)) @@ -105,7 +105,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole TotalSize = _diskProvider.GetFileSize(videoFile), - OutputPath = videoFile + OutputPath = new OsPath(videoFile) }; if (_diskProvider.IsFileLocked(videoFile)) @@ -137,7 +137,7 @@ namespace NzbDrone.Core.Download.Clients.UsenetBlackhole return new DownloadClientStatus { IsLocalhost = true, - OutputRootFolders = new List { Settings.WatchFolder } + OutputRootFolders = new List { new OsPath(Settings.WatchFolder) } }; } diff --git a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs index 9e419e935..48ba7c935 100644 --- a/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs +++ b/src/NzbDrone.Core/Download/Clients/uTorrent/UTorrent.cs @@ -102,19 +102,15 @@ namespace NzbDrone.Core.Download.Clients.UTorrent item.RemainingTime = TimeSpan.FromSeconds(torrent.Eta); } - var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, torrent.RootDownloadPath); + var outputPath = _remotePathMappingService.RemapRemoteToLocal(Settings.Host, new OsPath(torrent.RootDownloadPath)); - if (outputPath == null || Path.GetFileName(outputPath) == torrent.Name) + if (outputPath == null || outputPath.FileName == torrent.Name) { item.OutputPath = outputPath; } - else if (OsInfo.IsWindows && outputPath.EndsWith(":")) - { - item.OutputPath = Path.Combine(outputPath + Path.DirectorySeparatorChar, torrent.Name); - } else { - item.OutputPath = Path.Combine(outputPath, torrent.Name); + item.OutputPath = outputPath + torrent.Name; } if (torrent.Status.HasFlag(UTorrentTorrentStatus.Error)) @@ -162,20 +158,20 @@ namespace NzbDrone.Core.Download.Clients.UTorrent { var config = _proxy.GetConfig(Settings); - String destDir = null; + OsPath destDir = new OsPath(null); if (config.GetValueOrDefault("dir_active_download_flag") == "true") { - destDir = config.GetValueOrDefault("dir_active_download"); + destDir = new OsPath(config.GetValueOrDefault("dir_active_download")); } if (config.GetValueOrDefault("dir_completed_download_flag") == "true") { - destDir = config.GetValueOrDefault("dir_completed_download"); + destDir = new OsPath(config.GetValueOrDefault("dir_completed_download")); if (config.GetValueOrDefault("dir_add_label") == "true") { - destDir = Path.Combine(destDir, Settings.TvCategory); + destDir = destDir + Settings.TvCategory; } } @@ -184,9 +180,9 @@ namespace NzbDrone.Core.Download.Clients.UTorrent IsLocalhost = Settings.Host == "127.0.0.1" || Settings.Host == "localhost" }; - if (destDir != null) + if (!destDir.IsEmpty) { - status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }; + status.OutputRootFolders = new List { _remotePathMappingService.RemapRemoteToLocal(Settings.Host, destDir) }; } return status; diff --git a/src/NzbDrone.Core/Download/CompletedDownloadService.cs b/src/NzbDrone.Core/Download/CompletedDownloadService.cs index 54cd15cd6..f9d6462bf 100644 --- a/src/NzbDrone.Core/Download/CompletedDownloadService.cs +++ b/src/NzbDrone.Core/Download/CompletedDownloadService.cs @@ -79,16 +79,16 @@ namespace NzbDrone.Core.Download else { - var downloadedEpisodesFolder = _configService.DownloadedEpisodesFolder; + var downloadedEpisodesFolder = new OsPath(_configService.DownloadedEpisodesFolder); var downloadItemOutputPath = trackedDownload.DownloadItem.OutputPath; - if (downloadItemOutputPath.IsNullOrWhiteSpace()) + if (downloadItemOutputPath.IsEmpty) { UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Download doesn't contain intermediate path, ignoring download."); return; } - if (!downloadedEpisodesFolder.IsNullOrWhiteSpace() && (downloadedEpisodesFolder.PathEquals(downloadItemOutputPath) || downloadedEpisodesFolder.IsParentPath(downloadItemOutputPath))) + if (!downloadedEpisodesFolder.IsEmpty && downloadedEpisodesFolder.Contains(downloadItemOutputPath)) { UpdateStatusMessage(trackedDownload, LogLevel.Warn, "Intermediate Download path inside drone factory, ignoring download."); return; @@ -113,7 +113,7 @@ namespace NzbDrone.Core.Download public List Import(TrackedDownload trackedDownload, String overrideOutputPath = null) { var importResults = new List(); - var outputPath = overrideOutputPath ?? trackedDownload.DownloadItem.OutputPath; + var outputPath = overrideOutputPath ?? trackedDownload.DownloadItem.OutputPath.FullPath; if (_diskProvider.FolderExists(outputPath)) { @@ -216,16 +216,16 @@ namespace NzbDrone.Core.Download _logger.Debug("[{0}] Removing completed download from history.", trackedDownload.DownloadItem.Title); downloadClient.RemoveItem(trackedDownload.DownloadItem.DownloadClientId); - if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath)) + if (_diskProvider.FolderExists(trackedDownload.DownloadItem.OutputPath.FullPath)) { _logger.Debug("Removing completed download directory: {0}", trackedDownload.DownloadItem.OutputPath); - _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath, true); + _diskProvider.DeleteFolder(trackedDownload.DownloadItem.OutputPath.FullPath, true); } - else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath)) + else if (_diskProvider.FileExists(trackedDownload.DownloadItem.OutputPath.FullPath)) { _logger.Debug("Removing completed download file: {0}", trackedDownload.DownloadItem.OutputPath); - _diskProvider.DeleteFile(trackedDownload.DownloadItem.OutputPath); + _diskProvider.DeleteFile(trackedDownload.DownloadItem.OutputPath.FullPath); } trackedDownload.State = TrackedDownloadState.Removed; diff --git a/src/NzbDrone.Core/Download/DownloadClientItem.cs b/src/NzbDrone.Core/Download/DownloadClientItem.cs index e82cde7bd..11c360994 100644 --- a/src/NzbDrone.Core/Download/DownloadClientItem.cs +++ b/src/NzbDrone.Core/Download/DownloadClientItem.cs @@ -1,4 +1,6 @@ using System; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Parser.Model; namespace NzbDrone.Core.Download { @@ -14,7 +16,7 @@ namespace NzbDrone.Core.Download public TimeSpan? DownloadTime { get; set; } public TimeSpan? RemainingTime { get; set; } - public String OutputPath { get; set; } + public OsPath OutputPath { get; set; } public String Message { get; set; } public DownloadItemStatus Status { get; set; } diff --git a/src/NzbDrone.Core/Download/DownloadClientStatus.cs b/src/NzbDrone.Core/Download/DownloadClientStatus.cs index ef4f71b38..6bb47c3a2 100644 --- a/src/NzbDrone.Core/Download/DownloadClientStatus.cs +++ b/src/NzbDrone.Core/Download/DownloadClientStatus.cs @@ -2,12 +2,13 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using NzbDrone.Common.Disk; namespace NzbDrone.Core.Download { public class DownloadClientStatus { public Boolean IsLocalhost { get; set; } - public List OutputRootFolders { get; set; } + public List OutputRootFolders { get; set; } } } diff --git a/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs index 6a0ca2f7a..eb21be154 100644 --- a/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs +++ b/src/NzbDrone.Core/HealthCheck/Checks/ImportMechanismCheck.cs @@ -25,12 +25,12 @@ namespace NzbDrone.Core.HealthCheck.Checks public override HealthCheck Check() { - var droneFactoryFolder = _configService.DownloadedEpisodesFolder; + var droneFactoryFolder = new OsPath(_configService.DownloadedEpisodesFolder); var downloadClients = _provideDownloadClient.GetDownloadClients().Select(v => new { downloadClient = v, status = v.GetStatus() }).ToList(); var downloadClientIsLocalHost = downloadClients.All(v => v.status.IsLocalhost); - var downloadClientOutputInDroneFactory = !droneFactoryFolder.IsNullOrWhiteSpace() - && downloadClients.Any(v => v.status.OutputRootFolders != null && v.status.OutputRootFolders.Contains(droneFactoryFolder, PathEqualityComparer.Instance)); + var downloadClientOutputInDroneFactory = !droneFactoryFolder.IsEmpty + && downloadClients.Any(v => v.status.OutputRootFolders != null && v.status.OutputRootFolders.Any(droneFactoryFolder.Contains)); if (!_configService.IsDefined("EnableCompletedDownloadHandling")) { @@ -66,14 +66,14 @@ namespace NzbDrone.Core.HealthCheck.Checks } } - if (!_configService.EnableCompletedDownloadHandling && droneFactoryFolder.IsNullOrWhiteSpace()) + if (!_configService.EnableCompletedDownloadHandling && droneFactoryFolder.IsEmpty) { return new HealthCheck(GetType(), HealthCheckResult.Warning, "Enable Completed Download Handling or configure Drone factory"); } - if (_configService.EnableCompletedDownloadHandling && !droneFactoryFolder.IsNullOrWhiteSpace()) + if (_configService.EnableCompletedDownloadHandling && !droneFactoryFolder.IsEmpty) { - if (_downloadTrackingService.GetCompletedDownloads().Any(v => droneFactoryFolder.PathEquals(v.DownloadItem.OutputPath) || droneFactoryFolder.IsParentPath(v.DownloadItem.OutputPath))) + if (_downloadTrackingService.GetCompletedDownloads().Any(v => droneFactoryFolder.Contains(v.DownloadItem.OutputPath))) { return new HealthCheck(GetType(), HealthCheckResult.Warning, "Completed Download Handling conflict with Drone Factory (Conflicting History Item)", "Migrating-to-Completed-Download-Handling#conflicting-download-client-category"); } diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index a1efd10cc..fc25420d8 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -148,6 +148,7 @@ + diff --git a/src/NzbDrone.Core/RemotePathMappings/RemotePathMapping.cs b/src/NzbDrone.Core/RemotePathMappings/RemotePathMapping.cs index 17b2c8692..e2cdc4c17 100644 --- a/src/NzbDrone.Core/RemotePathMappings/RemotePathMapping.cs +++ b/src/NzbDrone.Core/RemotePathMappings/RemotePathMapping.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using NzbDrone.Common.Disk; using NzbDrone.Core.Datastore; diff --git a/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs b/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs index dda99ae13..7a69a1b7f 100644 --- a/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs +++ b/src/NzbDrone.Core/RemotePathMappings/RemotePathMappingService.cs @@ -22,11 +22,11 @@ namespace NzbDrone.Core.RemotePathMappings RemotePathMapping Get(int id); RemotePathMapping Update(RemotePathMapping mapping); - String RemapRemoteToLocal(String host, String remotePath); - String RemapLocalToRemote(String host, String localPath); + OsPath RemapRemoteToLocal(String host, OsPath remotePath); + OsPath RemapLocalToRemote(String host, OsPath localPath); // TODO: Remove around January 2015. Used to migrate legacy Local Category Path settings. - void MigrateLocalCategoryPath(Int32 downloadClientId, IProviderConfig newSettings, String host, String remotePath, String localPath); + void MigrateLocalCategoryPath(Int32 downloadClientId, IProviderConfig newSettings, String host, OsPath remotePath, OsPath localPath); } public class RemotePathMappingService : IRemotePathMappingService @@ -60,8 +60,8 @@ namespace NzbDrone.Core.RemotePathMappings public RemotePathMapping Add(RemotePathMapping mapping) { - mapping.LocalPath = CleanPath(mapping.LocalPath); - mapping.RemotePath = CleanPath(mapping.RemotePath); + mapping.LocalPath = new OsPath(mapping.LocalPath).AsDirectory().FullPath; + mapping.RemotePath = new OsPath(mapping.RemotePath).AsDirectory().FullPath; var all = All(); @@ -106,17 +106,20 @@ namespace NzbDrone.Core.RemotePathMappings throw new ArgumentException("Invalid Host"); } - if (mapping.RemotePath.IsNullOrWhiteSpace()) + var remotePath = new OsPath(mapping.RemotePath); + var localPath = new OsPath(mapping.LocalPath); + + if (remotePath.IsEmpty) { throw new ArgumentException("Invalid RemotePath"); } - if (mapping.LocalPath.IsNullOrWhiteSpace() || !Path.IsPathRooted(mapping.LocalPath)) + if (localPath.IsEmpty || !localPath.IsRooted) { throw new ArgumentException("Invalid LocalPath"); } - if (!_diskProvider.FolderExists(mapping.LocalPath)) + if (!_diskProvider.FolderExists(localPath.FullPath)) { throw new DirectoryNotFoundException("Can't add mount point directory that doesn't exist."); } @@ -127,27 +130,18 @@ namespace NzbDrone.Core.RemotePathMappings } } - public String RemapRemoteToLocal(String host, String remotePath) + public OsPath RemapRemoteToLocal(String host, OsPath remotePath) { - if (remotePath.IsNullOrWhiteSpace()) + if (remotePath.IsEmpty) { return remotePath; } - var cleanRemotePath = CleanPath(remotePath); - foreach (var mapping in All()) { - if (host == mapping.Host && cleanRemotePath.StartsWith(mapping.RemotePath)) + if (host == mapping.Host && new OsPath(mapping.RemotePath).Contains(remotePath)) { - var localPath = mapping.LocalPath + cleanRemotePath.Substring(mapping.RemotePath.Length); - - localPath = localPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar); - - if (!remotePath.EndsWith("/") && !remotePath.EndsWith("\\")) - { - localPath = localPath.TrimEnd('/', '\\'); - } + var localPath = new OsPath(mapping.LocalPath) + (remotePath - new OsPath(mapping.RemotePath)); return localPath; } @@ -156,29 +150,18 @@ namespace NzbDrone.Core.RemotePathMappings return remotePath; } - public String RemapLocalToRemote(String host, String localPath) + public OsPath RemapLocalToRemote(String host, OsPath localPath) { - if (localPath.IsNullOrWhiteSpace()) + if (localPath.IsEmpty) { return localPath; } - var cleanLocalPath = CleanPath(localPath); - foreach (var mapping in All()) { - if (host != mapping.Host) continue; - - if (cleanLocalPath.StartsWith(mapping.LocalPath)) + if (host == mapping.Host && new OsPath(mapping.LocalPath).Contains(localPath)) { - var remotePath = mapping.RemotePath + cleanLocalPath.Substring(mapping.LocalPath.Length); - - remotePath = remotePath.Replace(Path.DirectorySeparatorChar, mapping.RemotePath.Contains('\\') ? '\\' : '/'); - - if (!localPath.EndsWith("/") && !localPath.EndsWith("\\")) - { - remotePath = remotePath.TrimEnd('/', '\\'); - } + var remotePath = new OsPath(mapping.RemotePath) + (localPath - new OsPath(mapping.LocalPath)); return remotePath; } @@ -188,35 +171,20 @@ namespace NzbDrone.Core.RemotePathMappings } // TODO: Remove around January 2015. Used to migrate legacy Local Category Path settings. - public void MigrateLocalCategoryPath(Int32 downloadClientId, IProviderConfig newSettings, String host, String remotePath, String localPath) + public void MigrateLocalCategoryPath(Int32 downloadClientId, IProviderConfig newSettings, String host, OsPath remotePath, OsPath localPath) { _logger.Debug("Migrating local category path for Host {0}/{1} to {2}", host, remotePath, localPath); var existingMappings = All().Where(v => v.Host == host).ToList(); - remotePath = CleanPath(remotePath); - localPath = CleanPath(localPath); - - if (!existingMappings.Any(v => v.LocalPath == localPath && v.RemotePath == remotePath)) + if (!existingMappings.Any(v => new OsPath(v.LocalPath) == localPath && new OsPath(v.RemotePath) == remotePath)) { - Add(new RemotePathMapping { Host = host, RemotePath = remotePath, LocalPath = localPath }); + Add(new RemotePathMapping { Host = host, RemotePath = remotePath.FullPath, LocalPath = localPath.FullPath }); } var downloadClient = _downloadClientRepository.Get(downloadClientId); downloadClient.Settings = newSettings; _downloadClientRepository.Update(downloadClient); } - - private static String CleanPath(String path) - { - if (path.StartsWith(@"\\") || path.Contains(':')) - { - return path.TrimEnd('\\', '/').Replace('/', '\\') + "\\"; - } - else - { - return path.TrimEnd('\\', '/').Replace('\\', '/') + "/"; - } - } } } \ No newline at end of file