using System; using System.Collections.Generic; using System.IO; using System.Linq; using Umbraco.Core.Logging; namespace Umbraco.Core.IO { public class PhysicalFileSystem : IFileSystem { private readonly string _rootPath; private readonly string _rootUrl; 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 = IOHelper.MapPath(virtualRoot); _rootPath = EnsureDirectorySeparatorChar(_rootPath); _rootPath = _rootPath.TrimEnd(Path.DirectorySeparatorChar); _rootUrl = IOHelper.ResolveUrl(virtualRoot); _rootUrl = _rootUrl.TrimEnd('/'); } public PhysicalFileSystem(string rootPath, string rootUrl) { if (string.IsNullOrEmpty(rootPath)) throw new ArgumentException("The argument 'rootPath' cannot be null or empty."); if (string.IsNullOrEmpty(rootUrl)) throw new ArgumentException("The argument 'rootUrl' cannot be null or empty."); 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! // but the test suite App.config cannot really "root" anything so we'll have to do it here //var localRoot = AppDomain.CurrentDomain.BaseDirectory; var localRoot = IOHelper.GetRootDirectorySafe(); if (Path.IsPathRooted(rootPath) == false) { rootPath = Path.Combine(localRoot, rootPath); } _rootPath = rootPath.TrimEnd(Path.DirectorySeparatorChar); _rootUrl = rootUrl.TrimEnd('/'); } public IEnumerable GetDirectories(string path) { var fullPath = GetFullPath(path); try { if (Directory.Exists(fullPath)) return Directory.EnumerateDirectories(fullPath).Select(GetRelativePath); } catch (UnauthorizedAccessException ex) { LogHelper.Error("Not authorized to get directories", ex); } catch (DirectoryNotFoundException ex) { LogHelper.Error("Directory not found", ex); } return Enumerable.Empty(); } public void DeleteDirectory(string path) { DeleteDirectory(path, false); } 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) { LogHelper.Error("Directory not found", ex); } } public bool DirectoryExists(string path) { var fullPath = GetFullPath(path); return Directory.Exists(fullPath); } public void AddFile(string path, Stream stream) { AddFile(path, stream, true); } public void AddFile(string path, Stream stream, bool overrideIfExists) { var fullPath = GetFullPath(path); var exists = File.Exists(fullPath); if (exists && overrideIfExists == false) throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // ensure it exists if (stream.CanSeek) stream.Seek(0, 0); using (var destination = (Stream)File.Create(fullPath)) stream.CopyTo(destination); } public IEnumerable GetFiles(string path) { return GetFiles(path, "*.*"); } 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) { LogHelper.Error("Not authorized to get directories", ex); } catch (DirectoryNotFoundException ex) { LogHelper.Error("Directory not found", ex); } return Enumerable.Empty(); } public Stream OpenFile(string path) { var fullPath = GetFullPath(path); return File.OpenRead(fullPath); } public void DeleteFile(string path) { var fullPath = GetFullPath(path); if (File.Exists(fullPath) == false) return; try { File.Delete(fullPath); } catch (FileNotFoundException ex) { LogHelper.Info(string.Format("DeleteFile failed with FileNotFoundException: {0}", ex.InnerException)); } } public bool FileExists(string path) { var fullpath = GetFullPath(path); return File.Exists(fullpath); } /// /// Gets the relative path. /// /// 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 converted to Path.DirectorySeparatorChar. /// public string GetRelativePath(string fullPathOrUrl) { // test url var path = EnsureUrlSeparatorChar(fullPathOrUrl); if (PathStartsWith(path, _rootUrl, '/')) return path.Substring(_rootUrl.Length) .Replace('/', Path.DirectorySeparatorChar) .TrimStart(Path.DirectorySeparatorChar); // test path path = EnsureDirectorySeparatorChar(fullPathOrUrl); if (PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) return path.Substring(_rootPath.Length) .TrimStart(Path.DirectorySeparatorChar); return fullPathOrUrl; // previous code kept for reference //var relativePath = fullPathOrUrl // .TrimStart(_rootUrl) // .Replace('/', Path.DirectorySeparatorChar) // .TrimStart(RootPath) // .TrimStart(Path.DirectorySeparatorChar); //return relativePath; } /// /// Gets the full path. /// /// The full or 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 converted to Path.DirectorySeparatorChar. /// public string GetFullPath(string path) { // normalize var opath = path; path = EnsureDirectorySeparatorChar(path); // 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 already a full path, return if (PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) return path; // else combine and sanitize, ie 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 var fpath = Path.Combine(_rootPath, path); fpath = Path.GetFullPath(fpath); // 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 (PathStartsWith(fpath, _rootPath, Path.DirectorySeparatorChar)) return fpath; throw new FileSecurityException("File '" + opath + "' is outside this filesystem's root."); } private static bool PathStartsWith(string path, string root, char separator) { // either it is identical to root, // or it is root + separator + anything if (path.StartsWith(root, StringComparison.OrdinalIgnoreCase) == false) return false; if (path.Length == root.Length) return true; if (path.Length < root.Length) return false; return path[root.Length] == separator; } public string GetUrl(string path) { path = EnsureUrlSeparatorChar(path).Trim('/'); return _rootUrl + "/" + path; } public DateTimeOffset GetLastModified(string path) { return DirectoryExists(path) ? new DirectoryInfo(GetFullPath(path)).LastWriteTimeUtc : new FileInfo(GetFullPath(path)).LastWriteTimeUtc; } public DateTimeOffset GetCreated(string path) { return DirectoryExists(path) ? Directory.GetCreationTimeUtc(GetFullPath(path)) : File.GetCreationTimeUtc(GetFullPath(path)); } #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 } }