diff --git a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs index 8ebaef6fb..61dbb1ffc 100644 --- a/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs +++ b/src/NzbDrone.Common.Test/DiskTests/DiskTransferServiceFixture.cs @@ -24,6 +24,10 @@ namespace NzbDrone.Common.Test.DiskTests { Mocker.GetMock(MockBehavior.Strict); + Mocker.GetMock() + .Setup(v => v.GetMount(It.IsAny())) + .Returns((IMount)null); + WithEmulatedDiskProvider(); WithExistingFile(_sourcePath); diff --git a/src/NzbDrone.Common/Disk/DiskProviderBase.cs b/src/NzbDrone.Common/Disk/DiskProviderBase.cs index d3474c7dd..57c9e8016 100644 --- a/src/NzbDrone.Common/Disk/DiskProviderBase.cs +++ b/src/NzbDrone.Common/Disk/DiskProviderBase.cs @@ -346,12 +346,12 @@ namespace NzbDrone.Common.Disk public string[] GetFixedDrives() { - return (DriveInfo.GetDrives().Where(x => x.DriveType == DriveType.Fixed).Select(x => x.Name)).ToArray(); + return GetMounts().Where(x => x.DriveType == DriveType.Fixed).Select(x => x.RootDirectory).ToArray(); } public string GetVolumeLabel(string path) { - var driveInfo = DriveInfo.GetDrives().SingleOrDefault(d => d.Name == path); + var driveInfo = GetMounts().SingleOrDefault(d => d.RootDirectory.PathEquals(path)); if (driveInfo == null) { @@ -376,11 +376,28 @@ namespace NzbDrone.Common.Disk return new FileStream(path, FileMode.Create); } - public List GetDrives() + public virtual List GetMounts() + { + return GetDriveInfoMounts(); + } + + public virtual IMount GetMount(string path) + { + var mounts = GetMounts(); + + return mounts.Where(drive => drive.RootDirectory.PathEquals(path) || + drive.RootDirectory.IsParentPath(path)) + .OrderByDescending(drive => drive.RootDirectory.Length) + .FirstOrDefault(); + } + + protected List GetDriveInfoMounts() { return DriveInfo.GetDrives() - .Where(d => d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network) + .Where(d => d.DriveType == DriveType.Fixed || d.DriveType == DriveType.Network || d.DriveType == DriveType.Removable) .Where(d => d.IsReady) + .Select(d => new DriveInfoMount(d)) + .Cast() .ToList(); } diff --git a/src/NzbDrone.Common/Disk/DiskTransferService.cs b/src/NzbDrone.Common/Disk/DiskTransferService.cs index d670b5fdb..77b3fbdf8 100644 --- a/src/NzbDrone.Common/Disk/DiskTransferService.cs +++ b/src/NzbDrone.Common/Disk/DiskTransferService.cs @@ -45,6 +45,13 @@ namespace NzbDrone.Common.Disk } public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode, bool verified = true) + { + var verificationMode = verified ? VerificationMode : DiskTransferVerificationMode.VerifyOnly; + + return TransferFolder(sourcePath, targetPath, mode, verificationMode); + } + + public TransferMode TransferFolder(string sourcePath, string targetPath, TransferMode mode, DiskTransferVerificationMode verificationMode) { Ensure.That(sourcePath, () => sourcePath).IsValidPath(); Ensure.That(targetPath, () => targetPath).IsValidPath(); @@ -58,14 +65,14 @@ namespace NzbDrone.Common.Disk foreach (var subDir in _diskProvider.GetDirectoryInfos(sourcePath)) { - result &= TransferFolder(subDir.FullName, Path.Combine(targetPath, subDir.Name), mode, verified); + result &= TransferFolder(subDir.FullName, Path.Combine(targetPath, subDir.Name), mode, verificationMode); } foreach (var sourceFile in _diskProvider.GetFileInfos(sourcePath)) { var destFile = Path.Combine(targetPath, sourceFile.Name); - result &= TransferFile(sourceFile.FullName, destFile, mode, true, verified); + result &= TransferFile(sourceFile.FullName, destFile, mode, true, verificationMode); } if (mode.HasFlag(TransferMode.Move)) @@ -77,15 +84,17 @@ namespace NzbDrone.Common.Disk } public TransferMode TransferFile(string sourcePath, string targetPath, TransferMode mode, bool overwrite = false, bool verified = true) + { + var verificationMode = verified ? VerificationMode : DiskTransferVerificationMode.None; + + return TransferFile(sourcePath, targetPath, mode, overwrite, verificationMode); + } + + public TransferMode TransferFile(string sourcePath, string targetPath, TransferMode mode, bool overwrite, DiskTransferVerificationMode verificationMode) { Ensure.That(sourcePath, () => sourcePath).IsValidPath(); Ensure.That(targetPath, () => targetPath).IsValidPath(); - if (VerificationMode != DiskTransferVerificationMode.Transactional && VerificationMode != DiskTransferVerificationMode.TryTransactional) - { - verified = false; - } - _logger.Debug("{0} [{1}] > [{2}]", mode, sourcePath, targetPath); var originalSize = _diskProvider.GetFileSize(sourcePath); @@ -154,49 +163,59 @@ namespace NzbDrone.Common.Disk } } - if (verified) + // We force a transactional transfer if the transfer occurs between mounts and one of the mounts is cifs, it would be a copy anyway. + if (verificationMode == DiskTransferVerificationMode.TryTransactional && OsInfo.IsNotWindows) { - if (mode.HasFlag(TransferMode.Copy)) + var sourceMount = _diskProvider.GetMount(sourcePath); + var targetMount = _diskProvider.GetMount(targetPath); + + if (sourceMount != null && targetMount != null && sourceMount.RootDirectory != targetMount.RootDirectory && + (sourceMount.DriveFormat == "cifs" || targetMount.DriveFormat == "cifs")) { - if (TryCopyFileTransactional(sourcePath, targetPath, originalSize)) - { - return TransferMode.Copy; - } + verificationMode = DiskTransferVerificationMode.Transactional; } + } - if (mode.HasFlag(TransferMode.Move)) + if (mode.HasFlag(TransferMode.Copy)) + { + if (verificationMode == DiskTransferVerificationMode.Transactional || verificationMode == DiskTransferVerificationMode.TryTransactional) { - if (TryMoveFileTransactional(sourcePath, targetPath, originalSize)) + if (TryCopyFileTransactional(sourcePath, targetPath, originalSize)) { - return TransferMode.Move; + return TransferMode.Copy; } - } - throw new IOException(string.Format("Failed to completely transfer [{0}] to [{1}], aborting.", sourcePath, targetPath)); - } - else if (VerificationMode != DiskTransferVerificationMode.None) - { - if (mode.HasFlag(TransferMode.Copy)) + throw new IOException(string.Format("Failed to completely transfer [{0}] to [{1}], aborting.", sourcePath, targetPath)); + } + else if (verificationMode == DiskTransferVerificationMode.VerifyOnly) { TryCopyFileVerified(sourcePath, targetPath, originalSize); return TransferMode.Copy; } - - if (mode.HasFlag(TransferMode.Move)) + else { - TryMoveFileVerified(sourcePath, targetPath, originalSize); - return TransferMode.Move; + _diskProvider.CopyFile(sourcePath, targetPath); + return TransferMode.Copy; } } - else + + if (mode.HasFlag(TransferMode.Move)) { - if (mode.HasFlag(TransferMode.Copy)) + if (verificationMode == DiskTransferVerificationMode.Transactional || verificationMode == DiskTransferVerificationMode.TryTransactional) { - _diskProvider.CopyFile(sourcePath, targetPath); - return TransferMode.Copy; - } + if (TryMoveFileTransactional(sourcePath, targetPath, originalSize, verificationMode)) + { + return TransferMode.Move; + } - if (mode.HasFlag(TransferMode.Move)) + throw new IOException(string.Format("Failed to completely transfer [{0}] to [{1}], aborting.", sourcePath, targetPath)); + } + else if (verificationMode == DiskTransferVerificationMode.VerifyOnly) + { + TryMoveFileVerified(sourcePath, targetPath, originalSize); + return TransferMode.Move; + } + else { _diskProvider.MoveFile(sourcePath, targetPath); return TransferMode.Move; @@ -340,7 +359,7 @@ namespace NzbDrone.Common.Disk return false; } - private bool TryMoveFileTransactional(string sourcePath, string targetPath, long originalSize) + private bool TryMoveFileTransactional(string sourcePath, string targetPath, long originalSize, DiskTransferVerificationMode verificationMode) { var backupPath = sourcePath + ".backup~"; var tempTargetPath = targetPath + ".partial~"; @@ -394,7 +413,7 @@ namespace NzbDrone.Common.Disk } } - if (VerificationMode == DiskTransferVerificationMode.Transactional) + if (verificationMode == DiskTransferVerificationMode.Transactional) { _logger.Trace("Hardlink move failed, reverting to copy."); if (TryCopyFileTransactional(sourcePath, targetPath, originalSize)) diff --git a/src/NzbDrone.Common/Disk/DriveInfoMount.cs b/src/NzbDrone.Common/Disk/DriveInfoMount.cs new file mode 100644 index 000000000..31523db96 --- /dev/null +++ b/src/NzbDrone.Common/Disk/DriveInfoMount.cs @@ -0,0 +1,76 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using NzbDrone.Common.Extensions; + +namespace NzbDrone.Common.Disk +{ + public class DriveInfoMount : IMount + { + private readonly DriveInfo _driveInfo; + + public DriveInfoMount(DriveInfo driveInfo) + { + _driveInfo = driveInfo; + } + + public long AvailableFreeSpace + { + get { return _driveInfo.AvailableFreeSpace; } + } + + public string DriveFormat + { + get { return _driveInfo.DriveFormat; } + } + + public DriveType DriveType + { + get { return _driveInfo.DriveType; } + } + + public bool IsReady + { + get { return _driveInfo.IsReady; } + } + + public string Name + { + get { return _driveInfo.Name; } + } + + public string RootDirectory + { + get { return _driveInfo.RootDirectory.FullName; } + } + + public long TotalFreeSpace + { + get { return _driveInfo.TotalFreeSpace; } + } + + public long TotalSize + { + get { return _driveInfo.TotalSize; } + } + + public string VolumeLabel + { + get { return _driveInfo.VolumeLabel; } + } + + public string VolumeName + { + get + { + if (VolumeLabel.IsNullOrWhiteSpace()) + { + return Name; + } + + return string.Format("{0} ({1})", Name, VolumeLabel); + } + } + } +} diff --git a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs index c3470f212..740bbd6c7 100644 --- a/src/NzbDrone.Common/Disk/FileSystemLookupService.cs +++ b/src/NzbDrone.Common/Disk/FileSystemLookupService.cs @@ -103,12 +103,12 @@ namespace NzbDrone.Common.Disk private List GetDrives() { - return _diskProvider.GetDrives() + return _diskProvider.GetMounts() .Select(d => new FileSystemModel { Type = FileSystemEntityType.Drive, - Name = GetVolumeName(d), - Path = d.Name, + Name = d.VolumeLabel, + Path = d.RootDirectory, LastModified = null }) .ToList(); @@ -157,16 +157,6 @@ namespace NzbDrone.Common.Disk return path; } - - private string GetVolumeName(DriveInfo driveInfo) - { - if (driveInfo.VolumeLabel.IsNullOrWhiteSpace()) - { - return driveInfo.Name; - } - - return string.Format("{0} ({1})", driveInfo.Name, driveInfo.VolumeLabel); - } private string GetParent(string path) { diff --git a/src/NzbDrone.Common/Disk/IDiskProvider.cs b/src/NzbDrone.Common/Disk/IDiskProvider.cs index ad4cba263..002264abc 100644 --- a/src/NzbDrone.Common/Disk/IDiskProvider.cs +++ b/src/NzbDrone.Common/Disk/IDiskProvider.cs @@ -40,11 +40,11 @@ namespace NzbDrone.Common.Disk void SetPermissions(string filename, WellKnownSidType accountSid, FileSystemRights rights, AccessControlType controlType); FileAttributes GetFileAttributes(string path); void EmptyFolder(string path); - string[] GetFixedDrives(); string GetVolumeLabel(string path); FileStream OpenReadStream(string path); FileStream OpenWriteStream(string path); - List GetDrives(); + List GetMounts(); + IMount GetMount(string path); List GetDirectoryInfos(string path); List GetFileInfos(string path); } diff --git a/src/NzbDrone.Common/Disk/IMount.cs b/src/NzbDrone.Common/Disk/IMount.cs new file mode 100644 index 000000000..7e5ed499e --- /dev/null +++ b/src/NzbDrone.Common/Disk/IMount.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; + +namespace NzbDrone.Common.Disk +{ + public interface IMount + { + long AvailableFreeSpace { get; } + string DriveFormat { get; } + DriveType DriveType { get; } + bool IsReady { get; } + string Name { get; } + string RootDirectory { get; } + long TotalFreeSpace { get; } + long TotalSize { get; } + string VolumeLabel { get; } + } +} diff --git a/src/NzbDrone.Common/Extensions/PathExtensions.cs b/src/NzbDrone.Common/Extensions/PathExtensions.cs index e77e66e74..5d7ff3e19 100644 --- a/src/NzbDrone.Common/Extensions/PathExtensions.cs +++ b/src/NzbDrone.Common/Extensions/PathExtensions.cs @@ -73,8 +73,14 @@ namespace NzbDrone.Common.Extensions public static bool IsParentPath(this string parentPath, string childPath) { - parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); - childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); + if (parentPath != "/") + { + parentPath = parentPath.TrimEnd(Path.DirectorySeparatorChar); + } + if (childPath != "/") + { + childPath = childPath.TrimEnd(Path.DirectorySeparatorChar); + } var parent = new DirectoryInfo(parentPath); var child = new DirectoryInfo(childPath); diff --git a/src/NzbDrone.Common/NzbDrone.Common.csproj b/src/NzbDrone.Common/NzbDrone.Common.csproj index 26f9cc74d..d8b3478f4 100644 --- a/src/NzbDrone.Common/NzbDrone.Common.csproj +++ b/src/NzbDrone.Common/NzbDrone.Common.csproj @@ -72,6 +72,8 @@ + + diff --git a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs index 9010dc873..2d925ee08 100644 --- a/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs +++ b/src/NzbDrone.Core/DiskSpace/DiskSpaceService.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Collections.Generic; using System.Linq; using NLog; @@ -58,7 +59,7 @@ namespace NzbDrone.Core.DiskSpace private IEnumerable GetFixedDisksFreeSpace() { - return GetDiskSpace(_diskProvider.GetFixedDrives(), true); + return GetDiskSpace(_diskProvider.GetMounts().Where(d => d.DriveType == DriveType.Fixed).Select(d => d.RootDirectory), true); } private IEnumerable GetDiskSpace(IEnumerable paths, bool suppressWarnings = false) diff --git a/src/NzbDrone.Core/Validation/Paths/MappedNetworkDriveValidator.cs b/src/NzbDrone.Core/Validation/Paths/MappedNetworkDriveValidator.cs index 1b2fd1d51..94d4257cb 100644 --- a/src/NzbDrone.Core/Validation/Paths/MappedNetworkDriveValidator.cs +++ b/src/NzbDrone.Core/Validation/Paths/MappedNetworkDriveValidator.cs @@ -4,6 +4,7 @@ using System.Text.RegularExpressions; using FluentValidation.Validators; using NzbDrone.Common.Disk; using NzbDrone.Common.EnvironmentInfo; +using NzbDrone.Common.Extensions; namespace NzbDrone.Core.Validation.Paths { @@ -31,17 +32,11 @@ namespace NzbDrone.Core.Validation.Paths if (!DriveRegex.IsMatch(path)) return true; - var drives = _diskProvider.GetDrives(); + var mount = _diskProvider.GetMount(path); - foreach (var drive in drives) + if (mount != null && mount.DriveType == DriveType.Network) { - if (path.StartsWith(drive.Name, StringComparison.InvariantCultureIgnoreCase)) - { - if (drive.DriveType == DriveType.Network) - { - return false; - } - } + return false; } return true; diff --git a/src/NzbDrone.Mono/DiskProvider.cs b/src/NzbDrone.Mono/DiskProvider.cs index bd0eed33b..141cc2dfd 100644 --- a/src/NzbDrone.Mono/DiskProvider.cs +++ b/src/NzbDrone.Mono/DiskProvider.cs @@ -1,13 +1,13 @@ using System; using System.IO; using System.Linq; +using Mono.Unix; using Mono.Unix.Native; using NLog; using NzbDrone.Common.Disk; using NzbDrone.Common.EnsureThat; using NzbDrone.Common.Extensions; using NzbDrone.Common.Instrumentation; -using Mono.Unix; namespace NzbDrone.Mono { @@ -15,26 +15,28 @@ namespace NzbDrone.Mono { private static readonly Logger Logger = NzbDroneLogger.GetLogger(typeof(DiskProvider)); + private readonly IProcMountProvider _procMountProvider; + + public DiskProvider(IProcMountProvider procMountProvider) + { + _procMountProvider = procMountProvider; + } + public override long? GetAvailableSpace(string path) { Ensure.That(path, () => path).IsValidPath(); - var root = GetPathRoot(path); - - if (!FolderExists(root)) - throw new DirectoryNotFoundException(root); - try { - var driveInfo = GetDriveInfo(path); + var mount = GetMount(path); - if (driveInfo == null) + if (mount == null) { Logger.Debug("Unable to get free space for '{0}', unable to find suitable drive", path); return null; } - return driveInfo.AvailableFreeSpace; + return mount.AvailableFreeSpace; } catch (InvalidOperationException ex) { @@ -116,22 +118,25 @@ namespace NzbDrone.Mono } } + public override System.Collections.Generic.List GetMounts() + { + return base.GetMounts() + .Concat(_procMountProvider.GetMounts()) + .DistinctBy(v => v.RootDirectory) + .ToList(); + } + public override long? GetTotalSize(string path) { Ensure.That(path, () => path).IsValidPath(); - var root = GetPathRoot(path); - - if (!FolderExists(root)) - throw new DirectoryNotFoundException(root); - try { - var driveInfo = GetDriveInfo(path); + var mount = GetMount(path); - if (driveInfo == null) return null; + if (mount == null) return null; - return driveInfo.TotalSize; + return mount.TotalSize; } catch (InvalidOperationException e) { @@ -141,18 +146,6 @@ namespace NzbDrone.Mono return null; } - private DriveInfo GetDriveInfo(string path) - { - var drives = DriveInfo.GetDrives(); - - return - drives.Where(drive => drive.IsReady && - drive.Name.IsNotNullOrWhiteSpace() && - path.StartsWith(drive.Name, StringComparison.CurrentCultureIgnoreCase)) - .OrderByDescending(drive => drive.Name.Length) - .FirstOrDefault(); - } - public override bool TryCreateHardLink(string source, string destination) { try diff --git a/src/NzbDrone.Mono/NzbDrone.Mono.csproj b/src/NzbDrone.Mono/NzbDrone.Mono.csproj index 95196edde..ff183e318 100644 --- a/src/NzbDrone.Mono/NzbDrone.Mono.csproj +++ b/src/NzbDrone.Mono/NzbDrone.Mono.csproj @@ -71,6 +71,8 @@ + + @@ -90,4 +92,4 @@ --> - + \ No newline at end of file diff --git a/src/NzbDrone.Mono/ProcMount.cs b/src/NzbDrone.Mono/ProcMount.cs new file mode 100644 index 000000000..f6912d579 --- /dev/null +++ b/src/NzbDrone.Mono/ProcMount.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using Mono.Unix; + +namespace NzbDrone.Mono +{ + public class ProcMount : IMount + { + private readonly UnixDriveInfo _unixDriveInfo; + + public ProcMount(DriveType driveType, string name, string mount, string type, Dictionary options) + { + DriveType = driveType; + Name = name; + RootDirectory = mount; + DriveFormat = type; + + _unixDriveInfo = new UnixDriveInfo(mount); + } + + public long AvailableFreeSpace + { + get { return _unixDriveInfo.AvailableFreeSpace; } + } + + public string DriveFormat { get; private set; } + + public DriveType DriveType { get; private set; } + + public bool IsReady + { + get { return _unixDriveInfo.IsReady; } + } + + public string Name { get; private set; } + + public string RootDirectory { get; private set; } + + public long TotalFreeSpace + { + get { return _unixDriveInfo.TotalFreeSpace; } + } + + public long TotalSize + { + get { return _unixDriveInfo.TotalSize; } + } + + public string VolumeLabel + { + get { return _unixDriveInfo.VolumeLabel; } + } + } +} diff --git a/src/NzbDrone.Mono/ProcMountProvider.cs b/src/NzbDrone.Mono/ProcMountProvider.cs new file mode 100644 index 000000000..cdf7d0359 --- /dev/null +++ b/src/NzbDrone.Mono/ProcMountProvider.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using NLog; +using NzbDrone.Common.Disk; +using NzbDrone.Common.Extensions; +using Mono.Unix; + +namespace NzbDrone.Mono +{ + public interface IProcMountProvider + { + List GetMounts(); + } + + public class ProcMountProvider : IProcMountProvider + { + private static string[] _fixedTypes = new [] { "ext3", "ext2", "ext4", "vfat", "fuseblk", "xfs", "jfs", "msdos", "ntfs", "minix", "hfs", "hfsplus", "qnx4", "ufs", "btrfs" }; + private static string[] _networkDriveTypes = new [] { "cifs", "nfs", "nfs4", "nfsd", "sshfs" }; + + private static Dictionary _fileSystems; + + private readonly Logger _logger; + + public ProcMountProvider(Logger logger) + { + _logger = logger; + } + + public List GetMounts() + { + try + { + if (File.Exists(@"/proc/mounts")) + { + var lines = File.ReadAllLines(@"/proc/mounts"); + + return lines.Select(ParseLine).OfType().ToList(); + } + } + catch (Exception ex) + { + _logger.DebugException("Failed to retrieve mounts from /proc/mounts", ex); + } + + return new List(); + } + + private Dictionary GetFileSystems() + { + if (_fileSystems == null) + { + var result = new Dictionary(); + try + { + if (File.Exists(@"/proc/filesystems")) + { + var lines = File.ReadAllLines(@"/proc/filesystems"); + + foreach (var line in lines) + { + var split = line.Split('\t'); + + result.Add(split[1], split[0] != "nodev"); + } + } + } + catch (Exception ex) + { + _logger.DebugException("Failed to get filesystem types from /proc/filesystems, using default set.", ex); + } + + if (result.Empty()) + { + foreach (var type in _fixedTypes) + { + result.Add(type, true); + } + } + + _fileSystems = result; + } + + return _fileSystems; + } + + private IMount ParseLine(string line) + { + var split = line.Split(' '); + + if (split.Length != 6) + { + _logger.Debug("Unable to parser /proc/mount line: {0}", line); + } + + var name = split[0]; + var mount = split[1]; + var type = split[2]; + var options = ParseOptions(split[3]); + + var driveType = DriveType.Unknown; + + if (name.StartsWith("/dev/") || GetFileSystems().GetValueOrDefault(type, false)) + { + // Not always fixed, but lets assume it. + driveType = DriveType.Fixed; + } + + if (_networkDriveTypes.Contains(type)) + { + driveType = DriveType.Network; + } + + if (type == "zfs") + { + driveType = DriveType.Fixed; + } + + return new ProcMount(driveType, name, mount, type, options); + } + + private Dictionary ParseOptions(string options) + { + var result = new Dictionary(); + + foreach (var option in options.Split(',')) + { + var split = option.Split(new[] { '=' }, 2); + + result.Add(split[0], split.Length == 2 ? split[1] : string.Empty); + } + + return result; + } + } +}