From 767cdc1f6f6a63ce997fc9476911e2c361f9d402 Mon Sep 17 00:00:00 2001 From: LukePulverenti Date: Wed, 20 Feb 2013 20:33:05 -0500 Subject: Pushing missing changes --- MediaBrowser.Controller/IO/DirectoryWatchers.cs | 697 ++++++++++++++++++------ MediaBrowser.Controller/IO/FileData.cs | 381 +++++-------- MediaBrowser.Controller/IO/FileSystemHelper.cs | 132 ----- MediaBrowser.Controller/IO/FileSystemManager.cs | 112 ++++ MediaBrowser.Controller/IO/Shortcut.cs | 185 ------- 5 files changed, 767 insertions(+), 740 deletions(-) delete mode 100644 MediaBrowser.Controller/IO/FileSystemHelper.cs create mode 100644 MediaBrowser.Controller/IO/FileSystemManager.cs delete mode 100644 MediaBrowser.Controller/IO/Shortcut.cs (limited to 'MediaBrowser.Controller/IO') diff --git a/MediaBrowser.Controller/IO/DirectoryWatchers.cs b/MediaBrowser.Controller/IO/DirectoryWatchers.cs index eb1358e16f..29353bc251 100644 --- a/MediaBrowser.Controller/IO/DirectoryWatchers.cs +++ b/MediaBrowser.Controller/IO/DirectoryWatchers.cs @@ -1,172 +1,525 @@ -using MediaBrowser.Controller.Entities; -using MediaBrowser.Common.Logging; -using MediaBrowser.Common.Extensions; -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; - -namespace MediaBrowser.Controller.IO -{ - public class DirectoryWatchers - { - private readonly List FileSystemWatchers = new List(); - private Timer updateTimer; - private List affectedPaths = new List(); - - private const int TimerDelayInSeconds = 30; - - public void Start() - { - var pathsToWatch = new List(); - - var rootFolder = Kernel.Instance.RootFolder; - - pathsToWatch.Add(rootFolder.Path); - - foreach (Folder folder in rootFolder.Children.OfType()) - { - foreach (string path in folder.PhysicalLocations) - { - if (Path.IsPathRooted(path) && !pathsToWatch.ContainsParentFolder(path)) - { - pathsToWatch.Add(path); - } - } - } - - foreach (string path in pathsToWatch) - { - Logger.LogInfo("Watching directory " + path + " for changes."); - - var watcher = new FileSystemWatcher(path, "*") { }; - watcher.IncludeSubdirectories = true; - - //watcher.Changed += watcher_Changed; - - // All the others seem to trigger change events on the parent, so let's keep it simple for now. - // Actually, we really need to only watch created, deleted and renamed as changed fires too much -ebr - watcher.Created += watcher_Changed; - watcher.Deleted += watcher_Changed; - watcher.Renamed += watcher_Changed; - - watcher.EnableRaisingEvents = true; - FileSystemWatchers.Add(watcher); - } - } - - void watcher_Changed(object sender, FileSystemEventArgs e) - { - Logger.LogDebugInfo("****** Watcher sees change of type " + e.ChangeType.ToString() + " to " + e.FullPath); - lock (affectedPaths) - { - //Since we're watching created, deleted and renamed we always want the parent of the item to be the affected path - var affectedPath = Path.GetDirectoryName(e.FullPath); - - if (e.ChangeType == WatcherChangeTypes.Renamed) - { - var renamedArgs = e as RenamedEventArgs; - if (affectedPaths.Contains(renamedArgs.OldFullPath)) - { - Logger.LogDebugInfo("****** Removing " + renamedArgs.OldFullPath + " from affected paths."); - affectedPaths.Remove(renamedArgs.OldFullPath); - } - } - - //If anything underneath this path was already marked as affected - remove it as it will now get captured by this one - affectedPaths.RemoveAll(p => p.StartsWith(e.FullPath, StringComparison.OrdinalIgnoreCase)); - - if (!affectedPaths.ContainsParentFolder(affectedPath)) - { - Logger.LogDebugInfo("****** Adding " + affectedPath + " to affected paths."); - affectedPaths.Add(affectedPath); - } - } - - if (updateTimer == null) - { - updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1)); - } - else - { - updateTimer.Change(TimeSpan.FromSeconds(TimerDelayInSeconds), TimeSpan.FromMilliseconds(-1)); - } - } - - private async void TimerStopped(object stateInfo) - { - updateTimer.Dispose(); - updateTimer = null; - List paths; - lock (affectedPaths) - { - paths = affectedPaths; - affectedPaths = new List(); - } - - await ProcessPathChanges(paths).ConfigureAwait(false); - } - - private Task ProcessPathChanges(IEnumerable paths) - { - var itemsToRefresh = new List(); - - foreach (BaseItem item in paths.Select(p => GetAffectedBaseItem(p))) - { - if (item != null && !itemsToRefresh.Contains(item)) - { - itemsToRefresh.Add(item); - } - } - - if (itemsToRefresh.Any(i => - { - var folder = i as Folder; - - return folder != null && folder.IsRoot; - })) - { - return Kernel.Instance.ReloadRoot(); - } - - foreach (var p in paths) Logger.LogDebugInfo("********* "+ p + " reports change."); - foreach (var i in itemsToRefresh) Logger.LogDebugInfo("********* "+i.Name + " ("+ i.Path + ") will be refreshed."); - return Task.WhenAll(itemsToRefresh.Select(i => i.ChangedExternally())); - } - - private BaseItem GetAffectedBaseItem(string path) - { - BaseItem item = null; - - while (item == null && !string.IsNullOrEmpty(path)) - { - item = Kernel.Instance.RootFolder.FindByPath(path); - - path = Path.GetDirectoryName(path); - } - - return item; - } - - public void Stop() - { - foreach (FileSystemWatcher watcher in FileSystemWatchers) - { - watcher.Changed -= watcher_Changed; - watcher.EnableRaisingEvents = false; - watcher.Dispose(); - } - - if (updateTimer != null) - { - updateTimer.Dispose(); - updateTimer = null; - } - - FileSystemWatchers.Clear(); - affectedPaths.Clear(); - } - } -} +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Logging; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.ScheduledTasks; +using MediaBrowser.Model.Logging; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.IO +{ + /// + /// Class DirectoryWatchers + /// + public class DirectoryWatchers : IDisposable + { + /// + /// The file system watchers + /// + private ConcurrentBag FileSystemWatchers = new ConcurrentBag(); + /// + /// The update timer + /// + private Timer updateTimer; + /// + /// The affected paths + /// + private readonly ConcurrentDictionary affectedPaths = new ConcurrentDictionary(); + + /// + /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications. + /// + private readonly ConcurrentDictionary TempIgnoredPaths = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// The timer lock + /// + private readonly object timerLock = new object(); + + /// + /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. + /// + /// The path. + public void TemporarilyIgnore(string path) + { + TempIgnoredPaths[path] = path; + } + + /// + /// Removes the temp ignore. + /// + /// The path. + public void RemoveTempIgnore(string path) + { + string val; + TempIgnoredPaths.TryRemove(path, out val); + } + + /// + /// Gets or sets the logger. + /// + /// The logger. + private ILogger Logger { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public DirectoryWatchers() + { + Logger = LogManager.GetLogger(GetType().Name); + } + + /// + /// Starts this instance. + /// + internal void Start() + { + Kernel.Instance.LibraryManager.LibraryChanged += Instance_LibraryChanged; + + var pathsToWatch = new List { Kernel.Instance.RootFolder.Path }; + + var paths = Kernel.Instance.RootFolder.Children.OfType() + .SelectMany(f => + { + try + { + // Accessing ResolveArgs could involve file system access + return f.ResolveArgs.PhysicalLocations; + } + catch (IOException) + { + return new string[] {}; + } + + }) + .Where(Path.IsPathRooted); + + foreach (var path in paths) + { + if (!ContainsParentFolder(pathsToWatch, path)) + { + pathsToWatch.Add(path); + } + } + + foreach (var path in pathsToWatch) + { + StartWatchingPath(path); + } + } + + /// + /// Examine a list of strings assumed to be file paths to see if it contains a parent of + /// the provided path. + /// + /// The LST. + /// The path. + /// true if [contains parent folder] [the specified LST]; otherwise, false. + /// path + private static bool ContainsParentFolder(IEnumerable lst, string path) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException("path"); + } + + path = path.TrimEnd(Path.DirectorySeparatorChar); + + return lst.Any(str => + { + //this should be a little quicker than examining each actual parent folder... + var compare = str.TrimEnd(Path.DirectorySeparatorChar); + + return (path.Equals(compare, StringComparison.OrdinalIgnoreCase) || (path.StartsWith(compare, StringComparison.OrdinalIgnoreCase) && path[compare.Length] == Path.DirectorySeparatorChar)); + }); + } + + /// + /// Starts the watching path. + /// + /// The path. + private void StartWatchingPath(string path) + { + // Creating a FileSystemWatcher over the LAN can take hundreds of milliseconds, so wrap it in a Task to do them all in parallel + Task.Run(() => + { + var newWatcher = new FileSystemWatcher(path, "*") { IncludeSubdirectories = true, InternalBufferSize = 32767 }; + + newWatcher.Created += watcher_Changed; + newWatcher.Deleted += watcher_Changed; + newWatcher.Renamed += watcher_Changed; + newWatcher.Changed += watcher_Changed; + + newWatcher.Error += watcher_Error; + + try + { + newWatcher.EnableRaisingEvents = true; + FileSystemWatchers.Add(newWatcher); + + Logger.Info("Watching directory " + path); + } + catch (IOException ex) + { + Logger.ErrorException("Error watching path: {0}", ex, path); + } + catch (PlatformNotSupportedException ex) + { + Logger.ErrorException("Error watching path: {0}", ex, path); + } + }); + } + + /// + /// Stops the watching path. + /// + /// The path. + private void StopWatchingPath(string path) + { + var watcher = FileSystemWatchers.FirstOrDefault(f => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); + + if (watcher != null) + { + DisposeWatcher(watcher); + } + } + + /// + /// Disposes the watcher. + /// + /// The watcher. + private void DisposeWatcher(FileSystemWatcher watcher) + { + Logger.Info("Stopping directory watching for path {0}", watcher.Path); + + watcher.EnableRaisingEvents = false; + watcher.Dispose(); + + var watchers = FileSystemWatchers.ToList(); + + watchers.Remove(watcher); + + FileSystemWatchers = new ConcurrentBag(watchers); + } + + /// + /// Handles the LibraryChanged event of the Kernel + /// + /// The source of the event. + /// The instance containing the event data. + void Instance_LibraryChanged(object sender, ChildrenChangedEventArgs e) + { + if (e.Folder is AggregateFolder && e.HasAddOrRemoveChange) + { + if (e.ItemsRemoved != null) + { + foreach (var item in e.ItemsRemoved.OfType()) + { + StopWatchingPath(item.Path); + } + } + if (e.ItemsAdded != null) + { + foreach (var item in e.ItemsAdded.OfType()) + { + StartWatchingPath(item.Path); + } + } + } + } + + /// + /// Handles the Error event of the watcher control. + /// + /// The source of the event. + /// The instance containing the event data. + async void watcher_Error(object sender, ErrorEventArgs e) + { + var ex = e.GetException(); + var dw = (FileSystemWatcher) sender; + + Logger.ErrorException("Error in Directory watcher for: "+dw.Path, ex); + + if (ex.Message.Contains("network name is no longer available")) + { + //Network either dropped or, we are coming out of sleep and it hasn't reconnected yet - wait and retry + Logger.Warn("Network connection lost - will retry..."); + var retries = 0; + var success = false; + while (!success && retries < 10) + { + await Task.Delay(500).ConfigureAwait(false); + + try + { + dw.EnableRaisingEvents = false; + dw.EnableRaisingEvents = true; + success = true; + } + catch (IOException) + { + Logger.Warn("Network still unavailable..."); + retries++; + } + } + if (!success) + { + Logger.Warn("Unable to access network. Giving up."); + DisposeWatcher(dw); + } + + } + else + { + if (!ex.Message.Contains("BIOS command limit")) + { + Logger.Info("Attempting to re-start watcher."); + + dw.EnableRaisingEvents = false; + dw.EnableRaisingEvents = true; + } + + } + } + + /// + /// Handles the Changed event of the watcher control. + /// + /// The source of the event. + /// The instance containing the event data. + void watcher_Changed(object sender, FileSystemEventArgs e) + { + if (e.ChangeType == WatcherChangeTypes.Created && e.Name == "New folder") + { + return; + } + if (TempIgnoredPaths.ContainsKey(e.FullPath)) + { + Logger.Info("Watcher requested to ignore change to " + e.FullPath); + return; + } + + Logger.Info("Watcher sees change of type " + e.ChangeType.ToString() + " to " + e.FullPath); + + //Since we're watching created, deleted and renamed we always want the parent of the item to be the affected path + var affectedPath = e.FullPath; + + affectedPaths.AddOrUpdate(affectedPath, affectedPath, (key, oldValue) => affectedPath); + + lock (timerLock) + { + if (updateTimer == null) + { + updateTimer = new Timer(TimerStopped, null, TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1)); + } + else + { + updateTimer.Change(TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1)); + } + } + } + + /// + /// Timers the stopped. + /// + /// The state info. + private async void TimerStopped(object stateInfo) + { + lock (timerLock) + { + // Extend the timer as long as any of the paths are still being written to. + if (affectedPaths.Any(p => IsFileLocked(p.Key))) + { + Logger.Info("Timer extended."); + updateTimer.Change(TimeSpan.FromSeconds(Kernel.Instance.Configuration.FileWatcherDelay), TimeSpan.FromMilliseconds(-1)); + return; + } + + Logger.Info("Timer stopped."); + + updateTimer.Dispose(); + updateTimer = null; + } + + var paths = affectedPaths.Keys.ToList(); + affectedPaths.Clear(); + + await ProcessPathChanges(paths).ConfigureAwait(false); + } + + /// + /// Try and determine if a file is locked + /// This is not perfect, and is subject to race conditions, so I'd rather not make this a re-usable library method. + /// + /// The path. + /// true if [is file locked] [the specified path]; otherwise, false. + private bool IsFileLocked(string path) + { + try + { + var data = FileSystem.GetFileData(path); + + if (!data.HasValue || data.Value.IsDirectory) + { + return false; + } + } + catch (IOException) + { + return false; + } + + FileStream stream = null; + + try + { + stream = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite); + } + catch + { + //the file is unavailable because it is: + //still being written to + //or being processed by another thread + //or does not exist (has already been processed) + return true; + } + finally + { + if (stream != null) + stream.Close(); + } + + //file is not locked + return false; + } + + /// + /// Processes the path changes. + /// + /// The paths. + /// Task. + private async Task ProcessPathChanges(List paths) + { + var itemsToRefresh = paths.Select(Path.GetDirectoryName) + .Select(GetAffectedBaseItem) + .Where(item => item != null) + .Distinct() + .ToList(); + + foreach (var p in paths) Logger.Info(p + " reports change."); + + // If the root folder changed, run the library task so the user can see it + if (itemsToRefresh.Any(i => i is AggregateFolder)) + { + Kernel.Instance.TaskManager.CancelIfRunningAndQueue(); + return; + } + + await Task.WhenAll(itemsToRefresh.Select(i => Task.Run(async () => + { + Logger.Info(i.Name + " (" + i.Path + ") will be refreshed."); + + try + { + await i.ChangedExternally().ConfigureAwait(false); + } + catch (IOException ex) + { + // For now swallow and log. + // Research item: If an IOException occurs, the item may be in a disconnected state (media unavailable) + // Should we remove it from it's parent? + Logger.ErrorException("Error refreshing {0}", ex, i.Name); + } + + }))).ConfigureAwait(false); + } + + /// + /// Gets the affected base item. + /// + /// The path. + /// BaseItem. + private BaseItem GetAffectedBaseItem(string path) + { + BaseItem item = null; + + while (item == null && !string.IsNullOrEmpty(path)) + { + item = Kernel.Instance.RootFolder.FindByPath(path); + + path = Path.GetDirectoryName(path); + } + + if (item != null) + { + // If the item has been deleted find the first valid parent that still exists + while (!Directory.Exists(item.Path) && !File.Exists(item.Path)) + { + item = item.Parent; + + if (item == null) + { + break; + } + } + } + + return item; + } + + /// + /// Stops this instance. + /// + private void Stop() + { + Kernel.Instance.LibraryManager.LibraryChanged -= Instance_LibraryChanged; + + FileSystemWatcher watcher; + + while (FileSystemWatchers.TryTake(out watcher)) + { + watcher.Changed -= watcher_Changed; + watcher.EnableRaisingEvents = false; + watcher.Dispose(); + } + + lock (timerLock) + { + if (updateTimer != null) + { + updateTimer.Dispose(); + updateTimer = null; + } + } + + affectedPaths.Clear(); + } + + /// + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected virtual void Dispose(bool dispose) + { + if (dispose) + { + Stop(); + } + } + } +} diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs index 4ae2ee72f3..74d0a7e6fe 100644 --- a/MediaBrowser.Controller/IO/FileData.cs +++ b/MediaBrowser.Controller/IO/FileData.cs @@ -1,251 +1,130 @@ -using MediaBrowser.Common.Logging; -using System; -using System.Collections.Generic; -using System.IO; -using System.Runtime.InteropServices; - -namespace MediaBrowser.Controller.IO -{ - /// - /// Provides low level File access that is much faster than the File/Directory api's - /// - public static class FileData - { - public const int MAX_PATH = 260; - public const int MAX_ALTERNATE = 14; - public static IntPtr INVALID_HANDLE_VALUE = new IntPtr(-1); - - /// - /// Gets information about a path - /// - public static WIN32_FIND_DATA GetFileData(string path) - { - WIN32_FIND_DATA data; - IntPtr handle = FindFirstFile(path, out data); - bool getFilename = false; - - if (handle == INVALID_HANDLE_VALUE && !Path.HasExtension(path)) - { - if (!path.EndsWith("*")) - { - Logger.LogInfo("Handle came back invalid for {0}. Since this is a directory we'll try appending \\*.", path); - - FindClose(handle); - - handle = FindFirstFile(Path.Combine(path, "*"), out data); - - getFilename = true; - } - } - - if (handle == IntPtr.Zero) - { - throw new IOException("FindFirstFile failed"); - } - - if (getFilename) - { - data.cFileName = Path.GetFileName(path); - } - - FindClose(handle); - - data.Path = path; - return data; - } - - /// - /// Gets all file system entries within a foler - /// - public static IEnumerable GetFileSystemEntries(string path, string searchPattern) - { - return GetFileSystemEntries(path, searchPattern, true, true); - } - - /// - /// Gets all files within a folder - /// - public static IEnumerable GetFiles(string path, string searchPattern) - { - return GetFileSystemEntries(path, searchPattern, true, false); - } - - /// - /// Gets all sub-directories within a folder - /// - public static IEnumerable GetDirectories(string path, string searchPattern) - { - return GetFileSystemEntries(path, searchPattern, false, true); - } - - /// - /// Gets all file system entries within a foler - /// - public static IEnumerable GetFileSystemEntries(string path, string searchPattern, bool includeFiles, bool includeDirectories) - { - string lpFileName = Path.Combine(path, searchPattern); - - WIN32_FIND_DATA lpFindFileData; - var handle = FindFirstFile(lpFileName, out lpFindFileData); - - if (handle == IntPtr.Zero) - { - int hr = Marshal.GetLastWin32Error(); - if (hr != 2 && hr != 0x12) - { - throw new IOException("GetFileSystemEntries failed"); - } - yield break; - } - - if (IncludeInOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories)) - { - yield return lpFindFileData; - } - - while (FindNextFile(handle, out lpFindFileData) != IntPtr.Zero) - { - if (IncludeInOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories)) - { - lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName); - yield return lpFindFileData; - } - } - - FindClose(handle); - } - - private static bool IncludeInOutput(string cFileName, FileAttributes attributes, bool includeFiles, bool includeDirectories) - { - if (cFileName.Equals(".", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - if (cFileName.Equals("..", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - if (!includeFiles && !attributes.HasFlag(FileAttributes.Directory)) - { - return false; - } - - if (!includeDirectories && attributes.HasFlag(FileAttributes.Directory)) - { - return false; - } - - return true; - } - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern IntPtr FindFirstFile(string fileName, out WIN32_FIND_DATA data); - - [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)] - private static extern IntPtr FindNextFile(IntPtr hFindFile, out WIN32_FIND_DATA data); - - [DllImport("kernel32")] - private static extern bool FindClose(IntPtr hFindFile); - - private const char SpaceChar = ' '; - private static readonly char[] InvalidFileNameChars = Path.GetInvalidFileNameChars(); - - /// - /// Takes a filename and removes invalid characters - /// - public static string GetValidFilename(string filename) - { - foreach (char c in InvalidFileNameChars) - { - filename = filename.Replace(c, SpaceChar); - } - - return filename; - } - } - - [StructLayout(LayoutKind.Sequential)] - public struct FILETIME - { - public uint dwLowDateTime; - public uint dwHighDateTime; - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] - public struct WIN32_FIND_DATA - { - public FileAttributes dwFileAttributes; - public FILETIME ftCreationTime; - public FILETIME ftLastAccessTime; - public FILETIME ftLastWriteTime; - public int nFileSizeHigh; - public int nFileSizeLow; - public int dwReserved0; - public int dwReserved1; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = FileData.MAX_PATH)] - public string cFileName; - - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = FileData.MAX_ALTERNATE)] - public string cAlternate; - - public bool IsHidden - { - get - { - return dwFileAttributes.HasFlag(FileAttributes.Hidden); - } - } - - public bool IsSystemFile - { - get - { - return dwFileAttributes.HasFlag(FileAttributes.System); - } - } - - public bool IsDirectory - { - get - { - return dwFileAttributes.HasFlag(FileAttributes.Directory); - } - } - - public DateTime CreationTimeUtc - { - get - { - return ParseFileTime(ftCreationTime); - } - } - - public DateTime LastAccessTimeUtc - { - get - { - return ParseFileTime(ftLastAccessTime); - } - } - - public DateTime LastWriteTimeUtc - { - get - { - return ParseFileTime(ftLastWriteTime); - } - } - - private DateTime ParseFileTime(FILETIME filetime) - { - long highBits = filetime.dwHighDateTime; - highBits = highBits << 32; - return DateTime.FromFileTimeUtc(highBits + (long)filetime.dwLowDateTime); - } - - public string Path { get; set; } - } - -} +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Logging; +using MediaBrowser.Common.Win32; +using MediaBrowser.Controller.Library; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.InteropServices; + +namespace MediaBrowser.Controller.IO +{ + /// + /// Provides low level File access that is much faster than the File/Directory api's + /// + public static class FileData + { + /// + /// Gets all file system entries within a foler + /// + /// The path. + /// The search pattern. + /// if set to true [include files]. + /// if set to true [include directories]. + /// The flatten folder depth. + /// The args. + /// Dictionary{System.StringWIN32_FIND_DATA}. + /// + /// GetFileSystemEntries failed + public static Dictionary GetFilteredFileSystemEntries(string path, string searchPattern = "*", bool includeFiles = true, bool includeDirectories = true, int flattenFolderDepth = 0, ItemResolveArgs args = null) + { + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + + var lpFileName = Path.Combine(path, searchPattern); + + WIN32_FIND_DATA lpFindFileData; + var handle = NativeMethods.FindFirstFileEx(lpFileName, FINDEX_INFO_LEVELS.FindExInfoBasic, out lpFindFileData, + FINDEX_SEARCH_OPS.FindExSearchNameMatch, IntPtr.Zero, FindFirstFileExFlags.FIND_FIRST_EX_LARGE_FETCH); + + if (handle == IntPtr.Zero) + { + int hr = Marshal.GetLastWin32Error(); + if (hr != 2 && hr != 0x12) + { + throw new IOException("GetFileSystemEntries failed"); + } + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + var dict = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if (FileSystem.IncludeInFindFileOutput(lpFindFileData.cFileName, lpFindFileData.dwFileAttributes, includeFiles, includeDirectories)) + { + if (!string.IsNullOrEmpty(lpFindFileData.cFileName)) + { + lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName); + + dict[lpFindFileData.Path] = lpFindFileData; + } + } + + while (NativeMethods.FindNextFile(handle, out lpFindFileData) != IntPtr.Zero) + { + // This is the one circumstance where we can completely disregard a file + if (lpFindFileData.IsSystemFile) + { + continue; + } + + // Filter out invalid entries + if (lpFindFileData.cFileName.Equals(".", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + if (lpFindFileData.cFileName.Equals("..", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + lpFindFileData.Path = Path.Combine(path, lpFindFileData.cFileName); + + if (FileSystem.IsShortcut(lpFindFileData.Path)) + { + var newPath = FileSystem.ResolveShortcut(lpFindFileData.Path); + if (string.IsNullOrWhiteSpace(newPath)) + { + //invalid shortcut - could be old or target could just be unavailable + Logger.LogWarning("Encountered invalid shortuct: "+lpFindFileData.Path); + continue; + } + var data = FileSystem.GetFileData(newPath); + + if (data.HasValue) + { + lpFindFileData = data.Value; + + // Find out if the shortcut is pointing to a directory or file + if (lpFindFileData.IsDirectory) + { + // add to our physical locations + if (args != null) + { + args.AddAdditionalLocation(newPath); + } + } + + dict[lpFindFileData.Path] = lpFindFileData; + } + } + else if (flattenFolderDepth > 0 && lpFindFileData.IsDirectory) + { + foreach (var child in GetFilteredFileSystemEntries(lpFindFileData.Path, flattenFolderDepth: flattenFolderDepth - 1)) + { + dict[child.Key] = child.Value; + } + } + else + { + dict[lpFindFileData.Path] = lpFindFileData; + } + } + + NativeMethods.FindClose(handle); + return dict; + } + } + +} diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs deleted file mode 100644 index 732cf0803e..0000000000 --- a/MediaBrowser.Controller/IO/FileSystemHelper.cs +++ /dev/null @@ -1,132 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.IO; -using System.Threading.Tasks; -using MediaBrowser.Controller.Resolvers; -using MediaBrowser.Controller.Library; - -namespace MediaBrowser.Controller.IO -{ - public static class FileSystemHelper - { - /// - /// Transforms shortcuts into their actual paths and filters out items that should be ignored - /// - public static ItemResolveEventArgs FilterChildFileSystemEntries(ItemResolveEventArgs args, bool flattenShortcuts) - { - - List returnChildren = new List(); - List resolvedShortcuts = new List(); - - foreach (var file in args.FileSystemChildren) - { - // If it's a shortcut, resolve it - if (Shortcut.IsShortcut(file.Path)) - { - string newPath = Shortcut.ResolveShortcut(file.Path); - WIN32_FIND_DATA newPathData = FileData.GetFileData(newPath); - - // Find out if the shortcut is pointing to a directory or file - if (newPathData.IsDirectory) - { - // add to our physical locations - args.AdditionalLocations.Add(newPath); - - // If we're flattening then get the shortcut's children - if (flattenShortcuts) - { - returnChildren.Add(file); - ItemResolveEventArgs newArgs = new ItemResolveEventArgs() - { - FileSystemChildren = FileData.GetFileSystemEntries(newPath, "*").ToArray() - }; - - resolvedShortcuts.AddRange(FilterChildFileSystemEntries(newArgs, false).FileSystemChildren); - } - else - { - returnChildren.Add(newPathData); - } - } - else - { - returnChildren.Add(newPathData); - } - } - else - { - //not a shortcut check to see if we should filter it out - if (EntityResolutionHelper.ShouldResolvePath(file)) - { - returnChildren.Add(file); - } - else - { - //filtered - see if it is one of our "indicator" folders and mark it now - no reason to search for it again - args.IsBDFolder |= file.cFileName.Equals("bdmv", StringComparison.OrdinalIgnoreCase); - args.IsDVDFolder |= file.cFileName.Equals("video_ts", StringComparison.OrdinalIgnoreCase); - args.IsHDDVDFolder |= file.cFileName.Equals("hvdvd_ts", StringComparison.OrdinalIgnoreCase); - - //and check to see if it is a metadata folder and collect contents now if so - if (IsMetadataFolder(file.cFileName)) - { - args.MetadataFiles = Directory.GetFiles(Path.Combine(args.Path, "metadata"), "*", SearchOption.TopDirectoryOnly); - } - } - } - } - - if (resolvedShortcuts.Count > 0) - { - resolvedShortcuts.InsertRange(0, returnChildren); - args.FileSystemChildren = resolvedShortcuts.ToArray(); - } - else - { - args.FileSystemChildren = returnChildren.ToArray(); - } - return args; - } - - public static bool IsMetadataFolder(string path) - { - return path.TrimEnd('\\').EndsWith("metadata", StringComparison.OrdinalIgnoreCase); - } - - public static bool IsVideoFile(string path) - { - string extension = System.IO.Path.GetExtension(path).ToLower(); - - switch (extension) - { - case ".mkv": - case ".m2ts": - case ".iso": - case ".ts": - case ".rmvb": - case ".mov": - case ".avi": - case ".mpg": - case ".mpeg": - case ".wmv": - case ".mp4": - case ".divx": - case ".dvr-ms": - case ".wtv": - case ".ogm": - case ".ogv": - case ".asf": - case ".m4v": - case ".flv": - case ".f4v": - case ".3gp": - return true; - - default: - return false; - } - } - } -} diff --git a/MediaBrowser.Controller/IO/FileSystemManager.cs b/MediaBrowser.Controller/IO/FileSystemManager.cs new file mode 100644 index 0000000000..e33983489b --- /dev/null +++ b/MediaBrowser.Controller/IO/FileSystemManager.cs @@ -0,0 +1,112 @@ +using MediaBrowser.Common.IO; +using MediaBrowser.Common.Kernel; +using MediaBrowser.Controller.Entities; +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace MediaBrowser.Controller.IO +{ + /// + /// This class will manage our file system watching and modifications. Any process that needs to + /// modify the directories that the system is watching for changes should use the methods of + /// this class to do so. This way we can have the watchers correctly respond to only external changes. + /// + public class FileSystemManager : BaseManager + { + /// + /// Gets or sets the directory watchers. + /// + /// The directory watchers. + private DirectoryWatchers DirectoryWatchers { get; set; } + + /// + /// Initializes a new instance of the class. + /// + /// The kernel. + public FileSystemManager(Kernel kernel) + : base(kernel) + { + DirectoryWatchers = new DirectoryWatchers(); + } + + /// + /// Start the directory watchers on our library folders + /// + public void StartWatchers() + { + DirectoryWatchers.Start(); + } + + /// + /// Saves to library filesystem. + /// + /// The item. + /// The path. + /// The data to save. + /// The cancellation token. + /// Task. + /// + public async Task SaveToLibraryFilesystem(BaseItem item, string path, Stream dataToSave, CancellationToken cancellationToken) + { + if (item == null) + { + throw new ArgumentNullException(); + } + if (string.IsNullOrEmpty(path)) + { + throw new ArgumentNullException(); + } + if (dataToSave == null) + { + throw new ArgumentNullException(); + } + if (cancellationToken == null) + { + throw new ArgumentNullException(); + } + + cancellationToken.ThrowIfCancellationRequested(); + + //Tell the watchers to ignore + DirectoryWatchers.TemporarilyIgnore(path); + + //Make the mod + + dataToSave.Position = 0; + + try + { + using (var fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous)) + { + await dataToSave.CopyToAsync(fs, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); + + dataToSave.Dispose(); + + // If this is ever used for something other than metadata we can add a file type param + item.ResolveArgs.AddMetadataFile(path); + } + } + finally + { + //Remove the ignore + DirectoryWatchers.RemoveTempIgnore(path); + } + } + + /// + /// Releases unmanaged and - optionally - managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool dispose) + { + if (dispose) + { + DirectoryWatchers.Dispose(); + } + + base.Dispose(dispose); + } + } +} diff --git a/MediaBrowser.Controller/IO/Shortcut.cs b/MediaBrowser.Controller/IO/Shortcut.cs deleted file mode 100644 index e9ea21f17a..0000000000 --- a/MediaBrowser.Controller/IO/Shortcut.cs +++ /dev/null @@ -1,185 +0,0 @@ -using System; -using System.IO; -using System.Runtime.InteropServices; -using System.Text; - -namespace MediaBrowser.Controller.IO -{ - /// - /// Contains helpers to interact with shortcut files (.lnk) - /// - public static class Shortcut - { - #region Signitures were imported from http://pinvoke.net - [Flags()] - enum SLGP_FLAGS - { - /// Retrieves the standard short (8.3 format) file name - SLGP_SHORTPATH = 0x1, - /// Retrieves the Universal Naming Convention (UNC) path name of the file - SLGP_UNCPRIORITY = 0x2, - /// Retrieves the raw path name. A raw path is something that might not exist and may include environment variables that need to be expanded - SLGP_RAWPATH = 0x4 - } - - [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)] - struct WIN32_FIND_DATAW - { - public uint dwFileAttributes; - public long ftCreationTime; - public long ftLastAccessTime; - public long ftLastWriteTime; - public uint nFileSizeHigh; - public uint nFileSizeLow; - public uint dwReserved0; - public uint dwReserved1; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)] - public string cFileName; - [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)] - public string cAlternateFileName; - } - - [Flags()] - - enum SLR_FLAGS - { - /// - /// Do not display a dialog box if the link cannot be resolved. When SLR_NO_UI is set, - /// the high-order word of fFlags can be set to a time-out value that specifies the - /// maximum amount of time to be spent resolving the link. The function returns if the - /// link cannot be resolved within the time-out duration. If the high-order word is set - /// to zero, the time-out duration will be set to the default value of 3,000 milliseconds - /// (3 seconds). To specify a value, set the high word of fFlags to the desired time-out - /// duration, in milliseconds. - /// - SLR_NO_UI = 0x1, - /// Obsolete and no longer used - SLR_ANY_MATCH = 0x2, - /// If the link object has changed, update its path and list of identifiers. - /// If SLR_UPDATE is set, you do not need to call IPersistFile::IsDirty to determine - /// whether or not the link object has changed. - SLR_UPDATE = 0x4, - /// Do not update the link information - SLR_NOUPDATE = 0x8, - /// Do not execute the search heuristics - SLR_NOSEARCH = 0x10, - /// Do not use distributed link tracking - SLR_NOTRACK = 0x20, - /// Disable distributed link tracking. By default, distributed link tracking tracks - /// removable media across multiple devices based on the volume name. It also uses the - /// Universal Naming Convention (UNC) path to track remote file systems whose drive letter - /// has changed. Setting SLR_NOLINKINFO disables both types of tracking. - SLR_NOLINKINFO = 0x40, - /// Call the Microsoft Windows Installer - SLR_INVOKE_MSI = 0x80 - } - - - /// The IShellLink interface allows Shell links to be created, modified, and resolved - [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")] - interface IShellLinkW - { - /// Retrieves the path and file name of a Shell link object - void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags); - /// Retrieves the list of item identifiers for a Shell link object - void GetIDList(out IntPtr ppidl); - /// Sets the pointer to an item identifier list (PIDL) for a Shell link object. - void SetIDList(IntPtr pidl); - /// Retrieves the description string for a Shell link object - void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName); - /// Sets the description for a Shell link object. The description can be any application-defined string - void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName); - /// Retrieves the name of the working directory for a Shell link object - void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath); - /// Sets the name of the working directory for a Shell link object - void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir); - /// Retrieves the command-line arguments associated with a Shell link object - void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath); - /// Sets the command-line arguments for a Shell link object - void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs); - /// Retrieves the hot key for a Shell link object - void GetHotkey(out short pwHotkey); - /// Sets a hot key for a Shell link object - void SetHotkey(short wHotkey); - /// Retrieves the show command for a Shell link object - void GetShowCmd(out int piShowCmd); - /// Sets the show command for a Shell link object. The show command sets the initial show state of the window. - void SetShowCmd(int iShowCmd); - /// Retrieves the location (path and index) of the icon for a Shell link object - void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, - int cchIconPath, out int piIcon); - /// Sets the location (path and index) of the icon for a Shell link object - void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon); - /// Sets the relative path to the Shell link object - void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved); - /// Attempts to find the target of a Shell link, even if it has been moved or renamed - void Resolve(IntPtr hwnd, SLR_FLAGS fFlags); - /// Sets the path and file name of a Shell link object - void SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile); - - } - - [ComImport, Guid("0000010c-0000-0000-c000-000000000046"), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IPersist - { - [PreserveSig] - void GetClassID(out Guid pClassID); - } - - - [ComImport, Guid("0000010b-0000-0000-C000-000000000046"), - InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] - public interface IPersistFile : IPersist - { - new void GetClassID(out Guid pClassID); - [PreserveSig] - int IsDirty(); - - [PreserveSig] - void Load([In, MarshalAs(UnmanagedType.LPWStr)] - string pszFileName, uint dwMode); - - [PreserveSig] - void Save([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName, - [In, MarshalAs(UnmanagedType.Bool)] bool remember); - - [PreserveSig] - void SaveCompleted([In, MarshalAs(UnmanagedType.LPWStr)] string pszFileName); - - [PreserveSig] - void GetCurFile([In, MarshalAs(UnmanagedType.LPWStr)] string ppszFileName); - } - - const uint STGM_READ = 0; - const int MAX_PATH = 260; - - // CLSID_ShellLink from ShlGuid.h - [ - ComImport(), - Guid("00021401-0000-0000-C000-000000000046") - ] - public class ShellLink - { - } - - #endregion - - public static string ResolveShortcut(string filename) - { - var link = new ShellLink(); - ((IPersistFile)link).Load(filename, 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(MAX_PATH); - var data = new WIN32_FIND_DATAW(); - ((IShellLinkW)link).GetPath(sb, sb.Capacity, out data, 0); - return sb.ToString(); - } - - public static bool IsShortcut(string filename) - { - return filename != null ? Path.GetExtension(filename).EndsWith("lnk", StringComparison.OrdinalIgnoreCase) : false; - } - } -} -- cgit v1.2.3