diff --git a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj index dddbd0d7f..ce3d9dcdf 100644 --- a/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj +++ b/src/NzbDrone.Api.Test/NzbDrone.Api.Test.csproj @@ -69,7 +69,6 @@ - diff --git a/src/NzbDrone.Api/Directories/DirectoryLookupService.cs b/src/NzbDrone.Api/Directories/DirectoryLookupService.cs deleted file mode 100644 index 095e490b2..000000000 --- a/src/NzbDrone.Api/Directories/DirectoryLookupService.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using NzbDrone.Common.Disk; - -namespace NzbDrone.Api.Directories -{ - public interface IDirectoryLookupService - { - List LookupSubDirectories(string query); - } - - public class DirectoryLookupService : IDirectoryLookupService - { - private readonly IDiskProvider _diskProvider; - private readonly HashSet _setToRemove = new HashSet { "$Recycle.Bin", "System Volume Information" }; - - public DirectoryLookupService(IDiskProvider diskProvider) - { - _diskProvider = diskProvider; - } - - public List LookupSubDirectories(string query) - { - var dirs = new List(); - var lastSeparatorIndex = query.LastIndexOf(Path.DirectorySeparatorChar); - var path = query.Substring(0, lastSeparatorIndex + 1); - - if (lastSeparatorIndex != -1) - { - dirs = GetSubDirectories(path); - dirs.RemoveAll(x => _setToRemove.Contains(new DirectoryInfo(x).Name)); - } - - return dirs; - } - - private List GetSubDirectories(string path) - { - try - { - return _diskProvider.GetDirectories(path).ToList(); - } - catch (DirectoryNotFoundException) - { - return new List(); - } - catch (ArgumentException) - { - return new List(); - } - catch (IOException) - { - return new List(); - } - } - } -} diff --git a/src/NzbDrone.Api/Directories/DirectoryModule.cs b/src/NzbDrone.Api/Directories/DirectoryModule.cs deleted file mode 100644 index d15a00a4c..000000000 --- a/src/NzbDrone.Api/Directories/DirectoryModule.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Nancy; -using NzbDrone.Api.Extensions; -using NzbDrone.Common.Extensions; - -namespace NzbDrone.Api.Directories -{ - public class DirectoryModule : NzbDroneApiModule - { - private readonly IDirectoryLookupService _directoryLookupService; - - public DirectoryModule(IDirectoryLookupService directoryLookupService) - : base("/directories") - { - _directoryLookupService = directoryLookupService; - Get["/"] = x => GetDirectories(); - } - - private Response GetDirectories() - { - if (!Request.Query.query.HasValue) - return new List().AsResponse(); - - string query = Request.Query.query.Value; - - var dirs = _directoryLookupService.LookupSubDirectories(query) - .Select(p => p.GetActualCasing()) - .ToList(); - - return dirs.AsResponse(); - } - } -} \ No newline at end of file diff --git a/src/NzbDrone.Api/FileSystem/FileSystemModule.cs b/src/NzbDrone.Api/FileSystem/FileSystemModule.cs new file mode 100644 index 000000000..64ef7d8b0 --- /dev/null +++ b/src/NzbDrone.Api/FileSystem/FileSystemModule.cs @@ -0,0 +1,33 @@ +using System; +using Nancy; +using NzbDrone.Api.Extensions; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Api.FileSystem +{ + public class FileSystemModule : NzbDroneApiModule + { + private readonly IFileSystemLookupService _fileSystemLookupService; + + public FileSystemModule(IFileSystemLookupService fileSystemLookupService) + : base("/filesystem") + { + _fileSystemLookupService = fileSystemLookupService; + Get["/"] = x => GetContents(); + } + + private Response GetContents() + { + var pathQuery = Request.Query.path; + var includeFilesQuery = Request.Query.includeFiles; + bool includeFiles = false; + + if (includeFilesQuery.HasValue) + { + includeFiles = Convert.ToBoolean(includeFilesQuery.Value); + } + + return _fileSystemLookupService.LookupContents((string)pathQuery.Value, includeFiles).AsResponse(); + } + } +} \ No newline at end of file diff --git a/src/NzbDrone.Api/NzbDrone.Api.csproj b/src/NzbDrone.Api/NzbDrone.Api.csproj index 585b40ec7..ce03f5f22 100644 --- a/src/NzbDrone.Api/NzbDrone.Api.csproj +++ b/src/NzbDrone.Api/NzbDrone.Api.csproj @@ -114,8 +114,7 @@ - - + diff --git a/src/NzbDrone.Api.Test/DirectoryLookupServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs similarity index 51% rename from src/NzbDrone.Api.Test/DirectoryLookupServiceFixture.cs rename to src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs index 35fd2d18f..81150a1e2 100644 --- a/src/NzbDrone.Api.Test/DirectoryLookupServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DirectoryLookupServiceFixture.cs @@ -1,26 +1,26 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using FluentAssertions; using Moq; using NUnit.Framework; -using NzbDrone.Api.Directories; using NzbDrone.Common.Disk; using NzbDrone.Test.Common; -namespace NzbDrone.Api.Test +namespace NzbDrone.Common.Test.DiskTests { [TestFixture] - public class DirectoryLookupServiceFixture : TestBase + public class DirectoryLookupServiceFixture : TestBase { private const string RECYCLING_BIN = "$Recycle.Bin"; private const string SYSTEM_VOLUME_INFORMATION = "System Volume Information"; - private List _folders; + private const string WINDOWS = "Windows"; + private List _folders; - [SetUp] - public void Setup() + private void SetupFolders(string root) { - _folders = new List + var folders = new List { RECYCLING_BIN, "Chocolatey", @@ -34,17 +34,10 @@ namespace NzbDrone.Api.Test SYSTEM_VOLUME_INFORMATION, "Test", "Users", - "Windows" + WINDOWS }; - } - - private void SetupFolders(string root) - { - _folders.ForEach(e => - { - e = Path.Combine(root, e); - }); + _folders = folders.Select(f => new DirectoryInfo(Path.Combine(root, f))).ToList(); } [Test] @@ -54,10 +47,10 @@ namespace NzbDrone.Api.Test SetupFolders(root); Mocker.GetMock() - .Setup(s => s.GetDirectories(It.IsAny())) - .Returns(_folders.ToArray()); + .Setup(s => s.GetDirectoryInfos(It.IsAny())) + .Returns(_folders); - Subject.LookupSubDirectories(root).Should().NotContain(Path.Combine(root, RECYCLING_BIN)); + Subject.LookupContents(root, false).Directories.Should().NotContain(Path.Combine(root, RECYCLING_BIN)); } [Test] @@ -67,27 +60,29 @@ namespace NzbDrone.Api.Test SetupFolders(root); Mocker.GetMock() - .Setup(s => s.GetDirectories(It.IsAny())) - .Returns(_folders.ToArray()); + .Setup(s => s.GetDirectoryInfos(It.IsAny())) + .Returns(_folders); - Subject.LookupSubDirectories(root).Should().NotContain(Path.Combine(root, SYSTEM_VOLUME_INFORMATION)); + Subject.LookupContents(root, false).Directories.Should().NotContain(Path.Combine(root, SYSTEM_VOLUME_INFORMATION)); } [Test] public void should_not_contain_recycling_bin_or_system_volume_information_for_root_of_drive() { - string root = @"C:\"; + string root = @"C:\".AsOsAgnostic(); SetupFolders(root); Mocker.GetMock() - .Setup(s => s.GetDirectories(It.IsAny())) - .Returns(_folders.ToArray()); + .Setup(s => s.GetDirectoryInfos(It.IsAny())) + .Returns(_folders); - var result = Subject.LookupSubDirectories(root); + var result = Subject.LookupContents(root, false); + + result.Directories.Should().HaveCount(_folders.Count - 3); - result.Should().HaveCount(_folders.Count - 2); - result.Should().NotContain(RECYCLING_BIN); - result.Should().NotContain(SYSTEM_VOLUME_INFORMATION); + result.Directories.Should().NotContain(f => f.Name == RECYCLING_BIN); + result.Directories.Should().NotContain(f => f.Name == SYSTEM_VOLUME_INFORMATION); + result.Directories.Should().NotContain(f => f.Name == WINDOWS); } } } diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs b/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs similarity index 99% rename from src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs rename to src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs index 6f36cf59b..3c98cdfe2 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/DiskProviderFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskProviderFixtureBase.cs @@ -6,7 +6,7 @@ using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Test.Common; -namespace NzbDrone.Common.Test.DiskProviderTests +namespace NzbDrone.Common.Test.DiskTests { public class DiskProviderFixtureBase : TestBase where TSubject : class, IDiskProvider { diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs b/src/NzbDrone.Common.Test/DiskTests/FreeSpaceFixtureBase.cs similarity index 97% rename from src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs rename to src/NzbDrone.Common.Test/DiskTests/FreeSpaceFixtureBase.cs index 09325c84e..a4dbe737b 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/FreeSpaceFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskTests/FreeSpaceFixtureBase.cs @@ -5,7 +5,7 @@ using NUnit.Framework; using NzbDrone.Common.Disk; using NzbDrone.Test.Common; -namespace NzbDrone.Common.Test.DiskProviderTests +namespace NzbDrone.Common.Test.DiskTests { public abstract class FreeSpaceFixtureBase : TestBase where TSubject : class, IDiskProvider { diff --git a/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs b/src/NzbDrone.Common.Test/DiskTests/IsParentFixtureBase.cs similarity index 66% rename from src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs rename to src/NzbDrone.Common.Test/DiskTests/IsParentFixtureBase.cs index 3101be597..26ea1d84b 100644 --- a/src/NzbDrone.Common.Test/DiskProviderTests/IsParentFixtureBase.cs +++ b/src/NzbDrone.Common.Test/DiskTests/IsParentFixtureBase.cs @@ -1,6 +1,6 @@ using NzbDrone.Test.Common; -namespace NzbDrone.Common.Test.DiskProviderTests +namespace NzbDrone.Common.Test.DiskTests { public class IsParentPathFixture : TestBase { diff --git a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj index 08adf6e38..9fec6430b 100644 --- a/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj +++ b/src/NzbDrone.Common.Test/NzbDrone.Common.Test.csproj @@ -68,9 +68,10 @@ - - - + + + + diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index 8040ce587..c858f417b 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Security.AccessControl; @@ -21,7 +22,6 @@ namespace NzbDrone.Common.Disk public abstract void SetPermissions(string path, string mask, string user, string group); public abstract long? GetTotalSize(string path); - public DateTime FolderGetCreationTime(string path) { CheckFolderExists(path); @@ -430,5 +430,23 @@ namespace NzbDrone.Common.Disk return new FileStream(path, FileMode.Open, FileAccess.Read); } + + public List GetDirectoryInfos(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + var di = new DirectoryInfo(path); + + return di.GetDirectories().ToList(); + } + + public List GetFileInfos(string path) + { + Ensure.That(path, () => path).IsValidPath(); + + var di = new DirectoryInfo(path); + + return di.GetFiles().ToList(); + } } } diff --git a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs new file mode 100644 index 000000000..b5b7db9b6 --- /dev/null +++ b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs @@ -0,0 +1,183 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NLog; +using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Common.Disk +{ + public interface IFileSystemLookupService + { + FileSystemResult LookupContents(string query, bool includeFiles); + } + + public class FileSystemLookupService : IFileSystemLookupService + { + private readonly IDiskProvider _diskProvider; + private readonly Logger _logger; + + private readonly HashSet _setToRemove = new HashSet + { + //Windows + "boot", + "bootmgr", + "cache", + "msocache", + "recovery", + "$recycle.bin", + "recycler", + "system volume information", + "temporary internet files", + "windows", + + //OS X + ".fseventd", + ".spotlight", + ".trashes", + ".vol", + "cachedmessages", + "caches", + "trash" + }; + + public FileSystemLookupService(IDiskProvider diskProvider, Logger logger) + { + _diskProvider = diskProvider; + _logger = logger; + } + + public FileSystemResult LookupContents(string query, bool includeFiles) + { + var result = new FileSystemResult(); + + if (query.IsNullOrWhiteSpace()) + { + if (OsInfo.IsWindows) + { + result.Directories = GetDrives(); + + return result; + } + + query = "/"; + } + + var lastSeparatorIndex = query.LastIndexOf(Path.DirectorySeparatorChar); + var path = query.Substring(0, lastSeparatorIndex + 1); + + if (lastSeparatorIndex != -1) + { + try + { + result.Parent = GetParent(path); + result.Directories = GetDirectories(path); + + if (includeFiles) + { + result.Files = GetFiles(path); + } + } + + catch (DirectoryNotFoundException) + { + return new FileSystemResult { Parent = GetParent(path) }; + } + catch (ArgumentException) + { + return new FileSystemResult(); + } + catch (IOException) + { + return new FileSystemResult { Parent = GetParent(path) }; + } + catch (UnauthorizedAccessException) + { + return new FileSystemResult { Parent = GetParent(path) }; + } + } + + return result; + } + + private List GetDrives() + { + return _diskProvider.GetFixedDrives() + .Select(d => new FileSystemModel + { + Type = FileSystemEntityType.Drive, + Name = d, + Path = d, + LastModified = null + }) + .ToList(); + } + + private List GetDirectories(string path) + { + var directories = _diskProvider.GetDirectoryInfos(path) + .Select(d => new FileSystemModel + { + Name = d.Name, + Path = GetDirectoryPath(d.FullName.GetActualCasing()), + LastModified = d.LastWriteTimeUtc, + Type = FileSystemEntityType.Folder + }) + .ToList(); + + directories.RemoveAll(d => _setToRemove.Contains(d.Name.ToLowerInvariant())); + + return directories; + } + + private List GetFiles(string path) + { + return _diskProvider.GetFileInfos(path) + .Select(d => new FileSystemModel + { + Name = d.Name, + Path = d.FullName.GetActualCasing(), + LastModified = d.LastWriteTimeUtc, + Extension = d.Extension, + Size = d.Length, + Type = FileSystemEntityType.File + }) + .ToList(); + } + + private string GetDirectoryPath(string path) + { + if (path.Last() != Path.DirectorySeparatorChar) + { + path += Path.DirectorySeparatorChar; + } + + return path; + } + + private string GetParent(string path) + { + var di = new DirectoryInfo(path); + + if (di.Parent != null) + { + var parent = di.Parent.FullName; + + if (parent.Last() != Path.DirectorySeparatorChar) + { + parent += Path.DirectorySeparatorChar; + } + + return parent; + } + + if (!path.Equals("/")) + { + return String.Empty; + } + + return null; + } + } +} diff --git a/src/NzbDrone.Common/Disk/FileSystemModel.cs b/src/NzbDrone.Common/Disk/FileSystemModel.cs new file mode 100644 index 000000000..486769932 --- /dev/null +++ b/src/NzbDrone.Common/Disk/FileSystemModel.cs @@ -0,0 +1,22 @@ +using System; + +namespace NzbDrone.Common.Disk +{ + public class FileSystemModel + { + public FileSystemEntityType Type { get; set; } + public string Name { get; set; } + public string Path { get; set; } + public string Extension { get; set; } + public long Size { get; set; } + public DateTime? LastModified { get; set; } + } + + public enum FileSystemEntityType + { + Parent, + Drive, + Folder, + File + } +} diff --git a/src/NzbDrone.Common/Disk/FileSystemResult.cs b/src/NzbDrone.Common/Disk/FileSystemResult.cs new file mode 100644 index 000000000..10f5cac74 --- /dev/null +++ b/src/NzbDrone.Common/Disk/FileSystemResult.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; + +namespace NzbDrone.Common.Disk +{ + public class FileSystemResult + { + public String Parent { get; set; } + public List Directories { get; set; } + public List Files { get; set; } + + public FileSystemResult() + { + Directories = new List(); + Files = new List(); + } + } +} diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index 57817c8fa..950c005db 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Security.AccessControl; using System.Security.Principal; @@ -45,5 +46,7 @@ namespace NzbDrone.Common.Disk string[] GetFixedDrives(); string GetVolumeLabel(string path); FileStream StreamFile(string path); + List GetDirectoryInfos(string path); + List GetFileInfos(string path); } -} \ No newline at end of file +} diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index da7170735..246b23919 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -71,6 +71,9 @@ + + + diff --git a/src/NzbDrone.Core.Test/IndexerTests/KickassTorrentsTests/KickassTorrentsFixture.cs b/src/NzbDrone.Core.Test/IndexerTests/KickassTorrentsTests/KickassTorrentsFixture.cs index 4fa34b012..9bfbbafd5 100644 --- a/src/NzbDrone.Core.Test/IndexerTests/KickassTorrentsTests/KickassTorrentsFixture.cs +++ b/src/NzbDrone.Core.Test/IndexerTests/KickassTorrentsTests/KickassTorrentsFixture.cs @@ -39,7 +39,7 @@ namespace NzbDrone.Core.Test.IndexerTests.KickassTorrentsTests releases.Should().HaveCount(5); releases.First().Should().BeOfType(); - var torrentInfo = releases.First() as TorrentInfo; + var torrentInfo = (TorrentInfo) releases.First(); torrentInfo.Title.Should().Be("Doctor Stranger.E03.140512.HDTV.H264.720p-iPOP.avi [CTRG]"); torrentInfo.DownloadProtocol.Should().Be(DownloadProtocol.Torrent); @@ -72,7 +72,7 @@ namespace NzbDrone.Core.Test.IndexerTests.KickassTorrentsTests [Test] public void should_not_return_unverified_releases_if_not_configured() { - (Subject.Definition.Settings as KickassTorrentsSettings).VerifiedOnly = true; + ((KickassTorrentsSettings) Subject.Definition.Settings).VerifiedOnly = true; var recentFeed = ReadAllText(@"Files/RSS/KickassTorrents.xml"); @@ -84,5 +84,6 @@ namespace NzbDrone.Core.Test.IndexerTests.KickassTorrentsTests releases.Should().HaveCount(4); } + } } diff --git a/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs b/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs index 542cf6d58..130d72e92 100644 --- a/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs +++ b/src/NzbDrone.Mono.Test/DiskProviderTests/DiskProviderFixture.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using NzbDrone.Common.Test.DiskProviderTests; +using NzbDrone.Common.Test.DiskTests; namespace NzbDrone.Mono.Test.DiskProviderTests { diff --git a/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs b/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs index 768fb73fa..cf484be1e 100644 --- a/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs +++ b/src/NzbDrone.Mono.Test/DiskProviderTests/FreeSpaceFixture.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using NzbDrone.Common.Test.DiskProviderTests; +using NzbDrone.Common.Test.DiskTests; namespace NzbDrone.Mono.Test.DiskProviderTests { diff --git a/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs b/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs index f81c379f1..88def60e3 100644 --- a/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs +++ b/src/NzbDrone.Windows.Test/DiskProviderTests/DiskProviderFixture.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using NzbDrone.Common.Test.DiskProviderTests; +using NzbDrone.Common.Test.DiskTests; namespace NzbDrone.Windows.Test.DiskProviderTests { diff --git a/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs b/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs index 0334a6d3c..384cc4ea3 100644 --- a/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs +++ b/src/NzbDrone.Windows.Test/DiskProviderTests/FreeSpaceFixture.cs @@ -1,5 +1,5 @@ using NUnit.Framework; -using NzbDrone.Common.Test.DiskProviderTests; +using NzbDrone.Common.Test.DiskTests; namespace NzbDrone.Windows.Test.DiskProviderTests { diff --git a/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.hbs b/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.hbs index 7e1ee2ea9..a04d9b288 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.hbs +++ b/src/UI/AddSeries/RootFolders/RootFolderItemViewTemplate.hbs @@ -4,7 +4,6 @@ {{Bytes freeSpace}} - -
-
+ + diff --git a/src/UI/AddSeries/RootFolders/RootFolderLayout.js b/src/UI/AddSeries/RootFolders/RootFolderLayout.js index 7a0886c6f..a313e1bd5 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderLayout.js +++ b/src/UI/AddSeries/RootFolders/RootFolderLayout.js @@ -8,14 +8,14 @@ define( 'AddSeries/RootFolders/RootFolderModel', 'Shared/LoadingView', 'Mixins/AsValidatedView', - 'Mixins/AutoComplete' + 'Mixins/FileBrowser' ], function (Marionette, RootFolderCollectionView, RootFolderCollection, RootFolderModel, LoadingView, AsValidatedView) { var layout = Marionette.Layout.extend({ template: 'AddSeries/RootFolders/RootFolderLayoutTemplate', ui: { - pathInput: '.x-path input' + pathInput: '.x-path' }, regions: { @@ -42,7 +42,7 @@ define( this._showCurrentDirs(); } - this.ui.pathInput.autoComplete('/directories'); + this.ui.pathInput.fileBrowser({ showFiles: true, showLastModified: true }); }, _onFolderSelected: function (options) { diff --git a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.hbs b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.hbs index 8209c2da6..d50f68168 100644 --- a/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.hbs +++ b/src/UI/AddSeries/RootFolders/RootFolderLayoutTemplate.hbs @@ -7,22 +7,28 @@
Enter the path that contains some or all of your TV series, you will be able to choose which series you want to import
-
-
-   - - - - +
+
+ +
+ +
+   + + +
+
- {{#if items}} -

Recent Folders

- {{/if}} -
+
+
+ {{#if items}} +

Recent Folders

+ {{/if}} +
+
+
diff --git a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js index f4ef2d225..ba2c8697a 100644 --- a/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js +++ b/src/UI/Settings/MediaManagement/Permissions/PermissionsView.js @@ -3,37 +3,11 @@ define( [ 'marionette', 'Mixins/AsModelBoundView', - 'Mixins/AsValidatedView', - 'Mixins/AutoComplete' + 'Mixins/AsValidatedView' ], function (Marionette, AsModelBoundView, AsValidatedView) { var view = Marionette.ItemView.extend({ - template: 'Settings/MediaManagement/Permissions/PermissionsViewTemplate', - - ui: { - recyclingBin : '.x-path', - failedDownloadHandlingCheckbox: '.x-failed-download-handling', - failedDownloadOptions : '.x-failed-download-options' - }, - - events: { - 'change .x-failed-download-handling': '_setFailedDownloadOptionsVisibility' - }, - - onShow: function () { - this.ui.recyclingBin.autoComplete('/directories'); - }, - - _setFailedDownloadOptionsVisibility: function () { - var checked = this.ui.failedDownloadHandlingCheckbox.prop('checked'); - if (checked) { - this.ui.failedDownloadOptions.slideDown(); - } - - else { - this.ui.failedDownloadOptions.slideUp(); - } - } + template: 'Settings/MediaManagement/Permissions/PermissionsViewTemplate' }); AsModelBoundView.call(view); diff --git a/src/UI/Shared/FileBrowser/EmptyView.js b/src/UI/Shared/FileBrowser/EmptyView.js new file mode 100644 index 000000000..6ffda684d --- /dev/null +++ b/src/UI/Shared/FileBrowser/EmptyView.js @@ -0,0 +1,11 @@ +'use strict'; + +define( + [ + 'marionette' + ], function (Marionette) { + + return Marionette.CompositeView.extend({ + template: 'Shared/FileBrowser/EmptyViewTemplate' + }); + }); diff --git a/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs b/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs new file mode 100644 index 000000000..cc3053509 --- /dev/null +++ b/src/UI/Shared/FileBrowser/EmptyViewTemplate.hbs @@ -0,0 +1,3 @@ +
+ No files/folders were found, edit the path above, or clear to start again +
diff --git a/src/UI/Shared/FileBrowser/FileBrowserCollection.js b/src/UI/Shared/FileBrowser/FileBrowserCollection.js new file mode 100644 index 000000000..78e7ad06d --- /dev/null +++ b/src/UI/Shared/FileBrowser/FileBrowserCollection.js @@ -0,0 +1,39 @@ +'use strict'; +define( + [ + 'jquery', + 'backbone', + 'Shared/FileBrowser/FileBrowserModel' + ], function ($, Backbone, FileBrowserModel) { + + return Backbone.Collection.extend({ + model: FileBrowserModel, + url : window.NzbDrone.ApiRoot + '/filesystem', + + parse: function(response) { + var contents = []; + + if (response.parent || response.parent === '') { + + var type = 'parent'; + var name = '...'; + + if (response.parent === '') { + type = 'computer'; + name = 'My Computer'; + } + + contents.push({ + type : type, + name : name, + path : response.parent + }); + } + + $.merge(contents, response.directories); + $.merge(contents, response.files); + + return contents; + } + }); + }); diff --git a/src/UI/Shared/FileBrowser/FileBrowserLayout.js b/src/UI/Shared/FileBrowser/FileBrowserLayout.js new file mode 100644 index 000000000..0eb9813b0 --- /dev/null +++ b/src/UI/Shared/FileBrowser/FileBrowserLayout.js @@ -0,0 +1,169 @@ +'use strict'; +define( + [ + 'underscore', + 'vent', + 'marionette', + 'backgrid', + 'Shared/FileBrowser/FileBrowserCollection', + 'Shared/FileBrowser/EmptyView', + 'Shared/FileBrowser/FileBrowserRow', + 'Shared/FileBrowser/FileBrowserTypeCell', + 'Shared/FileBrowser/FileBrowserNameCell', + 'Cells/RelativeDateCell', + 'Cells/FileSizeCell', + 'Shared/LoadingView', + 'Mixins/DirectoryAutoComplete' + ], function (_, + vent, + Marionette, + Backgrid, + FileBrowserCollection, + EmptyView, + FileBrowserRow, + FileBrowserTypeCell, + FileBrowserNameCell, + RelativeDateCell, + FileSizeCell, + LoadingView) { + + return Marionette.Layout.extend({ + template: 'Shared/FileBrowser/FileBrowserLayoutTemplate', + + regions: { + browser : '#x-browser' + }, + + ui: { + path: '.x-path' + }, + + events: { + 'typeahead:selected .x-path' : '_pathChanged', + 'typeahead:autocompleted .x-path' : '_pathChanged', + 'keyup .x-path' : '_inputChanged', + 'click .x-ok' : '_selectPath' + }, + + initialize: function (options) { + this.collection = new FileBrowserCollection(); + this.collection.showFiles = options.showFiles || false; + this.collection.showLastModified = options.showLastModified || false; + + this.input = options.input; + + this._setColumns(); + this._fetchCollection(this.input.val()); + this.listenTo(this.collection, 'sync', this._showGrid); + this.listenTo(this.collection, 'filebrowser:folderselected', this._rowSelected); + }, + + onRender: function () { + this.browser.show(new LoadingView()); + }, + + onShow: function () { + this.ui.path.directoryAutoComplete(); + this._updatePath(this.input.val()); + }, + + _setColumns: function () { + this.columns = [ + { + name : 'type', + label : '', + sortable : false, + cell : FileBrowserTypeCell + }, + { + name : 'name', + label : 'Name', + sortable : false, + cell : FileBrowserNameCell + } + ]; + + if (this.collection.showLastModified) { + this.columns.push({ + name : 'lastModified', + label : 'Last Modified', + sortable : false, + cell : RelativeDateCell + }); + } + + if (this.collection.showFiles) { + this.columns.push({ + name : 'size', + label : 'Size', + sortable : false, + cell : FileSizeCell + }); + } + }, + + _fetchCollection: function (path) { + var data = { + includeFiles : this.collection.showFiles + }; + + if (path) { + data.path = path; + } + + this.collection.fetch({ + data: data + }); + }, + + _showGrid: function () { + + if (this.collection.models.length === 0) { + this.browser.show(new EmptyView()); + return; + } + + var grid = new Backgrid.Grid({ + row : FileBrowserRow, + collection : this.collection, + columns : this.columns, + className : 'table table-hover' + }); + + this.browser.show(grid); + }, + + _rowSelected: function (model) { + var path = model.get('path'); + + this._updatePath(path); + this._fetchCollection(path); + }, + + _pathChanged: function (e, path) { + this._fetchCollection(path.value); + this._updatePath(path.value); + }, + + _inputChanged: function () { + var path = this.ui.path.val(); + + if (path === '' || path.endsWith('\\') || path.endsWith('/')) { + this._fetchCollection(path); + } + }, + + _updatePath: function (path) { + if (path !== undefined || path !== null) { + this.ui.path.val(path); + } + }, + + _selectPath: function () { + this.input.val(this.ui.path.val()); + this.input.trigger('change'); + + vent.trigger(vent.Commands.CloseFileBrowser); + } + }); + }); diff --git a/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs b/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs new file mode 100644 index 000000000..b225d7135 --- /dev/null +++ b/src/UI/Shared/FileBrowser/FileBrowserLayoutTemplate.hbs @@ -0,0 +1,26 @@ + diff --git a/src/UI/Shared/FileBrowser/FileBrowserModalRegion.js b/src/UI/Shared/FileBrowser/FileBrowserModalRegion.js new file mode 100644 index 000000000..c0c7037e5 --- /dev/null +++ b/src/UI/Shared/FileBrowser/FileBrowserModalRegion.js @@ -0,0 +1,63 @@ +'use strict'; +define( + [ + 'jquery', + 'backbone', + 'marionette', + 'bootstrap' + ], function ($, Backbone, Marionette) { + var region = Marionette.Region.extend({ + el: '#file-browser-modal-region', + + constructor: function () { + Backbone.Marionette.Region.prototype.constructor.apply(this, arguments); + this.on('show', this.showModal, this); + }, + + getEl: function (selector) { + var $el = $(selector); + $el.on('hidden', this.close); + return $el; + }, + + showModal: function () { + this.$el.addClass('modal fade'); + + //need tab index so close on escape works + //https://github.com/twitter/bootstrap/issues/4663 + this.$el.attr('tabindex', '-1'); + this.$el.css('z-index', '1060'); + + this.$el.modal({ + show : true, + keyboard : true, + backdrop : true + }); + + this.$el.on('hide.bs.modal', $.proxy(this._closing, this)); + + this.$el.on('shown.bs.modal', function () { + $('.modal-backdrop:last').css('z-index', 1059); + }); + + this.currentView.$el.addClass('modal-dialog'); + }, + + closeModal: function () { + $(this.el).modal('hide'); + this.reset(); + }, + + _closing: function () { + + if (this.$el) { + this.$el.off('hide.bs.modal'); + this.$el.off('shown.bs.modal'); + } + + this.reset(); + } + }); + + return region; + }); diff --git a/src/UI/Shared/FileBrowser/FileBrowserModel.js b/src/UI/Shared/FileBrowser/FileBrowserModel.js new file mode 100644 index 000000000..c426f5dbe --- /dev/null +++ b/src/UI/Shared/FileBrowser/FileBrowserModel.js @@ -0,0 +1,10 @@ +'use strict'; +define( + [ + 'backbone' + ], function (Backbone) { + return Backbone.Model.extend({ + + }); + }); + diff --git a/src/UI/Shared/FileBrowser/FileBrowserNameCell.js b/src/UI/Shared/FileBrowser/FileBrowserNameCell.js new file mode 100644 index 000000000..5e1bb38c1 --- /dev/null +++ b/src/UI/Shared/FileBrowser/FileBrowserNameCell.js @@ -0,0 +1,23 @@ +'use strict'; + +define( + [ + 'vent', + 'Cells/NzbDroneCell' + ], function (vent, NzbDroneCell) { + return NzbDroneCell.extend({ + + className: 'file-browser-name-cell', + + render: function () { + this.$el.empty(); + + var name = this.model.get(this.column.get('name')); + + this.$el.html(name); + + this.delegateEvents(); + return this; + } + }); + }); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserRow.js b/src/UI/Shared/FileBrowser/FileBrowserRow.js new file mode 100644 index 000000000..fceeb30ae --- /dev/null +++ b/src/UI/Shared/FileBrowser/FileBrowserRow.js @@ -0,0 +1,31 @@ +'use strict'; +define( + [ + 'underscore', + 'backgrid' + ], function (_, Backgrid) { + + return Backgrid.Row.extend({ + className: 'file-browser-row', + + events: { + 'click': '_selectRow' + }, + + _originalInit: Backgrid.Row.prototype.initialize, + + initialize: function () { + this._originalInit.apply(this, arguments); + }, + + _selectRow: function () { + if (this.model.get('type') === 'file') { + this.model.collection.trigger('filebrowser:fileselected', this.model); + } + + else { + this.model.collection.trigger('filebrowser:folderselected', this.model); + } + } + }); + }); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js b/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js new file mode 100644 index 000000000..de90dd2d1 --- /dev/null +++ b/src/UI/Shared/FileBrowser/FileBrowserTypeCell.js @@ -0,0 +1,40 @@ +'use strict'; + +define( + [ + 'vent', + 'Cells/NzbDroneCell' + ], function (vent, NzbDroneCell) { + return NzbDroneCell.extend({ + + className: 'file-browser-type-cell', + + render: function () { + this.$el.empty(); + + var type = this.model.get(this.column.get('name')); + var icon = 'icon-hdd'; + + if (type === 'computer') { + icon = 'icon-desktop'; + } + + else if (type === 'parent') { + icon = 'icon-level-up'; + } + + else if (type === 'folder') { + icon = 'icon-folder-close'; + } + + else if (type === 'file') { + icon = 'icon-file'; + } + + this.$el.html(''.format(icon)); + + this.delegateEvents(); + return this; + } + }); + }); \ No newline at end of file diff --git a/src/UI/Shared/FileBrowser/filebrowser.less b/src/UI/Shared/FileBrowser/filebrowser.less new file mode 100644 index 000000000..c8810147b --- /dev/null +++ b/src/UI/Shared/FileBrowser/filebrowser.less @@ -0,0 +1,24 @@ +.file-browser-row { + cursor : pointer; + + .file-size-cell { + white-space : nowrap; + } + + .relative-date-cell { + width : 120px; + white-space : nowrap; + } +} + +.file-browser-type-cell { + width : 16px; +} + +.file-browser-name-cell { + word-break : break-all; +} + +.file-browser-empty { + margin-top : 20px; +} \ No newline at end of file diff --git a/src/UI/Shared/Modal/ModalController.js b/src/UI/Shared/Modal/ModalController.js index d435d56b9..daea74c8f 100644 --- a/src/UI/Shared/Modal/ModalController.js +++ b/src/UI/Shared/Modal/ModalController.js @@ -9,8 +9,18 @@ define( 'Episode/EpisodeDetailsLayout', 'Activity/History/Details/HistoryDetailsLayout', 'System/Logs/Table/Details/LogDetailsView', - 'Rename/RenamePreviewLayout' - ], function (vent, AppLayout, Marionette, EditSeriesView, DeleteSeriesView, EpisodeDetailsLayout, HistoryDetailsLayout, LogDetailsView, RenamePreviewLayout) { + 'Rename/RenamePreviewLayout', + 'Shared/FileBrowser/FileBrowserLayout' + ], function (vent, + AppLayout, + Marionette, + EditSeriesView, + DeleteSeriesView, + EpisodeDetailsLayout, + HistoryDetailsLayout, + LogDetailsView, + RenamePreviewLayout, + FileBrowserLayout) { return Marionette.AppRouter.extend({ @@ -23,6 +33,8 @@ define( vent.on(vent.Commands.ShowHistoryDetails, this._showHistory, this); vent.on(vent.Commands.ShowLogDetails, this._showLogDetails, this); vent.on(vent.Commands.ShowRenamePreview, this._showRenamePreview, this); + vent.on(vent.Commands.ShowFileBrowser, this._showFileBrowser, this); + vent.on(vent.Commands.CloseFileBrowser, this._closeFileBrowser, this); }, _openModal: function (view) { @@ -61,6 +73,15 @@ define( _showRenamePreview: function (options) { var view = new RenamePreviewLayout(options); AppLayout.modalRegion.show(view); + }, + + _showFileBrowser: function (options) { + var view = new FileBrowserLayout(options); + AppLayout.fileBrowserModalRegion.show(view); + }, + + _closeFileBrowser: function () { + AppLayout.fileBrowserModalRegion.closeModal(); } }); }); diff --git a/src/UI/index.html b/src/UI/index.html index 5a0970c2d..ab88a6668 100644 --- a/src/UI/index.html +++ b/src/UI/index.html @@ -52,6 +52,7 @@
+