using System.IO.Compression; using System.Text; using Microsoft.Extensions.FileProviders; using Newtonsoft.Json; using Nop.Core.Domain.Media; using Nop.Core.Infrastructure; using SkiaSharp; namespace Nop.Services.Media.RoxyFileman; /// /// Looks up and manages uploaded files using the on-disk file system /// public partial class RoxyFilemanFileProvider : PhysicalFileProvider, IRoxyFilemanFileProvider { #region Fields protected INopFileProvider _nopFileProvider; protected readonly MediaSettings _mediaSettings; #endregion #region Ctor public RoxyFilemanFileProvider(INopFileProvider nopFileProvider) : base(nopFileProvider.Combine(nopFileProvider.WebRootPath, NopRoxyFilemanDefaults.DefaultRootDirectory)) { _nopFileProvider = nopFileProvider; } public RoxyFilemanFileProvider(INopFileProvider defaultFileProvider, MediaSettings mediaSettings) : this(defaultFileProvider) { _mediaSettings = mediaSettings; } #endregion #region Utilities /// /// Adjust image measures to target size /// /// Source image /// Target width /// Target height /// Adjusted width and height protected virtual (int width, int height) ValidateImageMeasures(SKBitmap image, int maxWidth = 0, int maxHeight = 0) { ArgumentNullException.ThrowIfNull(image); float width = Math.Min(image.Width, maxWidth); float height = Math.Min(image.Height, maxHeight); var targetSize = Math.Max(width, height); if (image.Height > image.Width) { // portrait width = image.Width * (targetSize / image.Height); height = targetSize; } else { // landscape or square width = targetSize; height = image.Height * (targetSize / image.Width); } return ((int)width, (int)height); } /// /// Get a file type by the specified path string /// /// The path string from which to get the file type /// File type protected virtual string GetFileType(string subpath) { var fileExtension = Path.GetExtension(subpath)?.ToLowerInvariant(); return fileExtension switch { ".jpg" or ".jpeg" or ".png" or ".gif" or ".webp" or ".svg" => "image", ".swf" or ".flv" => "flash", ".mp4" or ".webm" or ".ogg" or ".mov" or ".m4a" or ".mp3" or ".wav" => "media", _ => "file" }; /* These media extensions are not supported by HTML5 or tinyMCE out of the box * but may possibly be supported if You find players for them. * if (fileExtension == ".3gp" || fileExtension == ".flv" * || fileExtension == ".rmvb" || fileExtension == ".wmv" || fileExtension == ".divx" * || fileExtension == ".divx" || fileExtension == ".mpg" || fileExtension == ".rmvb" * || fileExtension == ".vob" // video * || fileExtension == ".aif" || fileExtension == ".aiff" || fileExtension == ".amr" * || fileExtension == ".asf" || fileExtension == ".asx" || fileExtension == ".wma" * || fileExtension == ".mid" || fileExtension == ".mp2") // audio * fileType = "media"; */ } /// /// Get the absolute path for the specified path string in the root directory for this instance /// /// The file or directory for which to obtain absolute path information /// The fully qualified location of path, such as "C:\MyFile.txt" protected virtual string GetFullPath(string path) { if (string.IsNullOrEmpty(path)) throw new RoxyFilemanException("NoFilesFound"); path = path.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); if (Path.IsPathRooted(path)) throw new RoxyFilemanException("NoFilesFound"); var fullPath = Path.GetFullPath(Path.Combine(Root, path)); if (!IsUnderneathRoot(fullPath)) throw new RoxyFilemanException("NoFilesFound"); return fullPath; } /// /// Get image format by mime type /// /// Mime type /// SKEncodedImageFormat protected virtual SKEncodedImageFormat GetImageFormatByMimeType(string mimeType) { var format = SKEncodedImageFormat.Jpeg; if (string.IsNullOrEmpty(mimeType)) return format; var parts = mimeType.ToLowerInvariant().Split('/'); var lastPart = parts[^1]; switch (lastPart) { case "webp": format = SKEncodedImageFormat.Webp; break; case "png": case "gif": case "bmp": case "x-icon": format = SKEncodedImageFormat.Png; break; default: break; } return format; } /// /// Get the unique name of the file (add -copy-(N) to the file name if there is already a file with that name in the directory) /// /// Path to the file directory /// Original file name /// Unique name of the file protected virtual string GetUniqueFileName(string directoryPath, string fileName) { var uniqueFileName = fileName; var i = 0; while (GetFileInfo(Path.Combine(directoryPath, uniqueFileName)) is IFileInfo fileInfo && fileInfo.Exists) { uniqueFileName = $"{Path.GetFileNameWithoutExtension(fileName)}-Copy-{++i}{Path.GetExtension(fileName)}"; } return uniqueFileName; } /// /// Check the specified path is in the root directory of this instance /// /// The absolute path /// True if passed path is in the root; otherwise false protected virtual bool IsUnderneathRoot(string fullPath) { return fullPath .StartsWith(Root, StringComparison.OrdinalIgnoreCase); } /// /// Scale image to fit the destination sizes /// /// Image data /// SkiaSharp image format /// Target width /// Target height /// The byte array of resized image protected virtual byte[] ResizeImage(byte[] data, SKEncodedImageFormat format, int maxWidth, int maxHeight) { using var sourceStream = new SKMemoryStream(data); using var inputData = SKData.Create(sourceStream); using var image = SKBitmap.Decode(inputData); var (width, height) = ValidateImageMeasures(image, maxWidth, maxHeight); var toBitmap = new SKBitmap(width, height, image.ColorType, image.AlphaType); if (!image.ScalePixels(toBitmap, SKFilterQuality.None)) throw new Exception("Image scaling"); var newImage = SKImage.FromBitmap(toBitmap); var imageData = newImage.Encode(format, _mediaSettings.DefaultImageQuality); newImage.Dispose(); return imageData.ToArray(); } #endregion #region Methods /// /// 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) { if (destDirName.StartsWith(sourceDirName, StringComparison.InvariantCulture)) throw new RoxyFilemanException("E_CannotMoveDirToChild"); var sourceDirInfo = new DirectoryInfo(GetFullPath(sourceDirName)); if (!sourceDirInfo.Exists) throw new RoxyFilemanException("E_MoveDirInvalisPath"); if (string.Equals(sourceDirInfo.FullName, Root, StringComparison.InvariantCultureIgnoreCase)) throw new RoxyFilemanException("E_MoveDir"); var newFullPath = Path.Combine(GetFullPath(destDirName), sourceDirInfo.Name); var destinationDirInfo = new DirectoryInfo(newFullPath); if (destinationDirInfo.Exists) throw new RoxyFilemanException("E_DirAlreadyExists"); try { sourceDirInfo.MoveTo(destinationDirInfo.FullName); } catch { throw new RoxyFilemanException("E_MoveDir"); } } /// /// Locate a file at the given path by directly mapping path segments to physical directories. /// /// A path under the root directory /// The file information. Caller must check Microsoft.Extensions.FileProviders.IFileInfo.Exists property. public new IFileInfo GetFileInfo(string subpath) { if (string.IsNullOrEmpty(subpath)) return new NotFoundFileInfo(subpath); subpath = subpath.TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); // Absolute paths not permitted. if (Path.IsPathRooted(subpath)) return new NotFoundFileInfo(subpath); return base.GetFileInfo(subpath); } /// /// Create configuration file for RoxyFileman /// /// The base path for the store /// Two-letter language code /// A task that represents the asynchronous operation public virtual async Task GetOrCreateConfigurationAsync(string pathBase, string lang) { //check whether the path base has changed, otherwise there is no need to overwrite the configuration file if (Singleton.Instance?.RETURN_URL_PREFIX?.Equals(pathBase) ?? false) { return Singleton.Instance; } var filePath = _nopFileProvider.GetAbsolutePath(NopRoxyFilemanDefaults.ConfigurationFile); //create file if not exists _nopFileProvider.CreateFile(filePath); //try to read existing configuration var existingText = await _nopFileProvider.ReadAllTextAsync(filePath, Encoding.UTF8); var existingConfiguration = JsonConvert.DeserializeObject(existingText); //create configuration var configuration = new RoxyFilemanConfig { FILES_ROOT = existingConfiguration?.FILES_ROOT ?? NopRoxyFilemanDefaults.DefaultRootDirectory, SESSION_PATH_KEY = existingConfiguration?.SESSION_PATH_KEY ?? string.Empty, THUMBS_VIEW_WIDTH = existingConfiguration?.THUMBS_VIEW_WIDTH ?? 140, THUMBS_VIEW_HEIGHT = existingConfiguration?.THUMBS_VIEW_HEIGHT ?? 120, PREVIEW_THUMB_WIDTH = existingConfiguration?.PREVIEW_THUMB_WIDTH ?? 300, PREVIEW_THUMB_HEIGHT = existingConfiguration?.PREVIEW_THUMB_HEIGHT ?? 200, MAX_IMAGE_WIDTH = existingConfiguration?.MAX_IMAGE_WIDTH ?? _mediaSettings.MaximumImageSize, MAX_IMAGE_HEIGHT = existingConfiguration?.MAX_IMAGE_HEIGHT ?? _mediaSettings.MaximumImageSize, DEFAULTVIEW = existingConfiguration?.DEFAULTVIEW ?? "list", FORBIDDEN_UPLOADS = existingConfiguration?.FORBIDDEN_UPLOADS ?? string.Join(" ", NopRoxyFilemanDefaults.ForbiddenUploadExtensions), ALLOWED_UPLOADS = existingConfiguration?.ALLOWED_UPLOADS ?? string.Empty, FILEPERMISSIONS = existingConfiguration?.FILEPERMISSIONS ?? "0644", DIRPERMISSIONS = existingConfiguration?.DIRPERMISSIONS ?? "0755", LANG = existingConfiguration?.LANG ?? lang, DATEFORMAT = existingConfiguration?.DATEFORMAT ?? "dd/MM/yyyy HH:mm", OPEN_LAST_DIR = existingConfiguration?.OPEN_LAST_DIR ?? "yes", //no need user to configure INTEGRATION = "custom", RETURN_URL_PREFIX = $"{pathBase}/images/uploaded/", DIRLIST = $"{pathBase}/Admin/RoxyFileman/DirectoriesList", CREATEDIR = $"{pathBase}/Admin/RoxyFileman/CreateDirectory", DELETEDIR = $"{pathBase}/Admin/RoxyFileman/DeleteDirectory", MOVEDIR = $"{pathBase}/Admin/RoxyFileman/MoveDirectory", COPYDIR = $"{pathBase}/Admin/RoxyFileman/CopyDirectory", RENAMEDIR = $"{pathBase}/Admin/RoxyFileman/RenameDirectory", FILESLIST = $"{pathBase}/Admin/RoxyFileman/FilesList", UPLOAD = $"{pathBase}/Admin/RoxyFileman/UploadFiles", DOWNLOAD = $"{pathBase}/Admin/RoxyFileman/DownloadFile", DOWNLOADDIR = $"{pathBase}/Admin/RoxyFileman/DownloadDirectory", DELETEFILE = $"{pathBase}/Admin/RoxyFileman/DeleteFile", MOVEFILE = $"{pathBase}/Admin/RoxyFileman/MoveFile", COPYFILE = $"{pathBase}/Admin/RoxyFileman/CopyFile", RENAMEFILE = $"{pathBase}/Admin/RoxyFileman/RenameFile", GENERATETHUMB = $"{pathBase}/Admin/RoxyFileman/CreateImageThumbnail" }; //save the file var text = JsonConvert.SerializeObject(configuration, Formatting.Indented); await File.WriteAllTextAsync(filePath, text, Encoding.UTF8); Singleton.Instance = configuration; return configuration; } /// /// Get all available directories as a directory tree /// /// Type of the file /// A value indicating whether to return a directory tree recursively /// Path to start directory public virtual IEnumerable GetDirectories(string type, bool isRecursive = true, string rootDirectoryPath = "") { foreach (var item in GetDirectoryContents(rootDirectoryPath)) { if (item.IsDirectory) { var dirInfo = new DirectoryInfo(item.PhysicalPath); yield return new RoxyDirectoryInfo( getRelativePath(item.Name), dirInfo.GetFiles().Count(x => isMatchType(x.Name)), dirInfo.GetDirectories().Length); } if (!isRecursive) break; foreach (var subDir in GetDirectories(type, isRecursive, getRelativePath(item.Name))) yield return subDir; } string getRelativePath(string name) => Path.Combine(rootDirectoryPath, name); bool isMatchType(string name) => string.IsNullOrEmpty(type) || GetFileType(name) == type; } /// /// Get files in the passed directory /// /// Path to the files directory /// Type of the files /// /// The list of /// public virtual IEnumerable GetFiles(string directoryPath = "", string type = "") { var files = GetDirectoryContents(directoryPath); return files .Where(f => !f.IsDirectory && isMatchType(f.Name)) .Select(f => { var width = 0; var height = 0; if (GetFileType(f.Name) == "image") { using var skData = SKData.Create(f.PhysicalPath); if (skData != null) { var image = SKBitmap.DecodeBounds(skData); width = image.Width; height = image.Height; } } return new RoxyFileInfo(getRelativePath(f.Name), f.LastModified, f.Length, width, height); }); bool isMatchType(string name) => string.IsNullOrEmpty(type) || GetFileType(name) == type; string getRelativePath(string name) => Path.Combine(directoryPath, name); } /// /// 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 sourcePath, string destinationPath) { var sourceFile = GetFileInfo(sourcePath); if (!sourceFile.Exists) throw new RoxyFilemanException("E_MoveFileInvalisPath"); var destinationFile = GetFileInfo(destinationPath); if (destinationFile.Exists) throw new RoxyFilemanException("E_MoveFileAlreadyExists"); try { new FileInfo(sourceFile.PhysicalPath) .MoveTo(GetFullPath(destinationPath)); } catch { throw new RoxyFilemanException("E_MoveFile"); } } /// /// Copy the directory with the embedded files and directories /// /// Path to the source directory /// Path to the destination directory public virtual void CopyDirectory(string sourcePath, string destinationPath) { var sourceDirInfo = new DirectoryInfo(GetFullPath(sourcePath)); if (!sourceDirInfo.Exists) throw new RoxyFilemanException("E_CopyDirInvalidPath"); var newPath = Path.Combine(GetFullPath(destinationPath), sourceDirInfo.Name); var destinationDirInfo = new DirectoryInfo(newPath); if (destinationDirInfo.Exists) throw new RoxyFilemanException("E_DirAlreadyExists"); try { destinationDirInfo.Create(); foreach (var file in sourceDirInfo.GetFiles()) { var newFile = GetFileInfo(Path.Combine(destinationPath, file.Name)); if (!newFile.Exists) file.CopyTo(Path.Combine(destinationDirInfo.FullName, file.Name)); } foreach (var directory in sourceDirInfo.GetDirectories()) { var destinationSubPath = Path.Combine(destinationPath, sourceDirInfo.Name, directory.Name); var sourceSubPath = Path.Combine(sourcePath, directory.Name); CopyDirectory(sourceSubPath, destinationSubPath); } } catch { throw new RoxyFilemanException("E_CopyFile"); } } /// /// Rename the directory /// /// Path to the source directory /// New name of the directory /// A task that represents the asynchronous operation public virtual void RenameDirectory(string sourcePath, string newName) { try { var destinationPath = Path.Combine(Path.GetDirectoryName(sourcePath), newName); DirectoryMove(sourcePath, destinationPath); } catch (Exception ex) { throw new RoxyFilemanException("E_RenameDir", ex); } } /// /// Rename the file /// /// Path to the source file /// New name of the file /// A task that represents the asynchronous operation public virtual void RenameFile(string sourcePath, string newName) { try { var destinationPath = Path.Combine(Path.GetDirectoryName(sourcePath), newName); FileMove(sourcePath, destinationPath); } catch (Exception ex) { throw new RoxyFilemanException("E_RenameFile", ex); } } /// /// Delete the file /// /// Path to the file /// A task that represents the asynchronous operation public virtual void DeleteFile(string path) { var fileToDelete = GetFileInfo(path); if (!fileToDelete.Exists) throw new RoxyFilemanException("E_DeleteFileInvalidPath"); try { File.Delete(GetFullPath(path)); } catch { throw new RoxyFilemanException("E_DeleteFile"); } } /// /// Copy the file /// /// Path to the source file /// Path to the destination file /// A task that represents the asynchronous operation public virtual void CopyFile(string sourcePath, string destinationPath) { var sourceFile = GetFileInfo(sourcePath); if (!sourceFile.Exists) throw new RoxyFilemanException("E_CopyFileInvalidPath"); var newFilePath = Path.Combine(destinationPath, sourceFile.Name); var destinationFile = GetFileInfo(newFilePath); if (destinationFile.Exists) newFilePath = Path.Combine(destinationPath, GetUniqueFileName(destinationPath, sourceFile.Name)); try { File.Copy(sourceFile.PhysicalPath, GetFullPath(newFilePath)); } catch { throw new RoxyFilemanException("E_CopyFile"); } } /// /// Create the new directory /// /// Path to the parent directory /// Name of the new directory /// A task that represents the asynchronous operation public virtual void CreateDirectory(string parentDirectoryPath, string name) { //validate path and get absolute form var fullPath = GetFullPath(Path.Combine(parentDirectoryPath, name)); var newDirectory = new DirectoryInfo(fullPath); if (!newDirectory.Exists) newDirectory.Create(); } /// /// Delete the directory /// /// Path to the directory public virtual void DeleteDirectory(string path) { var sourceDirInfo = new DirectoryInfo(GetFullPath(path)); if (!sourceDirInfo.Exists) throw new RoxyFilemanException("E_DeleteDirInvalidPath"); if (string.Equals(sourceDirInfo.FullName, Root, StringComparison.InvariantCultureIgnoreCase)) throw new RoxyFilemanException("E_CannotDeleteRoot"); if (sourceDirInfo.GetFiles().Length > 0 || sourceDirInfo.GetDirectories().Length > 0) throw new RoxyFilemanException("E_DeleteNonEmpty"); try { sourceDirInfo.Delete(); } catch { throw new RoxyFilemanException("E_CannotDeleteDir"); } } /// /// Save file in the root directory for this instance /// /// Directory path in the root /// The file name and extension /// Mime type /// The stream to read /// A task that represents the asynchronous operation public virtual async Task SaveFileAsync(string directoryPath, string fileName, string contentType, Stream fileStream) { var uniqueFileName = GetUniqueFileName(directoryPath, Path.GetFileName(fileName)); var destinationFile = Path.Combine(directoryPath, uniqueFileName); await using var stream = new FileStream(GetFullPath(destinationFile), FileMode.Create); if (GetFileType(Path.GetExtension(uniqueFileName)) == "image") { using var memoryStream = new MemoryStream(); await fileStream.CopyToAsync(memoryStream); var roxyConfig = Singleton.Instance; var imageData = ResizeImage(memoryStream.ToArray(), GetImageFormatByMimeType(contentType), roxyConfig?.MAX_IMAGE_WIDTH ?? _mediaSettings.MaximumImageSize, roxyConfig?.MAX_IMAGE_HEIGHT ?? _mediaSettings.MaximumImageSize); await stream.WriteAsync(imageData); } else { await fileStream.CopyToAsync(stream); } await stream.FlushAsync(); } /// /// Get the thumbnail of the image /// /// File path /// Mime type /// Byte array of the specified image public virtual byte[] CreateImageThumbnail(string sourcePath, string contentType) { var imageInfo = GetFileInfo(sourcePath); if (!imageInfo.Exists) throw new RoxyFilemanException("Image not found"); var roxyConfig = Singleton.Instance; using var imageStream = imageInfo.CreateReadStream(); using var ms = new MemoryStream(); imageStream.CopyTo(ms); return ResizeImage( ms.ToArray(), GetImageFormatByMimeType(contentType), roxyConfig.THUMBS_VIEW_WIDTH, roxyConfig.THUMBS_VIEW_HEIGHT); } /// /// Create a zip archive of the specified directory. /// /// The directory path with files to compress /// The byte array public virtual byte[] CreateZipArchiveFromDirectory(string directoryPath) { var sourceDirInfo = new DirectoryInfo(GetFullPath(directoryPath)); if (!sourceDirInfo.Exists) throw new RoxyFilemanException("E_CreateArchive"); using var memoryStream = new MemoryStream(); using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) { foreach (var file in sourceDirInfo.EnumerateFiles("*", SearchOption.AllDirectories)) { var fileRelativePath = file.FullName.Replace(sourceDirInfo.FullName, string.Empty) .TrimStart('\\'); using var fileStream = file.OpenRead(); using var fileStreamInZip = archive.CreateEntry(fileRelativePath).Open(); fileStream.CopyTo(fileStreamInZip); } } //ToArray() should be outside of the archive using return memoryStream.ToArray(); } #endregion }