diff --git a/src/NzbDrone.Core/Download/DownloadClientPath.cs b/src/NzbDrone.Core/Download/DownloadClientPath.cs new file mode 100644 index 000000000..e3891e4a6 --- /dev/null +++ b/src/NzbDrone.Core/Download/DownloadClientPath.cs @@ -0,0 +1,8 @@ +namespace NzbDrone.Core.Download +{ + public class DownloadClientPath + { + public int DownloadClientId { get; set; } + public string Path { get; set; } + } +} diff --git a/src/NzbDrone.Core/NzbDrone.Core.csproj b/src/NzbDrone.Core/NzbDrone.Core.csproj index e729263de..b88d16322 100644 --- a/src/NzbDrone.Core/NzbDrone.Core.csproj +++ b/src/NzbDrone.Core/NzbDrone.Core.csproj @@ -503,6 +503,7 @@ + @@ -947,7 +948,10 @@ + + + diff --git a/src/NzbDrone.Core/TransferProviders/ITransferProvider.cs b/src/NzbDrone.Core/TransferProviders/ITransferProvider.cs index 9f35e6476..e06b85d53 100644 --- a/src/NzbDrone.Core/TransferProviders/ITransferProvider.cs +++ b/src/NzbDrone.Core/TransferProviders/ITransferProvider.cs @@ -7,14 +7,10 @@ namespace NzbDrone.Core.TransferProviders { public interface ITransferProvider : IProvider { - // TODO: Perhaps change 'string' to 'DownloadClientPath' struct/class so we're more typesafe. - // Whether the TransferProvider is ready to be accessed. (Useful for external transfers that may not have finished yet) - bool IsAvailable(string downloadClientPath); - bool IsAvailable(DownloadClientItem item); + bool IsAvailable(DownloadClientPath item); - // Returns a wrapper for the specific download. Optionally we might want to supply a 'tempDir' that's close to the series path, in case the TransferProvider needs an intermediate location. - IVirtualDiskProvider GetFileSystemWrapper(string downloadClientPath); - IVirtualDiskProvider GetFileSystemWrapper(DownloadClientItem item); + // Returns a wrapper for the specific download. Optionally we might want to supply a 'tempPath' that's close to the series path, in case the TransferProvider needs an intermediate location. + IVirtualDiskProvider GetFileSystemWrapper(DownloadClientPath item, string tempPath = null); } } diff --git a/src/NzbDrone.Core/TransferProviders/IVirtualDiskProvider.cs b/src/NzbDrone.Core/TransferProviders/IVirtualDiskProvider.cs index a5542028c..1b591c78d 100644 --- a/src/NzbDrone.Core/TransferProviders/IVirtualDiskProvider.cs +++ b/src/NzbDrone.Core/TransferProviders/IVirtualDiskProvider.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using NzbDrone.Common.Disk; @@ -16,6 +17,9 @@ namespace NzbDrone.Core.TransferProviders // Returns recursive list of all files in the 'volume'/'filesystem'/'dataset' (whatever we want to call it). string[] GetFiles(); + // Opens a readable stream. + Stream OpenFile(string vfsFilePath); + // Copies file from the virtual filesystem to the actual one. TransferTask CopyFile(string vfsSourcePath, string destinationPath); diff --git a/src/NzbDrone.Core/TransferProviders/Providers/DefaultTransfer.cs b/src/NzbDrone.Core/TransferProviders/Providers/DefaultTransfer.cs index 83c1cb8e2..98e652cd9 100644 --- a/src/NzbDrone.Core/TransferProviders/Providers/DefaultTransfer.cs +++ b/src/NzbDrone.Core/TransferProviders/Providers/DefaultTransfer.cs @@ -1,7 +1,12 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.EnsureThat; +using NzbDrone.Core.Download; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.TransferProviders.Providers @@ -9,6 +14,19 @@ namespace NzbDrone.Core.TransferProviders.Providers // Represents a local filesystem transfer. class DefaultTransfer : TransferProviderBase { + private readonly Logger _logger; + private readonly IDiskProvider _diskProvider; + private readonly IDiskTransferService _transferService; + + public override string Name => "Default"; + + public DefaultTransfer(IDiskTransferService transferService, IDiskProvider diskProvider, Logger logger) + { + _logger = logger; + _diskProvider = diskProvider; + _transferService = transferService; + } + public override IEnumerable DefaultDefinitions { get @@ -23,19 +41,39 @@ namespace NzbDrone.Core.TransferProviders.Providers }; } } - public override string Link + + public override ValidationResult Test() { - get { throw new NotImplementedException(); } + throw new NotImplementedException(); } - public override string Name + public override bool IsAvailable(DownloadClientPath item) { - get { throw new NotImplementedException(); } + if (item == null) return false; + + var path = ResolvePath(item); + + return _diskProvider.FolderExists(path) || _diskProvider.FileExists(path); } - public override ValidationResult Test() + // TODO: Give DirectVirtualDiskProvider the tempPath. + public override IVirtualDiskProvider GetFileSystemWrapper(DownloadClientPath item, string tempPath = null) { - throw new NotImplementedException(); + var path = ResolvePath(item); + + if (_diskProvider.FolderExists(path) || _diskProvider.FileExists(path)) + { + // Expose a virtual filesystem with only that directory/file in it. + // This allows the caller to delete the directory if desired, but not it's siblings. + return new DirectVirtualDiskProvider(_diskProvider, _transferService, Path.GetDirectoryName(path), path); + } + + return new EmptyVirtualDiskProvider(); + } + + protected string ResolvePath(DownloadClientPath path) + { + return path.Path; } } } diff --git a/src/NzbDrone.Core/TransferProviders/Providers/DirectVirtualDiskProvider.cs b/src/NzbDrone.Core/TransferProviders/Providers/DirectVirtualDiskProvider.cs new file mode 100644 index 000000000..b05695361 --- /dev/null +++ b/src/NzbDrone.Core/TransferProviders/Providers/DirectVirtualDiskProvider.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using NzbDrone.Common.Timeline; +using NzbDrone.Common.TPL; + +namespace NzbDrone.Core.TransferProviders.Providers +{ + public class DirectVirtualDiskProvider : IVirtualDiskProvider + { + private readonly IDiskProvider _diskProvider; + private readonly IDiskTransferService _transferService; + private readonly string _rootFolder; + private readonly List _items; + + public bool SupportStreaming => true; + + public DirectVirtualDiskProvider(IDiskProvider diskProvider, IDiskTransferService transferService, string rootFolder, params string[] items) + { + _diskProvider = diskProvider; + _transferService = transferService; + _rootFolder = rootFolder; + _items = items.ToList(); + } + + public string[] GetFiles() + { + return _items.SelectMany(GetFiles).Select(_rootFolder.GetRelativePath).ToArray(); + } + + private string[] GetFiles(string sourcePath) + { + if (_diskProvider.FileExists(sourcePath)) + { + return new [] { sourcePath }; + } + else + { + return _diskProvider.GetFiles(sourcePath, SearchOption.AllDirectories); + } + } + + public TransferTask MoveFile(string vfsSourcePath, string destinationPath) + { + return TransferFile(vfsSourcePath, destinationPath, TransferMode.Move); + } + + public TransferTask CopyFile(string vfsSourcePath, string destinationPath) + { + return TransferFile(vfsSourcePath, destinationPath, TransferMode.Copy); + } + + private TransferTask TransferFile(string vfsSourcePath, string destinationPath, TransferMode mode) + { + var sourcePath = ResolveVirtualPath(vfsSourcePath); + + var fileSize = _diskProvider.GetFileSize(sourcePath); + var progress = new TimelineContext($"{mode} {Path.GetFileName(sourcePath)}", 0, fileSize); + var task = Task.Factory.StartNew(() => + { + progress.UpdateState(TimelineState.Started); + _transferService.TransferFile(sourcePath, destinationPath, mode); + if (mode == TransferMode.Move && _items.Contains(vfsSourcePath)) + { + // If it was moved, then remove it from the list. + _items.Remove(vfsSourcePath); + } + progress.FinishProgress(); + }); + + return new TransferTask(progress, task); + } + + public Stream OpenFile(string vfsFilePath) + { + var sourcePath = ResolveVirtualPath(vfsFilePath); + + return _diskProvider.OpenReadStream(sourcePath); + } + + private string ResolveVirtualPath(string virtualPath) + { + if (Path.IsPathRooted(virtualPath)) + { + throw new InvalidOperationException("Path not valid in the virtual filesystem"); + } + + var basePath = virtualPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar)[0]; + + if (!_items.Contains(basePath)) + { + throw new InvalidOperationException("Path not valid in the virtual filesystem"); + } + + return Path.Combine(_rootFolder, virtualPath); + } + } +} diff --git a/src/NzbDrone.Core/TransferProviders/Providers/Dummy.cs b/src/NzbDrone.Core/TransferProviders/Providers/Dummy.cs deleted file mode 100644 index 40aaa8538..000000000 --- a/src/NzbDrone.Core/TransferProviders/Providers/Dummy.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using FluentValidation.Results; -using NzbDrone.Core.ThingiProvider; - -namespace NzbDrone.Core.TransferProviders.Providers -{ - // Marks the files are permanently unavailable. Perhaps useful in fire-and-forget. - class Dummy : TransferProviderBase - { - public override string Link - { - get { throw new NotImplementedException(); } - } - - public override string Name - { - get { throw new NotImplementedException(); } - } - - public override ValidationResult Test() - { - throw new NotImplementedException(); - } - } -} diff --git a/src/NzbDrone.Core/TransferProviders/Providers/EmptyVirtualDiskProvider.cs b/src/NzbDrone.Core/TransferProviders/Providers/EmptyVirtualDiskProvider.cs new file mode 100644 index 000000000..40c32e5b5 --- /dev/null +++ b/src/NzbDrone.Core/TransferProviders/Providers/EmptyVirtualDiskProvider.cs @@ -0,0 +1,32 @@ +using System; +using System.IO; +using System.Linq; + +namespace NzbDrone.Core.TransferProviders.Providers +{ + public class EmptyVirtualDiskProvider : IVirtualDiskProvider + { + public bool SupportStreaming => true; + + + public string[] GetFiles() + { + return new string[0]; + } + + public TransferTask MoveFile(string vfsSourcePath, string destinationPath) + { + throw new FileNotFoundException("File not found in virtual filesystem", vfsSourcePath); + } + + public TransferTask CopyFile(string vfsSourcePath, string destinationPath) + { + throw new FileNotFoundException("File not found in virtual filesystem", vfsSourcePath); + } + + public Stream OpenFile(string vfsFilePath) + { + throw new FileNotFoundException("File not found in virtual filesystem", vfsFilePath); + } + } +} diff --git a/src/NzbDrone.Core/TransferProviders/Providers/MountTransfer.cs b/src/NzbDrone.Core/TransferProviders/Providers/MountTransfer.cs index 38a7252d0..d2b1880aa 100644 --- a/src/NzbDrone.Core/TransferProviders/Providers/MountTransfer.cs +++ b/src/NzbDrone.Core/TransferProviders/Providers/MountTransfer.cs @@ -1,6 +1,10 @@ using System; +using System.IO; using System.Linq; using FluentValidation.Results; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Core.Download; using NzbDrone.Core.ThingiProvider; using NzbDrone.Core.Validation; @@ -21,18 +25,51 @@ namespace NzbDrone.Core.TransferProviders.Providers public class MountTransfer : TransferProviderBase { - public override string Link + private readonly Logger _logger; + private readonly IDiskProvider _diskProvider; + private readonly IDiskTransferService _transferService; + + public override string Name => "Mount"; + + public MountTransfer(IDiskTransferService transferService, IDiskProvider diskProvider, Logger logger) { - get { throw new NotImplementedException(); } + _logger = logger; + _diskProvider = diskProvider; + _transferService = transferService; } - public override string Name + public override ValidationResult Test() { - get { throw new NotImplementedException(); } + throw new NotImplementedException(); } - public override ValidationResult Test() + public override bool IsAvailable(DownloadClientPath item) + { + if (item == null) return false; + + var path = ResolvePath(item); + + return _diskProvider.FolderExists(path) || _diskProvider.FileExists(path); + } + + // TODO: Give MountVirtualDiskProvider the tempPath. + public override IVirtualDiskProvider GetFileSystemWrapper(DownloadClientPath item, string tempPath = null) + { + var path = ResolvePath(item); + + if (_diskProvider.FolderExists(path) || _diskProvider.FileExists(path)) + { + // Expose a virtual filesystem with only that directory/file in it. + // This allows the caller to delete the directory if desired, but not it's siblings. + return new MountVirtualDiskProvider(_diskProvider, _transferService, Path.GetDirectoryName(path), path); + } + + return new EmptyVirtualDiskProvider(); + } + + protected string ResolvePath(DownloadClientPath path) { + // Same logic as RemotePathMapping service. throw new NotImplementedException(); } } diff --git a/src/NzbDrone.Core/TransferProviders/Providers/MountVirtualDiskProvider.cs b/src/NzbDrone.Core/TransferProviders/Providers/MountVirtualDiskProvider.cs new file mode 100644 index 000000000..257f3be21 --- /dev/null +++ b/src/NzbDrone.Core/TransferProviders/Providers/MountVirtualDiskProvider.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using NzbDrone.Common.Disk; + +namespace NzbDrone.Core.TransferProviders.Providers +{ + // Empty wrapper, it would server in dealing with stuff being slower and remote mounts potentially being unavailable temporarily. + // Ideally it should wrap a DirectVirtualDiskProvider instance, rather than inheriting from it. + public class MountVirtualDiskProvider : DirectVirtualDiskProvider + { + public MountVirtualDiskProvider(IDiskProvider diskProvider, IDiskTransferService transferService, string rootFolder, params string[] items) + : base(diskProvider, transferService, rootFolder, items) + { + + } + } +} diff --git a/src/NzbDrone.Core/TransferProviders/TransferProviderBase.cs b/src/NzbDrone.Core/TransferProviders/TransferProviderBase.cs index 3155ba4cf..fea5b8900 100644 --- a/src/NzbDrone.Core/TransferProviders/TransferProviderBase.cs +++ b/src/NzbDrone.Core/TransferProviders/TransferProviderBase.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using FluentValidation.Results; +using NzbDrone.Core.Download; using NzbDrone.Core.ThingiProvider; namespace NzbDrone.Core.TransferProviders @@ -19,8 +20,10 @@ namespace NzbDrone.Core.TransferProviders public ProviderDefinition Definition { get; set; } public abstract ValidationResult Test(); - public abstract string Link { get; } - public virtual object RequestAction(string action, IDictionary query) { return null; } + + public abstract bool IsAvailable(DownloadClientPath item); + + public abstract IVirtualDiskProvider GetFileSystemWrapper(DownloadClientPath item, string tempPath = null); } } diff --git a/src/NzbDrone.Core/TransferProviders/TransferTask.cs b/src/NzbDrone.Core/TransferProviders/TransferTask.cs index f306195d1..2fecbb0c6 100644 --- a/src/NzbDrone.Core/TransferProviders/TransferTask.cs +++ b/src/NzbDrone.Core/TransferProviders/TransferTask.cs @@ -2,13 +2,22 @@ using System.Collections.Generic; using System.Linq; using System.Text; +using System.Threading.Tasks; +using NzbDrone.Common.Timeline; namespace NzbDrone.Core.TransferProviders { public class TransferTask { - // TODO: Progress reporting + public ITimelineContext Timeline { get; private set; } - // TODO: Async task or waitable object so Importing can handle. + // Async task that is completed once the Transfer has finished or ended in failure. (Do not rely on ProgressReporter for finished detection) + public Task CompletionTask { get; private set; } + + public TransferTask(ITimelineContext timeline, Task completionTask) + { + Timeline = timeline; + CompletionTask = completionTask.ContinueWith(t => Timeline.FinishProgress()); + } } }