using System; using System.Collections.Generic; using System.Drawing; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using LightInject; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Composing; using Umbraco.Core.Exceptions; using Umbraco.Core.IO.MediaPathSchemes; using Umbraco.Core.Logging; using Umbraco.Core.Media; using Umbraco.Core.Media.Exif; using Umbraco.Core.Models; namespace Umbraco.Core.IO { /// /// A custom file system provider for media /// [FileSystemProvider("media")] public class MediaFileSystem : FileSystemWrapper { public MediaFileSystem(IFileSystem wrapped) : base(wrapped) { // due to how FileSystems is written at the moment, the ctor cannot be used to inject // dependencies, so we have to rely on property injection for anything we might need Current.Container.InjectProperties(this); UploadAutoFillProperties = new UploadAutoFillProperties(this, Logger, ContentConfig); } [Inject] internal IMediaPathScheme MediaPathScheme { get; set; } [Inject] internal IContentSection ContentConfig { get; set; } [Inject] internal ILogger Logger { get; set; } internal UploadAutoFillProperties UploadAutoFillProperties { get; } /// /// Deletes all files passed in. /// /// /// /// internal bool DeleteFiles(IEnumerable files, Action onError = null) { //ensure duplicates are removed files = files.Distinct(); var allsuccess = true; var rootRelativePath = GetRelativePath("/"); Parallel.ForEach(files, file => { try { if (file.IsNullOrWhiteSpace()) return; var relativeFilePath = GetRelativePath(file); if (FileExists(relativeFilePath) == false) return; var parentDirectory = Path.GetDirectoryName(relativeFilePath); // don't want to delete the media folder if not using directories. if (ContentConfig.UploadAllowDirectories && parentDirectory != rootRelativePath) { //issue U4-771: if there is a parent directory the recursive parameter should be true DeleteDirectory(parentDirectory, string.IsNullOrEmpty(parentDirectory) == false); } else { DeleteFile(file); } } catch (Exception e) { onError?.Invoke(file, e); allsuccess = false; } }); return allsuccess; } public void DeleteMediaFiles(IEnumerable files) { files = files.Distinct(); Parallel.ForEach(files, file => { try { if (file.IsNullOrWhiteSpace()) return; if (FileExists(file) == false) return; DeleteFile(file); var directory = MediaPathScheme.GetDeleteDirectory(file); if (!directory.IsNullOrWhiteSpace()) DeleteDirectory(directory, true); } catch (Exception e) { Logger.Error("Failed to delete attached file \"" + file + "\".", e); } }); } #region Media Path /// /// Gets the file path of a media file. /// /// The file name. /// The unique identifier of the content/media owning the file. /// The unique identifier of the property type owning the file. /// The filesystem-relative path to the media file. /// With the old media path scheme, this CREATES a new media path each time it is invoked. public string GetMediaPath(string filename, Guid cuid, Guid puid) { filename = Path.GetFileName(filename); if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); return MediaPathScheme.GetFilePath(cuid, puid, filename); } /// /// Gets the file path of a media file. /// /// The file name. /// A previous file path. /// The unique identifier of the content/media owning the file. /// The unique identifier of the property type owning the file. /// The filesystem-relative path to the media file. /// In the old, legacy, number-based scheme, we try to re-use the media folder /// specified by . Else, we CREATE a new one. Each time we are invoked. public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid) { filename = Path.GetFileName(filename); if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); return MediaPathScheme.GetFilePath(cuid, puid, filename, prevpath); } #endregion #region Associated Media Files /// /// Stores a media file associated to a property of a content item. /// /// The content item owning the media file. /// The property type owning the media file. /// The media file name. /// A stream containing the media bytes. /// An optional filesystem-relative filepath to the previous media file. /// The filesystem-relative filepath to the media file. /// /// The file is considered "owned" by the content/propertyType. /// If an is provided then that file (and associated thumbnails if any) is deleted /// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new file. /// public string StoreFile(IContentBase content, PropertyType propertyType, string filename, Stream filestream, string oldpath) { if (content == null) throw new ArgumentNullException(nameof(content)); if (propertyType == null) throw new ArgumentNullException(nameof(propertyType)); if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullOrEmptyException(nameof(filename)); if (filestream == null) throw new ArgumentNullException(nameof(filestream)); // clear the old file, if any if (string.IsNullOrWhiteSpace(oldpath) == false) DeleteFile(oldpath); // get the filepath, store the data // use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme var filepath = GetMediaPath(filename, oldpath, content.Key, propertyType.Key); AddFile(filepath, filestream); return filepath; } /// /// Copies a media file as a new media file, associated to a property of a content item. /// /// The content item owning the copy of the media file. /// The property type owning the copy of the media file. /// The filesystem-relative path to the source media file. /// The filesystem-relative path to the copy of the media file. public string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath) { if (content == null) throw new ArgumentNullException(nameof(content)); if (propertyType == null) throw new ArgumentNullException(nameof(propertyType)); if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentNullOrEmptyException(nameof(sourcepath)); // ensure we have a file to copy if (FileExists(sourcepath) == false) return null; // get the filepath var filename = Path.GetFileName(sourcepath); var filepath = GetMediaPath(filename, content.Key, propertyType.Key); this.CopyFile(sourcepath, filepath); return filepath; } // gets or creates a property for a content item. private static Property GetProperty(IContentBase content, string propertyTypeAlias) { var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (property != null) return property; var propertyType = content.GetContentType().CompositionPropertyTypes .FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new Exception("No property type exists with alias " + propertyTypeAlias + "."); property = new Property(propertyType); content.Properties.Add(property); return property; } // fixme - what's below belongs to the upload property editor, not the media filesystem! public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null) { var property = GetProperty(content, propertyTypeAlias); var oldpath = property.GetValue(culture, segment) is string svalue ? GetRelativePath(svalue) : null; var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); property.SetValue(GetUrl(filepath), culture, segment); SetUploadFile(content, property, filepath, filestream, culture, segment); } public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath, string culture = null, string segment = null) { var property = GetProperty(content, propertyTypeAlias); // fixme delete? var oldpath = property.GetValue(culture, segment) is string svalue ? GetRelativePath(svalue) : null; if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) DeleteFile(oldpath); property.SetValue(GetUrl(filepath), culture, segment); using (var filestream = OpenFile(filepath)) { SetUploadFile(content, property, filepath, filestream, culture, segment); } } // sets a file for the FileUpload property editor // ie generates thumbnails and populates autofill properties private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream, string culture = null, string segment = null) { // will use filepath for extension, and filestream for length UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, culture, segment); } #endregion #region Image /// /// Gets a value indicating whether the file extension corresponds to an image. /// /// The file extension. /// A value indicating whether the file extension corresponds to an image. public bool IsImageFile(string extension) { if (extension == null) return false; extension = extension.TrimStart('.'); return ContentConfig.ImageFileTypes.InvariantContains(extension); } /// /// Gets the dimensions of an image. /// /// A stream containing the image bytes. /// The dimension of the image. /// First try with EXIF as it is faster and does not load the entire image /// in memory. Fallback to GDI which means loading the image in memory and thus /// use potentially large amounts of memory. public Size GetDimensions(Stream stream) { //Try to load with exif try { var jpgInfo = ImageFile.FromStream(stream); if (jpgInfo.Format != ImageFileFormat.Unknown && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) && jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension)) { var height = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelYDimension].Value); var width = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelXDimension].Value); if (height > 0 && width > 0) { return new Size(width, height); } } } catch (Exception) { //We will just swallow, just means we can't read exif data, we don't want to log an error either } //we have no choice but to try to read in via GDI using (var image = Image.FromStream(stream)) { var fileWidth = image.Width; var fileHeight = image.Height; return new Size(fileWidth, fileHeight); } } #endregion } }