using System.Runtime.Versioning; using System.Security.AccessControl; using System.Text; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.FileProviders; namespace Nop.Core.Infrastructure; /// /// IO functions using the on-disk file system /// public partial class NopFileProvider : PhysicalFileProvider, INopFileProvider { #region Ctor /// /// Initializes a new instance of a NopFileProvider /// /// Hosting environment public NopFileProvider(IWebHostEnvironment webHostEnvironment) : base(File.Exists(webHostEnvironment.ContentRootPath) ? Path.GetDirectoryName(webHostEnvironment.ContentRootPath)! : webHostEnvironment.ContentRootPath) { WebRootPath = File.Exists(webHostEnvironment.WebRootPath) ? Path.GetDirectoryName(webHostEnvironment.WebRootPath) : webHostEnvironment.WebRootPath; } #endregion #region Utilities /// /// Depth-first recursive delete /// /// protected virtual void DeleteDirectoryRecursive(string path) { Directory.Delete(path, true); const int maxIterationToWait = 10; var curIteration = 0; //according to the documentation(https://msdn.microsoft.com/ru-ru/library/windows/desktop/aa365488.aspx) //System.IO.Directory.Delete method ultimately (after removing the files) calls native //RemoveDirectory function which marks the directory as "deleted". That's why we wait until //the directory is actually deleted. For more details see https://stackoverflow.com/a/4245121 while (Directory.Exists(path)) { curIteration += 1; if (curIteration > maxIterationToWait) return; Thread.Sleep(100); } } /// /// Determines if the string is a valid Universal Naming Convention (UNC) /// for a server and share path. /// /// The path to be tested. /// if the path is a valid UNC path; /// otherwise, . protected static bool IsUncPath(string path) { return Uri.TryCreate(path, UriKind.Absolute, out var uri) && uri.IsUnc; } #endregion #region Methods /// /// Combines an array of strings into a path /// /// An array of parts of the path /// The combined paths public virtual string Combine(params string[] paths) { var path = Path.Combine(paths.SelectMany(p => IsUncPath(p) ? [p] : p.Split('\\', '/')).ToArray()); if (Environment.OSVersion.Platform == PlatformID.Unix && !IsUncPath(path)) //add leading slash to correctly form path in the UNIX system path = "/" + path; return path; } /// /// Creates all directories and subdirectories in the specified path unless they already exist /// /// The directory to create public virtual void CreateDirectory(string path) { if (!DirectoryExists(path)) Directory.CreateDirectory(path); } /// /// Creates a file in the specified path /// /// The path and name of the file to create public virtual void CreateFile(string path) { if (FileExists(path)) return; var fileInfo = new FileInfo(path); CreateDirectory(fileInfo.DirectoryName); //we use 'using' to close the file after it's created using (File.Create(path)) { } } /// /// Depth-first recursive delete, with handling for descendant directories open in Windows Explorer. /// /// Directory path public virtual void DeleteDirectory(string path) { ArgumentException.ThrowIfNullOrEmpty(path); //find more info about directory deletion //and why we use this approach at https://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true foreach (var directory in Directory.GetDirectories(path)) { DeleteDirectory(directory); } try { DeleteDirectoryRecursive(path); } catch (IOException) { DeleteDirectoryRecursive(path); } catch (UnauthorizedAccessException) { DeleteDirectoryRecursive(path); } } /// /// Deletes the specified file /// /// The name of the file to be deleted. Wildcard characters are not supported public virtual void DeleteFile(string filePath) { if (!FileExists(filePath)) return; File.Delete(filePath); } /// /// Determines whether the given path refers to an existing directory on disk /// /// The path to test /// /// true if path refers to an existing directory; false if the directory does not exist or an error occurs when /// trying to determine if the specified file exists /// public virtual bool DirectoryExists(string path) { return Directory.Exists(path); } /// /// Moves a file or a directory and its contents to a new location /// /// The path of the file or directory to move /// /// The path to the new location for sourceDirName. If sourceDirName is a file, then destDirName /// must also be a file name /// public virtual void DirectoryMove(string sourceDirName, string destDirName) { Directory.Move(sourceDirName, destDirName); } /// /// Returns an enumerable collection of file names that match a search pattern in /// a specified path, and optionally searches subdirectories. /// /// The path to the directory to search /// /// The search string to match against the names of files in path. This parameter /// can contain a combination of valid literal path and wildcard (* and ?) characters /// , but doesn't support regular expressions. /// /// /// Specifies whether to search the current directory, or the current directory and all /// subdirectories /// /// /// An enumerable collection of the full names (including paths) for the files in /// the directory specified by path and that match the specified search pattern /// public virtual IEnumerable EnumerateFiles(string directoryPath, string searchPattern, bool topDirectoryOnly = true) { return Directory.EnumerateFiles(directoryPath, searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories); } /// /// Copies an existing file to a new file. Overwriting a file of the same name is allowed /// /// The file to copy /// The name of the destination file. This cannot be a directory /// true if the destination file can be overwritten; otherwise, false public virtual void FileCopy(string sourceFileName, string destFileName, bool overwrite = false) { File.Copy(sourceFileName, destFileName, overwrite); } /// /// Determines whether the specified file exists /// /// The file to check /// /// True if the caller has the required permissions and path contains the name of an existing file; otherwise, /// false. /// public virtual bool FileExists(string filePath) { return File.Exists(filePath); } /// /// Gets the length of the file in bytes, or -1 for a directory or non-existing files /// /// File path /// The length of the file public virtual long FileLength(string path) { if (!FileExists(path)) return -1; return new FileInfo(path).Length; } /// /// Moves a specified file to a new location, providing the option to specify a new file name /// /// The name of the file to move. Can include a relative or absolute path /// The new path and name for the file public virtual void FileMove(string sourceFileName, string destFileName) { File.Move(sourceFileName, destFileName); } /// /// Returns the absolute path to the directory /// /// An array of parts of the path /// The absolute path to the directory public virtual string GetAbsolutePath(params string[] paths) { var allPaths = new List(); if (paths.Any() && !paths[0].Contains(WebRootPath, StringComparison.InvariantCulture)) allPaths.Add(WebRootPath); allPaths.AddRange(paths); return Combine(allPaths.ToArray()); } /// /// Gets a System.Security.AccessControl.DirectorySecurity object that encapsulates the access control list (ACL) entries for a specified directory /// /// The path to a directory containing a System.Security.AccessControl.DirectorySecurity object that describes the file's access control list (ACL) information /// An object that encapsulates the access control rules for the file described by the path parameter [SupportedOSPlatform("windows")] public virtual DirectorySecurity GetAccessControl(string path) { return new DirectoryInfo(path).GetAccessControl(); } /// /// Returns the creation date and time of the specified file or directory /// /// The file or directory for which to obtain creation date and time information /// /// A System.DateTime structure set to the creation date and time for the specified file or directory. This value /// is expressed in local time /// public virtual DateTime GetCreationTime(string path) { return File.GetCreationTime(path); } /// /// Returns the names of the subdirectories (including their paths) that match the /// specified search pattern in the specified directory /// /// The path to the directory to search /// /// The search string to match against the names of subdirectories in path. This /// parameter can contain a combination of valid literal and wildcard characters /// , but doesn't support regular expressions. /// /// /// Specifies whether to search the current directory, or the current directory and all /// subdirectories /// /// /// An array of the full names (including paths) of the subdirectories that match /// the specified criteria, or an empty array if no directories are found /// public virtual string[] GetDirectories(string path, string searchPattern = "", bool topDirectoryOnly = true) { if (string.IsNullOrEmpty(searchPattern)) searchPattern = "*"; return Directory.GetDirectories(path, searchPattern, topDirectoryOnly ? SearchOption.TopDirectoryOnly : SearchOption.AllDirectories); } /// /// Returns the directory information for the specified path string /// /// The path of a file or directory /// /// Directory information for path, or null if path denotes a root directory or is null. Returns /// System.String.Empty if path does not contain directory information /// public virtual string GetDirectoryName(string path) { return Path.GetDirectoryName(path); } /// /// Returns the directory name only for the specified path string /// /// The path of directory /// The directory name public virtual string GetDirectoryNameOnly(string path) { return new DirectoryInfo(path).Name; } /// /// Returns the extension of the specified path string /// /// The path string from which to get the extension /// The extension of the specified path (including the period ".") public virtual string GetFileExtension(string filePath) { return Path.GetExtension(filePath); } /// /// Returns the file name and extension of the specified path string /// /// The path string from which to obtain the file name and extension /// The characters after the last directory character in path public virtual string GetFileName(string path) { return Path.GetFileName(path); } /// /// Returns the file name of the specified path string without the extension /// /// The path of the file /// The file name, minus the last period (.) and all characters following it public virtual string GetFileNameWithoutExtension(string filePath) { return Path.GetFileNameWithoutExtension(filePath); } /// /// Returns the names of files (including their paths) that match the specified search /// pattern in the specified directory, using a value to determine whether to search subdirectories. /// /// The path to the directory to search /// /// The search string to match against the names of files in path. This parameter /// can contain a combination of valid literal path and wildcard (* and ?) characters /// , but doesn't support regular expressions. /// /// /// Specifies whether to search the current directory, or the current directory and all /// subdirectories /// /// /// An array of the full names (including paths) for the files in the specified directory /// that match the specified search pattern, or an empty array if no files are found. /// public virtual string[] GetFiles(string directoryPath, string searchPattern = "", bool topDirectoryOnly = true) { if (string.IsNullOrEmpty(searchPattern)) searchPattern = "*.*"; return Directory.GetFileSystemEntries(directoryPath, searchPattern, new EnumerationOptions { IgnoreInaccessible = true, MatchCasing = MatchCasing.CaseInsensitive, RecurseSubdirectories = !topDirectoryOnly, }); } /// /// Returns the date and time the specified file or directory was last accessed /// /// The file or directory for which to obtain access date and time information /// A System.DateTime structure set to the date and time that the specified file public virtual DateTime GetLastAccessTime(string path) { return File.GetLastAccessTime(path); } /// /// Returns the date and time the specified file or directory was last written to /// /// The file or directory for which to obtain write date and time information /// /// A System.DateTime structure set to the date and time that the specified file or directory was last written to. /// This value is expressed in local time /// public virtual DateTime GetLastWriteTime(string path) { return File.GetLastWriteTime(path); } /// /// Returns the date and time, in coordinated universal time (UTC), that the specified file or directory was last /// written to /// /// The file or directory for which to obtain write date and time information /// /// A System.DateTime structure set to the date and time that the specified file or directory was last written to. /// This value is expressed in UTC time /// public virtual DateTime GetLastWriteTimeUtc(string path) { return File.GetLastWriteTimeUtc(path); } /// /// Creates or opens a file at the specified path for read/write access /// /// The path and name of the file to create /// A that provides read/write access to the file specified in public FileStream GetOrCreateFile(string path) { if (FileExists(path)) return File.Open(path, FileMode.Open, FileAccess.ReadWrite); var fileInfo = new FileInfo(path); CreateDirectory(fileInfo.DirectoryName); return File.Create(path); } /// /// Retrieves the parent directory of the specified path /// /// The path for which to retrieve the parent directory /// The parent directory, or null if path is the root directory, including the root of a UNC server or share name public virtual string GetParentDirectory(string directoryPath) { return Directory.GetParent(directoryPath).FullName; } /// /// Gets a virtual path from a physical disk path. /// /// The physical disk path /// The virtual path. E.g. "~/bin" public virtual string GetVirtualPath(string path) { if (string.IsNullOrEmpty(path)) return path; if (!IsDirectory(path) && FileExists(path)) path = new FileInfo(path).DirectoryName; path = path?.Replace(WebRootPath, string.Empty).Replace('\\', '/').Trim('/').TrimStart('~', '/'); return $"~/{path ?? string.Empty}"; } /// /// Checks if the path is directory /// /// Path for check /// True, if the path is a directory, otherwise false public virtual bool IsDirectory(string path) { return DirectoryExists(path); } /// /// Maps a virtual path to a physical disk path. /// /// The path to map. E.g. "~/bin" /// The physical path. E.g. "c:\inetpub\wwwroot\bin" public virtual string MapPath(string path) { path = path.Replace("~/", string.Empty).TrimStart('/'); //if virtual path has slash on the end, it should be after transform the virtual path to physical path too var pathEnd = path.EndsWith('/') ? Path.DirectorySeparatorChar.ToString() : string.Empty; return Combine(Root ?? string.Empty, path) + pathEnd; } /// /// Reads the contents of the file into a byte array /// /// The file for reading /// /// A task that represents the asynchronous operation /// The task result contains a byte array containing the contents of the file /// public virtual async Task ReadAllBytesAsync(string filePath) { return File.Exists(filePath) ? await File.ReadAllBytesAsync(filePath) : Array.Empty(); } /// /// Opens a file, reads all lines of the file with the specified encoding, and then closes the file. /// /// The file to open for reading /// The encoding applied to the contents of the file /// /// A task that represents the asynchronous operation /// The task result contains a string containing all lines of the file /// public virtual async Task ReadAllTextAsync(string path, Encoding encoding) { await using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var streamReader = new StreamReader(fileStream, encoding); return await streamReader.ReadToEndAsync(); } /// /// Opens a file, reads all lines of the file with the specified encoding, and then closes the file. /// /// The file to open for reading /// The encoding applied to the contents of the file /// A string containing all lines of the file public virtual string ReadAllText(string path, Encoding encoding) { using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); using var streamReader = new StreamReader(fileStream, encoding); return streamReader.ReadToEnd(); } /// /// Writes the specified byte array to the file /// /// The file to write to /// The bytes to write to the file /// A task that represents the asynchronous operation public virtual async Task WriteAllBytesAsync(string filePath, byte[] bytes) { await File.WriteAllBytesAsync(filePath, bytes); } /// /// Creates a new file, writes the specified string to the file using the specified encoding, /// and then closes the file. If the target file already exists, it is overwritten. /// /// The file to write to /// The string to write to the file /// The encoding to apply to the string /// A task that represents the asynchronous operation public virtual async Task WriteAllTextAsync(string path, string contents, Encoding encoding) { await File.WriteAllTextAsync(path, contents, encoding); } /// /// Creates a new file, writes the specified string to the file using the specified encoding, /// and then closes the file. If the target file already exists, it is overwritten. /// /// The file to write to /// The string to write to the file /// The encoding to apply to the string public virtual void WriteAllText(string path, string contents, Encoding encoding) { File.WriteAllText(path, contents, encoding); } /// Locate a file at the given path. /// Relative path that identifies the file. /// The file information. Caller must check Exists property. public new virtual IFileInfo GetFileInfo(string subpath) { subpath = subpath.Replace(Root, string.Empty); return base.GetFileInfo(subpath); } #endregion #region Properties /// /// Gets or sets the absolute path to the directory that contains the web-servable application content files. /// public string WebRootPath { get; } #endregion }