From be7918e5f68f67ed32a50c2d86ee9cae79cf2b93 Mon Sep 17 00:00:00 2001 From: Luke Pulverenti Date: Wed, 30 Oct 2013 10:40:14 -0400 Subject: [PATCH] fixes #567 - Deprecate native shortcut code --- MediaBrowser.Api/Library/LibraryHelpers.cs | 35 ++- .../Library/LibraryStructureService.cs | 17 +- MediaBrowser.Controller/Entities/BaseItem.cs | 5 +- MediaBrowser.Controller/Entities/Folder.cs | 2 +- MediaBrowser.Controller/IO/FileData.cs | 9 +- MediaBrowser.Controller/IO/FileSystem.cs | 294 +----------------- MediaBrowser.Controller/IO/IFileSystem.cs | 45 +++ .../MediaBrowser.Controller.csproj | 2 +- .../Resolvers/EntityResolutionHelper.cs | 5 +- .../ImagesByNameProvider.cs | 7 +- MediaBrowser.Providers/RefreshIntrosTask.cs | 7 +- .../IO/DirectoryWatchers.cs | 7 +- .../Library/LibraryManager.cs | 15 +- .../Library/ResolverHelper.cs | 6 +- .../ApplicationHost.cs | 8 +- .../IO/CommonFileSystem.cs | 264 ++++++++++++++++ .../IO/FileSystemFactory.cs | 19 ++ .../IO/NativeFileSystem.cs | 44 ++- .../MediaBrowser.ServerApplication.csproj | 3 + 19 files changed, 452 insertions(+), 342 deletions(-) create mode 100644 MediaBrowser.Controller/IO/IFileSystem.cs create mode 100644 MediaBrowser.ServerApplication/IO/CommonFileSystem.cs create mode 100644 MediaBrowser.ServerApplication/IO/FileSystemFactory.cs rename MediaBrowser.Controller/IO/NativeMethods.cs => MediaBrowser.ServerApplication/IO/NativeFileSystem.cs (91%) diff --git a/MediaBrowser.Api/Library/LibraryHelpers.cs b/MediaBrowser.Api/Library/LibraryHelpers.cs index 008cfb27f7..906b47458f 100644 --- a/MediaBrowser.Api/Library/LibraryHelpers.cs +++ b/MediaBrowser.Api/Library/LibraryHelpers.cs @@ -12,20 +12,27 @@ namespace MediaBrowser.Api.Library /// public static class LibraryHelpers { + /// + /// The shortcut file extension + /// private const string ShortcutFileExtension = ".mblink"; + /// + /// The shortcut file search + /// private const string ShortcutFileSearch = "*" + ShortcutFileExtension; /// /// Adds the virtual folder. /// + /// The file system. /// The name. /// Type of the collection. /// The user. /// The app paths. /// There is already a media collection with the name + name + . - public static void AddVirtualFolder(string name, string collectionType, User user, IServerApplicationPaths appPaths) + public static void AddVirtualFolder(IFileSystem fileSystem, string name, string collectionType, User user, IServerApplicationPaths appPaths) { - name = FileSystem.GetValidFilename(name); + name = fileSystem.GetValidFilename(name); var rootFolderPath = user != null ? user.RootFolderPath : appPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, name); @@ -106,12 +113,13 @@ namespace MediaBrowser.Api.Library /// /// Deletes a shortcut from within a virtual folder, within either the default view or a user view /// + /// The file system. /// Name of the virtual folder. /// The media path. /// The user. /// The app paths. /// The media folder does not exist - public static void RemoveMediaPath(string virtualFolderName, string mediaPath, User user, IServerApplicationPaths appPaths) + public static void RemoveMediaPath(IFileSystem fileSystem, string virtualFolderName, string mediaPath, User user, IServerApplicationPaths appPaths) { var rootFolderPath = user != null ? user.RootFolderPath : appPaths.DefaultUserViewsPath; var path = Path.Combine(rootFolderPath, virtualFolderName); @@ -121,7 +129,7 @@ namespace MediaBrowser.Api.Library throw new DirectoryNotFoundException(string.Format("The media collection {0} does not exist", virtualFolderName)); } - var shortcut = Directory.EnumerateFiles(path, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => FileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); + var shortcut = Directory.EnumerateFiles(path, ShortcutFileSearch, SearchOption.AllDirectories).FirstOrDefault(f => fileSystem.ResolveShortcut(f).Equals(mediaPath, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(shortcut)) { @@ -132,13 +140,14 @@ namespace MediaBrowser.Api.Library /// /// Adds an additional mediaPath to an existing virtual folder, within either the default view or a user view /// + /// The file system. /// Name of the virtual folder. /// The path. /// The user. /// The app paths. /// The path is not valid. /// The path does not exist. - public static void AddMediaPath(string virtualFolderName, string path, User user, IServerApplicationPaths appPaths) + public static void AddMediaPath(IFileSystem fileSystem, string virtualFolderName, string path, User user, IServerApplicationPaths appPaths) { if (!Path.IsPathRooted(path)) { @@ -160,7 +169,7 @@ namespace MediaBrowser.Api.Library var rootFolderPath = user != null ? user.RootFolderPath : appPaths.DefaultUserViewsPath; var virtualFolderPath = Path.Combine(rootFolderPath, virtualFolderName); - ValidateNewMediaPath(rootFolderPath, path, appPaths); + ValidateNewMediaPath(fileSystem, rootFolderPath, path, appPaths); var shortcutFilename = Path.GetFileNameWithoutExtension(path); @@ -172,20 +181,22 @@ namespace MediaBrowser.Api.Library lnk = Path.Combine(virtualFolderPath, shortcutFilename + ShortcutFileExtension); } - FileSystem.CreateShortcut(lnk, path); + fileSystem.CreateShortcut(lnk, path); } /// /// Validates that a new media path can be added /// + /// The file system. /// The current view root folder path. /// The media path. /// The app paths. - /// - private static void ValidateNewMediaPath(string currentViewRootFolderPath, string mediaPath, IServerApplicationPaths appPaths) + /// + /// + private static void ValidateNewMediaPath(IFileSystem fileSystem, string currentViewRootFolderPath, string mediaPath, IServerApplicationPaths appPaths) { var duplicate = Directory.EnumerateFiles(appPaths.RootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories) - .Select(FileSystem.ResolveShortcut) + .Select(fileSystem.ResolveShortcut) .FirstOrDefault(p => !IsNewPathValid(mediaPath, p, false)); if (!string.IsNullOrEmpty(duplicate)) @@ -196,7 +207,7 @@ namespace MediaBrowser.Api.Library // Don't allow duplicate sub-paths within the same user library, or it will result in duplicate items // See comments in IsNewPathValid duplicate = Directory.EnumerateFiles(currentViewRootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories) - .Select(FileSystem.ResolveShortcut) + .Select(fileSystem.ResolveShortcut) .FirstOrDefault(p => !IsNewPathValid(mediaPath, p, true)); if (!string.IsNullOrEmpty(duplicate)) @@ -206,7 +217,7 @@ namespace MediaBrowser.Api.Library // Make sure the current root folder doesn't already have a shortcut to the same path duplicate = Directory.EnumerateFiles(currentViewRootFolderPath, ShortcutFileSearch, SearchOption.AllDirectories) - .Select(FileSystem.ResolveShortcut) + .Select(fileSystem.ResolveShortcut) .FirstOrDefault(p => mediaPath.Equals(p, StringComparison.OrdinalIgnoreCase)); if (!string.IsNullOrEmpty(duplicate)) diff --git a/MediaBrowser.Api/Library/LibraryStructureService.cs b/MediaBrowser.Api/Library/LibraryStructureService.cs index e6fa1d1c0b..a5476ec8b8 100644 --- a/MediaBrowser.Api/Library/LibraryStructureService.cs +++ b/MediaBrowser.Api/Library/LibraryStructureService.cs @@ -186,6 +186,8 @@ namespace MediaBrowser.Api.Library private readonly IDirectoryWatchers _directoryWatchers; + private readonly IFileSystem _fileSystem; + /// /// Initializes a new instance of the class. /// @@ -193,7 +195,7 @@ namespace MediaBrowser.Api.Library /// The user manager. /// The library manager. /// appPaths - public LibraryStructureService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IDirectoryWatchers directoryWatchers) + public LibraryStructureService(IServerApplicationPaths appPaths, IUserManager userManager, ILibraryManager libraryManager, IDirectoryWatchers directoryWatchers, IFileSystem fileSystem) { if (appPaths == null) { @@ -204,6 +206,7 @@ namespace MediaBrowser.Api.Library _appPaths = appPaths; _libraryManager = libraryManager; _directoryWatchers = directoryWatchers; + _fileSystem = fileSystem; } /// @@ -241,13 +244,13 @@ namespace MediaBrowser.Api.Library { if (string.IsNullOrEmpty(request.UserId)) { - LibraryHelpers.AddVirtualFolder(request.Name, request.CollectionType, null, _appPaths); + LibraryHelpers.AddVirtualFolder(_fileSystem, request.Name, request.CollectionType, null, _appPaths); } else { var user = _userManager.GetUserById(new Guid(request.UserId)); - LibraryHelpers.AddVirtualFolder(request.Name, request.CollectionType, user, _appPaths); + LibraryHelpers.AddVirtualFolder(_fileSystem, request.Name, request.CollectionType, user, _appPaths); } // Need to add a delay here or directory watchers may still pick up the changes @@ -352,13 +355,13 @@ namespace MediaBrowser.Api.Library { if (string.IsNullOrEmpty(request.UserId)) { - LibraryHelpers.AddMediaPath(request.Name, request.Path, null, _appPaths); + LibraryHelpers.AddMediaPath(_fileSystem, request.Name, request.Path, null, _appPaths); } else { var user = _userManager.GetUserById(new Guid(request.UserId)); - LibraryHelpers.AddMediaPath(request.Name, request.Path, user, _appPaths); + LibraryHelpers.AddMediaPath(_fileSystem, request.Name, request.Path, user, _appPaths); } // Need to add a delay here or directory watchers may still pick up the changes @@ -389,13 +392,13 @@ namespace MediaBrowser.Api.Library { if (string.IsNullOrEmpty(request.UserId)) { - LibraryHelpers.RemoveMediaPath(request.Name, request.Path, null, _appPaths); + LibraryHelpers.RemoveMediaPath(_fileSystem, request.Name, request.Path, null, _appPaths); } else { var user = _userManager.GetUserById(new Guid(request.UserId)); - LibraryHelpers.RemoveMediaPath(request.Name, request.Path, user, _appPaths); + LibraryHelpers.RemoveMediaPath(_fileSystem, request.Name, request.Path, user, _appPaths); } // Need to add a delay here or directory watchers may still pick up the changes diff --git a/MediaBrowser.Controller/Entities/BaseItem.cs b/MediaBrowser.Controller/Entities/BaseItem.cs index 8174dded26..f1937a4616 100644 --- a/MediaBrowser.Controller/Entities/BaseItem.cs +++ b/MediaBrowser.Controller/Entities/BaseItem.cs @@ -212,6 +212,7 @@ namespace MediaBrowser.Controller.Entities public static IProviderManager ProviderManager { get; set; } public static ILocalizationManager LocalizationManager { get; set; } public static IItemRepository ItemRepository { get; set; } + public static IFileSystem FileSystem { get; set; } /// /// Returns a that represents this instance. @@ -395,7 +396,7 @@ namespace MediaBrowser.Controller.Entities // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; - args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); + args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, FileSystem, Logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); // Need to remove subpaths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action @@ -413,7 +414,7 @@ namespace MediaBrowser.Controller.Entities } //update our dates - EntityResolutionHelper.EnsureDates(this, args, false); + EntityResolutionHelper.EnsureDates(FileSystem, this, args, false); IsOffline = false; diff --git a/MediaBrowser.Controller/Entities/Folder.cs b/MediaBrowser.Controller/Entities/Folder.cs index c54b812420..a4ba146165 100644 --- a/MediaBrowser.Controller/Entities/Folder.cs +++ b/MediaBrowser.Controller/Entities/Folder.cs @@ -693,7 +693,7 @@ namespace MediaBrowser.Controller.Entities //existing item - check if it has changed if (currentChild.HasChanged(child)) { - EntityResolutionHelper.EnsureDates(currentChild, child.ResolveArgs, false); + EntityResolutionHelper.EnsureDates(FileSystem, currentChild, child.ResolveArgs, false); validChildren.Add(new Tuple(currentChild, true)); } diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index b1fc28e7b7..726467fa6a 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -15,6 +15,7 @@ namespace MediaBrowser.Controller.IO /// Gets the filtered file system entries. /// /// The path. + /// The file system. /// The logger. /// The args. /// The search pattern. @@ -22,7 +23,7 @@ namespace MediaBrowser.Controller.IO /// if set to true [resolve shortcuts]. /// Dictionary{System.StringFileSystemInfo}. /// path - public static Dictionary GetFilteredFileSystemEntries(string path, ILogger logger, ItemResolveArgs args, string searchPattern = "*", int flattenFolderDepth = 0, bool resolveShortcuts = true) + public static Dictionary GetFilteredFileSystemEntries(string path, IFileSystem fileSystem, ILogger logger, ItemResolveArgs args, string searchPattern = "*", int flattenFolderDepth = 0, bool resolveShortcuts = true) { if (string.IsNullOrEmpty(path)) { @@ -56,9 +57,9 @@ namespace MediaBrowser.Controller.IO var fullName = entry.FullName; - if (resolveShortcuts && FileSystem.IsShortcut(fullName)) + if (resolveShortcuts && fileSystem.IsShortcut(fullName)) { - var newPath = FileSystem.ResolveShortcut(fullName); + var newPath = fileSystem.ResolveShortcut(fullName); if (string.IsNullOrWhiteSpace(newPath)) { @@ -77,7 +78,7 @@ namespace MediaBrowser.Controller.IO } else if (flattenFolderDepth > 0 && isDirectory) { - foreach (var child in GetFilteredFileSystemEntries(fullName, logger, args, flattenFolderDepth: flattenFolderDepth - 1, resolveShortcuts: resolveShortcuts)) + foreach (var child in GetFilteredFileSystemEntries(fullName, fileSystem, logger, args, flattenFolderDepth: flattenFolderDepth - 1, resolveShortcuts: resolveShortcuts)) { dict[child.Key] = child.Value; } diff --git a/MediaBrowser.Controller/IO/FileSystem.cs b/MediaBrowser.Controller/IO/FileSystem.cs index 1c49545be1..f31fc53de5 100644 --- a/MediaBrowser.Controller/IO/FileSystem.cs +++ b/MediaBrowser.Controller/IO/FileSystem.cs @@ -1,10 +1,6 @@ -using System.Collections.Generic; -using System.Linq; -using MediaBrowser.Model.Logging; +using MediaBrowser.Model.Logging; using System; -using System.Collections.Specialized; using System.IO; -using System.Text; namespace MediaBrowser.Controller.IO { @@ -13,38 +9,6 @@ namespace MediaBrowser.Controller.IO /// public static class FileSystem { - /// - /// Gets the file system info. - /// - /// The path. - /// FileSystemInfo. - public static FileSystemInfo GetFileSystemInfo(string path) - { - // Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists - if (Path.HasExtension(path)) - { - var fileInfo = new FileInfo(path); - - if (fileInfo.Exists) - { - return fileInfo; - } - - return new DirectoryInfo(path); - } - else - { - var fileInfo = new DirectoryInfo(path); - - if (fileInfo.Exists) - { - return fileInfo; - } - - return new FileInfo(path); - } - } - /// /// Gets the creation time UTC. /// @@ -87,116 +51,6 @@ namespace MediaBrowser.Controller.IO } } - /// - /// The space char - /// - private const char SpaceChar = ' '; - /// - /// The invalid file name chars - /// - private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars(); - - /// - /// Takes a filename and removes invalid characters - /// - /// The filename. - /// System.String. - /// filename - public static string GetValidFilename(string filename) - { - if (string.IsNullOrEmpty(filename)) - { - throw new ArgumentNullException("filename"); - } - - var builder = new StringBuilder(filename); - - foreach (var c in InvalidFileNameChars) - { - builder = builder.Replace(c, SpaceChar); - } - - return builder.ToString(); - } - - /// - /// Resolves the shortcut. - /// - /// The filename. - /// System.String. - /// filename - public static string ResolveShortcut(string filename) - { - if (string.IsNullOrEmpty(filename)) - { - throw new ArgumentNullException("filename"); - } - - if (string.Equals(Path.GetExtension(filename), ".mblink", StringComparison.OrdinalIgnoreCase)) - { - return File.ReadAllText(filename); - } - - //return new WindowsShortcut(filename).ResolvedPath; - - var link = new ShellLink(); - ((IPersistFile)link).Load(filename, NativeMethods.STGM_READ); - // TODO: if I can get hold of the hwnd call resolve first. This handles moved and renamed files. - // ((IShellLinkW)link).Resolve(hwnd, 0) - var sb = new StringBuilder(NativeMethods.MAX_PATH); - WIN32_FIND_DATA data; - ((IShellLinkW)link).GetPath(sb, sb.Capacity, out data, 0); - return sb.ToString(); - } - - /// - /// Creates a shortcut file pointing to a specified path - /// - /// The shortcut path. - /// The target. - /// shortcutPath - public static void CreateShortcut(string shortcutPath, string target) - { - if (string.IsNullOrEmpty(shortcutPath)) - { - throw new ArgumentNullException("shortcutPath"); - } - - if (string.IsNullOrEmpty(target)) - { - throw new ArgumentNullException("target"); - } - - File.WriteAllText(shortcutPath, target); - - //var link = new ShellLink(); - - //((IShellLinkW)link).SetPath(target); - - //((IPersistFile)link).Save(shortcutPath, true); - } - - private static readonly Dictionary ShortcutExtensionsDictionary = new[] { ".mblink", ".lnk" } - .ToDictionary(i => i, StringComparer.OrdinalIgnoreCase); - - /// - /// Determines whether the specified filename is shortcut. - /// - /// The filename. - /// true if the specified filename is shortcut; otherwise, false. - /// filename - public static bool IsShortcut(string filename) - { - if (string.IsNullOrEmpty(filename)) - { - throw new ArgumentNullException("filename"); - } - - var extension = Path.GetExtension(filename); - - return !string.IsNullOrEmpty(extension) && ShortcutExtensionsDictionary.ContainsKey(extension); - } - /// /// Copies all. /// @@ -234,151 +88,5 @@ namespace MediaBrowser.Controller.IO CopyAll(dir, Path.Combine(target, Path.GetFileName(dir))); } } - - /// - /// Parses the ini file. - /// - /// The path. - /// NameValueCollection. - public static NameValueCollection ParseIniFile(string path) - { - var values = new NameValueCollection(); - - foreach (var line in File.ReadAllLines(path)) - { - var data = line.Split('='); - - if (data.Length < 2) continue; - - var key = data[0]; - - var value = data.Length == 2 ? data[1] : string.Join(string.Empty, data, 1, data.Length - 1); - - values[key] = value; - } - - return values; - } - } - - /// - /// Adapted from http://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java - /// - internal class WindowsShortcut - { - public bool IsDirectory { get; private set; } - public bool IsLocal { get; private set; } - public string ResolvedPath { get; private set; } - - public WindowsShortcut(string file) - { - ParseLink(File.ReadAllBytes(file), Encoding.UTF8); - } - - private static bool isMagicPresent(byte[] link) - { - - const int magic = 0x0000004C; - const int magic_offset = 0x00; - - return link.Length >= 32 && bytesToDword(link, magic_offset) == magic; - } - - /** - * Gobbles up link data by parsing it and storing info in member fields - * @param link all the bytes from the .lnk file - */ - private void ParseLink(byte[] link, Encoding encoding) - { - if (!isMagicPresent(link)) - throw new IOException("Invalid shortcut; magic is missing", 0); - - // get the flags byte - byte flags = link[0x14]; - - // get the file attributes byte - const int file_atts_offset = 0x18; - byte file_atts = link[file_atts_offset]; - byte is_dir_mask = (byte)0x10; - if ((file_atts & is_dir_mask) > 0) - { - IsDirectory = true; - } - else - { - IsDirectory = false; - } - - // if the shell settings are present, skip them - const int shell_offset = 0x4c; - const byte has_shell_mask = (byte)0x01; - int shell_len = 0; - if ((flags & has_shell_mask) > 0) - { - // the plus 2 accounts for the length marker itself - shell_len = bytesToWord(link, shell_offset) + 2; - } - - // get to the file settings - int file_start = 0x4c + shell_len; - - const int file_location_info_flag_offset_offset = 0x08; - int file_location_info_flag = link[file_start + file_location_info_flag_offset_offset]; - IsLocal = (file_location_info_flag & 2) == 0; - // get the local volume and local system values - //final int localVolumeTable_offset_offset = 0x0C; - const int basename_offset_offset = 0x10; - const int networkVolumeTable_offset_offset = 0x14; - const int finalname_offset_offset = 0x18; - int finalname_offset = link[file_start + finalname_offset_offset] + file_start; - String finalname = getNullDelimitedString(link, finalname_offset, encoding); - if (IsLocal) - { - int basename_offset = link[file_start + basename_offset_offset] + file_start; - String basename = getNullDelimitedString(link, basename_offset, encoding); - ResolvedPath = basename + finalname; - } - else - { - int networkVolumeTable_offset = link[file_start + networkVolumeTable_offset_offset] + file_start; - int shareName_offset_offset = 0x08; - int shareName_offset = link[networkVolumeTable_offset + shareName_offset_offset] - + networkVolumeTable_offset; - String shareName = getNullDelimitedString(link, shareName_offset, encoding); - ResolvedPath = shareName + "\\" + finalname; - } - } - - private static string getNullDelimitedString(byte[] bytes, int off, Encoding encoding) - { - int len = 0; - - // count bytes until the null character (0) - while (true) - { - if (bytes[off + len] == 0) - { - break; - } - len++; - } - - return encoding.GetString(bytes, off, len); - } - - /* - * convert two bytes into a short note, this is little endian because it's - * for an Intel only OS. - */ - private static int bytesToWord(byte[] bytes, int off) - { - return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff); - } - - private static int bytesToDword(byte[] bytes, int off) - { - return (bytesToWord(bytes, off + 2) << 16) | bytesToWord(bytes, off); - } - } } diff --git a/MediaBrowser.Controller/IO/IFileSystem.cs b/MediaBrowser.Controller/IO/IFileSystem.cs new file mode 100644 index 0000000000..2654c3235e --- /dev/null +++ b/MediaBrowser.Controller/IO/IFileSystem.cs @@ -0,0 +1,45 @@ +using System.IO; + +namespace MediaBrowser.Controller.IO +{ + /// + /// Interface IFileSystem + /// + public interface IFileSystem + { + /// + /// Determines whether the specified filename is shortcut. + /// + /// The filename. + /// true if the specified filename is shortcut; otherwise, false. + bool IsShortcut(string filename); + + /// + /// Resolves the shortcut. + /// + /// The filename. + /// System.String. + string ResolveShortcut(string filename); + + /// + /// Creates the shortcut. + /// + /// The shortcut path. + /// The target. + void CreateShortcut(string shortcutPath, string target); + + /// + /// Gets the file system info. + /// + /// The path. + /// FileSystemInfo. + FileSystemInfo GetFileSystemInfo(string path); + + /// + /// Gets the valid filename. + /// + /// The filename. + /// System.String. + string GetValidFilename(string filename); + } +} diff --git a/MediaBrowser.Controller/MediaBrowser.Controller.csproj b/MediaBrowser.Controller/MediaBrowser.Controller.csproj index 8154cb0a28..77db7d2c24 100644 --- a/MediaBrowser.Controller/MediaBrowser.Controller.csproj +++ b/MediaBrowser.Controller/MediaBrowser.Controller.csproj @@ -94,6 +94,7 @@ + @@ -141,7 +142,6 @@ - diff --git a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs index dbac826b34..7961834d67 100644 --- a/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs +++ b/MediaBrowser.Controller/Resolvers/EntityResolutionHelper.cs @@ -126,10 +126,11 @@ namespace MediaBrowser.Controller.Resolvers /// /// Ensures DateCreated and DateModified have values /// + /// The file system. /// The item. /// The args. /// if set to true [include creation time]. - public static void EnsureDates(BaseItem item, ItemResolveArgs args, bool includeCreationTime) + public static void EnsureDates(IFileSystem fileSystem, BaseItem item, ItemResolveArgs args, bool includeCreationTime) { if (!Path.IsPathRooted(item.Path)) { @@ -152,7 +153,7 @@ namespace MediaBrowser.Controller.Resolvers } else { - var fileData = FileSystem.GetFileSystemInfo(item.Path); + var fileData = fileSystem.GetFileSystemInfo(item.Path); if (fileData.Exists) { diff --git a/MediaBrowser.Providers/ImagesByNameProvider.cs b/MediaBrowser.Providers/ImagesByNameProvider.cs index e4bfee6e33..2fdfe199ab 100644 --- a/MediaBrowser.Providers/ImagesByNameProvider.cs +++ b/MediaBrowser.Providers/ImagesByNameProvider.cs @@ -18,9 +18,12 @@ namespace MediaBrowser.Providers /// public class ImagesByNameProvider : ImageFromMediaLocationProvider { - public ImagesByNameProvider(ILogManager logManager, IServerConfigurationManager configurationManager) + private readonly IFileSystem _fileSystem; + + public ImagesByNameProvider(ILogManager logManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem) : base(logManager, configurationManager) { + _fileSystem = fileSystem; } public override ItemUpdateType ItemUpdateType @@ -150,7 +153,7 @@ namespace MediaBrowser.Providers /// System.String. protected string GetLocation(BaseItem item) { - var name = FileSystem.GetValidFilename(item.Name); + var name = _fileSystem.GetValidFilename(item.Name); return Path.Combine(ConfigurationManager.ApplicationPaths.GeneralPath, name); } diff --git a/MediaBrowser.Providers/RefreshIntrosTask.cs b/MediaBrowser.Providers/RefreshIntrosTask.cs index 3ff2b40a6b..5d87dc90f8 100644 --- a/MediaBrowser.Providers/RefreshIntrosTask.cs +++ b/MediaBrowser.Providers/RefreshIntrosTask.cs @@ -22,15 +22,18 @@ namespace MediaBrowser.Providers /// private readonly ILogger _logger; + private readonly IFileSystem _fileSystem; + /// /// Initializes a new instance of the class. /// /// The library manager. /// The logger. - public RefreshIntrosTask(ILibraryManager libraryManager, ILogger logger) + public RefreshIntrosTask(ILibraryManager libraryManager, ILogger logger, IFileSystem fileSystem) { _libraryManager = libraryManager; _logger = logger; + _fileSystem = fileSystem; } /// @@ -77,7 +80,7 @@ namespace MediaBrowser.Providers /// Task. private async Task RefreshIntro(string path, CancellationToken cancellationToken) { - var item = _libraryManager.ResolvePath(FileSystem.GetFileSystemInfo(path)); + var item = _libraryManager.ResolvePath(_fileSystem.GetFileSystemInfo(path)); if (item == null) { diff --git a/MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs b/MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs index a4d99ae17e..0ddd407cf0 100644 --- a/MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs +++ b/MediaBrowser.Server.Implementations/IO/DirectoryWatchers.cs @@ -87,10 +87,12 @@ namespace MediaBrowser.Server.Implementations.IO private ILibraryManager LibraryManager { get; set; } private IServerConfigurationManager ConfigurationManager { get; set; } + private readonly IFileSystem _fileSystem; + /// /// Initializes a new instance of the class. /// - public DirectoryWatchers(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager) + public DirectoryWatchers(ILogManager logManager, ITaskManager taskManager, ILibraryManager libraryManager, IServerConfigurationManager configurationManager, IFileSystem fileSystem) { if (taskManager == null) { @@ -101,6 +103,7 @@ namespace MediaBrowser.Server.Implementations.IO TaskManager = taskManager; Logger = logManager.GetLogger("DirectoryWatchers"); ConfigurationManager = configurationManager; + _fileSystem = fileSystem; SystemEvents.PowerModeChanged += SystemEvents_PowerModeChanged; } @@ -418,7 +421,7 @@ namespace MediaBrowser.Server.Implementations.IO { try { - var data = FileSystem.GetFileSystemInfo(path); + var data = _fileSystem.GetFileSystemInfo(path); if (!data.Exists || data.Attributes.HasFlag(FileAttributes.Directory) diff --git a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs index cbe15aa626..eed191aab2 100644 --- a/MediaBrowser.Server.Implementations/Library/LibraryManager.cs +++ b/MediaBrowser.Server.Implementations/Library/LibraryManager.cs @@ -169,6 +169,8 @@ namespace MediaBrowser.Server.Implementations.Library private readonly ConcurrentDictionary _userRootFolders = new ConcurrentDictionary(); + private readonly IFileSystem _fileSystem; + /// /// Initializes a new instance of the class. /// @@ -177,7 +179,7 @@ namespace MediaBrowser.Server.Implementations.Library /// The user manager. /// The configuration manager. /// The user data repository. - public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func directoryWatchersFactory) + public LibraryManager(ILogger logger, ITaskManager taskManager, IUserManager userManager, IServerConfigurationManager configurationManager, IUserDataManager userDataRepository, Func directoryWatchersFactory, IFileSystem fileSystem) { _logger = logger; _taskManager = taskManager; @@ -185,6 +187,7 @@ namespace MediaBrowser.Server.Implementations.Library ConfigurationManager = configurationManager; _userDataRepository = userDataRepository; _directoryWatchersFactory = directoryWatchersFactory; + _fileSystem = fileSystem; ByReferenceItems = new ConcurrentDictionary(); ConfigurationManager.ConfigurationUpdated += ConfigurationUpdated; @@ -417,7 +420,7 @@ namespace MediaBrowser.Server.Implementations.Library if (item != null) { - ResolverHelper.SetInitialItemValues(item, args); + ResolverHelper.SetInitialItemValues(item, args, _fileSystem); // Now handle the issue with posibly having the same item referenced from multiple physical // places within the library. Be sure we always end up with just one instance. @@ -482,7 +485,7 @@ namespace MediaBrowser.Server.Implementations.Library // When resolving the root, we need it's grandchildren (children of user views) var flattenFolderDepth = isPhysicalRoot ? 2 : 0; - args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); + args.FileSystemDictionary = FileData.GetFilteredFileSystemEntries(args.Path, _fileSystem, _logger, args, flattenFolderDepth: flattenFolderDepth, resolveShortcuts: isPhysicalRoot || args.IsVf); // Need to remove subpaths that may have been resolved from shortcuts // Example: if \\server\movies exists, then strip out \\server\movies\action @@ -701,7 +704,7 @@ namespace MediaBrowser.Server.Implementations.Library throw new ArgumentNullException(); } - var validFilename = FileSystem.GetValidFilename(name).Trim(); + var validFilename = _fileSystem.GetValidFilename(name).Trim(); string subFolderPrefix = null; @@ -1066,7 +1069,7 @@ namespace MediaBrowser.Server.Implementations.Library Name = Path.GetFileName(dir), Locations = Directory.EnumerateFiles(dir, "*.mblink", SearchOption.TopDirectoryOnly) - .Select(FileSystem.ResolveShortcut) + .Select(_fileSystem.ResolveShortcut) .OrderBy(i => i) .ToList(), @@ -1150,7 +1153,7 @@ namespace MediaBrowser.Server.Implementations.Library try { // Try to resolve the path into a video - video = ResolvePath(FileSystem.GetFileSystemInfo(info.Path)) as Video; + video = ResolvePath(_fileSystem.GetFileSystemInfo(info.Path)) as Video; if (video == null) { diff --git a/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs b/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs index a6b13f8dd6..22bd2e0e6f 100644 --- a/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs +++ b/MediaBrowser.Server.Implementations/Library/ResolverHelper.cs @@ -1,5 +1,6 @@ using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.IO; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Resolvers; using System; @@ -18,7 +19,8 @@ namespace MediaBrowser.Server.Implementations.Library /// /// The item. /// The args. - public static void SetInitialItemValues(BaseItem item, ItemResolveArgs args) + /// The file system. + public static void SetInitialItemValues(BaseItem item, ItemResolveArgs args, IFileSystem fileSystem) { item.ResetResolveArgs(args); @@ -48,7 +50,7 @@ namespace MediaBrowser.Server.Implementations.Library item.DontFetchMeta = item.Path.IndexOf("[dontfetchmeta]", StringComparison.OrdinalIgnoreCase) != -1; // Make sure DateCreated and DateModified have values - EntityResolutionHelper.EnsureDates(item, args, true); + EntityResolutionHelper.EnsureDates(fileSystem, item, args, true); } /// diff --git a/MediaBrowser.ServerApplication/ApplicationHost.cs b/MediaBrowser.ServerApplication/ApplicationHost.cs index 8ae5e34c28..4ec3b73122 100644 --- a/MediaBrowser.ServerApplication/ApplicationHost.cs +++ b/MediaBrowser.ServerApplication/ApplicationHost.cs @@ -47,6 +47,7 @@ using MediaBrowser.Server.Implementations.ServerManager; using MediaBrowser.Server.Implementations.Session; using MediaBrowser.Server.Implementations.WebSocket; using MediaBrowser.ServerApplication.FFMpeg; +using MediaBrowser.ServerApplication.IO; using MediaBrowser.ServerApplication.Native; using MediaBrowser.ServerApplication.Networking; using MediaBrowser.WebDashboard.Api; @@ -246,6 +247,9 @@ namespace MediaBrowser.ServerApplication RegisterSingleInstance(() => new BdInfoExaminer()); + var fileSystemManager = FileSystemFactory.CreateFileSystemManager(); + RegisterSingleInstance(fileSystemManager); + var mediaEncoderTask = RegisterMediaEncoder(); UserDataManager = new UserDataManager(LogManager); @@ -263,10 +267,10 @@ namespace MediaBrowser.ServerApplication UserManager = new UserManager(Logger, ServerConfigurationManager, UserRepository); RegisterSingleInstance(UserManager); - LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => DirectoryWatchers); + LibraryManager = new LibraryManager(Logger, TaskManager, UserManager, ServerConfigurationManager, UserDataManager, () => DirectoryWatchers, fileSystemManager); RegisterSingleInstance(LibraryManager); - DirectoryWatchers = new DirectoryWatchers(LogManager, TaskManager, LibraryManager, ServerConfigurationManager); + DirectoryWatchers = new DirectoryWatchers(LogManager, TaskManager, LibraryManager, ServerConfigurationManager, fileSystemManager); RegisterSingleInstance(DirectoryWatchers); ProviderManager = new ProviderManager(HttpClient, ServerConfigurationManager, DirectoryWatchers, LogManager, LibraryManager); diff --git a/MediaBrowser.ServerApplication/IO/CommonFileSystem.cs b/MediaBrowser.ServerApplication/IO/CommonFileSystem.cs new file mode 100644 index 0000000000..b777930f3a --- /dev/null +++ b/MediaBrowser.ServerApplication/IO/CommonFileSystem.cs @@ -0,0 +1,264 @@ +using MediaBrowser.Controller.IO; +using System; +using System.IO; +using System.Text; + +namespace MediaBrowser.ServerApplication.IO +{ + /// + /// Class CommonFileSystem + /// + public class CommonFileSystem : IFileSystem + { + /// + /// Determines whether the specified filename is shortcut. + /// + /// The filename. + /// true if the specified filename is shortcut; otherwise, false. + /// filename + public virtual bool IsShortcut(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentNullException("filename"); + } + + var extension = Path.GetExtension(filename); + + return string.Equals(extension, ".mblink", StringComparison.OrdinalIgnoreCase); + } + + /// + /// Resolves the shortcut. + /// + /// The filename. + /// System.String. + /// filename + public virtual string ResolveShortcut(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentNullException("filename"); + } + + if (string.Equals(Path.GetExtension(filename), ".mblink", StringComparison.OrdinalIgnoreCase)) + { + return File.ReadAllText(filename); + } + + return null; + } + + /// + /// Creates the shortcut. + /// + /// The shortcut path. + /// The target. + /// + /// shortcutPath + /// or + /// target + /// + public void CreateShortcut(string shortcutPath, string target) + { + if (string.IsNullOrEmpty(shortcutPath)) + { + throw new ArgumentNullException("shortcutPath"); + } + + if (string.IsNullOrEmpty(target)) + { + throw new ArgumentNullException("target"); + } + + File.WriteAllText(shortcutPath, target); + } + + /// + /// Gets the file system info. + /// + /// The path. + /// FileSystemInfo. + public FileSystemInfo GetFileSystemInfo(string path) + { + // Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists + if (Path.HasExtension(path)) + { + var fileInfo = new FileInfo(path); + + if (fileInfo.Exists) + { + return fileInfo; + } + + return new DirectoryInfo(path); + } + else + { + var fileInfo = new DirectoryInfo(path); + + if (fileInfo.Exists) + { + return fileInfo; + } + + return new FileInfo(path); + } + } + + /// + /// The space char + /// + private const char SpaceChar = ' '; + /// + /// The invalid file name chars + /// + private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars(); + + /// + /// Takes a filename and removes invalid characters + /// + /// The filename. + /// System.String. + /// filename + public string GetValidFilename(string filename) + { + if (string.IsNullOrEmpty(filename)) + { + throw new ArgumentNullException("filename"); + } + + var builder = new StringBuilder(filename); + + foreach (var c in InvalidFileNameChars) + { + builder = builder.Replace(c, SpaceChar); + } + + return builder.ToString(); + } + } + + + /// + /// Adapted from http://stackoverflow.com/questions/309495/windows-shortcut-lnk-parser-in-java + /// + internal class WindowsShortcut + { + public bool IsDirectory { get; private set; } + public bool IsLocal { get; private set; } + public string ResolvedPath { get; private set; } + + public WindowsShortcut(string file) + { + ParseLink(File.ReadAllBytes(file), Encoding.UTF8); + } + + private static bool isMagicPresent(byte[] link) + { + + const int magic = 0x0000004C; + const int magic_offset = 0x00; + + return link.Length >= 32 && bytesToDword(link, magic_offset) == magic; + } + + /** + * Gobbles up link data by parsing it and storing info in member fields + * @param link all the bytes from the .lnk file + */ + private void ParseLink(byte[] link, Encoding encoding) + { + if (!isMagicPresent(link)) + throw new IOException("Invalid shortcut; magic is missing", 0); + + // get the flags byte + byte flags = link[0x14]; + + // get the file attributes byte + const int file_atts_offset = 0x18; + byte file_atts = link[file_atts_offset]; + byte is_dir_mask = (byte)0x10; + if ((file_atts & is_dir_mask) > 0) + { + IsDirectory = true; + } + else + { + IsDirectory = false; + } + + // if the shell settings are present, skip them + const int shell_offset = 0x4c; + const byte has_shell_mask = (byte)0x01; + int shell_len = 0; + if ((flags & has_shell_mask) > 0) + { + // the plus 2 accounts for the length marker itself + shell_len = bytesToWord(link, shell_offset) + 2; + } + + // get to the file settings + int file_start = 0x4c + shell_len; + + const int file_location_info_flag_offset_offset = 0x08; + int file_location_info_flag = link[file_start + file_location_info_flag_offset_offset]; + IsLocal = (file_location_info_flag & 2) == 0; + // get the local volume and local system values + //final int localVolumeTable_offset_offset = 0x0C; + const int basename_offset_offset = 0x10; + const int networkVolumeTable_offset_offset = 0x14; + const int finalname_offset_offset = 0x18; + int finalname_offset = link[file_start + finalname_offset_offset] + file_start; + String finalname = getNullDelimitedString(link, finalname_offset, encoding); + if (IsLocal) + { + int basename_offset = link[file_start + basename_offset_offset] + file_start; + String basename = getNullDelimitedString(link, basename_offset, encoding); + ResolvedPath = basename + finalname; + } + else + { + int networkVolumeTable_offset = link[file_start + networkVolumeTable_offset_offset] + file_start; + int shareName_offset_offset = 0x08; + int shareName_offset = link[networkVolumeTable_offset + shareName_offset_offset] + + networkVolumeTable_offset; + String shareName = getNullDelimitedString(link, shareName_offset, encoding); + ResolvedPath = shareName + "\\" + finalname; + } + } + + private static string getNullDelimitedString(byte[] bytes, int off, Encoding encoding) + { + int len = 0; + + // count bytes until the null character (0) + while (true) + { + if (bytes[off + len] == 0) + { + break; + } + len++; + } + + return encoding.GetString(bytes, off, len); + } + + /* + * convert two bytes into a short note, this is little endian because it's + * for an Intel only OS. + */ + private static int bytesToWord(byte[] bytes, int off) + { + return ((bytes[off + 1] & 0xff) << 8) | (bytes[off] & 0xff); + } + + private static int bytesToDword(byte[] bytes, int off) + { + return (bytesToWord(bytes, off + 2) << 16) | bytesToWord(bytes, off); + } + + } + +} diff --git a/MediaBrowser.ServerApplication/IO/FileSystemFactory.cs b/MediaBrowser.ServerApplication/IO/FileSystemFactory.cs new file mode 100644 index 0000000000..78a9338a4a --- /dev/null +++ b/MediaBrowser.ServerApplication/IO/FileSystemFactory.cs @@ -0,0 +1,19 @@ +using MediaBrowser.Controller.IO; + +namespace MediaBrowser.ServerApplication.IO +{ + /// + /// Class FileSystemFactory + /// + public static class FileSystemFactory + { + /// + /// Creates the file system manager. + /// + /// IFileSystem. + public static IFileSystem CreateFileSystemManager() + { + return new NativeFileSystem(); + } + } +} diff --git a/MediaBrowser.Controller/IO/NativeMethods.cs b/MediaBrowser.ServerApplication/IO/NativeFileSystem.cs similarity index 91% rename from MediaBrowser.Controller/IO/NativeMethods.cs rename to MediaBrowser.ServerApplication/IO/NativeFileSystem.cs index 97c7dfe875..b5ceec6196 100644 --- a/MediaBrowser.Controller/IO/NativeMethods.cs +++ b/MediaBrowser.ServerApplication/IO/NativeFileSystem.cs @@ -4,8 +4,46 @@ using System.Runtime.InteropServices; using System.Security; using System.Text; -namespace MediaBrowser.Controller.IO +namespace MediaBrowser.ServerApplication.IO { + public class NativeFileSystem : CommonFileSystem + { + public override bool IsShortcut(string filename) + { + return base.IsShortcut(filename) || + string.Equals(Path.GetExtension(filename), ".lnk", StringComparison.OrdinalIgnoreCase); + } + + public override string ResolveShortcut(string filename) + { + var path = base.ResolveShortcut(filename); + + if (!string.IsNullOrEmpty(path)) + { + return path; + } + + if (string.Equals(Path.GetExtension(filename), ".lnk", StringComparison.OrdinalIgnoreCase)) + { + return ResolveLnk(filename); + } + + return null; + } + + private string ResolveLnk(string filename) + { + var link = new ShellLink(); + ((IPersistFile)link).Load(filename, NativeMethods.STGM_READ); + // TODO: if I can get hold of the hwnd call resolve first. This handles moved and renamed files. + // ((IShellLinkW)link).Resolve(hwnd, 0) + var sb = new StringBuilder(NativeMethods.MAX_PATH); + WIN32_FIND_DATA data; + ((IShellLinkW)link).GetPath(sb, sb.Capacity, out data, 0); + return sb.ToString(); + } + } + /// /// Class NativeMethods /// @@ -46,7 +84,6 @@ namespace MediaBrowser.Controller.IO public uint dwHighDateTime; } - /// /// Struct WIN32_FIND_DATA /// @@ -184,7 +221,6 @@ namespace MediaBrowser.Controller.IO SLR_INVOKE_MSI = 0x80 } - /// /// The IShellLink interface allows Shell links to be created, modified, and resolved /// @@ -311,7 +347,6 @@ namespace MediaBrowser.Controller.IO void GetClassID(out Guid pClassID); } - /// /// Interface IPersistFile /// @@ -374,4 +409,5 @@ namespace MediaBrowser.Controller.IO public class ShellLink { } + } diff --git a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj index 96b404e004..d191247ee0 100644 --- a/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj +++ b/MediaBrowser.ServerApplication/MediaBrowser.ServerApplication.csproj @@ -188,9 +188,12 @@ + + +