aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/IO
diff options
context:
space:
mode:
authorLukePulverenti <luke.pulverenti@gmail.com>2013-02-20 20:33:05 -0500
committerLukePulverenti <luke.pulverenti@gmail.com>2013-02-20 20:33:05 -0500
commit767cdc1f6f6a63ce997fc9476911e2c361f9d402 (patch)
tree49add55976f895441167c66cfa95e5c7688d18ce /MediaBrowser.Controller/IO
parent845554722efaed872948a9e0f7202e3ef52f1b6e (diff)
Pushing missing changes
Diffstat (limited to 'MediaBrowser.Controller/IO')
-rw-r--r--MediaBrowser.Controller/IO/DirectoryWatchers.cs697
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs381
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs132
-rw-r--r--MediaBrowser.Controller/IO/FileSystemManager.cs112
-rw-r--r--MediaBrowser.Controller/IO/Shortcut.cs185
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;
- }
- }
-}