using System; using System.Collections.Generic; using System.IO; using System.Linq; using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; namespace Umbraco.Core.IO { public class PhysicalFileSystem : IFileSystem { // the rooted, filesystem path, using directory separator chars, NOT ending with a separator // eg "c:" or "c:\path\to\site" or "\\server\path" private readonly string _rootPath; // _rootPath, but with separators replaced by forward-slashes // eg "c:" or "c:/path/to/site" or "//server/path" // (is used in GetRelativePath) private readonly string _rootPathFwd; // the relative url, using url separator chars, NOT ending with a separator // eg "" or "/Views" or "/Media" or "//Media" in case of a virtual path private readonly string _rootUrl; // virtualRoot should be "~/path/to/root" eg "~/Views" // the "~/" is mandatory. public PhysicalFileSystem(string virtualRoot) { if (virtualRoot == null) throw new ArgumentNullException("virtualRoot"); if (virtualRoot.StartsWith("~/") == false) throw new ArgumentException("The virtualRoot argument must be a virtual path and start with '~/'"); _rootPath = EnsureDirectorySeparatorChar(IOHelper.MapPath(virtualRoot)).TrimEnd(Path.DirectorySeparatorChar); _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); _rootUrl = EnsureUrlSeparatorChar(IOHelper.ResolveUrl(virtualRoot)).TrimEnd('/'); } public PhysicalFileSystem(string rootPath, string rootUrl) { if (string.IsNullOrEmpty(rootPath)) throw new ArgumentNullOrEmptyException(nameof(rootPath)); if (string.IsNullOrEmpty(rootUrl)) throw new ArgumentNullOrEmptyException(nameof(rootUrl)); if (rootPath.StartsWith("~/")) throw new ArgumentException("The rootPath argument cannot be a virtual path and cannot start with '~/'"); // rootPath should be... rooted, as in, it's a root path! if (Path.IsPathRooted(rootPath) == false) { // but the test suite App.config cannot really "root" anything so we have to do it here var localRoot = IOHelper.GetRootDirectorySafe(); rootPath = Path.Combine(localRoot, rootPath); } _rootPath = EnsureDirectorySeparatorChar(rootPath).TrimEnd(Path.DirectorySeparatorChar); _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); _rootUrl = EnsureUrlSeparatorChar(rootUrl).TrimEnd('/'); } /// /// Gets directories in a directory. /// /// The filesystem-relative path to the directory. /// The filesystem-relative path to the directories in the directory. /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetDirectories(string path) { var fullPath = GetFullPath(path); try { if (Directory.Exists(fullPath)) return Directory.EnumerateDirectories(fullPath).Select(GetRelativePath); } catch (UnauthorizedAccessException ex) { Current.Logger.Error("Not authorized to get directories", ex); } catch (DirectoryNotFoundException ex) { Current.Logger.Error("Directory not found", ex); } return Enumerable.Empty(); } /// /// Deletes a directory. /// /// The filesystem-relative path of the directory. public void DeleteDirectory(string path) { DeleteDirectory(path, false); } /// /// Deletes a directory. /// /// The filesystem-relative path of the directory. /// A value indicating whether to recursively delete sub-directories. public void DeleteDirectory(string path, bool recursive) { var fullPath = GetFullPath(path); if (Directory.Exists(fullPath) == false) return; try { Directory.Delete(fullPath, recursive); } catch (DirectoryNotFoundException ex) { Current.Logger.Error("Directory not found", ex); } } /// /// Gets a value indicating whether a directory exists. /// /// The filesystem-relative path of the directory. /// A value indicating whether a directory exists. public bool DirectoryExists(string path) { var fullPath = GetFullPath(path); return Directory.Exists(fullPath); } /// /// Saves a file. /// /// The filesystem-relative path of the file. /// A stream containing the file data. /// Overrides the existing file, if any. public void AddFile(string path, Stream stream) { AddFile(path, stream, true); } /// /// Saves a file. /// /// The filesystem-relative path of the file. /// A stream containing the file data. /// A value indicating whether to override the existing file, if any. /// If a file exists and is false, an exception is thrown. public void AddFile(string path, Stream stream, bool overrideExisting) { var fullPath = GetFullPath(path); var exists = File.Exists(fullPath); if (exists && overrideExisting == false) throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); var directory = Path.GetDirectoryName(fullPath); if (directory == null) throw new InvalidOperationException("Could not get directory."); Directory.CreateDirectory(directory); // ensure it exists if (stream.CanSeek) // fixme - what else? stream.Seek(0, 0); using (var destination = (Stream) File.Create(fullPath)) stream.CopyTo(destination); } /// /// Gets files in a directory. /// /// The filesystem-relative path of the directory. /// The filesystem-relative path to the files in the directory. /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetFiles(string path) { return GetFiles(path, "*.*"); } /// /// Gets files in a directory. /// /// The filesystem-relative path of the directory. /// A filter. /// The filesystem-relative path to the matching files in the directory. /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetFiles(string path, string filter) { var fullPath = GetFullPath(path); try { if (Directory.Exists(fullPath)) return Directory.EnumerateFiles(fullPath, filter).Select(GetRelativePath); } catch (UnauthorizedAccessException ex) { Current.Logger.Error("Not authorized to get directories", ex); } catch (DirectoryNotFoundException ex) { Current.Logger.Error("Directory not found", ex); } return Enumerable.Empty(); } /// /// Opens a file. /// /// The filesystem-relative path to the file. /// public Stream OpenFile(string path) { var fullPath = GetFullPath(path); return File.OpenRead(fullPath); } /// /// Deletes a file. /// /// The filesystem-relative path to the file. public void DeleteFile(string path) { var fullPath = GetFullPath(path); if (File.Exists(fullPath) == false) return; try { File.Delete(fullPath); } catch (FileNotFoundException ex) { Current.Logger.Info(string.Format("DeleteFile failed with FileNotFoundException: {0}", ex.InnerException)); } } /// /// Gets a value indicating whether a file exists. /// /// The filesystem-relative path to the file. /// A value indicating whether the file exists. public bool FileExists(string path) { var fullpath = GetFullPath(path); return File.Exists(fullpath); } /// /// Gets the filesystem-relative path of a full path or of an url. /// /// The full path or url. /// The path, relative to this filesystem's root. /// /// The relative path is relative to this filesystem's root, not starting with any /// directory separator. All separators are forward-slashes. /// public string GetRelativePath(string fullPathOrUrl) { // test url var path = fullPathOrUrl.Replace('\\', '/'); // ensure url separator char // if it starts with the root url, strip it and trim the starting slash to make it relative // eg "/Media/1234/img.jpg" => "1234/img.jpg" if (IOHelper.PathStartsWith(path, _rootUrl, '/')) return path.Substring(_rootUrl.Length).TrimStart('/'); // if it starts with the root path, strip it and trim the starting slash to make it relative // eg "c:/websites/test/root/Media/1234/img.jpg" => "1234/img.jpg" if (IOHelper.PathStartsWith(path, _rootPathFwd, '/')) return path.Substring(_rootPathFwd.Length).TrimStart('/'); // unchanged - what else? return path; } /// /// Gets the full path. /// /// The full or filesystem-relative path. /// The full path. /// /// On the physical filesystem, the full path is the rooted (ie non-relative), safe (ie within this /// filesystem's root) path. All separators are Path.DirectorySeparatorChar. /// public string GetFullPath(string path) { // normalize var opath = path; path = EnsureDirectorySeparatorChar(path); // fixme - this part should go! // not sure what we are doing here - so if input starts with a (back) slash, // we assume it's not a FS relative path and we try to convert it... but it // really makes little sense? if (path.StartsWith(Path.DirectorySeparatorChar.ToString())) path = GetRelativePath(path); // if not already rooted, combine with the root path if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar) == false) path = Path.Combine(_rootPath, path); // sanitize - GetFullPath will take care of any relative // segments in path, eg '../../foo.tmp' - it may throw a SecurityException // if the combined path reaches illegal parts of the filesystem path = Path.GetFullPath(path); // at that point, path is within legal parts of the filesystem, ie we have // permissions to reach that path, but it may nevertheless be outside of // our root path, due to relative segments, so better check if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) return path; // nothing prevents us to reach the file, security-wise, yet it is outside // this filesystem's root - throw throw new FileSecurityException("File '" + opath + "' is outside this filesystem's root."); } /// /// Gets the url. /// /// The filesystem-relative path. /// The url. /// All separators are forward-slashes. public string GetUrl(string path) { path = EnsureUrlSeparatorChar(path).Trim('/'); return _rootUrl + "/" + path; } /// /// Gets the last-modified date of a directory or file. /// /// The filesystem-relative path to the directory or the file. /// The last modified date of the directory or the file. public DateTimeOffset GetLastModified(string path) { var fullpath = GetFullPath(path); return DirectoryExists(fullpath) ? new DirectoryInfo(fullpath).LastWriteTimeUtc : new FileInfo(fullpath).LastWriteTimeUtc; } /// /// Gets the created date of a directory or file. /// /// The filesystem-relative path to the directory or the file. /// The created date of the directory or the file. public DateTimeOffset GetCreated(string path) { var fullpath = GetFullPath(path); return DirectoryExists(fullpath) ? Directory.GetCreationTimeUtc(fullpath) : File.GetCreationTimeUtc(fullpath); } /// /// Gets the size of a file. /// /// The filesystem-relative path to the file. /// The file of the size, in bytes. /// If the file does not exist, returns -1. public long GetSize(string path) { var fullPath = GetFullPath(path); var file = new FileInfo(fullPath); return file.Exists ? file.Length : -1; } public bool CanAddPhysical => true; public void AddFile(string path, string physicalPath, bool overrideIfExists = true, bool copy = false) { var fullPath = GetFullPath(path); if (File.Exists(fullPath)) { if (overrideIfExists == false) throw new InvalidOperationException($"A file at path '{path}' already exists"); File.Delete(fullPath); } var directory = Path.GetDirectoryName(fullPath); if (directory == null) throw new InvalidOperationException("Could not get directory."); Directory.CreateDirectory(directory); // ensure it exists if (copy) File.Copy(physicalPath, fullPath); else File.Move(physicalPath, fullPath); } #region Helper Methods protected virtual void EnsureDirectory(string path) { path = GetFullPath(path); Directory.CreateDirectory(path); } protected string EnsureTrailingSeparator(string path) { return path.EnsureEndsWith(Path.DirectorySeparatorChar); } protected string EnsureDirectorySeparatorChar(string path) { path = path.Replace('/', Path.DirectorySeparatorChar); path = path.Replace('\\', Path.DirectorySeparatorChar); return path; } protected string EnsureUrlSeparatorChar(string path) { path = path.Replace('\\', '/'); return path; } #endregion } }