using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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, with separators replaced by forward-slashes.
private readonly string _rootPathFwd;
// the ??? url, using url separator chars, NOT ending with a separator
// eg "" (?) or "/Scripts" or ???
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);
_rootPathFwd = EnsureUrlSeparatorChar(_rootPath);
_rootUrl = IOHelper.ResolveUrl(virtualRoot);
_rootUrl = EnsureUrlSeparatorChar(_rootUrl);
_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 = EnsureDirectorySeparatorChar(rootPath);
_rootPath = rootPath.TrimEnd(Path.DirectorySeparatorChar);
_rootPathFwd = EnsureUrlSeparatorChar(_rootPath);
rootUrl = EnsureUrlSeparatorChar(rootUrl);
_rootUrl = 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)
{
LogHelper.Error("Not authorized to get directories", ex);
}
catch (DirectoryNotFoundException ex)
{
LogHelper.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)
{
LogHelper.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)
{
LogHelper.Error("Not authorized to get directories", ex);
}
catch (DirectoryNotFoundException ex)
{
LogHelper.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)
{
LogHelper.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 (IOHelper.PathStartsWith(path, _rootUrl, '/')) // if it starts with the root url...
return path.Substring(_rootUrl.Length) // strip it
.TrimStart('/'); // it's relative
if (IOHelper.PathStartsWith(path, _rootPathFwd, '/')) // if it starts with the root url...
return path.Substring(_rootPathFwd.Length) // strip it
.TrimStart('/'); // it's relative
// 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;
}
#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
}
}