Files
Umbraco-CMS/src/Umbraco.Core/IO/IOHelper.cs
Bjarke Berg 9af77e5f51 Add temp logging, to debug azure pipeline
Signed-off-by: Bjarke Berg <mail@bergmania.dk>
2020-11-27 21:53:31 +01:00

251 lines
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using Umbraco.Core.Hosting;
using Umbraco.Core.Strings;
namespace Umbraco.Core.IO
{
public abstract class IOHelper : IIOHelper
{
private readonly IHostingEnvironment _hostingEnvironment;
public IOHelper(IHostingEnvironment hostingEnvironment)
{
_hostingEnvironment = hostingEnvironment ?? throw new ArgumentNullException(nameof(hostingEnvironment));
}
// static compiled regex for faster performance
//private static readonly Regex ResolveUrlPattern = new Regex("(=[\"\']?)(\\W?\\~(?:.(?![\"\']?\\s+(?:\\S+)=|[>\"\']))+.)[\"\']?", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
//helper to try and match the old path to a new virtual one
public string FindFile(string virtualPath)
{
string retval = virtualPath;
if (virtualPath.StartsWith("~"))
retval = virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath);
if (virtualPath.StartsWith("/") && !PathStartsWith(virtualPath, _hostingEnvironment.ApplicationVirtualPath))
retval = _hostingEnvironment.ApplicationVirtualPath + "/" + virtualPath.TrimStart('/');
return retval;
}
// TODO: This is the same as IHostingEnvironment.ToAbsolute
public string ResolveUrl(string virtualPath)
{
if (string.IsNullOrWhiteSpace(virtualPath)) return virtualPath;
return _hostingEnvironment.ToAbsolute(virtualPath);
}
public Attempt<string> TryResolveUrl(string virtualPath)
{
try
{
if (virtualPath.StartsWith("~"))
return Attempt.Succeed(virtualPath.Replace("~", _hostingEnvironment.ApplicationVirtualPath).Replace("//", "/"));
if (Uri.IsWellFormedUriString(virtualPath, UriKind.Absolute))
return Attempt.Succeed(virtualPath);
return Attempt.Succeed(_hostingEnvironment.ToAbsolute(virtualPath));
}
catch (Exception ex)
{
return Attempt.Fail(virtualPath, ex);
}
}
public string MapPath(string path)
{
if (path == null) throw new ArgumentNullException(nameof(path));
// Check if the path is already mapped - TODO: This should be switched to Path.IsPathFullyQualified once we are on Net Standard 2.1
if (IsPathFullyQualified(path))
{
return path;
}
if (_hostingEnvironment.IsHosted)
{
var result = (!string.IsNullOrEmpty(path) && (path.StartsWith("~") || PathStartsWith(path, _hostingEnvironment.ApplicationVirtualPath)))
? _hostingEnvironment.MapPathWebRoot(path)
: _hostingEnvironment.MapPathWebRoot("~/" + path.TrimStart('/'));
if (result != null) return result;
}
var dirSepChar = Path.DirectorySeparatorChar;
var root = Assembly.GetExecutingAssembly().GetRootDirectorySafe();
var newPath = path.TrimStart('~', '/').Replace('/', dirSepChar);
var retval = root + dirSepChar.ToString(CultureInfo.InvariantCulture) + newPath;
return retval;
}
/// <summary>
/// Returns true if the path has a root, and is considered fully qualified for the OS it is on
/// See https://github.com/dotnet/runtime/blob/30769e8f31b20be10ca26e27ec279cd4e79412b9/src/libraries/System.Private.CoreLib/src/System/IO/Path.cs#L281 for the .NET Standard 2.1 version of this
/// </summary>
/// <param name="path">The path to check</param>
/// <returns>True if the path is fully qualified, false otherwise</returns>
public abstract bool IsPathFullyQualified(string path);
/// <summary>
/// Verifies that the current filepath matches a directory where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDir">The valid directory.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyEditPath(string filePath, string validDir)
{
return VerifyEditPath(filePath, new[] { validDir });
}
/// <summary>
/// Verifies that the current filepath matches one of several directories where the user is allowed to edit a file.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validDirs">The valid directories.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyEditPath(string filePath, IEnumerable<string> validDirs)
{
// this is called from ScriptRepository, PartialViewRepository, etc.
// filePath is the fullPath (rooted, filesystem path, can be trusted)
// validDirs are virtual paths (eg ~/Views)
//
// except that for templates, filePath actually is a virtual path
// TODO: what's below is dirty, there are too many ways to get the root dir, etc.
// not going to fix everything today
var mappedRoot = MapPath(_hostingEnvironment.ApplicationVirtualPath);
if (!PathStartsWith(filePath, mappedRoot))
{
// TODO this is going to fail.. Scripts Stylesheets need to use WebRoot, PartialViews need to use ContentRoot
filePath = _hostingEnvironment.MapPathWebRoot(filePath);
}
// yes we can (see above)
//// don't trust what we get, it may contain relative segments
//filePath = Path.GetFullPath(filePath);
foreach (var dir in validDirs)
{
var validDir = dir;
if (!PathStartsWith(validDir, mappedRoot))
validDir = _hostingEnvironment.MapPathWebRoot(validDir);
if (PathStartsWith(filePath, validDir))
return true;
}
return false;
}
/// <summary>
/// Verifies that the current filepath has one of several authorized extensions.
/// </summary>
/// <param name="filePath">The filepath to validate.</param>
/// <param name="validFileExtensions">The valid extensions.</param>
/// <returns>A value indicating whether the filepath is valid.</returns>
public bool VerifyFileExtension(string filePath, IEnumerable<string> validFileExtensions)
{
var ext = Path.GetExtension(filePath);
return ext != null && validFileExtensions.Contains(ext.TrimStart('.'));
}
public abstract bool PathStartsWith(string path, string root, params char[] separators);
public void EnsurePathExists(string path)
{
var absolutePath = MapPath(path);
if (Directory.Exists(absolutePath) == false)
Directory.CreateDirectory(absolutePath);
}
/// <summary>
/// Get properly formatted relative path from an existing absolute or relative path
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public string GetRelativePath(string path)
{
if (path.IsFullPath())
{
var rootDirectory = MapPath("~");
var relativePath = PathStartsWith(path, rootDirectory) ? path.Substring(rootDirectory.Length) : path;
path = relativePath;
}
return PathUtility.EnsurePathIsApplicationRootPrefixed(path);
}
/// <summary>
/// Retrieves array of temporary folders from the hosting environment.
/// </summary>
/// <returns>Array of <see cref="DirectoryInfo"/> instances.</returns>
public DirectoryInfo[] GetTempFolders()
{
var tempFolderPaths = new[]
{
_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads)
};
foreach (var tempFolderPath in tempFolderPaths)
{
// Ensure it exists
Directory.CreateDirectory(tempFolderPath);
}
return tempFolderPaths.Select(x => new DirectoryInfo(x)).ToArray();
}
/// <summary>
/// Cleans contents of a folder by deleting all files older that the provided age.
/// If deletition of any file errors (e.g. due to a file lock) the process will continue to try to delete all that it can.
/// </summary>
/// <param name="folder">Folder to clean.</param>
/// <param name="age">Age of files within folder to delete.</param>
/// <returns>Result of operation.</returns>
public CleanFolderResult CleanFolder(DirectoryInfo folder, TimeSpan age)
{
folder.Refresh(); // In case it's changed during runtime.
if (!folder.Exists)
{
return CleanFolderResult.FailedAsDoesNotExist();
}
var files = folder.GetFiles("*.*", SearchOption.AllDirectories);
var errors = new List<CleanFolderResult.Error>();
foreach (var file in files)
{
if (DateTime.UtcNow - file.LastWriteTimeUtc > age)
{
try
{
file.IsReadOnly = false;
file.Delete();
}
catch (Exception ex)
{
errors.Add(new CleanFolderResult.Error(ex, file));
}
}
}
return errors.Any()
? CleanFolderResult.FailedWithErrors(errors)
: CleanFolderResult.Success();
}
}
}