using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using MediaBrowser.Model.IO; using MediaBrowser.Model.Logging; using MediaBrowser.Model.System; namespace Emby.Common.Implementations.IO { /// /// Class ManagedFileSystem /// public class ManagedFileSystem : IFileSystem { protected ILogger Logger; private readonly bool _supportsAsyncFileStreams; private char[] _invalidFileNameChars; private readonly List _shortcutHandlers = new List(); private bool EnableFileSystemRequestConcat; private string _tempPath; private SharpCifsFileSystem _sharpCifsFileSystem; public ManagedFileSystem(ILogger logger, IEnvironmentInfo environmentInfo, string tempPath) { Logger = logger; _supportsAsyncFileStreams = true; _tempPath = tempPath; // On Linux, this needs to be true or symbolic links are ignored EnableFileSystemRequestConcat = environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.Windows && environmentInfo.OperatingSystem != MediaBrowser.Model.System.OperatingSystem.OSX; SetInvalidFileNameChars(environmentInfo.OperatingSystem == MediaBrowser.Model.System.OperatingSystem.Windows); _sharpCifsFileSystem = new SharpCifsFileSystem(environmentInfo.OperatingSystem); } public void AddShortcutHandler(IShortcutHandler handler) { _shortcutHandlers.Add(handler); } protected void SetInvalidFileNameChars(bool enableManagedInvalidFileNameChars) { if (enableManagedInvalidFileNameChars) { _invalidFileNameChars = Path.GetInvalidFileNameChars(); } else { // GetInvalidFileNameChars is less restrictive in Linux/Mac than Windows, this mimic Windows behavior for mono under Linux/Mac. _invalidFileNameChars = new char[41] { '\x00', '\x01', '\x02', '\x03', '\x04', '\x05', '\x06', '\x07', '\x08', '\x09', '\x0A', '\x0B', '\x0C', '\x0D', '\x0E', '\x0F', '\x10', '\x11', '\x12', '\x13', '\x14', '\x15', '\x16', '\x17', '\x18', '\x19', '\x1A', '\x1B', '\x1C', '\x1D', '\x1E', '\x1F', '\x22', '\x3C', '\x3E', '\x7C', ':', '*', '?', '\\', '/' }; } } public char DirectorySeparatorChar { get { return Path.DirectorySeparatorChar; } } public string GetFullPath(string path) { return Path.GetFullPath(path); } /// /// Determines whether the specified filename is shortcut. /// /// The filename. /// true if the specified filename is shortcut; otherwise, false. /// filename public virtual bool IsShortcut(string filename) { if (string.IsNullOrEmpty(filename)) { throw new ArgumentNullException("filename"); } var extension = Path.GetExtension(filename); return _shortcutHandlers.Any(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); } /// /// Resolves the shortcut. /// /// The filename. /// System.String. /// filename public virtual string ResolveShortcut(string filename) { if (string.IsNullOrEmpty(filename)) { throw new ArgumentNullException("filename"); } var extension = Path.GetExtension(filename); var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); if (handler != null) { return handler.Resolve(filename); } return null; } /// /// Creates the shortcut. /// /// The shortcut path. /// The target. /// /// shortcutPath /// or /// target /// public void CreateShortcut(string shortcutPath, string target) { if (string.IsNullOrEmpty(shortcutPath)) { throw new ArgumentNullException("shortcutPath"); } if (string.IsNullOrEmpty(target)) { throw new ArgumentNullException("target"); } var extension = Path.GetExtension(shortcutPath); var handler = _shortcutHandlers.FirstOrDefault(i => string.Equals(extension, i.Extension, StringComparison.OrdinalIgnoreCase)); if (handler != null) { handler.Create(shortcutPath, target); } else { throw new NotImplementedException(); } } /// /// Returns a object for the specified file or directory path. /// /// A path to a file or directory. /// A object. /// If the specified path points to a directory, the returned object's /// property will be set to true and all other properties will reflect the properties of the directory. public FileSystemMetadata GetFileSystemInfo(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFileSystemInfo(path); } // Take a guess to try and avoid two file system hits, but we'll double-check by calling Exists if (Path.HasExtension(path)) { var fileInfo = new FileInfo(path); if (fileInfo.Exists) { return GetFileSystemMetadata(fileInfo); } return GetFileSystemMetadata(new DirectoryInfo(path)); } else { var fileInfo = new DirectoryInfo(path); if (fileInfo.Exists) { return GetFileSystemMetadata(fileInfo); } return GetFileSystemMetadata(new FileInfo(path)); } } /// /// Returns a object for the specified file path. /// /// A path to a file. /// A object. /// If the specified path points to a directory, the returned object's /// property and the property will both be set to false. /// For automatic handling of files and directories, use . public FileSystemMetadata GetFileInfo(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFileInfo(path); } var fileInfo = new FileInfo(path); return GetFileSystemMetadata(fileInfo); } /// /// Returns a object for the specified directory path. /// /// A path to a directory. /// A object. /// If the specified path points to a file, the returned object's /// property will be set to true and the property will be set to false. /// For automatic handling of files and directories, use . public FileSystemMetadata GetDirectoryInfo(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetDirectoryInfo(path); } var fileInfo = new DirectoryInfo(path); return GetFileSystemMetadata(fileInfo); } private FileSystemMetadata GetFileSystemMetadata(FileSystemInfo info) { var result = new FileSystemMetadata(); result.Exists = info.Exists; result.FullName = info.FullName; result.Extension = info.Extension; result.Name = info.Name; if (result.Exists) { var attributes = info.Attributes; result.IsDirectory = info is DirectoryInfo || (attributes & FileAttributes.Directory) == FileAttributes.Directory; result.IsHidden = (attributes & FileAttributes.Hidden) == FileAttributes.Hidden; result.IsReadOnly = (attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly; var fileInfo = info as FileInfo; if (fileInfo != null) { result.Length = fileInfo.Length; result.DirectoryName = fileInfo.DirectoryName; } result.CreationTimeUtc = GetCreationTimeUtc(info); result.LastWriteTimeUtc = GetLastWriteTimeUtc(info); } else { result.IsDirectory = info is DirectoryInfo; } return result; } /// /// The space char /// private const char SpaceChar = ' '; /// /// Takes a filename and removes invalid characters /// /// The filename. /// System.String. /// filename public string GetValidFilename(string filename) { if (string.IsNullOrEmpty(filename)) { throw new ArgumentNullException("filename"); } var builder = new StringBuilder(filename); foreach (var c in _invalidFileNameChars) { builder = builder.Replace(c, SpaceChar); } return builder.ToString(); } /// /// Gets the creation time UTC. /// /// The info. /// DateTime. public DateTime GetCreationTimeUtc(FileSystemInfo info) { // This could throw an error on some file systems that have dates out of range try { return info.CreationTimeUtc; } catch (Exception ex) { Logger.ErrorException("Error determining CreationTimeUtc for {0}", ex, info.FullName); return DateTime.MinValue; } } /// /// Gets the creation time UTC. /// /// The path. /// DateTime. public DateTime GetCreationTimeUtc(string path) { return GetCreationTimeUtc(GetFileSystemInfo(path)); } public DateTime GetCreationTimeUtc(FileSystemMetadata info) { return info.CreationTimeUtc; } public DateTime GetLastWriteTimeUtc(FileSystemMetadata info) { return info.LastWriteTimeUtc; } /// /// Gets the creation time UTC. /// /// The info. /// DateTime. public DateTime GetLastWriteTimeUtc(FileSystemInfo info) { // This could throw an error on some file systems that have dates out of range try { return info.LastWriteTimeUtc; } catch (Exception ex) { Logger.ErrorException("Error determining LastAccessTimeUtc for {0}", ex, info.FullName); return DateTime.MinValue; } } /// /// Gets the last write time UTC. /// /// The path. /// DateTime. public DateTime GetLastWriteTimeUtc(string path) { return GetLastWriteTimeUtc(GetFileSystemInfo(path)); } /// /// Gets the file stream. /// /// The path. /// The mode. /// The access. /// The share. /// if set to true [is asynchronous]. /// FileStream. public Stream GetFileStream(string path, FileOpenMode mode, FileAccessMode access, FileShareMode share, bool isAsync = false) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFileStream(path, mode, access, share); } if (_supportsAsyncFileStreams && isAsync) { return new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 262144, true); } return new FileStream(path, GetFileMode(mode), GetFileAccess(access), GetFileShare(share), 262144); } private FileMode GetFileMode(FileOpenMode mode) { switch (mode) { //case FileOpenMode.Append: // return FileMode.Append; case FileOpenMode.Create: return FileMode.Create; case FileOpenMode.CreateNew: return FileMode.CreateNew; case FileOpenMode.Open: return FileMode.Open; case FileOpenMode.OpenOrCreate: return FileMode.OpenOrCreate; //case FileOpenMode.Truncate: // return FileMode.Truncate; default: throw new Exception("Unrecognized FileOpenMode"); } } private FileAccess GetFileAccess(FileAccessMode mode) { switch (mode) { //case FileAccessMode.ReadWrite: // return FileAccess.ReadWrite; case FileAccessMode.Write: return FileAccess.Write; case FileAccessMode.Read: return FileAccess.Read; default: throw new Exception("Unrecognized FileAccessMode"); } } private FileShare GetFileShare(FileShareMode mode) { switch (mode) { case FileShareMode.ReadWrite: return FileShare.ReadWrite; case FileShareMode.Write: return FileShare.Write; case FileShareMode.Read: return FileShare.Read; case FileShareMode.None: return FileShare.None; default: throw new Exception("Unrecognized FileShareMode"); } } public void SetHidden(string path, bool isHidden) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.SetHidden(path, isHidden); return; } var info = GetFileInfo(path); if (info.Exists && info.IsHidden != isHidden) { if (isHidden) { File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.Hidden); } else { FileAttributes attributes = File.GetAttributes(path); attributes = RemoveAttribute(attributes, FileAttributes.Hidden); File.SetAttributes(path, attributes); } } } public void SetReadOnly(string path, bool isReadOnly) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.SetReadOnly(path, isReadOnly); return; } var info = GetFileInfo(path); if (info.Exists && info.IsReadOnly != isReadOnly) { if (isReadOnly) { File.SetAttributes(path, File.GetAttributes(path) | FileAttributes.ReadOnly); } else { FileAttributes attributes = File.GetAttributes(path); attributes = RemoveAttribute(attributes, FileAttributes.ReadOnly); File.SetAttributes(path, attributes); } } } private static FileAttributes RemoveAttribute(FileAttributes attributes, FileAttributes attributesToRemove) { return attributes & ~attributesToRemove; } /// /// Swaps the files. /// /// The file1. /// The file2. public void SwapFiles(string file1, string file2) { if (string.IsNullOrEmpty(file1)) { throw new ArgumentNullException("file1"); } if (string.IsNullOrEmpty(file2)) { throw new ArgumentNullException("file2"); } var temp1 = Path.Combine(_tempPath, Guid.NewGuid().ToString("N")); // Copying over will fail against hidden files SetHidden(file1, false); SetHidden(file2, false); Directory.CreateDirectory(_tempPath); CopyFile(file1, temp1, true); CopyFile(file2, file1, true); CopyFile(temp1, file2, true); } private char GetDirectorySeparatorChar(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetDirectorySeparatorChar(path); } return Path.DirectorySeparatorChar; } public bool ContainsSubPath(string parentPath, string path) { if (string.IsNullOrEmpty(parentPath)) { throw new ArgumentNullException("parentPath"); } if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } var separatorChar = GetDirectorySeparatorChar(parentPath); return path.IndexOf(parentPath.TrimEnd(separatorChar) + separatorChar, StringComparison.OrdinalIgnoreCase) != -1; } public bool IsRootPath(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } var parent = GetDirectoryName(path); if (!string.IsNullOrEmpty(parent)) { return false; } return true; } public string GetDirectoryName(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetDirectoryName(path); } return Path.GetDirectoryName(path); } public string NormalizePath(string path) { if (string.IsNullOrEmpty(path)) { throw new ArgumentNullException("path"); } if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.NormalizePath(path); } if (path.EndsWith(":\\", StringComparison.OrdinalIgnoreCase)) { return path; } return path.TrimEnd(GetDirectorySeparatorChar(path)); } public bool AreEqual(string path1, string path2) { if (path1 == null && path2 == null) { return true; } if (path1 == null || path2 == null) { return false; } return string.Equals(NormalizePath(path1), NormalizePath(path2), StringComparison.OrdinalIgnoreCase); } public string GetFileNameWithoutExtension(FileSystemMetadata info) { if (info.IsDirectory) { return info.Name; } return Path.GetFileNameWithoutExtension(info.FullName); } public string GetFileNameWithoutExtension(string path) { return Path.GetFileNameWithoutExtension(path); } public bool IsPathFile(string path) { if (string.IsNullOrWhiteSpace(path)) { throw new ArgumentNullException("path"); } // Cannot use Path.IsPathRooted because it returns false under mono when using windows-based paths, e.g. C:\\ if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return true; } if (path.IndexOf("://", StringComparison.OrdinalIgnoreCase) != -1 && !path.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) { return false; } return true; //return Path.IsPathRooted(path); } public void DeleteFile(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.DeleteFile(path); return; } var fileInfo = GetFileInfo(path); if (fileInfo.Exists) { if (fileInfo.IsHidden) { SetHidden(path, false); } if (fileInfo.IsReadOnly) { SetReadOnly(path, false); } } File.Delete(path); } public void DeleteDirectory(string path, bool recursive) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.DeleteDirectory(path, recursive); return; } Directory.Delete(path, recursive); } public void CreateDirectory(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.CreateDirectory(path); return; } Directory.CreateDirectory(path); } public List GetDrives() { // Only include drives in the ready state or this method could end up being very slow, waiting for drives to timeout return DriveInfo.GetDrives().Where(d => d.IsReady).Select(d => new FileSystemMetadata { Name = GetName(d), FullName = d.RootDirectory.FullName, IsDirectory = true }).ToList(); } private string GetName(DriveInfo drive) { return drive.Name; } public IEnumerable GetDirectories(string path, bool recursive = false) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetDirectories(path, recursive); } var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; return ToMetadata(new DirectoryInfo(path).EnumerateDirectories("*", searchOption)); } public IEnumerable GetFiles(string path, bool recursive = false) { return GetFiles(path, null, false, recursive); } public IEnumerable GetFiles(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFiles(path, extensions, enableCaseSensitiveExtensions, recursive); } var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if (enableCaseSensitiveExtensions && extensions != null && extensions.Length == 1) { return ToMetadata(new DirectoryInfo(path).EnumerateFiles("*" + extensions[0], searchOption)); } var files = new DirectoryInfo(path).EnumerateFiles("*", searchOption); if (extensions != null && extensions.Length > 0) { files = files.Where(i => { var ext = i.Extension; if (ext == null) { return false; } return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase); }); } return ToMetadata(files); } public IEnumerable GetFileSystemEntries(string path, bool recursive = false) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFileSystemEntries(path, recursive); } var directoryInfo = new DirectoryInfo(path); var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; if (EnableFileSystemRequestConcat) { return ToMetadata(directoryInfo.EnumerateDirectories("*", searchOption)) .Concat(ToMetadata(directoryInfo.EnumerateFiles("*", searchOption))); } return ToMetadata(directoryInfo.EnumerateFileSystemInfos("*", searchOption)); } private IEnumerable ToMetadata(IEnumerable infos) { return infos.Select(GetFileSystemMetadata); } public string[] ReadAllLines(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.ReadAllLines(path); } return File.ReadAllLines(path); } public void WriteAllLines(string path, IEnumerable lines) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.WriteAllLines(path, lines); return; } File.WriteAllLines(path, lines); } public Stream OpenRead(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.OpenRead(path); } return File.OpenRead(path); } public void CopyFile(string source, string target, bool overwrite) { if (_sharpCifsFileSystem.IsEnabledForPath(source)) { _sharpCifsFileSystem.CopyFile(source, target, overwrite); return; } File.Copy(source, target, overwrite); } public void MoveFile(string source, string target) { if (_sharpCifsFileSystem.IsEnabledForPath(source)) { _sharpCifsFileSystem.MoveFile(source, target); return; } File.Move(source, target); } public void MoveDirectory(string source, string target) { if (_sharpCifsFileSystem.IsEnabledForPath(source)) { _sharpCifsFileSystem.MoveDirectory(source, target); return; } Directory.Move(source, target); } public bool DirectoryExists(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.DirectoryExists(path); } return Directory.Exists(path); } public bool FileExists(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.FileExists(path); } return File.Exists(path); } public string ReadAllText(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.ReadAllText(path); } return File.ReadAllText(path); } public byte[] ReadAllBytes(string path) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.ReadAllBytes(path); } return File.ReadAllBytes(path); } public void WriteAllText(string path, string text, Encoding encoding) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.WriteAllText(path, text, encoding); return; } File.WriteAllText(path, text, encoding); } public void WriteAllText(string path, string text) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.WriteAllText(path, text); return; } File.WriteAllText(path, text); } public void WriteAllBytes(string path, byte[] bytes) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { _sharpCifsFileSystem.WriteAllBytes(path, bytes); return; } File.WriteAllBytes(path, bytes); } public string ReadAllText(string path, Encoding encoding) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.ReadAllText(path, encoding); } return File.ReadAllText(path, encoding); } public IEnumerable GetDirectoryPaths(string path, bool recursive = false) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetDirectoryPaths(path, recursive); } var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; return Directory.EnumerateDirectories(path, "*", searchOption); } public IEnumerable GetFilePaths(string path, bool recursive = false) { return GetFilePaths(path, null, false, recursive); } public IEnumerable GetFilePaths(string path, string[] extensions, bool enableCaseSensitiveExtensions, bool recursive = false) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFilePaths(path, extensions, enableCaseSensitiveExtensions, recursive); } var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; // On linux and osx the search pattern is case sensitive // If we're OK with case-sensitivity, and we're only filtering for one extension, then use the native method if (enableCaseSensitiveExtensions && extensions != null && extensions.Length == 1) { return Directory.EnumerateFiles(path, "*" + extensions[0], searchOption); } var files = Directory.EnumerateFiles(path, "*", searchOption); if (extensions != null && extensions.Length > 0) { files = files.Where(i => { var ext = Path.GetExtension(i); if (ext == null) { return false; } return extensions.Contains(ext, StringComparer.OrdinalIgnoreCase); }); } return files; } public IEnumerable GetFileSystemEntryPaths(string path, bool recursive = false) { if (_sharpCifsFileSystem.IsEnabledForPath(path)) { return _sharpCifsFileSystem.GetFileSystemEntryPaths(path, recursive); } var searchOption = recursive ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly; return Directory.EnumerateFileSystemEntries(path, "*", searchOption); } public virtual void SetExecutable(string path) { } } }