aboutsummaryrefslogtreecommitdiff
path: root/MediaBrowser.Controller/IO
diff options
context:
space:
mode:
Diffstat (limited to 'MediaBrowser.Controller/IO')
-rw-r--r--MediaBrowser.Controller/IO/DirectoryWatchers.cs172
-rw-r--r--MediaBrowser.Controller/IO/FileData.cs251
-rw-r--r--MediaBrowser.Controller/IO/FileSystemHelper.cs132
-rw-r--r--MediaBrowser.Controller/IO/Shortcut.cs185
4 files changed, 740 insertions, 0 deletions
diff --git a/MediaBrowser.Controller/IO/DirectoryWatchers.cs b/MediaBrowser.Controller/IO/DirectoryWatchers.cs
new file mode 100644
index 000000000..eb1358e16
--- /dev/null
+++ b/MediaBrowser.Controller/IO/DirectoryWatchers.cs
@@ -0,0 +1,172 @@
+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();
+ }
+ }
+}
diff --git a/MediaBrowser.Controller/IO/FileData.cs b/MediaBrowser.Controller/IO/FileData.cs
new file mode 100644
index 000000000..4ae2ee72f
--- /dev/null
+++ b/MediaBrowser.Controller/IO/FileData.cs
@@ -0,0 +1,251 @@
+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; }
+ }
+
+}
diff --git a/MediaBrowser.Controller/IO/FileSystemHelper.cs b/MediaBrowser.Controller/IO/FileSystemHelper.cs
new file mode 100644
index 000000000..732cf0803
--- /dev/null
+++ b/MediaBrowser.Controller/IO/FileSystemHelper.cs
@@ -0,0 +1,132 @@
+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/Shortcut.cs b/MediaBrowser.Controller/IO/Shortcut.cs
new file mode 100644
index 000000000..e9ea21f17
--- /dev/null
+++ b/MediaBrowser.Controller/IO/Shortcut.cs
@@ -0,0 +1,185 @@
+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;
+ }
+ }
+}