using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Globalization; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; 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 { private readonly IContentSection _contentConfig; private readonly UploadAutoFillProperties _uploadAutoFillProperties; private readonly ILogger _logger; private readonly object _folderCounterLock = new object(); private long _folderCounter; private bool _folderCounterInitialized; [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] private static readonly Dictionary DefaultSizes = new Dictionary { { 100, "thumb" }, { 500, "big-thumb" } }; public MediaFileSystem(IFileSystem wrapped) : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content, ApplicationContext.Current.ProfilingLogger.Logger) { } public MediaFileSystem(IFileSystem wrapped, IContentSection contentConfig, ILogger logger) : base(wrapped) { _logger = logger; _contentConfig = contentConfig; _uploadAutoFillProperties = new UploadAutoFillProperties(this, logger, contentConfig); } internal UploadAutoFillProperties UploadAutoFillProperties { get { return _uploadAutoFillProperties; } } // note - this is currently experimental / being developed //public static bool UseTheNewMediaPathScheme { get; set; } public const bool UseTheNewMediaPathScheme = false; // none of the methods below are used in Core anymore [Obsolete("This low-level method should NOT exist.")] public string GetRelativePath(int propertyId, string fileName) { var sep = _contentConfig.UploadAllowDirectories ? Path.DirectorySeparatorChar : '-'; return propertyId.ToString(CultureInfo.InvariantCulture) + sep + fileName; } [Obsolete("This low-level method should NOT exist.", false)] public string GetRelativePath(string subfolder, string fileName) { var sep = _contentConfig.UploadAllowDirectories ? Path.DirectorySeparatorChar : '-'; return subfolder + sep + fileName; } #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.", "filename"); filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); string folder; if (UseTheNewMediaPathScheme == false) { // old scheme: filepath is "/" OR "-" // default media filesystem maps to "~/media/" folder = GetNextFolder(); } else { // new scheme: path is "/" where xuid is a combination of cuid and puid // default media filesystem maps to "~/media/" // assumes that cuid and puid keys can be trusted - and that a single property type // for a single content cannot store two different files with the same name folder = Combine(cuid, puid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... } var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? Path.Combine(folder, filename) : folder + "-" + filename; return filepath; } private static byte[] Combine(Guid guid1, Guid guid2) { var bytes1 = guid1.ToByteArray(); var bytes2 = guid2.ToByteArray(); var bytes = new byte[bytes1.Length]; for (var i = 0; i < bytes1.Length; i++) bytes[i] = (byte)(bytes1[i] ^ bytes2[i]); return bytes; } /// /// 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) { if (UseTheNewMediaPathScheme || string.IsNullOrWhiteSpace(prevpath)) return GetMediaPath(filename, cuid, puid); filename = Path.GetFileName(filename); if (filename == null) throw new ArgumentException("Cannot become a safe filename.", "filename"); filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); // old scheme, with a previous path // prevpath should be "/" OR "-" // and we want to reuse the "" part, so try to find it var sep = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? "/" : "-"; var pos = prevpath.IndexOf(sep, StringComparison.Ordinal); var s = pos > 0 ? prevpath.Substring(0, pos) : null; int ignored; var folder = (pos > 0 && int.TryParse(s, out ignored)) ? s : GetNextFolder(); // ReSharper disable once AssignNullToNotNullAttribute var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? Path.Combine(folder, filename) : folder + "-" + filename; return filepath; } /// /// Gets the next media folder in the original number-based scheme. /// /// /// Should be private, is internal for legacy FileHandlerData which is obsolete. internal string GetNextFolder() { EnsureFolderCounterIsInitialized(); return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture); } private void EnsureFolderCounterIsInitialized() { lock (_folderCounterLock) { if (_folderCounterInitialized) return; _folderCounter = 1000; // seed var directories = GetDirectories(""); foreach (var directory in directories) { long folderNumber; if (long.TryParse(directory, out folderNumber) && folderNumber > _folderCounter) _folderCounter = folderNumber; } // note: not multi-domains ie LB safe as another domain could create directories // while we read and parse them - don't fix, move to new scheme eventually _folderCounterInitialized = true; } } #endregion #region Associated Media Files /// /// Stores a media file. /// /// 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 thumbnails) 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("content"); if (propertyType == null) throw new ArgumentNullException("propertyType"); if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentException("Null or empty.", "filename"); if (filestream == null) throw new ArgumentNullException("filestream"); // clear the old file, if any if (string.IsNullOrWhiteSpace(oldpath) == false) DeleteFile(oldpath, true); // 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; } /// /// Clears a media file. /// /// The filesystem-relative path to the media file. public new void DeleteFile(string filepath) { DeleteFile(filepath, true); } /// /// Copies a media file. /// /// 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("content"); if (propertyType == null) throw new ArgumentNullException("propertyType"); if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentException("Null or empty.", "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. /// /// The content item. /// The property type alias. /// The property. 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; } public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream) { var property = GetProperty(content, propertyTypeAlias); var svalue = property.Value as string; var oldpath = svalue == null ? null : GetRelativePath(svalue); var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); property.Value = GetUrl(filepath); SetUploadFile(content, property, filepath, filestream); } public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath) { var property = GetProperty(content, propertyTypeAlias); var svalue = property.Value as string; var oldpath = svalue == null ? null : GetRelativePath(svalue); if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) DeleteFile(oldpath, true); property.Value = GetUrl(filepath); using (var filestream = OpenFile(filepath)) { SetUploadFile(content, property, filepath, filestream); } } /// /// Sets a file for the FileUpload property editor and populates autofill properties /// /// /// /// /// private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream) { // will use filepath for extension, and filestream for length _uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); } #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 UmbracoConfig.For.UmbracoSettings().Content.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 #region Manage thumbnails // note: this does not find 'custom' thumbnails? // will find _thumb and _big-thumb but NOT _custom? [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public IEnumerable GetThumbnails(string path) { var parentDirectory = Path.GetDirectoryName(path); var extension = Path.GetExtension(path); return GetFiles(parentDirectory) .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) .ToList(); } public void DeleteFile(string path, bool deleteThumbnails) { base.DeleteFile(path); if (deleteThumbnails == false) return; DeleteThumbnails(path); } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public void DeleteThumbnails(string path) { GetThumbnails(path) .ForEach(x => base.DeleteFile(x)); } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public void CopyThumbnails(string sourcePath, string targetPath) { var targetPathBase = Path.GetDirectoryName(targetPath) ?? ""; foreach (var sourceThumbPath in GetThumbnails(sourcePath)) { var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? ""; var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename); this.CopyFile(sourceThumbPath, targetThumbPath); } } public void DeleteMediaFiles(IEnumerable files) { files = files.Distinct(); Parallel.ForEach(files, file => { try { if (file.IsNullOrWhiteSpace()) return; if (FileExists(file) == false) return; DeleteFile(file, true); if (UseTheNewMediaPathScheme == false) { // old scheme: filepath is "/" OR "-" // remove the directory if any var dir = Path.GetDirectoryName(file); if (string.IsNullOrWhiteSpace(dir) == false) DeleteDirectory(dir, true); } else { // new scheme: path is "/" where xuid is a combination of cuid and puid // remove the directory var dir = Path.GetDirectoryName(file); DeleteDirectory(dir, true); } } catch (Exception e) { _logger.Error("Failed to delete attached file \"" + file + "\".", e); } }); } #endregion #region GenerateThumbnails [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public IEnumerable GenerateThumbnails( Image image, string filepath, string preValue) { if (string.IsNullOrWhiteSpace(preValue)) return GenerateThumbnails(image, filepath); var additionalSizes = new List(); var sep = preValue.Contains(",") ? "," : ";"; var values = preValue.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries); foreach (var value in values) { int size; if (int.TryParse(value, out size)) additionalSizes.Add(size); } return GenerateThumbnails(image, filepath, additionalSizes); } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public IEnumerable GenerateThumbnails( Image image, string filepath, IEnumerable additionalSizes = null) { var w = image.Width; var h = image.Height; var sizes = additionalSizes == null ? DefaultSizes.Keys : DefaultSizes.Keys.Concat(additionalSizes); // start with default sizes, // add additional sizes, // filter out duplicates, // filter out those that would be larger that the original image // and create the thumbnail return sizes .Distinct() .Where(x => w >= x && h >= x) .Select(x => GenerateResized(image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x)) .ToList(); // now } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public IEnumerable GenerateThumbnails( Stream filestream, string filepath, PropertyType propertyType) { // get the original image from the original stream if (filestream.CanSeek) filestream.Seek(0, 0); using (var image = Image.FromStream(filestream)) { return GenerateThumbnails(image, filepath, propertyType); } } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public IEnumerable GenerateThumbnails( Image image, string filepath, PropertyType propertyType) { // if the editor is an upload field, check for additional thumbnail sizes // that can be defined in the prevalue for the property data type. otherwise, // just use the default sizes. var sizes = propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias ? ApplicationContext.Current.Services.DataTypeService .GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId) .FirstOrDefault() : string.Empty; return GenerateThumbnails(image, filepath, sizes); } #endregion #region GenerateResized - Generate at resized filepath derived from origin filepath [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight) { return GenerateResized(originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) { return GenerateResized(originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight, int fixedWidth, int fixedHeight) { if (string.IsNullOrWhiteSpace(sizeName)) sizeName = "UMBRACOSYSTHUMBNAIL"; var extension = Path.GetExtension(originFilepath) ?? string.Empty; var filebase = originFilepath.TrimEnd(extension); var resizedFilepath = filebase + "_" + sizeName + extension; return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight); } #endregion #region GenerateResizedAt - Generate at specified resized filepath [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight) { return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, -1, -1); } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public ResizedImage GenerateResizedAt(Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) { return GenerateResizedAt(originImage, resizedFilepath, -1, fixedWidth, fixedHeight); } [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight, int fixedWidth, int fixedHeight) { // target dimensions int width, height; // if maxWidthHeight then get ratio if (maxWidthHeight > 0) { var fx = (float)originImage.Size.Width / maxWidthHeight; var fy = (float)originImage.Size.Height / maxWidthHeight; var f = Math.Max(fx, fy); // fit in thumbnail size width = (int)Math.Round(originImage.Size.Width / f); height = (int)Math.Round(originImage.Size.Height / f); if (width == 0) width = 1; if (height == 0) height = 1; } else if (fixedWidth > 0 && fixedHeight > 0) { width = fixedWidth; height = fixedHeight; } else { width = height = 1; } // create new image with best quality settings using (var bitmap = new Bitmap(width, height)) using (var graphics = Graphics.FromImage(bitmap)) { // if the image size is rather large we cannot use the best quality interpolation mode // because we'll get out of mem exceptions. So we detect how big the image is and use // the mid quality interpolation mode when the image size exceeds our max limit. graphics.InterpolationMode = originImage.Width > 5000 || originImage.Height > 5000 ? InterpolationMode.Bilinear // mid quality : InterpolationMode.HighQualityBicubic; // best quality // everything else is best-quality graphics.SmoothingMode = SmoothingMode.HighQuality; graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; graphics.CompositingQuality = CompositingQuality.HighQuality; // copy the old image to the new and resize var rect = new Rectangle(0, 0, width, height); graphics.DrawImage(originImage, rect, 0, 0, originImage.Width, originImage.Height, GraphicsUnit.Pixel); // get an encoder - based upon the file type var extension = (Path.GetExtension(resizedFilepath) ?? "").TrimStart('.').ToLowerInvariant(); var encoders = ImageCodecInfo.GetImageEncoders(); ImageCodecInfo encoder; switch (extension) { case "png": encoder = encoders.Single(t => t.MimeType.Equals("image/png")); break; case "gif": encoder = encoders.Single(t => t.MimeType.Equals("image/gif")); break; case "tif": case "tiff": encoder = encoders.Single(t => t.MimeType.Equals("image/tiff")); break; case "bmp": encoder = encoders.Single(t => t.MimeType.Equals("image/bmp")); break; // TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the // place so left it here, but it needs to not set a codec if it doesn't know which one to pick // Note: when fixing this: both .jpg and .jpeg should be handled as extensions default: encoder = encoders.Single(t => t.MimeType.Equals("image/jpeg")); break; } // set compresion ratio to 90% var encoderParams = new EncoderParameters(); encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L); // save the new image using (var stream = new MemoryStream()) { bitmap.Save(stream, encoder, encoderParams); stream.Seek(0, 0); if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL")) { var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString()); AddFile(filepath, stream); if (extension != "jpg") { filepath = filepath.TrimEnd(extension) + "jpg"; stream.Seek(0, 0); AddFile(filepath, stream); } // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 stream.Seek(0, 0); resizedFilepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", width + "x" + height); } AddFile(resizedFilepath, stream); } return new ResizedImage(resizedFilepath, width, height); } } #endregion #region Inner classes [Obsolete("This should no longer be used, image manipulation should be done via ImageProcessor, Umbraco no longer generates '_thumb' files for media")] public class ResizedImage { public ResizedImage() { } public ResizedImage(string filepath, int width, int height) { Filepath = filepath; Width = width; Height = height; } public string Filepath { get; set; } public int Width { get; set; } public int Height { get; set; } } #endregion } }