diff --git a/src/Umbraco.Core/ByteArrayExtensions.cs b/src/Umbraco.Core/ByteArrayExtensions.cs new file mode 100644 index 0000000000..dacdd509ca --- /dev/null +++ b/src/Umbraco.Core/ByteArrayExtensions.cs @@ -0,0 +1,39 @@ +namespace Umbraco.Core +{ + public static class ByteArrayExtensions + { + private static readonly char[] BytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + public static string ToHexString(this byte[] bytes) + { + int i = 0, p = 0, bytesLength = bytes.Length; + var chars = new char[bytesLength * 2]; + while (i < bytesLength) + { + var b = bytes[i++]; + chars[p++] = BytesToHexStringLookup[b / 0x10]; + chars[p++] = BytesToHexStringLookup[b % 0x10]; + } + return new string(chars, 0, chars.Length); + } + + public static string ToHexString(this byte[] bytes, char separator, int blockSize, int blockCount) + { + int p = 0, bytesLength = bytes.Length, count = 0, size = 0; + var chars = new char[bytesLength * 2 + blockCount]; + for (var i = 0; i < bytesLength; i++) + { + var b = bytes[i++]; + chars[p++] = BytesToHexStringLookup[b / 0x10]; + chars[p++] = BytesToHexStringLookup[b % 0x10]; + if (count == blockCount) continue; + if (++size < blockSize) continue; + + chars[p++] = '/'; + size = 0; + count++; + } + return new string(chars, 0, chars.Length); + } + } +} diff --git a/src/Umbraco.Core/ICompletable.cs b/src/Umbraco.Core/ICompletable.cs new file mode 100644 index 0000000000..594d82b0ae --- /dev/null +++ b/src/Umbraco.Core/ICompletable.cs @@ -0,0 +1,9 @@ +using System; + +namespace Umbraco.Core +{ + public interface ICompletable : IDisposable + { + void Complete(); + } +} diff --git a/src/Umbraco.Core/IO/FileSystemProviderManager.cs b/src/Umbraco.Core/IO/FileSystemProviderManager.cs index 1e076bb979..d807832bcb 100644 --- a/src/Umbraco.Core/IO/FileSystemProviderManager.cs +++ b/src/Umbraco.Core/IO/FileSystemProviderManager.cs @@ -204,7 +204,7 @@ namespace Umbraco.Core.IO // _shadowEnabled = true; //} - public ShadowFileSystemsScope Shadow(Guid id) + public ICompletable Shadow(Guid id) { var typed = _wrappers.ToArray(); var wrappers = new ShadowWrapper[typed.Length + 7]; diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 063f63c437..003b4891f5 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -4,9 +4,6 @@ using System.IO; namespace Umbraco.Core.IO { - //TODO: There is no way to create a directory here without creating a file in a directory and then deleting it - //TODO: Should probably implement a rename? - public interface IFileSystem { IEnumerable GetDirectories(string path); @@ -46,5 +43,12 @@ namespace Umbraco.Core.IO public interface IFileSystem2 : IFileSystem { long GetSize(string path); + + // TODO: implement these + // + //void CreateDirectory(string path); + // + //// move or rename, directory or file + //void Move(string source, string target); } } diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index d0e1bbee53..6f32ef6da0 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -1,10 +1,18 @@ 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 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 { @@ -15,17 +23,35 @@ namespace Umbraco.Core.IO public class MediaFileSystem : FileSystemWrapper { private readonly IContentSection _contentConfig; + private readonly UploadAutoFillProperties _uploadAutoFillProperties; - public MediaFileSystem(IFileSystem2 wrapped) - : this(wrapped, UmbracoConfig.For.UmbracoSettings().Content) - { - } + private readonly object _folderCounterLock = new object(); + private long _folderCounter; + private bool _folderCounterInitialized; - public MediaFileSystem(IFileSystem2 wrapped, IContentSection contentConfig) : base(wrapped) + 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) { _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.")] @@ -48,28 +74,611 @@ namespace Umbraco.Core.IO return subfolder + sep + fileName; } - [Obsolete("Use ImageHelper.GetThumbnails instead.", false)] - public IEnumerable GetThumbnails(string path) + #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) { - return ImageHelper.GetThumbnails(this, path); - } + filename = Path.GetFileName(filename); + if (filename == null) throw new ArgumentException("Cannot become a safe filename.", "filename"); + filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); - [Obsolete("Use ImageHelper.DeleteFile instead.", false)] - public void DeleteFile(string path, bool deleteThumbnails) - { - ImageHelper.DeleteFile(this, path, deleteThumbnails); - } + string folder; + if (UseTheNewMediaPathScheme == false) + { + // old scheme: filepath is "/" OR "-" + // default media filesystem maps to "~/media/" + folder = GetNextFolder(); + } + else + { + // new scheme: path is "-/" OR "--" + // 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/... + } - [Obsolete("Use ImageHelper.DeleteThumbnails instead.", false)] - public void DeleteThumbnails(string path) - { - ImageHelper.DeleteThumbnails(this, path); - } + var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories + ? Path.Combine(folder, filename) + : folder + "-" + filename; - [Obsolete("Use ImageHelper.CopyThumbnails instead.", false)] - public void CopyThumbnails(string sourcePath, string targetPath) - { - ImageHelper.CopyThumbnails(this, sourcePath, targetPath); + 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); + CopyThumbnails(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); // FIXME DELETE? + 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 + // ie generates thumbnails and populates autofill properties + private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream) + { + // check if file is an image (and supports resizing and thumbnails etc) + var extension = Path.GetExtension(filepath); + var isImage = IsImageFile(extension); + + // specific stuff for images (thumbnails etc) + if (isImage) + { + using (var image = Image.FromStream(filestream)) + { + // use one image for all + GenerateThumbnails(image, filepath, property.PropertyType); + _uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, image); + } + } + else + { + // 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? + 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); + } + + public void DeleteThumbnails(string path) + { + GetThumbnails(path) + .ForEach(x => base.DeleteFile(x)); + } + + 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); + } + } + + #endregion + + #region GenerateThumbnails + + 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); + } + + 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 + } + + public IEnumerable GenerateThumbnails( + Stream filestream, + string filepath, + PropertyType propertyType) + { + // get the original image from the original stream + if (filestream.CanSeek) filestream.Seek(0, 0); // fixme - what if we cannot seek? + using (var image = Image.FromStream(filestream)) + { + return GenerateThumbnails(image, filepath, propertyType); + } + } + + 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 + + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight) + { + return GenerateResized(originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); + } + + public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) + { + return GenerateResized(originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); + } + + 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 + + public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight) + { + return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, -1, -1); + } + + public ResizedImage GenerateResizedAt(Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) + { + return GenerateResizedAt(originImage, resizedFilepath, -1, fixedWidth, fixedHeight); + } + + 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); + + // copy metadata + // fixme - er... no? + + // 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 + + 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 + } } diff --git a/src/Umbraco.Core/IO/ShadowFileSystem.cs b/src/Umbraco.Core/IO/ShadowFileSystem.cs index 276e058bcc..1e5da10bdc 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystem.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystem.cs @@ -5,7 +5,7 @@ using System.Linq; namespace Umbraco.Core.IO { - public class ShadowFileSystem : IFileSystem2 + internal class ShadowFileSystem : IFileSystem2 { private readonly IFileSystem _fs; private readonly IFileSystem2 _sfs; diff --git a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs index 5e0280ddca..0793946b62 100644 --- a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs +++ b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs @@ -5,7 +5,7 @@ using Umbraco.Core.Logging; namespace Umbraco.Core.IO { - public class ShadowFileSystemsScope : IDisposable + internal class ShadowFileSystemsScope : ICompletable { // note: taking a reference to the _manager instead of using manager.Current // to avoid using Current everywhere but really, we support only 1 scope at diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 7f9bc61a66..87d2b8db9f 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -2,11 +2,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Web.Hosting; namespace Umbraco.Core.IO { - public class ShadowWrapper : IFileSystem2 + internal class ShadowWrapper : IFileSystem2 { private readonly IFileSystem _innerFileSystem; private readonly string _shadowPath; diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs index 8e1d8c0987..18438831e2 100644 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ b/src/Umbraco.Core/IO/UmbracoMediaFile.cs @@ -119,7 +119,7 @@ namespace Umbraco.Core.IO public bool SupportsResizing { - get { return ImageHelper.IsImageFile(Extension); } + get { return _fs.IsImageFile(Extension); } } public string GetFriendlyName() @@ -137,7 +137,7 @@ namespace Umbraco.Core.IO using (var fs = _fs.OpenFile(Path)) { - _size = ImageHelper.GetDimensions(fs); + _size = _fs.GetDimensions(fs); } } else @@ -171,7 +171,7 @@ namespace Umbraco.Core.IO using (var filestream = _fs.OpenFile(Path)) using (var image = Image.FromStream(filestream)) { - return ImageHelper.GenerateResized(_fs, image, Path, sizeName, maxWidthHeight, width, height).Filepath; + return _fs.GenerateResized(image, Path, sizeName, maxWidthHeight, width, height).Filepath; } } diff --git a/src/Umbraco.Core/MainDom.cs b/src/Umbraco.Core/MainDom.cs index 52dbb21f21..e5d63cb6c9 100644 --- a/src/Umbraco.Core/MainDom.cs +++ b/src/Umbraco.Core/MainDom.cs @@ -1,13 +1,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; -using System.IO.MemoryMappedFiles; -using System.Text; using System.Threading; -using System.Threading.Tasks; using System.Web.Hosting; using Umbraco.Core.Logging; -using Umbraco.Core.ObjectResolution; namespace Umbraco.Core { @@ -109,7 +104,7 @@ namespace Umbraco.Core /// An action to execute before the AppDomain releases the main domain status. /// An optional weight (lower goes first). /// A value indicating whether it was possible to register. - /// If registering is successful, then the action + /// If registering is successful, then the action /// is guaranteed to execute before the AppDomain releases the main domain status. public bool Register(Action install, Action release, int weight = 100) { diff --git a/src/Umbraco.Core/Media/ImageExtensions.cs b/src/Umbraco.Core/Media/ImageExtensions.cs new file mode 100644 index 0000000000..c20be13d31 --- /dev/null +++ b/src/Umbraco.Core/Media/ImageExtensions.cs @@ -0,0 +1,21 @@ +using System.Drawing; +using System.Drawing.Imaging; +using System.Linq; + +namespace Umbraco.Core.Media +{ + public static class ImageExtensions + { + /// + /// Gets the MIME type of an image. + /// + /// The image. + /// The MIME type of the image. + public static string GetMimeType(this Image image) + { + var format = image.RawFormat; + var codec = ImageCodecInfo.GetImageDecoders().First(c => c.FormatID == format.Guid); + return codec.MimeType; + } + } +} diff --git a/src/Umbraco.Core/Media/ImageHelper.cs b/src/Umbraco.Core/Media/ImageHelper.cs deleted file mode 100644 index f3cab2d3b0..0000000000 --- a/src/Umbraco.Core/Media/ImageHelper.cs +++ /dev/null @@ -1,383 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.IO; -using System.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Media.Exif; -using Umbraco.Core.Models; - -namespace Umbraco.Core.Media -{ - /// - /// Provides helper methods for managing images. - /// - public static class ImageHelper - { - private static readonly Dictionary DefaultSizes = new Dictionary - { - { 100, "thumb" }, - { 500, "big-thumb" } - }; - - /// - /// 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 static 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 static 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); - } - } - - /// - /// Gets the MIME type of an image. - /// - /// The image. - /// The MIME type of the image. - public static string GetMimeType(this Image image) - { - var format = image.RawFormat; - var codec = ImageCodecInfo.GetImageDecoders().First(c => c.FormatID == format.Guid); - return codec.MimeType; - } - - #region Manage thumbnails - - // note: this does not find 'custom' thumbnails? - // will find _thumb and _big-thumb but NOT _custom? - public static IEnumerable GetThumbnails(IFileSystem fs, string path) - { - var parentDirectory = Path.GetDirectoryName(path); - var extension = Path.GetExtension(path); - - return fs.GetFiles(parentDirectory) - .Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb")) - .ToList(); - } - - public static void DeleteFile(IFileSystem fs, string path, bool deleteThumbnails) - { - fs.DeleteFile(path); - - if (deleteThumbnails == false) - return; - - DeleteThumbnails(fs, path); - } - - public static void DeleteThumbnails(IFileSystem fs, string path) - { - GetThumbnails(fs, path) - .ForEach(fs.DeleteFile); - } - - public static void CopyThumbnails(IFileSystem fs, string sourcePath, string targetPath) - { - var targetPathBase = Path.GetDirectoryName(targetPath) ?? ""; - foreach (var sourceThumbPath in GetThumbnails(fs, sourcePath)) - { - var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? ""; - var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename); - fs.CopyFile(sourceThumbPath, targetThumbPath); - } - } - - #endregion - - #region GenerateThumbnails - - public static IEnumerable GenerateThumbnails( - IFileSystem fs, - Image image, - string filepath, - string preValue) - { - if (string.IsNullOrWhiteSpace(preValue)) - return GenerateThumbnails(fs, 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(fs, image, filepath, additionalSizes); - } - - public static IEnumerable GenerateThumbnails( - IFileSystem fs, - 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(fs, image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x)) - .ToList(); // now - } - - public static IEnumerable GenerateThumbnails( - IFileSystem fs, - Stream filestream, - string filepath, - PropertyType propertyType) - { - // get the original image from the original stream - if (filestream.CanSeek) filestream.Seek(0, 0); // fixme - what if we cannot seek? - using (var image = Image.FromStream(filestream)) - { - return GenerateThumbnails(fs, image, filepath, propertyType); - } - } - - public static IEnumerable GenerateThumbnails( - IFileSystem fs, - 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(fs, image, filepath, sizes); - } - - #endregion - - #region GenerateResized - Generate at resized filepath derived from origin filepath - - public static ResizedImage GenerateResized(IFileSystem fs, Image originImage, string originFilepath, string sizeName, int maxWidthHeight) - { - return GenerateResized(fs, originImage, originFilepath, sizeName, maxWidthHeight, -1, -1); - } - - public static ResizedImage GenerateResized(IFileSystem fs, Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight) - { - return GenerateResized(fs, originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight); - } - - public static ResizedImage GenerateResized(IFileSystem fs, 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(fs, originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight); - } - - #endregion - - #region GenerateResizedAt - Generate at specified resized filepath - - public static ResizedImage GenerateResizedAt(IFileSystem fs, Image originImage, string resizedFilepath, int maxWidthHeight) - { - return GenerateResizedAt(fs, originImage, resizedFilepath, maxWidthHeight, -1, -1); - } - - public static ResizedImage GenerateResizedAt(IFileSystem fs, Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath) - { - return GenerateResizedAt(fs, originImage, resizedFilepath, -1, fixedWidth, fixedHeight); - } - - public static ResizedImage GenerateResizedAt(IFileSystem fs, 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); - - // copy metadata - // fixme - er... no? - - // 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()); - fs.AddFile(filepath, stream); - if (extension != "jpg") - { - filepath = filepath.TrimEnd(extension) + "jpg"; - stream.Seek(0, 0); - fs.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); - } - - fs.AddFile(resizedFilepath, stream); - } - - return new ResizedImage(resizedFilepath, width, height); - } - } - - #endregion - - #region Inner classes - - 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 - } -} diff --git a/src/Umbraco.Core/Media/MediaHelper.cs b/src/Umbraco.Core/Media/MediaHelper.cs deleted file mode 100644 index 040cfd5d7d..0000000000 --- a/src/Umbraco.Core/Media/MediaHelper.cs +++ /dev/null @@ -1,281 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Drawing; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; -using Umbraco.Core.Models; - -namespace Umbraco.Core.Media -{ - /// - /// Provides helper methods for managing medias. - /// - /// Medias can be anything that can be uploaded via an upload - /// property, including but not limited to, images. See ImageHelper for - /// image-specific methods. - internal static class MediaHelper - { - private static long _folderCounter; - private static bool _folderCounterInitialized; - private static readonly object FolderCounterLock = new object(); - - // fixme - should be a config option of some sort! - //public static bool UseTheNewMediaPathScheme { get; set; } - public const bool UseTheNewMediaPathScheme = false; - - public static MediaFileSystem FileSystem { get { return FileSystemProviderManager.Current.GetFileSystemProvider(); } } - - #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 static 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 "-/" OR "--" - // default media filesystem maps to "~/media/" - // fixme - this assumes that the keys exists and won't change (even when creating a new content) - // fixme - this is going to create looooong filepaths, any chance we can shorten them? - folder = cuid.ToString("N") + "-" + puid.ToString("N"); - } - - var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(folder, filename) - : folder + "-" + filename; - - return filepath; - } - - /// - /// 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 static 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 static string GetNextFolder() - { - lock (FolderCounterLock) - { - if (_folderCounterInitialized == false) - { - _folderCounter = 1000; // seed - was not respected in MediaSubfolderCounter? - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - var directories = fs.GetDirectories(""); - foreach (var directory in directories) - { - long folderNumber; - if (long.TryParse(directory, out folderNumber) && folderNumber > _folderCounter) - _folderCounter = folderNumber; - } - - _folderCounterInitialized = true; - } - } - - return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture); - } - - #endregion - - /// - /// 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 static 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 - var fs = FileSystem; - if (string.IsNullOrWhiteSpace(oldpath) == false) - ImageHelper.DeleteFile(fs, 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); - fs.AddFile(filepath, filestream); - return filepath; - } - - /// - /// Clears a media file. - /// - /// The filesystem-relative path to the media file. - public static void DeleteFile(string filepath) - { - ImageHelper.DeleteFile(FileSystem, 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 static 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 - var fs = FileSystem; - if (fs.FileExists(sourcepath) == false) return null; - - // get the filepath - var filename = Path.GetFileName(sourcepath); - var filepath = GetMediaPath(filename, content.Key, propertyType.Key); - fs.CopyFile(sourcepath, filepath); - ImageHelper.CopyThumbnails(fs, 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 static 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 : FileSystem.GetRelativePath(svalue); - var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath); - property.Value = FileSystem.GetUrl(filepath); - SetUploadFile(content, property, FileSystem, filepath, filestream); - } - - public static void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath) - { - var property = GetProperty(content, propertyTypeAlias); - var svalue = property.Value as string; - var oldpath = svalue == null ? null : FileSystem.GetRelativePath(svalue); // FIXME DELETE? - if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath) - FileSystem.DeleteFile(oldpath); - property.Value = FileSystem.GetUrl(filepath); - var fs = FileSystem; - using (var filestream = fs.OpenFile(filepath)) - { - SetUploadFile(content, property, fs, filepath, filestream); - } - } - - // sets a file for the FileUpload property editor - // ie generates thumbnails and populates autofill properties - private static void SetUploadFile(IContentBase content, Property property, IFileSystem fs, string filepath, Stream filestream) - { - // check if file is an image (and supports resizing and thumbnails etc) - var extension = Path.GetExtension(filepath); - var isImage = ImageHelper.IsImageFile(extension); - - // specific stuff for images (thumbnails etc) - if (isImage) - { - using (var image = Image.FromStream(filestream)) - { - // use one image for all - ImageHelper.GenerateThumbnails(fs, image, filepath, property.PropertyType); - UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, image); - } - } - else - { - // will use filepath for extension, and filestream for length - UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream); - } - } - } -} diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs index 625bdc4e47..d0e7347848 100644 --- a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -2,7 +2,6 @@ using System.Drawing; using System.IO; using System.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Logging; @@ -13,16 +12,27 @@ namespace Umbraco.Core.Media /// /// Provides methods to manage auto-fill properties for upload fields. /// - internal static class UploadAutoFillProperties + internal class UploadAutoFillProperties { + private readonly ILogger _logger; + private readonly MediaFileSystem _mediaFileSystem; + private readonly IContentSection _contentSettings; + + public UploadAutoFillProperties(MediaFileSystem mediaFileSystem, ILogger logger, IContentSection contentSettings) + { + _mediaFileSystem = mediaFileSystem; + _logger = logger; + _contentSettings = contentSettings; + } + /// /// Gets the auto-fill configuration for a specified property alias. /// /// The property type alias. /// The auto-fill configuration for the specified property alias, or null. - public static IImagingAutoFillUploadField GetConfig(string propertyTypeAlias) + public IImagingAutoFillUploadField GetConfig(string propertyTypeAlias) { - var autoFillConfigs = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties; + var autoFillConfigs = _contentSettings.ImageAutoFillProperties; return autoFillConfigs == null ? null : autoFillConfigs.FirstOrDefault(x => x.Alias == propertyTypeAlias); } @@ -31,7 +41,7 @@ namespace Umbraco.Core.Media /// /// The content item. /// The property type alias. - public static void Reset(IContentBase content, string propertyTypeAlias) + public void Reset(IContentBase content, string propertyTypeAlias) { if (content == null) throw new ArgumentNullException("content"); if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); @@ -49,7 +59,7 @@ namespace Umbraco.Core.Media /// /// The content item. /// The auto-fill configuration. - public static void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig) + public void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig) { if (content == null) throw new ArgumentNullException("content"); if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); @@ -63,7 +73,7 @@ namespace Umbraco.Core.Media /// The content item. /// The property type alias. /// The filesystem-relative filepath, or null to clear properties. - public static void Populate(IContentBase content, string propertyTypeAlias, string filepath) + public void Populate(IContentBase content, string propertyTypeAlias, string filepath) { if (content == null) throw new ArgumentNullException("content"); if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); @@ -87,7 +97,7 @@ namespace Umbraco.Core.Media /// The filesystem-relative filepath, or null to clear properties. /// The stream containing the file data. /// The file data as an image object. - public static void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream, Image image = null) + public void Populate(IContentBase content, string propertyTypeAlias, string filepath, Stream filestream, Image image = null) { if (content == null) throw new ArgumentNullException("content"); if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); @@ -110,7 +120,7 @@ namespace Umbraco.Core.Media /// The auto-fill configuration. /// The filesystem path to the uploaded file. /// The parameter is the path relative to the filesystem. - public static void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath) + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath) { if (content == null) throw new ArgumentNullException("content"); if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); @@ -125,16 +135,16 @@ namespace Umbraco.Core.Media // if anything goes wrong, just reset the properties try { - using (var filestream = MediaHelper.FileSystem.OpenFile(filepath)) + using (var filestream = _mediaFileSystem.OpenFile(filepath)) { var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); - var size = ImageHelper.IsImageFile(extension) ? (Size?)ImageHelper.GetDimensions(filestream) : null; + var size = _mediaFileSystem.IsImageFile(extension) ? (Size?) _mediaFileSystem.GetDimensions(filestream) : null; SetProperties(content, autoFillConfig, size, filestream.Length, extension); } } catch (Exception ex) { - LogHelper.Error(typeof(UploadAutoFillProperties), "Could not populate upload auto-fill properties for file \"" + _logger.Error(typeof(UploadAutoFillProperties), "Could not populate upload auto-fill properties for file \"" + filepath + "\".", ex); ResetProperties(content, autoFillConfig); } @@ -149,7 +159,7 @@ namespace Umbraco.Core.Media /// The filesystem-relative filepath, or null to clear properties. /// The stream containing the file data. /// The file data as an image object. - public static void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, Image image = null) + public void Populate(IContentBase content, IImagingAutoFillUploadField autoFillConfig, string filepath, Stream filestream, Image image = null) { if (content == null) throw new ArgumentNullException("content"); if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); @@ -164,7 +174,7 @@ namespace Umbraco.Core.Media var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); Size? size; if (image == null) - size = ImageHelper.IsImageFile(extension) ? (Size?) ImageHelper.GetDimensions(filestream) : null; + size = _mediaFileSystem.IsImageFile(extension) ? (Size?) _mediaFileSystem.GetDimensions(filestream) : null; else size = new Size(image.Width, image.Height); SetProperties(content, autoFillConfig, size, filestream.Length, extension); diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index e54653ea29..49a6e89226 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -474,7 +474,7 @@ namespace Umbraco.Core.Models if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? - MediaHelper.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream); + FileSystemProviderManager.Current.MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream); } /// @@ -519,7 +519,7 @@ namespace Umbraco.Core.Models if (string.IsNullOrWhiteSpace(filename)) return; filename = filename.ToLower(); // fixme - er... why? - MediaHelper.SetUploadFile(content, propertyTypeAlias, filename, filestream); + FileSystemProviderManager.Current.MediaFileSystem.SetUploadFile(content, propertyTypeAlias, filename, filestream); } /// @@ -543,7 +543,7 @@ namespace Umbraco.Core.Models var propertyType = content.GetContentType() .CompositionPropertyTypes.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias)); if (propertyType == null) throw new ArgumentException("Invalid property type alias " + propertyTypeAlias + "."); - return MediaHelper.StoreFile(content, propertyType, filename, filestream, filepath); + return FileSystemProviderManager.Current.MediaFileSystem.StoreFile(content, propertyType, filename, filestream, filepath); } #endregion diff --git a/src/Umbraco.Core/Models/PublicAccessEntry.cs b/src/Umbraco.Core/Models/PublicAccessEntry.cs index 4af6f9536a..969e853f05 100644 --- a/src/Umbraco.Core/Models/PublicAccessEntry.cs +++ b/src/Umbraco.Core/Models/PublicAccessEntry.cs @@ -107,10 +107,7 @@ namespace Umbraco.Core.Models public void ClearRules() { - for (var i = _ruleCollection.Count - 1; i >= 0; i--) - { - RemoveRule(_ruleCollection[i]); - } + _ruleCollection.Clear(); } [DataMember] diff --git a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs index 6f048bbf2c..57664b8a83 100644 --- a/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs +++ b/src/Umbraco.Core/Packaging/PackageBinaryInspector.cs @@ -82,7 +82,7 @@ namespace Umbraco.Core.Packaging /// /// Performs the assembly scanning /// - /// + /// /// /// /// @@ -111,7 +111,7 @@ namespace Umbraco.Core.Packaging /// /// Performs the assembly scanning /// - /// + /// /// /// /// @@ -158,7 +158,7 @@ namespace Umbraco.Core.Packaging //get the list of assembly names to compare below var loadedNames = loaded.Select(x => x.GetName().Name).ToArray(); - + //Then load each referenced assembly into the context foreach (var a in loaded) { @@ -174,7 +174,7 @@ namespace Umbraco.Core.Packaging } catch (FileNotFoundException) { - //if an exception occurs it means that a referenced assembly could not be found + //if an exception occurs it means that a referenced assembly could not be found errors.Add( string.Concat("This package references the assembly '", assemblyName.Name, @@ -183,7 +183,7 @@ namespace Umbraco.Core.Packaging } catch (Exception ex) { - //if an exception occurs it means that a referenced assembly could not be found + //if an exception occurs it means that a referenced assembly could not be found errors.Add( string.Concat("This package could not be verified for compatibility. An error occurred while loading a referenced assembly '", assemblyName.Name, @@ -201,7 +201,7 @@ namespace Umbraco.Core.Packaging { //now we need to see if they contain any type 'T' var reflectedAssembly = a; - + try { var found = reflectedAssembly.GetExportedTypes() @@ -214,8 +214,8 @@ namespace Umbraco.Core.Packaging } catch (Exception ex) { - //This is a hack that nobody can seem to get around, I've read everything and it seems that - // this is quite a common thing when loading types into reflection only load context, so + //This is a hack that nobody can seem to get around, I've read everything and it seems that + // this is quite a common thing when loading types into reflection only load context, so // we're just going to ignore this specific one for now var typeLoadEx = ex as TypeLoadException; if (typeLoadEx != null) @@ -236,7 +236,7 @@ namespace Umbraco.Core.Packaging LogHelper.Error("An error occurred scanning package assemblies", ex); } } - + } errorReport = errors.ToArray(); @@ -256,7 +256,7 @@ namespace Umbraco.Core.Packaging var contractType = contractAssemblyLoadFrom.GetExportedTypes() .FirstOrDefault(x => x.FullName == typeof(T).FullName && x.Assembly.FullName == typeof(T).Assembly.FullName); - + if (contractType == null) { throw new InvalidOperationException("Could not find type " + typeof(T) + " in the LoadFrom assemblies"); diff --git a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs index a9c7a976a3..781d496a33 100644 --- a/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs +++ b/src/Umbraco.Core/Persistence/DefaultDatabaseFactory.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.Persistence private readonly ILogger _logger; public string ConnectionString { get; private set; } public string ProviderName { get; private set; } - + // NO! see notes in v8 HybridAccessorBase //[ThreadStatic] //private static volatile UmbracoDatabase _nonHttpInstance; diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index 4d48652c0b..ded026bbec 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -657,7 +657,7 @@ WHERE EXISTS( var allsuccess = true; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fs = FileSystemProviderManager.Current.MediaFileSystem; Parallel.ForEach(files, file => { try @@ -677,7 +677,7 @@ WHERE EXISTS( } else { - ImageHelper.DeleteFile(fs, file, true); + fs.DeleteFile(file, true); } } catch (Exception e) diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 8a86e59cc3..b9026eb6c4 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -8,6 +8,7 @@ using System.Text.RegularExpressions; using System.Threading; using Umbraco.Core.Configuration; using Umbraco.Core.Events; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Media; using Umbraco.Core.Models; @@ -33,6 +34,7 @@ namespace Umbraco.Core.Services private readonly EntityXmlSerializer _entitySerializer = new EntityXmlSerializer(); private readonly IDataTypeService _dataTypeService; private readonly IUserService _userService; + private readonly MediaFileSystem _mediaFileSystem = FileSystemProviderManager.Current.MediaFileSystem; public MediaService(IDatabaseUnitOfWorkProvider provider, RepositoryFactory repositoryFactory, ILogger logger, IEventMessagesFactory eventMessagesFactory, IDataTypeService dataTypeService, IUserService userService) : base(provider, repositoryFactory, logger, eventMessagesFactory) @@ -1295,11 +1297,11 @@ namespace Umbraco.Core.Services public Stream GetMediaFileContentStream(string filepath) { - if (MediaHelper.FileSystem.FileExists(filepath) == false) + if (_mediaFileSystem.FileExists(filepath) == false) return null; try { - return MediaHelper.FileSystem.OpenFile(filepath); + return _mediaFileSystem.OpenFile(filepath); } catch { @@ -1309,19 +1311,19 @@ namespace Umbraco.Core.Services public void SetMediaFileContent(string filepath, Stream stream) { - MediaHelper.FileSystem.AddFile(filepath, stream, true); + _mediaFileSystem.AddFile(filepath, stream, true); } public void DeleteMediaFile(string filepath) { - ImageHelper.DeleteFile(MediaHelper.FileSystem, filepath, true); + _mediaFileSystem.DeleteFile(filepath, true); } public void GenerateThumbnails(string filepath, PropertyType propertyType) { - using (var filestream = MediaHelper.FileSystem.OpenFile(filepath)) + using (var filestream = _mediaFileSystem.OpenFile(filepath)) { - ImageHelper.GenerateThumbnails(MediaHelper.FileSystem, filestream, filepath, propertyType); + _mediaFileSystem.GenerateThumbnails(filestream, filepath, propertyType); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 6715f89c29..c9b86c4c8e 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -146,6 +146,7 @@ + @@ -356,6 +357,7 @@ + @@ -371,7 +373,7 @@ - + @@ -581,7 +583,6 @@ - diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs index 94ce25cc98..540070df31 100644 --- a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -6,12 +6,18 @@ using System.Threading; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.IO; +using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.IO { [TestFixture] public class ShadowFileSystemTests { + // tested: + // only 1 instance of this class is created + // SetUp and TearDown run before/after each test + // SetUp does not start before the previous TearDown returns + [SetUp] public void SetUp() { @@ -28,20 +34,8 @@ namespace Umbraco.Tests.IO private static void ClearFiles() { - var path = IOHelper.MapPath("FileSysTests"); - if (Directory.Exists(path)) - { - foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) - File.Delete(file); - Directory.Delete(path, true); - } - path = IOHelper.MapPath("App_Data"); - if (Directory.Exists(path)) - { - foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) - File.Delete(file); - Directory.Delete(path, true); - } + TestHelper.DeleteDirectory(IOHelper.MapPath("FileSysTests")); + TestHelper.DeleteDirectory(IOHelper.MapPath("App_Data")); } private static string NormPath(string path) @@ -341,8 +335,7 @@ namespace Umbraco.Tests.IO Assert.IsTrue(File.Exists(path + "/ShadowSystem/path/to/some/dir/f1.txt")); - // kill everything and let the shadow fs die - Directory.Delete(path + "/ShadowSystem", true); + // let the shadow fs die } [Test] @@ -420,7 +413,7 @@ namespace Umbraco.Tests.IO Assert.AreEqual(1, Directory.GetDirectories(appdata + "/Shadow").Length); scope.Complete(); Assert.IsTrue(fs.FileExists("sub/f4.txt")); - Assert.AreEqual(0, Directory.GetDirectories(appdata + "/Shadow").Length); + TestHelper.TryAssert(() => Assert.AreEqual(0, Directory.GetDirectories(appdata + "/Shadow").Length)); scope.Dispose(); Assert.IsTrue(fs.FileExists("sub/f4.txt")); Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); @@ -477,7 +470,7 @@ namespace Umbraco.Tests.IO Assert.IsTrue(fs.FileExists("sub/f2.txt")); scope.Dispose(); Assert.IsTrue(fs.FileExists("sub/f2.txt")); - Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); + TestHelper.TryAssert(() => Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id))); string text; using (var s = fs.OpenFile("sub/f2.txt")) @@ -580,9 +573,8 @@ namespace Umbraco.Tests.IO File.WriteAllText(path + "/test/inner/f3.txt", "foo"); path = NormPath(path); - Directory.Delete(path, true); - - Assert.IsFalse(File.Exists(path + "/test/inner/f3.txt")); + TestHelper.Try(() => Directory.Delete(path, true)); + TestHelper.TryAssert(() => Assert.IsFalse(File.Exists(path + "/test/inner/f3.txt"))); } } } diff --git a/src/Umbraco.Tests/TestHelpers/TestHelper.cs b/src/Umbraco.Tests/TestHelpers/TestHelper.cs index 6c8fc9308d..de62bff271 100644 --- a/src/Umbraco.Tests/TestHelpers/TestHelper.cs +++ b/src/Umbraco.Tests/TestHelpers/TestHelper.cs @@ -6,6 +6,7 @@ using System.Configuration; using System.IO; using System.Linq; using System.Reflection; +using System.Threading; using NUnit.Framework; using SqlCE4Umbraco; using Umbraco.Core; @@ -203,6 +204,50 @@ namespace Umbraco.Tests.TestHelpers } } + public static void DeleteDirectory(string path) + { + Try(() => + { + if (Directory.Exists(path) == false) return; + foreach (var file in Directory.EnumerateFiles(path, "*", SearchOption.AllDirectories)) + File.Delete(file); + }); + Try(() => + { + if (Directory.Exists(path) == false) return; + Directory.Delete(path, true); + }); + } + + public static void TryAssert(Action action, int maxTries = 5, int waitMilliseconds = 200) + { + Try(action, maxTries, waitMilliseconds); + } + + public static void Try(Action action, int maxTries = 5, int waitMilliseconds = 200) + { + Try(action, maxTries, waitMilliseconds); + } + + public static void Try(Action action, int maxTries = 5, int waitMilliseconds = 200) + where T : Exception + { + var tries = 0; + while (true) + { + try + { + action(); + break; + } + catch (T) + { + if (tries++ > maxTries) + throw; + Thread.Sleep(waitMilliseconds); + } + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index eae620e745..fa84ff345f 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -104,11 +104,11 @@ namespace Umbraco.Web.Editors /// private HttpResponseMessage GetResized(string imagePath, int width, string sizeName) { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fs = FileSystemProviderManager.Current.MediaFileSystem; var ext = Path.GetExtension(imagePath); // we need to check if it is an image by extension - if (ImageHelper.IsImageFile(ext) == false) + if (fs.IsImageFile(ext) == false) return Request.CreateResponse(HttpStatusCode.NotFound); //redirect to ImageProcessor thumbnail with rnd generated from last modified time of original media file diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 616c814731..7cef6fc1cc 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -4,7 +4,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; -using Umbraco.Core.Media; +using Umbraco.Core.IO; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -14,6 +14,12 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.UploadFieldAlias, "File upload", "fileupload", Icon = "icon-download-alt", Group = "media")] public class FileUploadPropertyEditor : PropertyEditor, IApplicationEventHandler { + private static MediaFileSystem MediaFileSystem + { + // v8 will get rid of singletons + get { return FileSystemProviderManager.Current.MediaFileSystem; } + } + /// /// Creates the corresponding property value editor. /// @@ -22,7 +28,7 @@ namespace Umbraco.Web.PropertyEditors { var baseEditor = base.CreateValueEditor(); baseEditor.Validators.Add(new UploadFileTypeValidator()); - return new FileUploadPropertyValueEditor(baseEditor); + return new FileUploadPropertyValueEditor(baseEditor, MediaFileSystem); } /// @@ -55,11 +61,9 @@ namespace Umbraco.Web.PropertyEditors /// The properties that were deleted. static IEnumerable GetFilesToDelete(IEnumerable properties) { - var fs = MediaHelper.FileSystem; - return properties .Where(x => IsUploadField(x, true)) - .Select(x => fs.GetRelativePath((string) x.Value)) + .Select(x => MediaFileSystem.GetRelativePath((string) x.Value)) .ToList(); } @@ -75,12 +79,11 @@ namespace Umbraco.Web.PropertyEditors // copy files var isUpdated = false; - var fs = MediaHelper.FileSystem; foreach (var property in properties) { - var sourcePath = fs.GetRelativePath((string) property.Value); - var copyPath = MediaHelper.CopyFile(args.Copy, property.PropertyType, sourcePath); - args.Copy.SetValue(property.Alias, fs.GetUrl(copyPath)); + var sourcePath = MediaFileSystem.GetRelativePath((string) property.Value); + var copyPath = MediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath); + args.Copy.SetValue(property.Alias, MediaFileSystem.GetUrl(copyPath)); isUpdated = true; } @@ -128,18 +131,17 @@ namespace Umbraco.Web.PropertyEditors static void AutoFillProperties(IContentBase content) { var properties = content.Properties.Where(x => IsUploadField(x, false)); - var fs = MediaHelper.FileSystem; foreach (var property in properties) { - var autoFillConfig = UploadAutoFillProperties.GetConfig(property.Alias); + var autoFillConfig = MediaFileSystem.UploadAutoFillProperties.GetConfig(property.Alias); if (autoFillConfig == null) continue; var svalue = property.Value as string; if (string.IsNullOrWhiteSpace(svalue)) - UploadAutoFillProperties.Reset(content, autoFillConfig); + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); else - UploadAutoFillProperties.Populate(content, autoFillConfig, fs.GetRelativePath(svalue)); + MediaFileSystem.UploadAutoFillProperties.Populate(content, autoFillConfig, MediaFileSystem.GetRelativePath(svalue)); } } diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index 1bba10c7a3..777a14b768 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -1,21 +1,13 @@ using System; using System.Collections.Generic; using System.Drawing; -using System.Globalization; using System.IO; using System.Linq; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core.Configuration; using Umbraco.Core.IO; -using Umbraco.Core.Logging; -using Umbraco.Core.Media; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; -using umbraco; -using umbraco.cms.businesslogic.Files; -using Umbraco.Core; namespace Umbraco.Web.PropertyEditors { @@ -24,9 +16,13 @@ namespace Umbraco.Web.PropertyEditors /// internal class FileUploadPropertyValueEditor : PropertyValueEditorWrapper { - public FileUploadPropertyValueEditor(PropertyValueEditor wrapped) + private readonly MediaFileSystem _mediaFileSystem; + + public FileUploadPropertyValueEditor(PropertyValueEditor wrapped, MediaFileSystem mediaFileSystem) : base(wrapped) - { } + { + _mediaFileSystem = mediaFileSystem; + } /// /// Converts the value received from the editor into the value can be stored in the database. @@ -65,17 +61,16 @@ namespace Umbraco.Web.PropertyEditors return currentValue; // get the current file paths - var fs = MediaHelper.FileSystem; var currentPaths = currentValue.ToString() .Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries) - .Select(x => fs.GetRelativePath(x)) // get the fs-relative path + .Select(x => _mediaFileSystem.GetRelativePath(x)) // get the fs-relative path .ToArray(); // if clearing, remove these files and return if (clears) { foreach (var pathToRemove in currentPaths) - ImageHelper.DeleteFile(fs, pathToRemove, true); + _mediaFileSystem.DeleteFile(pathToRemove, true); return string.Empty; // no more files } @@ -108,19 +103,19 @@ namespace Umbraco.Web.PropertyEditors // get the filepath // in case we are using the old path scheme, try to re-use numbers (bah...) var reuse = i < currentPaths.Length ? currentPaths[i] : null; // this would be WRONG with many files - var filepath = MediaHelper.GetMediaPath(file.FileName, reuse, cuid, puid); // fs-relative path + var filepath = _mediaFileSystem.GetMediaPath(file.FileName, reuse, cuid, puid); // fs-relative path using (var filestream = File.OpenRead(file.TempFilePath)) { - fs.AddFile(filepath, filestream, true); // must overwrite! + _mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite! - var ext = fs.GetExtension(filepath); - if (ImageHelper.IsImageFile(ext)) + var ext = _mediaFileSystem.GetExtension(filepath); + if (_mediaFileSystem.IsImageFile(ext)) { var preValues = editorValue.PreValues.FormatAsDictionary(); var sizes = preValues.Any() ? preValues.First().Value.Value : string.Empty; using (var image = Image.FromStream(filestream)) - ImageHelper.GenerateThumbnails(fs, image, filepath, sizes); + _mediaFileSystem.GenerateThumbnails(image, filepath, sizes); } // all related properties (auto-fill) are managed by FileUploadPropertyEditor @@ -136,10 +131,10 @@ namespace Umbraco.Web.PropertyEditors // remove files that are not there anymore foreach (var pathToRemove in currentPaths.Except(newPaths)) - ImageHelper.DeleteFile(fs, pathToRemove, true); + _mediaFileSystem.DeleteFile(pathToRemove, true); - return string.Join(",", newPaths.Select(x => fs.GetUrl(x))); + return string.Join(",", newPaths.Select(x => _mediaFileSystem.GetUrl(x))); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs index 9b9974c705..a7669b0c9c 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.IO; using Umbraco.Core.Logging; using Umbraco.Core.Media; using Umbraco.Core.Models; @@ -36,6 +37,12 @@ namespace Umbraco.Web.PropertyEditors }; } + private static MediaFileSystem MediaFileSystem + { + // v8 will get rid of singletons + get { return FileSystemProviderManager.Current.MediaFileSystem; } + } + /// /// Creates the corresponding property value editor. /// @@ -43,7 +50,7 @@ namespace Umbraco.Web.PropertyEditors protected override PropertyValueEditor CreateValueEditor() { var baseEditor = base.CreateValueEditor(); - return new ImageCropperPropertyValueEditor(baseEditor); + return new ImageCropperPropertyValueEditor(baseEditor, MediaFileSystem); } /// @@ -100,14 +107,12 @@ namespace Umbraco.Web.PropertyEditors /// The properties that were deleted. static IEnumerable GetFilesToDelete(IEnumerable properties) { - var fs = MediaHelper.FileSystem; - return properties.Where(x => IsCropperField(x, true)).Select(x => { var jo = GetJObject((string) x.Value, true); if (jo == null || jo["src"] == null) return null; var src = jo["src"].Value(); - return string.IsNullOrWhiteSpace(src) ? null : fs.GetRelativePath(src); + return string.IsNullOrWhiteSpace(src) ? null : MediaFileSystem.GetRelativePath(src); }).WhereNotNull(); } @@ -123,7 +128,6 @@ namespace Umbraco.Web.PropertyEditors // copy files var isUpdated = false; - var fs = MediaHelper.FileSystem; foreach (var property in properties) { var jo = GetJObject((string) property.Value, true); @@ -132,9 +136,9 @@ namespace Umbraco.Web.PropertyEditors var src = jo["src"].Value(); if (string.IsNullOrWhiteSpace(src)) continue; - var sourcePath = fs.GetRelativePath(src); - var copyPath = MediaHelper.CopyFile(args.Copy, property.PropertyType, sourcePath); - jo["src"] = fs.GetUrl(copyPath); + var sourcePath = MediaFileSystem.GetRelativePath(src); + var copyPath = MediaFileSystem.CopyFile(args.Copy, property.PropertyType, sourcePath); + jo["src"] = MediaFileSystem.GetUrl(copyPath); args.Copy.SetValue(property.Alias, jo.ToString()); isUpdated = true; } @@ -183,17 +187,16 @@ namespace Umbraco.Web.PropertyEditors static void AutoFillProperties(IContentBase content) { var properties = content.Properties.Where(x => IsCropperField(x, false)); - var fs = MediaHelper.FileSystem; foreach (var property in properties) { - var autoFillConfig = UploadAutoFillProperties.GetConfig(property.Alias); + var autoFillConfig = MediaFileSystem.UploadAutoFillProperties.GetConfig(property.Alias); if (autoFillConfig == null) continue; var svalue = property.Value as string; if (string.IsNullOrWhiteSpace(svalue)) { - UploadAutoFillProperties.Reset(content, autoFillConfig); + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); continue; } @@ -218,9 +221,9 @@ namespace Umbraco.Web.PropertyEditors } if (src == null) - UploadAutoFillProperties.Reset(content, autoFillConfig); + MediaFileSystem.UploadAutoFillProperties.Reset(content, autoFillConfig); else - UploadAutoFillProperties.Populate(content, autoFillConfig, fs.GetRelativePath(src)); + MediaFileSystem.UploadAutoFillProperties.Populate(content, autoFillConfig, MediaFileSystem.GetRelativePath(src)); } } diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index f1f26f003b..f717711bac 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -28,8 +28,13 @@ namespace Umbraco.Web.PropertyEditors /// internal class ImageCropperPropertyValueEditor : PropertyValueEditorWrapper { - public ImageCropperPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) - { } + private MediaFileSystem _mediaFileSystem; + + public ImageCropperPropertyValueEditor(PropertyValueEditor wrapped, MediaFileSystem mediaFileSystem) + : base(wrapped) + { + _mediaFileSystem = mediaFileSystem; + } /// /// This is called to merge in the prevalue crops with the value that is saved - similar to the property value converter for the front-end @@ -65,8 +70,6 @@ namespace Umbraco.Web.PropertyEditors /// public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { - var fs = MediaHelper.FileSystem; - // get the current path var currentPath = string.Empty; try @@ -82,7 +85,7 @@ namespace Umbraco.Web.PropertyEditors LogHelper.WarnWithException("Could not parse current db value to a JObject.", ex); } if (string.IsNullOrWhiteSpace(currentPath) == false) - currentPath = fs.GetRelativePath(currentPath); + currentPath = _mediaFileSystem.GetRelativePath(currentPath); // get the new json and path JObject editorJson = null; @@ -122,7 +125,7 @@ namespace Umbraco.Web.PropertyEditors // value is unchanged. if (string.IsNullOrWhiteSpace(editorFile) && string.IsNullOrWhiteSpace(currentPath) == false) { - ImageHelper.DeleteFile(fs, currentPath, true); + _mediaFileSystem.DeleteFile(currentPath, true); return null; // clear } @@ -130,7 +133,7 @@ namespace Umbraco.Web.PropertyEditors } // process the file - var filepath = editorJson == null ? null : ProcessFile(editorValue, file, fs, currentPath, cuid, puid); + var filepath = editorJson == null ? null : ProcessFile(editorValue, file, currentPath, cuid, puid); // remove all temp files foreach (var f in files) @@ -138,15 +141,15 @@ namespace Umbraco.Web.PropertyEditors // remove current file if replaced if (currentPath != filepath && string.IsNullOrWhiteSpace(currentPath) == false) - ImageHelper.DeleteFile(fs, currentPath, true); + _mediaFileSystem.DeleteFile(currentPath, true); // update json and return if (editorJson == null) return null; - editorJson["src"] = filepath == null ? string.Empty : fs.GetUrl(filepath); + editorJson["src"] = filepath == null ? string.Empty : _mediaFileSystem.GetUrl(filepath); return editorJson.ToString(); } - private string ProcessFile(ContentPropertyData editorValue, ContentItemFile file, IFileSystem fs, string currentPath, Guid cuid, Guid puid) + private string ProcessFile(ContentPropertyData editorValue, ContentItemFile file, string currentPath, Guid cuid, Guid puid) { // process the file // no file, invalid file, reject change @@ -155,19 +158,19 @@ namespace Umbraco.Web.PropertyEditors // get the filepath // in case we are using the old path scheme, try to re-use numbers (bah...) - var filepath = MediaHelper.GetMediaPath(file.FileName, currentPath, cuid, puid); // fs-relative path + var filepath = _mediaFileSystem.GetMediaPath(file.FileName, currentPath, cuid, puid); // fs-relative path using (var filestream = File.OpenRead(file.TempFilePath)) { - fs.AddFile(filepath, filestream, true); // must overwrite! + _mediaFileSystem.AddFile(filepath, filestream, true); // must overwrite! - var ext = fs.GetExtension(filepath); - if (ImageHelper.IsImageFile(ext)) + var ext = _mediaFileSystem.GetExtension(filepath); + if (_mediaFileSystem.IsImageFile(ext)) { var preValues = editorValue.PreValues.FormatAsDictionary(); var sizes = preValues.Any() ? preValues.First().Value.Value : string.Empty; using (var image = Image.FromStream(filestream)) - ImageHelper.GenerateThumbnails(fs, image, filepath, sizes); + _mediaFileSystem.GenerateThumbnails(image, filepath, sizes); } // all related properties (auto-fill) are managed by ImageCropperPropertyEditor @@ -194,6 +197,4 @@ namespace Umbraco.Web.PropertyEditors return newVal; } } - - } diff --git a/src/umbraco.cms/businesslogic/Content.cs b/src/umbraco.cms/businesslogic/Content.cs index e7b9d7fe66..c55204fe8e 100644 --- a/src/umbraco.cms/businesslogic/Content.cs +++ b/src/umbraco.cms/businesslogic/Content.cs @@ -632,7 +632,7 @@ namespace umbraco.cms.businesslogic } else { - ImageHelper.DeleteFile(fs, relativeFilePath, true); + fs.DeleteFile(relativeFilePath, true); } } } diff --git a/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs b/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs index b80b2892a4..5eecd607b5 100644 --- a/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs +++ b/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs @@ -73,7 +73,7 @@ namespace umbraco.cms.businesslogic.datatype int subfolderId; var numberedFolder = int.TryParse(subfolder, out subfolderId) ? subfolderId.ToString(CultureInfo.InvariantCulture) - : MediaHelper.GetNextFolder(); + : fs.GetNextFolder(); var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? Path.Combine(numberedFolder, name)