diff options
| author | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-20 20:33:05 -0500 |
|---|---|---|
| committer | LukePulverenti <luke.pulverenti@gmail.com> | 2013-02-20 20:33:05 -0500 |
| commit | 767cdc1f6f6a63ce997fc9476911e2c361f9d402 (patch) | |
| tree | 49add55976f895441167c66cfa95e5c7688d18ce /MediaBrowser.Controller/IO | |
| parent | 845554722efaed872948a9e0f7202e3ef52f1b6e (diff) | |
Pushing missing changes
Diffstat (limited to 'MediaBrowser.Controller/IO')
| -rw-r--r-- | MediaBrowser.Controller/IO/DirectoryWatchers.cs | 697 | ||||
| -rw-r--r-- | MediaBrowser.Controller/IO/FileData.cs | 381 | ||||
| -rw-r--r-- | MediaBrowser.Controller/IO/FileSystemHelper.cs | 132 | ||||
| -rw-r--r-- | MediaBrowser.Controller/IO/FileSystemManager.cs | 112 | ||||
| -rw-r--r-- | MediaBrowser.Controller/IO/Shortcut.cs | 185 |
5 files changed, 767 insertions, 740 deletions
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<FileSystemWatcher> FileSystemWatchers = new List<FileSystemWatcher>();
- private Timer updateTimer;
- private List<string> affectedPaths = new List<string>();
-
- private const int TimerDelayInSeconds = 30;
-
- public void Start()
- {
- var pathsToWatch = new List<string>();
-
- var rootFolder = Kernel.Instance.RootFolder;
-
- pathsToWatch.Add(rootFolder.Path);
-
- foreach (Folder folder in rootFolder.Children.OfType<Folder>())
- {
- 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<string> paths;
- lock (affectedPaths)
- {
- paths = affectedPaths;
- affectedPaths = new List<string>();
- }
-
- await ProcessPathChanges(paths).ConfigureAwait(false);
- }
-
- private Task ProcessPathChanges(IEnumerable<string> paths)
- {
- var itemsToRefresh = new List<BaseItem>();
-
- 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 +{ + /// <summary> + /// Class DirectoryWatchers + /// </summary> + public class DirectoryWatchers : IDisposable + { + /// <summary> + /// The file system watchers + /// </summary> + private ConcurrentBag<FileSystemWatcher> FileSystemWatchers = new ConcurrentBag<FileSystemWatcher>(); + /// <summary> + /// The update timer + /// </summary> + private Timer updateTimer; + /// <summary> + /// The affected paths + /// </summary> + private readonly ConcurrentDictionary<string, string> affectedPaths = new ConcurrentDictionary<string, string>(); + + /// <summary> + /// A dynamic list of paths that should be ignored. Added to during our own file sytem modifications. + /// </summary> + private readonly ConcurrentDictionary<string,string> TempIgnoredPaths = new ConcurrentDictionary<string, string>(StringComparer.OrdinalIgnoreCase); + + /// <summary> + /// The timer lock + /// </summary> + private readonly object timerLock = new object(); + + /// <summary> + /// Add the path to our temporary ignore list. Use when writing to a path within our listening scope. + /// </summary> + /// <param name="path">The path.</param> + public void TemporarilyIgnore(string path) + { + TempIgnoredPaths[path] = path; + } + + /// <summary> + /// Removes the temp ignore. + /// </summary> + /// <param name="path">The path.</param> + public void RemoveTempIgnore(string path) + { + string val; + TempIgnoredPaths.TryRemove(path, out val); + } + + /// <summary> + /// Gets or sets the logger. + /// </summary> + /// <value>The logger.</value> + private ILogger Logger { get; set; } + + /// <summary> + /// Initializes a new instance of the <see cref="DirectoryWatchers" /> class. + /// </summary> + public DirectoryWatchers() + { + Logger = LogManager.GetLogger(GetType().Name); + } + + /// <summary> + /// Starts this instance. + /// </summary> + internal void Start() + { + Kernel.Instance.LibraryManager.LibraryChanged += Instance_LibraryChanged; + + var pathsToWatch = new List<string> { Kernel.Instance.RootFolder.Path }; + + var paths = Kernel.Instance.RootFolder.Children.OfType<Folder>() + .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); + } + } + + /// <summary> + /// Examine a list of strings assumed to be file paths to see if it contains a parent of + /// the provided path. + /// </summary> + /// <param name="lst">The LST.</param> + /// <param name="path">The path.</param> + /// <returns><c>true</c> if [contains parent folder] [the specified LST]; otherwise, <c>false</c>.</returns> + /// <exception cref="System.ArgumentNullException">path</exception> + private static bool ContainsParentFolder(IEnumerable<string> 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)); + }); + } + + /// <summary> + /// Starts the watching path. + /// </summary> + /// <param name="path">The path.</param> + 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); + } + }); + } + + /// <summary> + /// Stops the watching path. + /// </summary> + /// <param name="path">The path.</param> + private void StopWatchingPath(string path) + { + var watcher = FileSystemWatchers.FirstOrDefault(f => f.Path.Equals(path, StringComparison.OrdinalIgnoreCase)); + + if (watcher != null) + { + DisposeWatcher(watcher); + } + } + + /// <summary> + /// Disposes the watcher. + /// </summary> + /// <param name="watcher">The watcher.</param> + 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<FileSystemWatcher>(watchers); + } + + /// <summary> + /// Handles the LibraryChanged event of the Kernel + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="Library.ChildrenChangedEventArgs" /> instance containing the event data.</param> + 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<Folder>()) + { + StopWatchingPath(item.Path); + } + } + if (e.ItemsAdded != null) + { + foreach (var item in e.ItemsAdded.OfType<Folder>()) + { + StartWatchingPath(item.Path); + } + } + } + } + + /// <summary> + /// Handles the Error event of the watcher control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="ErrorEventArgs" /> instance containing the event data.</param> + 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; + } + + } + } + + /// <summary> + /// Handles the Changed event of the watcher control. + /// </summary> + /// <param name="sender">The source of the event.</param> + /// <param name="e">The <see cref="FileSystemEventArgs" /> instance containing the event data.</param> + 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)); + } + } + } + + /// <summary> + /// Timers the stopped. + /// </summary> + /// <param name="stateInfo">The state info.</param> + 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); + } + + /// <summary> + /// 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. + /// </summary> + /// <param name="path">The path.</param> + /// <returns><c>true</c> if [is file locked] [the specified path]; otherwise, <c>false</c>.</returns> + 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; + } + + /// <summary> + /// Processes the path changes. + /// </summary> + /// <param name="paths">The paths.</param> + /// <returns>Task.</returns> + private async Task ProcessPathChanges(List<string> 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<RefreshMediaLibraryTask>(); + 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); + } + + /// <summary> + /// Gets the affected base item. + /// </summary> + /// <param name="path">The path.</param> + /// <returns>BaseItem.</returns> + 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; + } + + /// <summary> + /// Stops this instance. + /// </summary> + 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(); + } + + /// <summary> + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + /// </summary> + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + 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
-{
- /// <summary>
- /// Provides low level File access that is much faster than the File/Directory api's
- /// </summary>
- 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);
-
- /// <summary>
- /// Gets information about a path
- /// </summary>
- 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;
- }
-
- /// <summary>
- /// Gets all file system entries within a foler
- /// </summary>
- public static IEnumerable<WIN32_FIND_DATA> GetFileSystemEntries(string path, string searchPattern)
- {
- return GetFileSystemEntries(path, searchPattern, true, true);
- }
-
- /// <summary>
- /// Gets all files within a folder
- /// </summary>
- public static IEnumerable<WIN32_FIND_DATA> GetFiles(string path, string searchPattern)
- {
- return GetFileSystemEntries(path, searchPattern, true, false);
- }
-
- /// <summary>
- /// Gets all sub-directories within a folder
- /// </summary>
- public static IEnumerable<WIN32_FIND_DATA> GetDirectories(string path, string searchPattern)
- {
- return GetFileSystemEntries(path, searchPattern, false, true);
- }
-
- /// <summary>
- /// Gets all file system entries within a foler
- /// </summary>
- public static IEnumerable<WIN32_FIND_DATA> 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();
-
- /// <summary>
- /// Takes a filename and removes invalid characters
- /// </summary>
- 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 +{ + /// <summary> + /// Provides low level File access that is much faster than the File/Directory api's + /// </summary> + public static class FileData + { + /// <summary> + /// Gets all file system entries within a foler + /// </summary> + /// <param name="path">The path.</param> + /// <param name="searchPattern">The search pattern.</param> + /// <param name="includeFiles">if set to <c>true</c> [include files].</param> + /// <param name="includeDirectories">if set to <c>true</c> [include directories].</param> + /// <param name="flattenFolderDepth">The flatten folder depth.</param> + /// <param name="args">The args.</param> + /// <returns>Dictionary{System.StringWIN32_FIND_DATA}.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + /// <exception cref="System.IO.IOException">GetFileSystemEntries failed</exception> + public static Dictionary<string, WIN32_FIND_DATA> 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<string, WIN32_FIND_DATA>(StringComparer.OrdinalIgnoreCase); + } + + var dict = new Dictionary<string, WIN32_FIND_DATA>(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
- {
- /// <summary>
- /// Transforms shortcuts into their actual paths and filters out items that should be ignored
- /// </summary>
- public static ItemResolveEventArgs FilterChildFileSystemEntries(ItemResolveEventArgs args, bool flattenShortcuts)
- {
-
- List<WIN32_FIND_DATA> returnChildren = new List<WIN32_FIND_DATA>();
- List<WIN32_FIND_DATA> resolvedShortcuts = new List<WIN32_FIND_DATA>();
-
- 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 +{ + /// <summary> + /// 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. + /// </summary> + public class FileSystemManager : BaseManager<Kernel> + { + /// <summary> + /// Gets or sets the directory watchers. + /// </summary> + /// <value>The directory watchers.</value> + private DirectoryWatchers DirectoryWatchers { get; set; } + + /// <summary> + /// Initializes a new instance of the <see cref="FileSystemManager" /> class. + /// </summary> + /// <param name="kernel">The kernel.</param> + public FileSystemManager(Kernel kernel) + : base(kernel) + { + DirectoryWatchers = new DirectoryWatchers(); + } + + /// <summary> + /// Start the directory watchers on our library folders + /// </summary> + public void StartWatchers() + { + DirectoryWatchers.Start(); + } + + /// <summary> + /// Saves to library filesystem. + /// </summary> + /// <param name="item">The item.</param> + /// <param name="path">The path.</param> + /// <param name="dataToSave">The data to save.</param> + /// <param name="cancellationToken">The cancellation token.</param> + /// <returns>Task.</returns> + /// <exception cref="System.ArgumentNullException"></exception> + 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); + } + } + + /// <summary> + /// Releases unmanaged and - optionally - managed resources. + /// </summary> + /// <param name="dispose"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param> + 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
-{
- /// <summary>
- /// Contains helpers to interact with shortcut files (.lnk)
- /// </summary>
- public static class Shortcut
- {
- #region Signitures were imported from http://pinvoke.net
- [Flags()]
- enum SLGP_FLAGS
- {
- /// <summary>Retrieves the standard short (8.3 format) file name</summary>
- SLGP_SHORTPATH = 0x1,
- /// <summary>Retrieves the Universal Naming Convention (UNC) path name of the file</summary>
- SLGP_UNCPRIORITY = 0x2,
- /// <summary>Retrieves the raw path name. A raw path is something that might not exist and may include environment variables that need to be expanded</summary>
- 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
- {
- /// <summary>
- /// 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.
- /// </summary>
- SLR_NO_UI = 0x1,
- /// <summary>Obsolete and no longer used</summary>
- SLR_ANY_MATCH = 0x2,
- /// <summary>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.</summary>
- SLR_UPDATE = 0x4,
- /// <summary>Do not update the link information</summary>
- SLR_NOUPDATE = 0x8,
- /// <summary>Do not execute the search heuristics</summary>
- SLR_NOSEARCH = 0x10,
- /// <summary>Do not use distributed link tracking</summary>
- SLR_NOTRACK = 0x20,
- /// <summary>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.</summary>
- SLR_NOLINKINFO = 0x40,
- /// <summary>Call the Microsoft Windows Installer</summary>
- SLR_INVOKE_MSI = 0x80
- }
-
-
- /// <summary>The IShellLink interface allows Shell links to be created, modified, and resolved</summary>
- [ComImport(), InterfaceType(ComInterfaceType.InterfaceIsIUnknown), Guid("000214F9-0000-0000-C000-000000000046")]
- interface IShellLinkW
- {
- /// <summary>Retrieves the path and file name of a Shell link object</summary>
- void GetPath([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cchMaxPath, out WIN32_FIND_DATAW pfd, SLGP_FLAGS fFlags);
- /// <summary>Retrieves the list of item identifiers for a Shell link object</summary>
- void GetIDList(out IntPtr ppidl);
- /// <summary>Sets the pointer to an item identifier list (PIDL) for a Shell link object.</summary>
- void SetIDList(IntPtr pidl);
- /// <summary>Retrieves the description string for a Shell link object</summary>
- void GetDescription([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cchMaxName);
- /// <summary>Sets the description for a Shell link object. The description can be any application-defined string</summary>
- void SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
- /// <summary>Retrieves the name of the working directory for a Shell link object</summary>
- void GetWorkingDirectory([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cchMaxPath);
- /// <summary>Sets the name of the working directory for a Shell link object</summary>
- void SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
- /// <summary>Retrieves the command-line arguments associated with a Shell link object</summary>
- void GetArguments([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cchMaxPath);
- /// <summary>Sets the command-line arguments for a Shell link object</summary>
- void SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
- /// <summary>Retrieves the hot key for a Shell link object</summary>
- void GetHotkey(out short pwHotkey);
- /// <summary>Sets a hot key for a Shell link object</summary>
- void SetHotkey(short wHotkey);
- /// <summary>Retrieves the show command for a Shell link object</summary>
- void GetShowCmd(out int piShowCmd);
- /// <summary>Sets the show command for a Shell link object. The show command sets the initial show state of the window.</summary>
- void SetShowCmd(int iShowCmd);
- /// <summary>Retrieves the location (path and index) of the icon for a Shell link object</summary>
- void GetIconLocation([Out(), MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath,
- int cchIconPath, out int piIcon);
- /// <summary>Sets the location (path and index) of the icon for a Shell link object</summary>
- void SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
- /// <summary>Sets the relative path to the Shell link object</summary>
- void SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, int dwReserved);
- /// <summary>Attempts to find the target of a Shell link, even if it has been moved or renamed</summary>
- void Resolve(IntPtr hwnd, SLR_FLAGS fFlags);
- /// <summary>Sets the path and file name of a Shell link object</summary>
- 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;
- }
- }
-}
|
