diff --git a/src/Umbraco.Core/IO/FileSystemExtensions.cs b/src/Umbraco.Core/IO/FileSystemExtensions.cs index 64dcfc25a0..b71a4cf0c7 100644 --- a/src/Umbraco.Core/IO/FileSystemExtensions.cs +++ b/src/Umbraco.Core/IO/FileSystemExtensions.cs @@ -39,13 +39,19 @@ namespace Umbraco.Core.IO public static long GetSize(this IFileSystem fs, string path) { + // no idea why GetSize is not part of IFileSystem, but + // for physical file system we have way better & faster ways + // to get the size, than to read the entire thing in memory! + var physical = fs as PhysicalFileSystem; + if (physical != null) + return physical.GetSize(path); + + // other filesystems... bah... using (var file = fs.OpenFile(path)) + using (var sr = new StreamReader(file)) { - using (var sr = new StreamReader(file)) - { - var str = sr.ReadToEnd(); - return str.Length; - } + var str = sr.ReadToEnd(); + return str.Length; } } diff --git a/src/Umbraco.Core/IO/FileSystemWrapper.cs b/src/Umbraco.Core/IO/FileSystemWrapper.cs index ba2ad8f48b..676626d069 100644 --- a/src/Umbraco.Core/IO/FileSystemWrapper.cs +++ b/src/Umbraco.Core/IO/FileSystemWrapper.cs @@ -48,9 +48,9 @@ namespace Umbraco.Core.IO _wrapped.AddFile(path, stream); } - public void AddFile(string path, Stream stream, bool overrideIfExists) + public void AddFile(string path, Stream stream, bool overrideExisting) { - _wrapped.AddFile(path, stream, overrideIfExists); + _wrapped.AddFile(path, stream, overrideExisting); } public IEnumerable GetFiles(string path) diff --git a/src/Umbraco.Core/IO/IFileSystem.cs b/src/Umbraco.Core/IO/IFileSystem.cs index 3b38432c5c..b4e173b256 100644 --- a/src/Umbraco.Core/IO/IFileSystem.cs +++ b/src/Umbraco.Core/IO/IFileSystem.cs @@ -19,7 +19,7 @@ namespace Umbraco.Core.IO void AddFile(string path, Stream stream); - void AddFile(string path, Stream stream, bool overrideIfExists); + void AddFile(string path, Stream stream, bool overrideExisting); IEnumerable GetFiles(string path); @@ -31,7 +31,6 @@ namespace Umbraco.Core.IO bool FileExists(string path); - string GetRelativePath(string fullPathOrUrl); string GetFullPath(string path); diff --git a/src/Umbraco.Core/IO/MediaFileSystem.cs b/src/Umbraco.Core/IO/MediaFileSystem.cs index b35264d752..face234970 100644 --- a/src/Umbraco.Core/IO/MediaFileSystem.cs +++ b/src/Umbraco.Core/IO/MediaFileSystem.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -25,24 +26,31 @@ namespace Umbraco.Core.IO _contentConfig = contentConfig; } - public string GetRelativePath(int propertyId, string fileName) + [Obsolete("This low-level method should NOT exist.")] + public string GetRelativePath(int propertyId, string fileName) { - var seperator = _contentConfig.UploadAllowDirectories + var sep = _contentConfig.UploadAllowDirectories ? Path.DirectorySeparatorChar : '-'; - return propertyId.ToString(CultureInfo.InvariantCulture) + seperator + fileName; + return propertyId.ToString(CultureInfo.InvariantCulture) + sep + fileName; } + [Obsolete("This low-level method should NOT exist.")] public string GetRelativePath(string subfolder, string fileName) { - var seperator = _contentConfig.UploadAllowDirectories + var sep = _contentConfig.UploadAllowDirectories ? Path.DirectorySeparatorChar : '-'; - return subfolder + seperator + fileName; + return subfolder + sep + fileName; } + // what's below is weird + // we are not deleting custom thumbnails + // MediaFileSystem is not just IFileSystem + // etc + public IEnumerable GetThumbnails(string path) { var parentDirectory = Path.GetDirectoryName(path); @@ -68,5 +76,16 @@ namespace Umbraco.Core.IO GetThumbnails(path) .ForEach(DeleteFile); } + + 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); + } + } } } diff --git a/src/Umbraco.Core/IO/PhysicalFileSystem.cs b/src/Umbraco.Core/IO/PhysicalFileSystem.cs index 47daff932d..7456fb29a4 100644 --- a/src/Umbraco.Core/IO/PhysicalFileSystem.cs +++ b/src/Umbraco.Core/IO/PhysicalFileSystem.cs @@ -12,6 +12,9 @@ namespace Umbraco.Core.IO // eg "c:" or "c:\path\to\site" or "\\server\path" private readonly string _rootPath; + // _rootPath, with separators replaced by forward-slashes. + private readonly string _rootPathFwd; + // the ??? url, using url separator chars, NOT ending with a separator // eg "" (?) or "/Scripts" or ??? private readonly string _rootUrl; @@ -25,6 +28,7 @@ namespace Umbraco.Core.IO _rootPath = IOHelper.MapPath(virtualRoot); _rootPath = EnsureDirectorySeparatorChar(_rootPath); _rootPath = _rootPath.TrimEnd(Path.DirectorySeparatorChar); + _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); _rootUrl = IOHelper.ResolveUrl(virtualRoot); _rootUrl = EnsureUrlSeparatorChar(_rootUrl); @@ -48,17 +52,22 @@ namespace Umbraco.Core.IO //var localRoot = AppDomain.CurrentDomain.BaseDirectory; var localRoot = IOHelper.GetRootDirectorySafe(); if (Path.IsPathRooted(rootPath) == false) - { rootPath = Path.Combine(localRoot, rootPath); - } rootPath = EnsureDirectorySeparatorChar(rootPath); - rootUrl = EnsureUrlSeparatorChar(rootUrl); - _rootPath = rootPath.TrimEnd(Path.DirectorySeparatorChar); + _rootPathFwd = EnsureUrlSeparatorChar(_rootPath); + + rootUrl = EnsureUrlSeparatorChar(rootUrl); _rootUrl = rootUrl.TrimEnd('/'); } + /// + /// Gets directories in a directory. + /// + /// The filesystem-relative path to the directory. + /// The filesystem-relative path to the directories in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetDirectories(string path) { var fullPath = GetFullPath(path); @@ -80,11 +89,20 @@ namespace Umbraco.Core.IO return Enumerable.Empty(); } + /// + /// Deletes a directory. + /// + /// The filesystem-relative path of the directory. public void DeleteDirectory(string path) { DeleteDirectory(path, false); } + /// + /// Deletes a directory. + /// + /// The filesystem-relative path of the directory. + /// A value indicating whether to recursively delete sub-directories. public void DeleteDirectory(string path, bool recursive) { var fullPath = GetFullPath(path); @@ -101,38 +119,71 @@ namespace Umbraco.Core.IO } } + /// + /// Gets a value indicating whether a directory exists. + /// + /// The filesystem-relative path of the directory. + /// A value indicating whether a directory exists. public bool DirectoryExists(string path) { var fullPath = GetFullPath(path); return Directory.Exists(fullPath); } + /// + /// Saves a file. + /// + /// The filesystem-relative path of the file. + /// A stream containing the file data. + /// Overrides the existing file, if any. public void AddFile(string path, Stream stream) { AddFile(path, stream, true); } - public void AddFile(string path, Stream stream, bool overrideIfExists) + /// + /// Saves a file. + /// + /// The filesystem-relative path of the file. + /// A stream containing the file data. + /// A value indicating whether to override the existing file, if any. + /// If a file exists and is false, an exception is thrown. + public void AddFile(string path, Stream stream, bool overrideExisting) { var fullPath = GetFullPath(path); var exists = File.Exists(fullPath); - if (exists && overrideIfExists == false) + if (exists && overrideExisting == false) throw new InvalidOperationException(string.Format("A file at path '{0}' already exists", path)); - Directory.CreateDirectory(Path.GetDirectoryName(fullPath)); // ensure it exists + var directory = Path.GetDirectoryName(fullPath); + if (directory == null) throw new InvalidOperationException("Could not get directory."); + Directory.CreateDirectory(directory); // ensure it exists - if (stream.CanSeek) + if (stream.CanSeek) // fixme - what else? stream.Seek(0, 0); - using (var destination = (Stream)File.Create(fullPath)) + using (var destination = (Stream) File.Create(fullPath)) stream.CopyTo(destination); } + /// + /// Gets files in a directory. + /// + /// The filesystem-relative path of the directory. + /// The filesystem-relative path to the files in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetFiles(string path) { return GetFiles(path, "*.*"); } + /// + /// Gets files in a directory. + /// + /// The filesystem-relative path of the directory. + /// A filter. + /// The filesystem-relative path to the matching files in the directory. + /// Filesystem-relative paths use forward-slashes as directory separators. public IEnumerable GetFiles(string path, string filter) { var fullPath = GetFullPath(path); @@ -154,12 +205,21 @@ namespace Umbraco.Core.IO return Enumerable.Empty(); } + /// + /// Opens a file. + /// + /// The filesystem-relative path to the file. + /// public Stream OpenFile(string path) { var fullPath = GetFullPath(path); return File.OpenRead(fullPath); } + /// + /// Deletes a file. + /// + /// The filesystem-relative path to the file. public void DeleteFile(string path) { var fullPath = GetFullPath(path); @@ -176,23 +236,25 @@ namespace Umbraco.Core.IO } } + /// + /// Gets a value indicating whether a file exists. + /// + /// The filesystem-relative path to the file. + /// A value indicating whether the file exists. public bool FileExists(string path) { var fullpath = GetFullPath(path); return File.Exists(fullpath); } - // beware, many things depend on how the GetRelative/AbsolutePath methods work! - /// - /// Gets the relative path. + /// Gets the filesystem-relative path of a full path or of an url. /// /// The full path or url. /// The path, relative to this filesystem's root. /// /// The relative path is relative to this filesystem's root, not starting with any - /// directory separator. If input was recognized as a url (path), then output uses url (path) separator - /// chars. + /// directory separator. All separators are forward-slashes. /// public string GetRelativePath(string fullPathOrUrl) { @@ -203,25 +265,22 @@ namespace Umbraco.Core.IO return path.Substring(_rootUrl.Length) // strip it .TrimStart('/'); // it's relative - // test path - path = EnsureDirectorySeparatorChar(fullPathOrUrl); + if (IOHelper.PathStartsWith(path, _rootPathFwd, '/')) // if it starts with the root url... + return path.Substring(_rootPathFwd.Length) // strip it + .TrimStart('/'); // it's relative - if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) // if it starts with the root path - return path.Substring(_rootPath.Length) // strip it - .TrimStart(Path.DirectorySeparatorChar); // it's relative - - // unchanged - including separators - return fullPathOrUrl; + // unchanged - what else? + return path; } /// /// Gets the full path. /// - /// The full or relative path. + /// The full or filesystem-relative path. /// The full path. /// /// On the physical filesystem, the full path is the rooted (ie non-relative), safe (ie within this - /// filesystem's root) path. All separators are converted to Path.DirectorySeparatorChar. + /// filesystem's root) path. All separators are Path.DirectorySeparatorChar. /// public string GetFullPath(string path) { @@ -229,49 +288,82 @@ namespace Umbraco.Core.IO var opath = path; path = EnsureDirectorySeparatorChar(path); + // fixme - this part should go! // not sure what we are doing here - so if input starts with a (back) slash, // we assume it's not a FS relative path and we try to convert it... but it // really makes little sense? if (path.StartsWith(Path.DirectorySeparatorChar.ToString())) path = GetRelativePath(path); - // if already a full path, return - if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) - return path; + // if not already rooted, combine with the root path + if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar) == false) + path = Path.Combine(_rootPath, path); - // else combine and sanitize, ie GetFullPath will take care of any relative + // sanitize - GetFullPath will take care of any relative // segments in path, eg '../../foo.tmp' - it may throw a SecurityException // if the combined path reaches illegal parts of the filesystem - var fpath = Path.Combine(_rootPath, path); - fpath = Path.GetFullPath(fpath); + path = Path.GetFullPath(path); // at that point, path is within legal parts of the filesystem, ie we have // permissions to reach that path, but it may nevertheless be outside of // our root path, due to relative segments, so better check - if (IOHelper.PathStartsWith(fpath, _rootPath, Path.DirectorySeparatorChar)) - return fpath; + if (IOHelper.PathStartsWith(path, _rootPath, Path.DirectorySeparatorChar)) + return path; + // nothing prevents us to reach the file, security-wise, yet it is outside + // this filesystem's root - throw throw new FileSecurityException("File '" + opath + "' is outside this filesystem's root."); } + /// + /// Gets the url. + /// + /// The filesystem-relative path. + /// The url. + /// All separators are forward-slashes. public string GetUrl(string path) { path = EnsureUrlSeparatorChar(path).Trim('/'); return _rootUrl + "/" + path; } + /// + /// Gets the last-modified date of a directory or file. + /// + /// The filesystem-relative path to the directory or the file. + /// The last modified date of the directory or the file. public DateTimeOffset GetLastModified(string path) { - return DirectoryExists(path) - ? new DirectoryInfo(GetFullPath(path)).LastWriteTimeUtc - : new FileInfo(GetFullPath(path)).LastWriteTimeUtc; + var fullpath = GetFullPath(path); + return DirectoryExists(fullpath) + ? new DirectoryInfo(fullpath).LastWriteTimeUtc + : new FileInfo(fullpath).LastWriteTimeUtc; } + /// + /// Gets the created date of a directory or file. + /// + /// The filesystem-relative path to the directory or the file. + /// The created date of the directory or the file. public DateTimeOffset GetCreated(string path) { - return DirectoryExists(path) - ? Directory.GetCreationTimeUtc(GetFullPath(path)) - : File.GetCreationTimeUtc(GetFullPath(path)); + var fullpath = GetFullPath(path); + return DirectoryExists(fullpath) + ? Directory.GetCreationTimeUtc(fullpath) + : File.GetCreationTimeUtc(fullpath); + } + + /// + /// Gets the size of a file. + /// + /// The filesystem-relative path to the file. + /// The file of the size, in bytes. + /// If the file does not exist, returns -1. + public long GetSize(string path) + { + var fullPath = GetFullPath(path); + var file = new FileInfo(fullPath); + return file.Exists ? file.Length : -1; } #region Helper Methods diff --git a/src/Umbraco.Core/IO/ResizedImage.cs b/src/Umbraco.Core/IO/ResizedImage.cs deleted file mode 100644 index 6586699ecf..0000000000 --- a/src/Umbraco.Core/IO/ResizedImage.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Umbraco.Core.IO -{ - internal class ResizedImage - { - public ResizedImage() - { - } - - public ResizedImage(int width, int height, string fileName) - { - Width = width; - Height = height; - FileName = fileName; - } - - public int Width { get; set; } - public int Height { get; set; } - public string FileName { get; set; } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/UmbracoMediaFile.cs b/src/Umbraco.Core/IO/UmbracoMediaFile.cs index b8fe310f54..17162bcfa8 100644 --- a/src/Umbraco.Core/IO/UmbracoMediaFile.cs +++ b/src/Umbraco.Core/IO/UmbracoMediaFile.cs @@ -1,11 +1,6 @@ using System; -using System.Collections.Generic; using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; using System.IO; -using System.Linq; -using System.Threading.Tasks; using System.Web; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; @@ -87,14 +82,13 @@ namespace Umbraco.Core.IO { Filename = _fs.GetFileName(Path); Extension = _fs.GetExtension(Path) != null - ? _fs.GetExtension(Path).Substring(1).ToLowerInvariant() + ? _fs.GetExtension(Path).TrimStart('.').ToLowerInvariant() : ""; Url = _fs.GetUrl(Path); + Exists = _fs.FileExists(Path); if (Exists == false) - { LogHelper.Warn("The media file doesn't exist: " + Path); - } } public bool Exists { get; private set; } @@ -117,27 +111,15 @@ namespace Umbraco.Core.IO { get { - if (_length == null) - { - if (Exists) - { - _length = _fs.GetSize(Path); - } - else - { - _length = -1; - } - } + if (_length != null) return _length.Value; + _length = Exists ? _fs.GetSize(Path) : -1; return _length.Value; } } public bool SupportsResizing { - get - { - return UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(Extension); - } + get { return ImageHelper.IsImageFile(Extension); } } public string GetFriendlyName() @@ -147,67 +129,49 @@ namespace Umbraco.Core.IO public Size GetDimensions() { - if (_size == null) - { - if (_fs.FileExists(Path)) - { - EnsureFileSupportsResizing(); + if (_size != null) return _size.Value; - using (var fs = _fs.OpenFile(Path)) - { - _size = ImageHelper.GetDimensions(fs); - } - } - else + if (_fs.FileExists(Path)) + { + EnsureFileSupportsResizing(); + + using (var fs = _fs.OpenFile(Path)) { - _size = new Size(-1, -1); + _size = ImageHelper.GetDimensions(fs); } } + else + { + _size = new Size(-1, -1); + } + return _size.Value; } public string Resize(int width, int height) { - if (Exists) - { - EnsureFileSupportsResizing(); + if (Exists == false) return string.Empty; - var fileNameThumb = DoResize(width, height, -1, string.Empty); - - return _fs.GetUrl(fileNameThumb); - } - return string.Empty; + EnsureFileSupportsResizing(); + var filepath = Resize(width, height, -1, string.Empty); + return _fs.GetUrl(filepath); } - public string Resize(int maxWidthHeight, string fileNameAddition) + public string Resize(int maxWidthHeight, string filenameAddition) { - if (Exists) - { - EnsureFileSupportsResizing(); + if (Exists == false) return string.Empty; - var fileNameThumb = DoResize(-1, -1, maxWidthHeight, fileNameAddition); - - return _fs.GetUrl(fileNameThumb); - } - return string.Empty; + EnsureFileSupportsResizing(); + var filepath = Resize(-1, -1, maxWidthHeight, filenameAddition); + return _fs.GetUrl(filepath); } - private string DoResize(int width, int height, int maxWidthHeight, string fileNameAddition) + private string Resize(int width, int height, int maxWidthHeight, string sizeName) { - using (var fs = _fs.OpenFile(Path)) + using (var filestream = _fs.OpenFile(Path)) + using (var image = Image.FromStream(filestream)) { - using (var image = Image.FromStream(fs)) - { - var fileNameThumb = string.IsNullOrWhiteSpace(fileNameAddition) - ? string.Format("{0}_UMBRACOSYSTHUMBNAIL.jpg", Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal))) - : string.Format("{0}_{1}.jpg", Path.Substring(0, Path.LastIndexOf(".", StringComparison.Ordinal)), fileNameAddition); - - var thumbnail = maxWidthHeight == -1 - ? ImageHelper.GenerateThumbnail(image, width, height, fileNameThumb, Extension, _fs) - : ImageHelper.GenerateThumbnail(image, maxWidthHeight, fileNameThumb, Extension, _fs); - - return thumbnail.FileName; - } + return ImageHelper.GenerateResized(_fs, image, Path, sizeName, maxWidthHeight, width, height).Filepath; } } @@ -216,7 +180,5 @@ namespace Umbraco.Core.IO if (SupportsResizing == false) throw new InvalidOperationException(string.Format("The file {0} is not an image, so can't get dimensions", Filename)); } - - } } diff --git a/src/Umbraco.Core/Media/ImageHelper.cs b/src/Umbraco.Core/Media/ImageHelper.cs index 5842bb67bb..2b62c37645 100644 --- a/src/Umbraco.Core/Media/ImageHelper.cs +++ b/src/Umbraco.Core/Media/ImageHelper.cs @@ -3,36 +3,52 @@ 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 Umbraco.Core.Configuration; using Umbraco.Core.IO; using Umbraco.Core.Media.Exif; +using Umbraco.Core.Models; namespace Umbraco.Core.Media { /// - /// A helper class used for imaging + /// Provides helper methods for managing images. /// internal static class ImageHelper { + private static readonly Dictionary DefaultSizes = new Dictionary + { + { 100, "thumb" }, + { 500, "big-thumb" } + }; + /// - /// Gets the dimensions of an image based on a stream + /// Gets a value indicating whether the file extension corresponds to an image. /// - /// - /// - /// - /// First try with EXIF, this is because it is insanely faster and doesn't use any memory to read exif data than to load in the entire - /// image via GDI. Otherwise loading an image into GDI consumes a crazy amount of memory on large images. - /// - /// Of course EXIF data might not exist in every file and can only exist in JPGs - /// - public static Size GetDimensions(Stream imageStream) + /// 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(imageStream); + var jpgInfo = ImageFile.FromStream(stream); if (jpgInfo.Format != ImageFileFormat.Unknown && jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension) @@ -52,16 +68,20 @@ namespace Umbraco.Core.Media } //we have no choice but to try to read in via GDI - using (var image = Image.FromStream(imageStream)) + 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; @@ -69,172 +89,226 @@ namespace Umbraco.Core.Media return codec.MimeType; } - /// - /// Creates the thumbnails if the image is larger than all of the specified ones. - /// - /// - /// - /// - /// - /// - /// - internal static IEnumerable GenerateMediaThumbnails( + #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, - string fileName, - string extension, - Image originalImage, - IEnumerable additionalThumbSizes) + Stream filestream, + string filepath, + PropertyType propertyType) { - - var result = new List(); - - var allSizesDictionary = new Dictionary {{100,"thumb"}, {500,"big-thumb"}}; - - //combine the static dictionary with the additional sizes with only unique values - var allSizes = allSizesDictionary.Select(kv => kv.Key) - .Union(additionalThumbSizes.Where(x => x > 0).Distinct()); - - var sizesDictionary = allSizes.ToDictionary(s => s, s => allSizesDictionary.ContainsKey(s) ? allSizesDictionary[s]: ""); - - foreach (var s in sizesDictionary) + // 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)) { - var size = s.Key; - var name = s.Value; - if (originalImage.Width >= size && originalImage.Height >= size) + 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 + ".jpg"; + + 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(); + var encoder = extension == "png" || extension == "gif" + ? encoders.Single(t => t.MimeType.Equals("image/png")) + : encoders.Single(t => t.MimeType.Equals("image/jpeg")); + + // 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()) { - result.Add(Resize(fs, fileName, extension, size, name, originalImage)); + bitmap.Save(stream, encoder, encoderParams); + stream.Seek(0, 0); + if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL")) + { + var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString()); + 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); } - - return result; } - /// - /// Performs an image resize - /// - /// - /// - /// - /// - /// - /// - /// - private static ResizedImage Resize(IFileSystem fileSystem, string path, string extension, int maxWidthHeight, string fileNameAddition, Image originalImage) + #endregion + + #region Inner classes + + public class ResizedImage { - var fileNameThumb = String.IsNullOrEmpty(fileNameAddition) - ? string.Format("{0}_UMBRACOSYSTHUMBNAIL.jpg", path.Substring(0, path.LastIndexOf("."))) - : string.Format("{0}_{1}.jpg", path.Substring(0, path.LastIndexOf(".")), fileNameAddition); + public ResizedImage() + { } - var thumb = GenerateThumbnail( - originalImage, - maxWidthHeight, - fileNameThumb, - extension, - fileSystem); - - return thumb; - } - - internal static ResizedImage GenerateThumbnail(Image image, int maxWidthHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - return GenerateThumbnail(image, maxWidthHeight, -1, -1, thumbnailFileName, extension, fs); - } - - internal static ResizedImage GenerateThumbnail(Image image, int fixedWidth, int fixedHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - return GenerateThumbnail(image, -1, fixedWidth, fixedHeight, thumbnailFileName, extension, fs); - } - - private static ResizedImage GenerateThumbnail(Image image, int maxWidthHeight, int fixedWidth, int fixedHeight, string thumbnailFileName, string extension, IFileSystem fs) - { - // Generate thumbnail - float f = 1; - if (maxWidthHeight >= 0) + public ResizedImage(string filepath, int width, int height) { - var fx = (float)image.Size.Width / maxWidthHeight; - var fy = (float)image.Size.Height / maxWidthHeight; - - // must fit in thumbnail size - f = Math.Max(fx, fy); + Filepath = filepath; + Width = width; + Height = height; } - //depending on if we are doing fixed width resizing or not. - fixedWidth = (maxWidthHeight > 0) ? image.Width : fixedWidth; - fixedHeight = (maxWidthHeight > 0) ? image.Height : fixedHeight; - - var widthTh = (int)Math.Round(fixedWidth / f); - var heightTh = (int)Math.Round(fixedHeight / f); - - // fixes for empty width or height - if (widthTh == 0) - widthTh = 1; - if (heightTh == 0) - heightTh = 1; - - // Create new image with best quality settings - using (var bp = new Bitmap(widthTh, heightTh)) - { - using (var g = Graphics.FromImage(bp)) - { - //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'll detect how big the image is and use - // the mid quality interpolation mode when the image size exceeds our max limit. - - if (image.Width > 5000 || image.Height > 5000) - { - //use mid quality - g.InterpolationMode = InterpolationMode.Bilinear; - } - else - { - //use best quality - g.InterpolationMode = InterpolationMode.HighQualityBicubic; - } - - - g.SmoothingMode = SmoothingMode.HighQuality; - g.PixelOffsetMode = PixelOffsetMode.HighQuality; - g.CompositingQuality = CompositingQuality.HighQuality; - - // Copy the old image to the new and resized - var rect = new Rectangle(0, 0, widthTh, heightTh); - g.DrawImage(image, rect, 0, 0, image.Width, image.Height, GraphicsUnit.Pixel); - - // Copy metadata - var imageEncoders = ImageCodecInfo.GetImageEncoders(); - - var codec = extension.ToLower() == "png" || extension.ToLower() == "gif" - ? imageEncoders.Single(t => t.MimeType.Equals("image/png")) - : imageEncoders.Single(t => t.MimeType.Equals("image/jpeg")); - - // Set compresion ratio to 90% - var ep = new EncoderParameters(); - ep.Param[0] = new EncoderParameter(Encoder.Quality, 90L); - - // Save the new image using the dimensions of the image - var predictableThumbnailName = thumbnailFileName.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToString(CultureInfo.InvariantCulture)); - using (var ms = new MemoryStream()) - { - bp.Save(ms, codec, ep); - ms.Seek(0, 0); - - fs.AddFile(predictableThumbnailName, ms); - } - - // TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385 - var newFileName = thumbnailFileName.Replace("UMBRACOSYSTHUMBNAIL", string.Format("{0}x{1}", widthTh, heightTh)); - using (var ms = new MemoryStream()) - { - bp.Save(ms, codec, ep); - ms.Seek(0, 0); - - fs.AddFile(newFileName, ms); - } - - return new ResizedImage(widthTh, heightTh, newFileName); - } - } + 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 new file mode 100644 index 0000000000..55bbd80d3b --- /dev/null +++ b/src/Umbraco.Core/Media/MediaHelper.cs @@ -0,0 +1,286 @@ +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. + 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. + 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. + /// + /// + internal static string GetNextFolder() + { + lock (FolderCounterLock) + { + if (_folderCounterInitialized == false) + { + // fixme - seed was not respected in MediaSubfolderCounter? + _folderCounter = 1000; // seed + 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)) + fs.DeleteFile(oldpath, true); + + // sanity check - fixme - every entity should be created with a proper Guid + if (content.Key == Guid.Empty) content.Key = Guid.NewGuid(); + + // 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) + { + FileSystem.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 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; + + // sanity check - fixme - every entity should be created with a proper Guid + if (content.Key == Guid.Empty) content.Key = Guid.NewGuid(); + + // get the filepath + var filename = Path.GetFileName(sourcepath); + var filepath = GetMediaPath(filename, content.Key, propertyType.Key); + fs.CopyFile(sourcepath, filepath); + fs.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 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, null, 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/MediaSubfolderCounter.cs b/src/Umbraco.Core/Media/MediaSubfolderCounter.cs deleted file mode 100644 index 6f0142cacf..0000000000 --- a/src/Umbraco.Core/Media/MediaSubfolderCounter.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using Umbraco.Core.IO; - -namespace Umbraco.Core.Media -{ - /// - /// Internal singleton to handle the numbering of subfolders within the Media-folder. - /// When this class is initiated it will look for numbered subfolders and select the highest number, - /// which will be the start point for the naming of the next subfolders. If no subfolders exists - /// then the starting point will be 1000, ie. /media/1000/koala.jpg - /// - internal class MediaSubfolderCounter - { - #region Singleton - - private long _numberedFolder = 1000;//Default starting point - private static readonly ReaderWriterLockSlim ClearLock = new ReaderWriterLockSlim(); - private static readonly Lazy Lazy = new Lazy(() => new MediaSubfolderCounter()); - - public static MediaSubfolderCounter Current { get { return Lazy.Value; } } - - private MediaSubfolderCounter() - { - var folders = new List(); - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - var directories = fs.GetDirectories(""); - foreach (var directory in directories) - { - long dirNum; - if (long.TryParse(directory, out dirNum)) - { - folders.Add(dirNum); - } - } - var last = folders.OrderBy(x => x).LastOrDefault(); - if(last != default(long)) - _numberedFolder = last; - } - - #endregion - - /// - /// Returns an increment of the numbered media subfolders. - /// - /// A value - public long Increment() - { - using (new ReadLock(ClearLock)) - { - _numberedFolder = _numberedFolder + 1; - return _numberedFolder; - } - } - } -} \ No newline at end of file diff --git a/src/Umbraco.Core/Media/UploadAutoFillProperties.cs b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs new file mode 100644 index 0000000000..c7157c4eb6 --- /dev/null +++ b/src/Umbraco.Core/Media/UploadAutoFillProperties.cs @@ -0,0 +1,200 @@ +using System; +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.Models; + +namespace Umbraco.Core.Media +{ + /// + /// Provides extension methods to manage auto-fill properties for upload fields. + /// + internal static class UploadAutoFillProperties + { + /// + /// 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) + { + var autoFillConfigs = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties; + return autoFillConfigs == null ? null : autoFillConfigs.FirstOrDefault(x => x.Alias == propertyTypeAlias); + } + + /// + /// Resets the auto-fill properties of a content item, for a specified property alias. + /// + /// The content item. + /// The property type alias. + public static void Reset(IContentBase content, string propertyTypeAlias) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // reset + Reset(content, autoFillConfig); + } + + /// + /// Resets the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// The auto-fill configuration. + public static void Reset(IContentBase content, IImagingAutoFillUploadField autoFillConfig) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + ResetProperties(content, autoFillConfig); + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// 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) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); + + // no property = nothing to do + if (content.Properties.Contains(propertyTypeAlias) == false) return; + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // populate + Populate(content, autoFillConfig, filepath); + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// The property type alias. + /// 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) + { + if (content == null) throw new ArgumentNullException("content"); + if (propertyTypeAlias == null) throw new ArgumentNullException("propertyTypeAlias"); + + // no property = nothing to do + if (content.Properties.Contains(propertyTypeAlias) == false) return; + + // get the config, no config = nothing to do + var autoFillConfig = GetConfig(propertyTypeAlias); + if (autoFillConfig == null) return; // nothing + + // populate + Populate(content, autoFillConfig, filepath, filestream, image); + } + + /// + /// Populates the auto-fill properties of a content item, for a specified auto-fill configuration. + /// + /// The content item. + /// 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) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace()) + { + ResetProperties(content, autoFillConfig); + } + else + { + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + using (var filestream = fs.OpenFile(filepath)) + { + var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); + var size = ImageHelper.IsImageFile(extension) ? (Size?) ImageHelper.GetDimensions(filestream) : null; + SetProperties(content, autoFillConfig, size, filestream.Length, extension); + } + } + } + + /// + /// Populates the auto-fill properties of a content item. + /// + /// The content item. + /// + /// 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) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + // no file = reset, file = auto-fill + if (filepath.IsNullOrWhiteSpace() || filestream == null) + { + ResetProperties(content, autoFillConfig); + } + else + { + var extension = (Path.GetExtension(filepath) ?? "").TrimStart('.'); + Size? size; + if (image == null) + size = ImageHelper.IsImageFile(extension) ? (Size?) ImageHelper.GetDimensions(filestream) : null; + else + size = new Size(image.Width, image.Height); + SetProperties(content, autoFillConfig, size, filestream.Length, extension); + } + } + + private static void SetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig, Size? size, long length, string extension) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + content.Properties[autoFillConfig.WidthFieldAlias].Value = size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty; + + if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + content.Properties[autoFillConfig.HeightFieldAlias].Value = size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty; + + if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + content.Properties[autoFillConfig.LengthFieldAlias].Value = length; + + if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + content.Properties[autoFillConfig.ExtensionFieldAlias].Value = extension; +} + + private static void ResetProperties(IContentBase content, IImagingAutoFillUploadField autoFillConfig) + { + if (content == null) throw new ArgumentNullException("content"); + if (autoFillConfig == null) throw new ArgumentNullException("autoFillConfig"); + + if (content.Properties.Contains(autoFillConfig.WidthFieldAlias)) + content.Properties[autoFillConfig.WidthFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.HeightFieldAlias)) + content.Properties[autoFillConfig.HeightFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.LengthFieldAlias)) + content.Properties[autoFillConfig.LengthFieldAlias].Value = string.Empty; + + if (content.Properties.Contains(autoFillConfig.ExtensionFieldAlias)) + content.Properties[autoFillConfig.ExtensionFieldAlias].Value = string.Empty; + } + } +} diff --git a/src/Umbraco.Core/Models/ContentExtensions.cs b/src/Umbraco.Core/Models/ContentExtensions.cs index e91996e32a..863f6b197a 100644 --- a/src/Umbraco.Core/Models/ContentExtensions.cs +++ b/src/Umbraco.Core/Models/ContentExtensions.cs @@ -1,25 +1,15 @@ 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.Web; -using System.Xml; using System.Xml.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.IO; using Umbraco.Core.Media; using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Models.Membership; -using Umbraco.Core.Strings; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.UnitOfWork; using Umbraco.Core.Services; namespace Umbraco.Core.Models @@ -329,7 +319,7 @@ namespace Umbraco.Core.Models { if (property.Value is string) { - var value = (string)property.Value; + var value = (string) property.Value; property.Value = value.ToValidXmlString(); } } @@ -444,6 +434,19 @@ namespace Umbraco.Core.Models } } + public static IContentTypeComposition GetContentType(this IContentBase contentBase) + { + if (contentBase == null) throw new ArgumentNullException("contentBase"); + + var content = contentBase as IContent; + if (content != null) return content.ContentType; + var media = contentBase as IMedia; + if (media != null) return media.ContentType; + var member = contentBase as IMember; + if (member != null) return member.ContentType; + throw new NotSupportedException("Unsupported IContentBase implementation: " + contentBase.GetType().FullName + "."); + } + #region SetValue for setting file contents /// @@ -454,20 +457,24 @@ namespace Umbraco.Core.Models /// The containing the file that will be uploaded public static void SetValue(this IContentBase content, string propertyTypeAlias, HttpPostedFileBase value) { - // Ensure we get the filename without the path in IE in intranet mode + // ensure we get the filename without the path in IE in intranet mode // http://stackoverflow.com/questions/382464/httppostedfile-filename-different-from-ie - var fileName = value.FileName; - if (fileName.LastIndexOf(@"\") > 0) - fileName = fileName.Substring(fileName.LastIndexOf(@"\") + 1); + var filename = value.FileName; + var pos = filename.LastIndexOf(@"\", StringComparison.InvariantCulture); + if (pos > 0) + filename = filename.Substring(pos + 1); - var name = - IOHelper.SafeFileName( - fileName.Substring(fileName.LastIndexOf(IOHelper.DirSepChar) + 1, - fileName.Length - fileName.LastIndexOf(IOHelper.DirSepChar) - 1) - .ToLower()); + // strip any directory info + pos = filename.LastIndexOf(IOHelper.DirSepChar); + if (pos > 0) + filename = filename.Substring(pos + 1); - if (string.IsNullOrEmpty(name) == false) - SetFileOnContent(content, propertyTypeAlias, name, value.InputStream); + // get a safe filename - should this be done by MediaHelper? + filename = IOHelper.SafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) return; + filename = filename.ToLower(); // fixme - er... why? + + MediaHelper.SetUploadFile(content, propertyTypeAlias, filename, value.InputStream); } /// @@ -494,104 +501,33 @@ namespace Umbraco.Core.Models } /// - /// Sets and uploads the file from a as the property value + /// Sets a content item property value with a file coming from a stream. /// - /// to add property value to - /// Alias of the property to save the value on - /// Name of the file - /// to save to disk - public static void SetValue(this IContentBase content, string propertyTypeAlias, string fileName, Stream fileStream) + /// The content item. + /// The property type alias. + /// Name of the file + /// The stream containing the file data. + /// This really is for FileUpload fields only, and should be obsoleted. For anything else, + /// you need to store the file by yourself using and then figure out + /// how to deal with auto-fill properties (if any) and thumbnails (if any) by yourself. + public static void SetValue(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream) { - var name = IOHelper.SafeFileName(fileName); + if (filename == null || filestream == null) return; - if (string.IsNullOrEmpty(name) == false && fileStream != null) - SetFileOnContent(content, propertyTypeAlias, name, fileStream); + // get a safe filename - should this be done by MediaHelper? + filename = IOHelper.SafeFileName(filename); + if (string.IsNullOrWhiteSpace(filename)) return; + filename = filename.ToLower(); // fixme - er... why? + + MediaHelper.SetUploadFile(content, propertyTypeAlias, filename, filestream); } - private static void SetFileOnContent(IContentBase content, string propertyTypeAlias, string filename, Stream fileStream) + public static string StoreFile(this IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string oldpath) { - var property = content.Properties.FirstOrDefault(x => x.Alias == propertyTypeAlias); - if (property == null) - return; - - //TODO: ALl of this naming logic needs to be put into the ImageHelper and then we need to change FileUploadPropertyValueEditor to do the same! - - var numberedFolder = MediaSubfolderCounter.Current.Increment(); - var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(numberedFolder.ToString(CultureInfo.InvariantCulture), filename) - : numberedFolder + "-" + filename; - - var extension = Path.GetExtension(filename).Substring(1).ToLowerInvariant(); - - //the file size is the length of the stream in bytes - var fileSize = fileStream.Length; - - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - fs.AddFile(fileName, fileStream); - - //Check if file supports resizing and create thumbnails - var supportsResizing = UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(extension); - - //the config section used to auto-fill properties - IImagingAutoFillUploadField uploadFieldConfigNode = null; - - //Check for auto fill of additional properties - if (UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties != null) - { - uploadFieldConfigNode = UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == propertyTypeAlias); - - } - - if (supportsResizing) - { - //get the original image from the original stream - if (fileStream.CanSeek) fileStream.Seek(0, 0); - using (var originalImage = Image.FromStream(fileStream)) - { - var additionalSizes = new List(); - - //Look up Prevalues for this upload datatype - if it is an upload datatype - get additional configured sizes - if (property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) - { - //Get Prevalues by the DataType's Id: property.PropertyType.DataTypeId - var values = ApplicationContext.Current.Services.DataTypeService.GetPreValuesByDataTypeId(property.PropertyType.DataTypeDefinitionId); - var thumbnailSizes = values.FirstOrDefault(); - //Additional thumbnails configured as prevalues on the DataType - if (thumbnailSizes != null) - { - foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) - { - int thumbSize; - if (thumb != "" && int.TryParse(thumb, out thumbSize)) - { - additionalSizes.Add(thumbSize); - } - } - } - } - - ImageHelper.GenerateMediaThumbnails(fs, fileName, extension, originalImage, additionalSizes); - - //while the image is still open, we'll check if we need to auto-populate the image properties - if (uploadFieldConfigNode != null) - { - content.SetValue(uploadFieldConfigNode.WidthFieldAlias, originalImage.Width.ToString(CultureInfo.InvariantCulture)); - content.SetValue(uploadFieldConfigNode.HeightFieldAlias, originalImage.Height.ToString(CultureInfo.InvariantCulture)); - } - - } - } - - //if auto-fill is true, then fill the remaining, non-image properties - if (uploadFieldConfigNode != null) - { - content.SetValue(uploadFieldConfigNode.LengthFieldAlias, fileSize.ToString(CultureInfo.InvariantCulture)); - content.SetValue(uploadFieldConfigNode.ExtensionFieldAlias, extension); - } - - //Set the value of the property to that of the uploaded file's url - property.Value = fs.GetUrl(fileName); + 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, oldpath); } #endregion diff --git a/src/Umbraco.Core/Models/File.cs b/src/Umbraco.Core/Models/File.cs index 8ead6da5f8..da093f792a 100644 --- a/src/Umbraco.Core/Models/File.cs +++ b/src/Umbraco.Core/Models/File.cs @@ -25,6 +25,21 @@ namespace Umbraco.Core.Models private string _content; internal Func GetFileContent { get; set; } + // whether to use whatever already exists on filesystem + internal bool _useExistingContent; + + /// + /// Indicates that the file should use whatever content already + /// exists on the filesystem which manages the file, bypassing content + /// management entirely. + /// + /// Only for the next save. Is resetted when the file is saved. + public void UseExistingContent() + { + _useExistingContent = true; + _content = null; // force content to be loaded + } + protected File(string path, Func getFileContent = null) { _path = SanitizePath(path); diff --git a/src/Umbraco.Core/Models/IFile.cs b/src/Umbraco.Core/Models/IFile.cs index de900c50ec..0d1cf24cdd 100644 --- a/src/Umbraco.Core/Models/IFile.cs +++ b/src/Umbraco.Core/Models/IFile.cs @@ -39,6 +39,14 @@ namespace Umbraco.Core.Models /// string Content { get; set; } + /// + /// Indicates that the file should use whatever content already + /// exists on the filesystem which manages the file, bypassing content + /// management entirely. + /// + /// Only for the next save. Is resetted when the file is saved. + void UseExistingContent(); + /// /// Gets or sets the file's virtual path (i.e. the file path relative to the root of the website) /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs index ad6939f847..147e0b40f8 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/ITemplateRepository.cs @@ -59,8 +59,15 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Gets the content of a template as a stream. /// - /// The filesystem path to the template. + /// The filesystem path to the template. /// The content of the template. - Stream GetFileStream(string path); + Stream GetFileStream(string filepath); + + /// + /// Sets the content of a template. + /// + /// The filesystem path to the template. + /// The content of the template. + void SetFile(string filepath, Stream content); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs index a2f7c841fc..da18a16598 100644 --- a/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/TemplateRepository.cs @@ -202,29 +202,7 @@ namespace Umbraco.Core.Persistence.Repositories template.Path = nodeDto.Path; //now do the file work - - if (DetermineTemplateRenderingEngine(entity) == RenderingEngine.Mvc) - { - var result = _viewHelper.CreateView(template, true); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } - else - { - var result = _masterPageHelper.CreateMasterPage(template, this, true); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } + SaveFile(template, dto); template.ResetDirtyProperties(); @@ -274,29 +252,7 @@ namespace Umbraco.Core.Persistence.Repositories template.IsMasterTemplate = axisDefs.Any(x => x.ParentId == dto.NodeId); //now do the file work - - if (DetermineTemplateRenderingEngine(entity) == RenderingEngine.Mvc) - { - var result = _viewHelper.UpdateViewFile(entity, originalAlias); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } - else - { - var result = _masterPageHelper.UpdateMasterPageFile(entity, originalAlias, this); - if (result != entity.Content) - { - entity.Content = result; - //re-persist it... though we don't really care about the templates in the db do we??!! - dto.Design = result; - Database.Update(dto); - } - } + SaveFile((Template) entity, dto, originalAlias); entity.ResetDirtyProperties(); @@ -305,6 +261,38 @@ namespace Umbraco.Core.Persistence.Repositories template.GetFileContent = file => GetFileContent((Template) file, false); } + private void SaveFile(Template template, TemplateDto dto, string originalAlias = null) + { + string content; + + if (template._useExistingContent) + { + content = _viewHelper.GetFileContents(template); // BUT the template does not exist yet?! + template._useExistingContent = false; // reset + } + else + { + if (DetermineTemplateRenderingEngine(template) == RenderingEngine.Mvc) + { + content = originalAlias == null + ? _viewHelper.CreateView(template, true) + : _viewHelper.UpdateViewFile(template, originalAlias); + } + else + { + content = originalAlias == null + ? _masterPageHelper.CreateMasterPage(template, this, true) + : _masterPageHelper.UpdateMasterPageFile(template, originalAlias, this); + } + } + + template.Content = content; + + if (dto.Design == content) return; + dto.Design = content; + Database.Update(dto); // though... we don't care about the db value really??!! + } + protected override void PersistDeletedItem(ITemplate entity) { var deletes = GetDeleteClauses().ToArray(); @@ -475,9 +463,19 @@ namespace Umbraco.Core.Persistence.Repositories } } - public Stream GetFileStream(string filename) + public Stream GetFileStream(string filepath) { - var ext = Path.GetExtension(filename); + return GetFileSystem(filepath).OpenFile(filepath); + } + + public void SetFile(string filepath, Stream content) + { + GetFileSystem(filepath).AddFile(filepath, content, true); + } + + private IFileSystem GetFileSystem(string filepath) + { + var ext = Path.GetExtension(filepath); IFileSystem fs; switch (ext) { @@ -491,7 +489,7 @@ namespace Umbraco.Core.Persistence.Repositories default: throw new Exception("Unsupported extension " + ext + "."); } - return fs.OpenFile(filename); + return fs; } #region Implementation of ITemplateRepository diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index 17ba21802e..5c2aaff930 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -515,11 +515,19 @@ namespace Umbraco.Core.Services } } - public Stream GetTemplateFileStream(string path) + public Stream GetTemplateFileStream(string filepath) { using (var repository = _repositoryFactory.CreateTemplateRepository(_dataUowProvider.GetUnitOfWork())) { - return repository.GetFileStream(path); + return repository.GetFileStream(filepath); + } + } + + public void SetTemplateFile(string filepath, Stream content) + { + using (var repository = _repositoryFactory.CreateTemplateRepository(_dataUowProvider.GetUnitOfWork())) + { + repository.SetFile(filepath, content); } } diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 45aa8d03dc..ef608850c0 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -229,8 +229,15 @@ namespace Umbraco.Core.Services /// /// Gets the content of a template as a stream. /// - /// The filesystem path to the template. + /// The filesystem path to the template. /// The content of the template. - Stream GetTemplateFileStream(string path); + Stream GetTemplateFileStream(string filepath); + + /// + /// Sets the content of a template. + /// + /// The filesystem path to the template. + /// The content of the template. + void SetTemplateFile(string filepath, Stream content); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IMediaService.cs b/src/Umbraco.Core/Services/IMediaService.cs index 863c5728b3..40f8c86ced 100644 --- a/src/Umbraco.Core/Services/IMediaService.cs +++ b/src/Umbraco.Core/Services/IMediaService.cs @@ -374,8 +374,29 @@ namespace Umbraco.Core.Services /// /// Gets the content of a media as a stream. /// - /// The filesystem path to the media. + /// The filesystem path to the media. /// The content of the media. - Stream GetMediaFileStream(string path); + Stream GetMediaFileStream(string filepath); + + /// + /// Sets the content of a media. + /// + /// The filesystem path to the media. + /// The content of the media. + void SetMediaFile(string filepath, Stream content); + + /// + /// Deletes a media file and all thumbnails. + /// + /// The filesystem path to the media. + void DeleteMediaFile(string filepath); + + /// + /// Generates thumbnails. + /// + /// The filesystem-relative path to the original image. + /// The property type. + /// This should be obsoleted, we should not generate thumbnails. + void GenerateThumbnails(string filepath, PropertyType propertyType); } } diff --git a/src/Umbraco.Core/Services/MediaService.cs b/src/Umbraco.Core/Services/MediaService.cs index 431eb52c8d..19bf01f991 100644 --- a/src/Umbraco.Core/Services/MediaService.cs +++ b/src/Umbraco.Core/Services/MediaService.cs @@ -12,6 +12,7 @@ using Umbraco.Core.Configuration; using Umbraco.Core.Events; using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.Models.Rdbms; using Umbraco.Core.Persistence; @@ -1258,10 +1259,27 @@ namespace Umbraco.Core.Services } } - public Stream GetMediaFileStream(string path) + public Stream GetMediaFileStream(string filepath) { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - return fs.OpenFile(path); + return MediaHelper.FileSystem.OpenFile(filepath); + } + + public void SetMediaFile(string filepath, Stream stream) + { + MediaHelper.FileSystem.AddFile(filepath, stream, true); + } + + public void DeleteMediaFile(string filepath) + { + MediaHelper.FileSystem.DeleteFile(filepath, true); + } + + public void GenerateThumbnails(string filepath, PropertyType propertyType) + { + using (var filestream = MediaHelper.FileSystem.OpenFile(filepath)) + { + ImageHelper.GenerateThumbnails(MediaHelper.FileSystem, filestream, filepath, propertyType); + } } #region Event Handlers diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 997f57b0ca..776a090884 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -356,6 +356,8 @@ + + @@ -482,7 +484,6 @@ - @@ -531,7 +532,6 @@ - diff --git a/src/Umbraco.Web/Editors/ContentControllerBase.cs b/src/Umbraco.Web/Editors/ContentControllerBase.cs index bf9f2056b3..ec9763b678 100644 --- a/src/Umbraco.Web/Editors/ContentControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentControllerBase.cs @@ -100,6 +100,12 @@ namespace Umbraco.Web.Editors if (files.Any()) { d.Add("files", files); + // add extra things needed to figure out where to put the files + // fixme - every entity should have a Guid when created - would that be breaking? + if (contentItem.PersistedContent.Key == Guid.Empty) + contentItem.PersistedContent.Key = Guid.NewGuid(); + d.Add("cuid", contentItem.PersistedContent.Key); + d.Add("puid", dboProperty.PropertyType.Key); } var data = new ContentPropertyData(p.Value, p.PreValues, d); diff --git a/src/Umbraco.Web/Editors/ImagesController.cs b/src/Umbraco.Web/Editors/ImagesController.cs index 39960317b1..ce55d16e95 100644 --- a/src/Umbraco.Web/Editors/ImagesController.cs +++ b/src/Umbraco.Web/Editors/ImagesController.cs @@ -33,17 +33,13 @@ namespace Umbraco.Web.Editors { var media = Services.MediaService.GetById(mediaId); if (media == null) - { return Request.CreateResponse(HttpStatusCode.NotFound); - } + var imageProp = media.Properties[Constants.Conventions.Media.File]; if (imageProp == null) - { return Request.CreateResponse(HttpStatusCode.NotFound); - } var imagePath = imageProp.Value.ToString(); - return GetBigThumbnail(imagePath); } @@ -57,10 +53,9 @@ namespace Umbraco.Web.Editors /// public HttpResponseMessage GetBigThumbnail(string originalImagePath) { - if (string.IsNullOrWhiteSpace(originalImagePath)) - return Request.CreateResponse(HttpStatusCode.OK); - - return GetResized(originalImagePath, 500, "big-thumb"); + return string.IsNullOrWhiteSpace(originalImagePath) + ? Request.CreateResponse(HttpStatusCode.OK) + : GetResized(originalImagePath, 500, "big-thumb"); } /// @@ -76,17 +71,13 @@ namespace Umbraco.Web.Editors { var media = Services.MediaService.GetById(mediaId); if (media == null) - { return new HttpResponseMessage(HttpStatusCode.NotFound); - } + var imageProp = media.Properties[Constants.Conventions.Media.File]; if (imageProp == null) - { return new HttpResponseMessage(HttpStatusCode.NotFound); - } var imagePath = imageProp.Value.ToString(); - return GetResized(imagePath, width); } @@ -111,63 +102,43 @@ namespace Umbraco.Web.Editors /// /// /// - /// + /// /// - private HttpResponseMessage GetResized(string imagePath, int width, string suffix) + private HttpResponseMessage GetResized(string imagePath, int width, string sizeName) { - var mediaFileSystem = FileSystemProviderManager.Current.GetFileSystemProvider(); + var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); var ext = Path.GetExtension(imagePath); - //we need to check if it is an image by extension - if (UmbracoConfig.For.UmbracoSettings().Content.ImageFileTypes.InvariantContains(ext.TrimStart('.')) == false) - { + // we need to check if it is an image by extension + if (ImageHelper.IsImageFile(ext) == false) return Request.CreateResponse(HttpStatusCode.NotFound); - } - var thumbFilePath = imagePath.TrimEnd(ext) + "_" + suffix + ".jpg"; - var fullOrgPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(imagePath)); - var fullNewPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(thumbFilePath)); - var thumbIsNew = mediaFileSystem.FileExists(fullNewPath) == false; - if (thumbIsNew) + var resizedPath = imagePath.TrimEnd(ext) + "_" + sizeName + ".jpg"; + var generate = fs.FileExists(resizedPath) == false; + if (generate) { - //we need to generate it - if (mediaFileSystem.FileExists(fullOrgPath) == false) - { + // we need to generate it - if we have a source + if (fs.FileExists(imagePath) == false) return Request.CreateResponse(HttpStatusCode.NotFound); - } - using (var fileStream = mediaFileSystem.OpenFile(fullOrgPath)) + using (var fileStream = fs.OpenFile(imagePath)) + using (var originalImage = Image.FromStream(fileStream)) { - if (fileStream.CanSeek) fileStream.Seek(0, 0); - using (var originalImage = Image.FromStream(fileStream)) - { - //If it is bigger, then do the resize - if (originalImage.Width >= width && originalImage.Height >= width) - { - ImageHelper.GenerateThumbnail( - originalImage, - width, - fullNewPath, - "jpg", - mediaFileSystem); - } - else - { - //just return the original image - fullNewPath = fullOrgPath; - } - - } + // if original image is bigger than requested size, then resize, else return original image + if (originalImage.Width >= width && originalImage.Height >= width) + ImageHelper.GenerateResizedAt(fs, originalImage, resizedPath, width); + else + resizedPath = imagePath; } } var result = Request.CreateResponse(HttpStatusCode.OK); //NOTE: That we are not closing this stream as the framework will do that for us, if we try it will // fail. See http://stackoverflow.com/questions/9541351/returning-binary-file-from-controller-in-asp-net-web-api - var stream = mediaFileSystem.OpenFile(fullNewPath); + var stream = fs.OpenFile(resizedPath); if (stream.CanSeek) stream.Seek(0, 0); result.Content = new StreamContent(stream); - result.Headers.Date = mediaFileSystem.GetLastModified(imagePath); + result.Headers.Date = fs.GetLastModified(imagePath); result.Content.Headers.ContentType = new MediaTypeHeaderValue("image/jpeg"); return result; } diff --git a/src/Umbraco.Web/MediaPropertyExtensions.cs b/src/Umbraco.Web/MediaPropertyExtensions.cs deleted file mode 100644 index cc6d018174..0000000000 --- a/src/Umbraco.Web/MediaPropertyExtensions.cs +++ /dev/null @@ -1,106 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Drawing; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; -using Umbraco.Core.Models; - -namespace Umbraco.Web -{ - internal static class MediaPropertyExtensions - { - - internal static void AutoPopulateFileMetaDataProperties(this IContentBase model, string propertyAlias, string relativefilePath = null) - { - var mediaFileSystem = FileSystemProviderManager.Current.GetFileSystemProvider(); - var uploadFieldConfigNode = - UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == propertyAlias); - - if (uploadFieldConfigNode != null && model.Properties.Contains(propertyAlias)) - { - if (relativefilePath == null) - relativefilePath = model.GetValue(propertyAlias); - - //now we need to check if there is a path - if (!string.IsNullOrEmpty(relativefilePath)) - { - var fullPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(relativefilePath)); - var umbracoFile = new UmbracoMediaFile(fullPath); - FillProperties(uploadFieldConfigNode, model, umbracoFile); - } - else - { - //for now I'm just resetting this - ResetProperties(uploadFieldConfigNode, model); - } - } - } - - internal static void ResetFileMetaDataProperties(this IContentBase content, IImagingAutoFillUploadField uploadFieldConfigNode) - { - if (uploadFieldConfigNode == null) throw new ArgumentNullException("uploadFieldConfigNode"); - ResetProperties(uploadFieldConfigNode, content); - } - - private static void ResetProperties(IImagingAutoFillUploadField uploadFieldConfigNode, IContentBase content) - { - if (content.Properties.Contains(uploadFieldConfigNode.WidthFieldAlias)) - content.Properties[uploadFieldConfigNode.WidthFieldAlias].Value = string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.HeightFieldAlias)) - content.Properties[uploadFieldConfigNode.HeightFieldAlias].Value = string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.LengthFieldAlias)) - content.Properties[uploadFieldConfigNode.LengthFieldAlias].Value = string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.ExtensionFieldAlias)) - content.Properties[uploadFieldConfigNode.ExtensionFieldAlias].Value = string.Empty; - } - - - internal static void PopulateFileMetaDataProperties(this IContentBase content, IImagingAutoFillUploadField uploadFieldConfigNode, string relativeFilePath) - { - if (uploadFieldConfigNode == null) throw new ArgumentNullException("uploadFieldConfigNode"); - if (relativeFilePath.IsNullOrWhiteSpace() == false) - { - var mediaFileSystem = FileSystemProviderManager.Current.GetFileSystemProvider(); - var fullPath = mediaFileSystem.GetFullPath(mediaFileSystem.GetRelativePath(relativeFilePath)); - var umbracoFile = new UmbracoMediaFile(fullPath); - FillProperties(uploadFieldConfigNode, content, umbracoFile); - } - else - { - //for now I'm just resetting this since we cant detect a file - ResetProperties(uploadFieldConfigNode, content); - } - } - - private static void FillProperties(IImagingAutoFillUploadField uploadFieldConfigNode, IContentBase content, UmbracoMediaFile um) - { - if (uploadFieldConfigNode == null) throw new ArgumentNullException("uploadFieldConfigNode"); - if (content == null) throw new ArgumentNullException("content"); - if (um == null) throw new ArgumentNullException("um"); - var size = um.SupportsResizing ? (Size?)um.GetDimensions() : null; - - if (content.Properties.Contains(uploadFieldConfigNode.WidthFieldAlias)) - content.Properties[uploadFieldConfigNode.WidthFieldAlias].Value = size.HasValue ? size.Value.Width.ToInvariantString() : string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.HeightFieldAlias)) - content.Properties[uploadFieldConfigNode.HeightFieldAlias].Value = size.HasValue ? size.Value.Height.ToInvariantString() : string.Empty; - - if (content.Properties.Contains(uploadFieldConfigNode.LengthFieldAlias)) - content.Properties[uploadFieldConfigNode.LengthFieldAlias].Value = um.Length; - - if (content.Properties.Contains(uploadFieldConfigNode.ExtensionFieldAlias)) - content.Properties[uploadFieldConfigNode.ExtensionFieldAlias].Value = um.Extension; - } - - - } -} diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs index 72beb09e69..bf1086513a 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyEditor.cs @@ -2,18 +2,11 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Drawing; -using System.Globalization; +using System.IO; using System.Linq; -using System.Text.RegularExpressions; -using System.Xml; -using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using umbraco.cms.businesslogic.Files; using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.Configuration.UmbracoSettings; -using Umbraco.Core.IO; -using Umbraco.Core.Logging; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -23,35 +16,55 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.UploadFieldAlias, "File upload", "fileupload", Icon = "icon-download-alt", Group = "media")] public class FileUploadPropertyEditor : PropertyEditor { - /// - /// We're going to bind to the MediaService Saving event so that we can populate the umbracoFile size, type, etc... label fields - /// if we find any attached to the current media item. - /// - /// - /// I think this kind of logic belongs on this property editor, I guess it could exist elsewhere but it all has to do with the upload field. - /// + // The FileUploadPropertyEditor properties own files and as such must manage these files, + // so we are binding to events in order to make sure that + // - files are deleted when the owning content/media is + // - files are copied when the owning content is + // - populate the auto-fill properties when the owning content/media is saved + // + // NOTE: + // although some code fragments seem to want to support uploading multiple files, + // this is NOT a feature of the FileUploadPropertyEditor and is NOT supported + // + // auto-fill properties are recalculated EVERYTIME the content/media is saved, + // even if the property has NOT been modified (it could be the same filename but + // a different file) - this is accepted (auto-fill props should die) + // + // FIXME + // for some weird backward compatibility reasons, + // - media copy is not supported + // - auto-fill properties are not supported for content items + // - auto-fill runs on MediaService.Created which makes no sense (no properties yet) + static FileUploadPropertyEditor() { + MediaService.Created += MediaServiceCreated; // see above - makes no sense MediaService.Saving += MediaServiceSaving; - MediaService.Created += MediaServiceCreating; - ContentService.Copied += ContentServiceCopied; + //MediaService.Copied += MediaServiceCopied; // see above - missing - MediaService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - MediaService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - ContentService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - ContentService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - MemberService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); + ContentService.Copied += ContentServiceCopied; + //ContentService.Saving += ContentServiceSaving; // see above - missing + + MediaService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + + MediaService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange( + GetFilesToDelete(args.AllPropertyData.SelectMany(x => x.Value))); + + ContentService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + + ContentService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange( + GetFilesToDelete(args.AllPropertyData.SelectMany(x => x.Value))); + + MemberService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); } /// - /// Creates our custom value editor + /// Creates the corresponding property value editor. /// - /// + /// The corresponding property value editor. protected override PropertyValueEditor CreateValueEditor() { var baseEditor = base.CreateValueEditor(); @@ -59,123 +72,121 @@ namespace Umbraco.Web.PropertyEditors return new FileUploadPropertyValueEditor(baseEditor); } + /// + /// Creates the corresponding preValue editor. + /// + /// The corresponding preValue editor. protected override PreValueEditor CreatePreValueEditor() { return new FileUploadPreValueEditor(); } /// - /// Ensures any files associated are removed + /// Gets a value indicating whether a property is an upload field. /// - /// - static IEnumerable ServiceEmptiedRecycleBin(Dictionary> allPropertyData) + /// The property. + /// A value indicating whether to check that the property has a non-empty value. + /// A value indicating whether a property is an upload field, and (optionaly) has a non-empty value. + private static bool IsUploadField(Property property, bool ensureValue) { - var list = new List(); - //Get all values for any image croppers found - foreach (var uploadVal in allPropertyData - .SelectMany(x => x.Value) - .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) - .Select(x => x.Value) - .WhereNotNull()) - { - if (uploadVal.ToString().IsNullOrWhiteSpace() == false) - { - list.Add(uploadVal.ToString()); - } - } - return list; + if (property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias) + return false; + if (ensureValue == false) + return true; + return property.Value is string && string.IsNullOrWhiteSpace((string) property.Value) == false; } /// - /// Ensures any files associated are removed + /// Gets the files that need to be deleted when entities are deleted. /// - /// - static IEnumerable ServiceDeleted(IEnumerable deletedEntities) + /// The properties that were deleted. + static IEnumerable GetFilesToDelete(IEnumerable properties) { - var list = new List(); - foreach (var property in deletedEntities.SelectMany(deletedEntity => deletedEntity - .Properties - .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias - && x.Value != null - && string.IsNullOrEmpty(x.Value.ToString()) == false))) - { - if (property.Value != null && property.Value.ToString().IsNullOrWhiteSpace() == false) - { - list.Add(property.Value.ToString()); - } - } - return list; + var fs = MediaHelper.FileSystem; + + return properties + .Where(x => IsUploadField(x, true)) + .Select(x => fs.GetRelativePath((string) x.Value)) + .ToList(); } /// - /// After the content is copied we need to check if there are files that also need to be copied + /// After a content has been copied, also copy uploaded files. /// - /// - /// - static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs e) + /// The event sender. + /// The event arguments. + static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs args) { - if (e.Original.Properties.Any(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)) + // get the upload field properties with a value + var properties = args.Original.Properties.Where(x => IsUploadField(x, true)); + + // copy files + var isUpdated = false; + var fs = MediaHelper.FileSystem; + foreach (var property in properties) { - bool isUpdated = false; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - //Loop through properties to check if the content contains media that should be deleted - foreach (var property in e.Original.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias - && x.Value != null - && string.IsNullOrEmpty(x.Value.ToString()) == false)) - { - if (fs.FileExists(fs.GetRelativePath(property.Value.ToString()))) - { - var currentPath = fs.GetRelativePath(property.Value.ToString()); - var propertyId = e.Copy.Properties.First(x => x.Alias == property.Alias).Id; - var newPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(currentPath)); - - fs.CopyFile(currentPath, newPath); - e.Copy.SetValue(property.Alias, fs.GetUrl(newPath)); - - //Copy thumbnails - foreach (var thumbPath in fs.GetThumbnails(currentPath)) - { - var newThumbPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(thumbPath)); - fs.CopyFile(thumbPath, newThumbPath); - } - isUpdated = true; - } - } - - if (isUpdated) - { - //need to re-save the copy with the updated path value - sender.Save(e.Copy); - } + var sourcePath = fs.GetRelativePath((string) property.Value); + var copyPath = MediaHelper.CopyFile(args.Copy, property.PropertyType, sourcePath); + args.Copy.SetValue(property.Alias, fs.GetUrl(copyPath)); + isUpdated = true; } + + // if updated, re-save the copy with the updated value + if (isUpdated) + sender.Save(args.Copy); } - static void MediaServiceCreating(IMediaService sender, Core.Events.NewEventArgs e) + /// + /// After a media has been created, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void MediaServiceCreated(IMediaService sender, Core.Events.NewEventArgs args) { - AutoFillProperties(e.Entity); + AutoFillProperties(args.Entity); } - static void MediaServiceSaving(IMediaService sender, Core.Events.SaveEventArgs e) + /// + /// After a media has been saved, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void MediaServiceSaving(IMediaService sender, Core.Events.SaveEventArgs args) { - foreach (var m in e.SavedEntities) + foreach (var entity in args.SavedEntities) + AutoFillProperties(entity); + } + + /// + /// After a content item has been saved, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void ContentServiceSaving(IContentService sender, Core.Events.SaveEventArgs args) + { + foreach (var entity in args.SavedEntities) + AutoFillProperties(entity); + } + + /// + /// Auto-fill properties (or clear). + /// + /// The content. + static void AutoFillProperties(IContentBase content) + { + var properties = content.Properties.Where(x => IsUploadField(x, false)); + var fs = MediaHelper.FileSystem; + + foreach (var property in properties) { - AutoFillProperties(m); - } - } + var autoFillConfig = UploadAutoFillProperties.GetConfig(property.Alias); + if (autoFillConfig == null) continue; - static void AutoFillProperties(IContentBase model) - { - foreach (var p in model.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)) - { - var uploadFieldConfigNode = - UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == p.Alias); - - if (uploadFieldConfigNode != null) - { - model.PopulateFileMetaDataProperties(uploadFieldConfigNode, p.Value == null ? string.Empty : p.Value.ToString()); - } + var svalue = property.Value as string; + if (string.IsNullOrWhiteSpace(svalue)) + UploadAutoFillProperties.Reset(content, autoFillConfig); + else + UploadAutoFillProperties.Populate(content, autoFillConfig, fs.GetRelativePath(svalue)); } } @@ -185,7 +196,6 @@ namespace Umbraco.Web.PropertyEditors internal class FileUploadPreValueEditor : ValueListPreValueEditor { public FileUploadPreValueEditor() - : base() { var field = Fields.First(); field.Description = "Enter a max width/height for each thumbnail"; @@ -210,14 +220,12 @@ namespace Umbraco.Web.PropertyEditors { //there should only be one val var delimited = dictionary.First().Value.Value.Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - for (var index = 0; index < delimited.Length; index++) - { - result.Add(new PreValue(index, delimited[index])); - } + var i = 0; + result.AddRange(delimited.Select(x => new PreValue(i++, x))); } //the items list will be a dictionary of it's id -> value we need to use the id for persistence for backwards compatibility - return new Dictionary { { "items", result.ToDictionary(x => x.Id, x => PreValueAsDictionary(x)) } }; + return new Dictionary { { "items", result.ToDictionary(x => x.Id, PreValueAsDictionary) } }; } private IDictionary PreValueAsDictionary(PreValue preValue) @@ -274,6 +282,5 @@ namespace Umbraco.Web.PropertyEditors } } } - } } diff --git a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs index e4ff4da1d6..f07928591e 100644 --- a/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/FileUploadPropertyValueEditor.cs @@ -20,159 +20,125 @@ using Umbraco.Core; namespace Umbraco.Web.PropertyEditors { /// - /// The editor for the file upload property editor + /// The value editor for the file upload property editor. /// internal class FileUploadPropertyValueEditor : PropertyValueEditorWrapper { - public FileUploadPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) - { - } + public FileUploadPropertyValueEditor(PropertyValueEditor wrapped) + : base(wrapped) + { } /// - /// Overrides the deserialize value so that we can save the file accordingly + /// Converts the value received from the editor into the value can be stored in the database. /// - /// - /// This is value passed in from the editor. We normally don't care what the editorValue.Value is set to because - /// we are more interested in the files collection associated with it, however we do care about the value if we - /// are clearing files. By default the editorValue.Value will just be set to the name of the file (but again, we - /// just ignore this and deal with the file collection in editorValue.AdditionalData.ContainsKey("files") ) - /// - /// - /// The current value persisted for this property. This will allow us to determine if we want to create a new - /// file path or use the existing file path. - /// - /// + /// The value received from the editor. + /// The current value of the property + /// The converted value. + /// + /// The is used to re-use the folder, if possible. + /// The is value passed in from the editor. We normally don't care what + /// the editorValue.Value is set to because we are more interested in the files collection associated with it, + /// however we do care about the value if we are clearing files. By default the editorValue.Value will just + /// be set to the name of the file - but again, we just ignore this and deal with the file collection in + /// editorValue.AdditionalData.ContainsKey("files") + /// We only process ONE file. We understand that the current value may contain more than one file, + /// and that more than one file may be uploaded, so we take care of them all, but we only store ONE file. + /// Other places (FileUploadPropertyEditor...) do NOT deal with multiple files, and our logic for reusing + /// folders would NOT work, etc. + /// public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { - if (currentValue == null) - { - currentValue = string.Empty; - } + currentValue = currentValue ?? string.Empty; - //if the value is the same then just return the current value so we don't re-process everything - if (string.IsNullOrEmpty(currentValue.ToString()) == false && editorValue.Value == currentValue.ToString()) - { + // at that point, + // currentValue is either empty or "/media/path/to/img.jpg" + // editorValue.Value is { "clearFiles": true } or { "selectedFiles": "img1.jpg,img2.jpg" } + // comparing them makes little sense + + // check the editorValue value to see whether we need to clear files + var editorJsonValue = editorValue.Value as JObject; + var clears = editorJsonValue != null && editorJsonValue["clearFiles"] != null && editorJsonValue["clearFiles"].Value(); + var uploads = editorValue.AdditionalData.ContainsKey("files") && editorValue.AdditionalData["files"] is IEnumerable; + + // nothing = no changes, return what we have already (leave existing files intact) + if (clears == false && uploads == false) return currentValue; - } - //check the editorValue value to see if we need to clear the files or not. - var clear = false; - var json = editorValue.Value as JObject; - if (json != null && json["clearFiles"] != null && json["clearFiles"].Value()) + // 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 + .ToArray(); + + // if clearing, remove these files and return + if (clears) { - clear = json["clearFiles"].Value(); + foreach (var pathToRemove in currentPaths) + fs.DeleteFile(pathToRemove, true); + return string.Empty; // no more files } + + // ensure we have the required guids + if (editorValue.AdditionalData.ContainsKey("cuid") == false // for the content item + || editorValue.AdditionalData.ContainsKey("puid") == false) // and the property type + throw new Exception("Missing cuid/puid additional data."); + var cuido = editorValue.AdditionalData["cuid"]; + var puido = editorValue.AdditionalData["puid"]; + if ((cuido is Guid) == false || (puido is Guid) == false) + throw new Exception("Invalid cuid/puid additional data."); + var cuid = (Guid) cuido; + var puid = (Guid) puido; + if (cuid == Guid.Empty || puid == Guid.Empty) + throw new Exception("Invalid cuid/puid additional data."); - var currentPersistedValues = new string[] {}; - if (string.IsNullOrEmpty(currentValue.ToString()) == false) + // process the files + var files = ((IEnumerable) editorValue.AdditionalData["files"]).ToArray(); + + var newPaths = new List(); + const int maxLength = 1; // we only process ONE file + for (var i = 0; i < maxLength /*files.Length*/; i++) { - currentPersistedValues = currentValue.ToString().Split(new[] {','}, StringSplitOptions.RemoveEmptyEntries); - } + var file = files[i]; - var newValue = new List(); + // skip invalid files + if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) + continue; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); + // 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 - if (clear) - { - //Remove any files that are saved for this item - foreach (var toRemove in currentPersistedValues) + using (var filestream = File.OpenRead(file.TempFilePath)) { - fs.DeleteFile(fs.GetRelativePath(toRemove), true); - } - return ""; - } - - //check for any files - if (editorValue.AdditionalData.ContainsKey("files")) - { - var files = editorValue.AdditionalData["files"] as IEnumerable; - if (files != null) - { - //now we just need to move the files to where they should be - var filesAsArray = files.ToArray(); - //a list of all of the newly saved files so we can compare with the current saved files and remove the old ones - var savedFilePaths = new List(); - for (var i = 0; i < filesAsArray.Length; i++) + fs.AddFile(filepath, filestream, true); // must overwrite! + + var ext = fs.GetExtension(filepath); + if (ImageHelper.IsImageFile(ext)) { - var file = filesAsArray[i]; - - //don't continue if this is not allowed! - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) - { - continue; - } - - //TODO: ALl of this naming logic needs to be put into the ImageHelper and then we need to change ContentExtensions to do the same! - - var currentPersistedFile = currentPersistedValues.Length >= (i + 1) - ? currentPersistedValues[i] - : ""; - - var name = IOHelper.SafeFileName(file.FileName.Substring(file.FileName.LastIndexOf(IOHelper.DirSepChar) + 1, file.FileName.Length - file.FileName.LastIndexOf(IOHelper.DirSepChar) - 1).ToLower()); - - var subfolder = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? currentPersistedFile.Replace(fs.GetUrl("/"), "").Split('/')[0] - : currentPersistedFile.Substring(currentPersistedFile.LastIndexOf("/", StringComparison.Ordinal) + 1).Split('-')[0]; - - int subfolderId; - var numberedFolder = int.TryParse(subfolder, out subfolderId) - ? subfolderId.ToString(CultureInfo.InvariantCulture) - : MediaSubfolderCounter.Current.Increment().ToString(CultureInfo.InvariantCulture); - - var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(numberedFolder, name) - : numberedFolder + "-" + name; - - using (var fileStream = File.OpenRead(file.TempFilePath)) - { - var umbracoFile = UmbracoMediaFile.Save(fileStream, fileName); - - if (umbracoFile.SupportsResizing) - { - var additionalSizes = new List(); - //get the pre-vals value - var thumbs = editorValue.PreValues.FormatAsDictionary(); - if (thumbs.Any()) - { - var thumbnailSizes = thumbs.First().Value.Value; - // additional thumbnails configured as prevalues on the DataType - foreach (var thumb in thumbnailSizes.Split(new[] { ";", "," }, StringSplitOptions.RemoveEmptyEntries)) - { - int thumbSize; - if (thumb == "" || int.TryParse(thumb, out thumbSize) == false) continue; - additionalSizes.Add(thumbSize); - } - } - - using (var image = Image.FromStream(fileStream)) - { - ImageHelper.GenerateMediaThumbnails(fs, fileName, umbracoFile.Extension, image, additionalSizes); - } - - } - newValue.Add(umbracoFile.Url); - //add to the saved paths - savedFilePaths.Add(umbracoFile.Url); - } - //now remove the temp file - File.Delete(file.TempFilePath); + 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); } - - //Remove any files that are no longer saved for this item - foreach (var toRemove in currentPersistedValues.Except(savedFilePaths)) - { - fs.DeleteFile(fs.GetRelativePath(toRemove), true); - } - - return string.Join(",", newValue); + // all related properties (auto-fill) are managed by FileUploadPropertyEditor + // when the content is saved (through event handlers) + + newPaths.Add(filepath); } } - //if we've made it here, we had no files to save and we were not clearing anything so just persist the same value we had before - return currentValue; + // remove all temp files + foreach (var file in files) + File.Delete(file.TempFilePath); + + // remove files that are not there anymore + foreach (var pathToRemove in currentPaths.Except(newPaths)) + fs.DeleteFile(pathToRemove, true); + + return string.Join(",", newPaths.Select(x => fs.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 9858217a5b..a913d2cb44 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyEditor.cs @@ -2,13 +2,11 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.IO; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core; -using Umbraco.Core.Configuration; -using Umbraco.Core.IO; using Umbraco.Core.Logging; +using Umbraco.Core.Media; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -18,48 +16,62 @@ namespace Umbraco.Web.PropertyEditors [PropertyEditor(Constants.PropertyEditors.ImageCropperAlias, "Image Cropper", "imagecropper", ValueType = "JSON", HideLabel = false, Group="media", Icon="icon-crop")] public class ImageCropperPropertyEditor : PropertyEditor { + // The ImageCropperPropertyEditor properties own files and as such must manage these files, + // so we are binding to events in order to make sure that + // - files are deleted when the owning content/media is + // - files are copied when the owning content is (NOTE: not supporting media copy here!) + // - populate the auto-fill properties when files are changing + // - populate the auto-fill properties when the owning content/media is saved + // + // NOTE: + // uploading multiple files is NOT a feature of the ImageCropperPropertyEditor + // + // auto-fill properties are recalculated EVERYTIME the content/media is saved, + // even if the property has NOT been modified (it could be the same filename but + // a different file) - this is accepted (auto-fill props should die) + // + // FIXME + // for some weird backward compatibility reasons, + // - media copy is not supported + // - auto-fill properties are not supported for content items + // - auto-fill runs on MediaService.Created which makes no sense (no properties yet) - /// - /// We're going to bind to the MediaService Saving event so that we can populate the umbracoFile size, type, etc... label fields - /// if we find any attached to the current media item. - /// - /// - /// I think this kind of logic belongs on this property editor, I guess it could exist elsewhere but it all has to do with the cropper. - /// static ImageCropperPropertyEditor() { + MediaService.Created += MediaServiceCreated; // see above - makes no sense MediaService.Saving += MediaServiceSaving; - MediaService.Created += MediaServiceCreated; - ContentService.Copied += ContentServiceCopied; + //MediaService.Copied += MediaServiceCopied; // see above - missing - MediaService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - MediaService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - ContentService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); - ContentService.EmptiedRecycleBin += (sender, args) => - args.Files.AddRange(ServiceEmptiedRecycleBin(args.AllPropertyData)); - MemberService.Deleted += (sender, args) => - args.MediaFilesToDelete.AddRange(ServiceDeleted(args.DeletedEntities.Cast())); + ContentService.Copied += ContentServiceCopied; + //ContentService.Saving += ContentServiceSaving; // see above - missing + + MediaService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + + MediaService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange( + GetFilesToDelete(args.AllPropertyData.SelectMany(x => x.Value))); + + ContentService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + + ContentService.EmptiedRecycleBin += (sender, args) => args.Files.AddRange( + GetFilesToDelete(args.AllPropertyData.SelectMany(x => x.Value))); + + MemberService.Deleted += (sender, args) => args.MediaFilesToDelete.AddRange( + GetFilesToDelete(args.DeletedEntities.SelectMany(x => x.Properties))); + } + + // preValues + private IDictionary _internalPreValues; + public override IDictionary DefaultPreValues + { + get { return _internalPreValues; } + set { _internalPreValues = value; } } /// - /// Creates our custom value editor + /// Initializes a new instance of the class. /// - /// - protected override PropertyValueEditor CreateValueEditor() - { - var baseEditor = base.CreateValueEditor(); - return new ImageCropperPropertyValueEditor(baseEditor); - } - - protected override PreValueEditor CreatePreValueEditor() - { - return new ImageCropperPreValueEditor(); - } - - public ImageCropperPropertyEditor() { _internalPreValues = new Dictionary @@ -70,194 +82,191 @@ namespace Umbraco.Web.PropertyEditors } /// - /// Ensures any files associated are removed + /// Creates the corresponding property value editor. /// - /// - static IEnumerable ServiceEmptiedRecycleBin(Dictionary> allPropertyData) + /// The corresponding property value editor. + protected override PropertyValueEditor CreateValueEditor() { - var list = new List(); - //Get all values for any image croppers found - foreach (var cropperVal in allPropertyData - .SelectMany(x => x.Value) - .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias) - .Select(x => x.Value) - .WhereNotNull()) - { - JObject json; - try - { - json = JsonConvert.DeserializeObject(cropperVal.ToString()); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred parsing the value stored in the image cropper value: " + cropperVal, ex); - continue; - } - - if (json["src"] != null && json["src"].ToString().IsNullOrWhiteSpace() == false) - { - list.Add(json["src"].ToString()); - } - } - return list; + var baseEditor = base.CreateValueEditor(); + return new ImageCropperPropertyValueEditor(baseEditor); } /// - /// Ensures any files associated are removed + /// Creates the corresponding preValue editor. /// - /// - static IEnumerable ServiceDeleted(IEnumerable deletedEntities) + /// The corresponding preValue editor. + protected override PreValueEditor CreatePreValueEditor() { - var list = new List(); - foreach (var property in deletedEntities.SelectMany(deletedEntity => deletedEntity - .Properties - .Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias - && x.Value != null - && string.IsNullOrEmpty(x.Value.ToString()) == false))) - { - JObject json; - try - { - json = JsonConvert.DeserializeObject(property.Value.ToString()); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred parsing the value stored in the image cropper value: " + property.Value, ex); - continue; - } - - if (json["src"] != null && json["src"].ToString().IsNullOrWhiteSpace() == false) - { - list.Add(json["src"].ToString()); - } - } - return list; + return new ImageCropperPreValueEditor(); } /// - /// After the content is copied we need to check if there are files that also need to be copied + /// Gets a value indicating whether a property is an image cropper field. /// - /// - /// - static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs e) + /// The property. + /// A value indicating whether to check that the property has a non-empty value. + /// A value indicating whether a property is an image cropper field, and (optionaly) has a non-empty value. + private static bool IsCropperField(Property property, bool ensureValue) { - if (e.Original.Properties.Any(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias)) + if (property.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias) + return false; + if (ensureValue == false) + return true; + return property.Value is string && string.IsNullOrWhiteSpace((string)property.Value) == false; + } + + /// + /// Parses the property value into a json object. + /// + /// The property value. + /// A value indicating whether to log the error. + /// The json object corresponding to the property value. + /// In case of an error, optionaly logs the error and returns null. + private static JObject GetJObject(string value, bool writeLog) + { + if (string.IsNullOrWhiteSpace(value)) + return null; + + try { - bool isUpdated = false; - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - //Loop through properties to check if the content contains media that should be deleted - foreach (var property in e.Original.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias - && x.Value != null - && string.IsNullOrEmpty(x.Value.ToString()) == false)) - { - JObject json; - try - { - json = JsonConvert.DeserializeObject(property.Value.ToString()); - } - catch (Exception ex) - { - LogHelper.Error("An error occurred parsing the value stored in the image cropper value: " + property.Value.ToString(), ex); - continue; - } - - if (json["src"] != null && json["src"].ToString().IsNullOrWhiteSpace() == false) - { - if (fs.FileExists(fs.GetRelativePath(json["src"].ToString()))) - { - var currentPath = fs.GetRelativePath(json["src"].ToString()); - var propertyId = e.Copy.Properties.First(x => x.Alias == property.Alias).Id; - var newPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(currentPath)); - - fs.CopyFile(currentPath, newPath); - json["src"] = fs.GetUrl(newPath); - e.Copy.SetValue(property.Alias, json.ToString()); - - //Copy thumbnails - foreach (var thumbPath in fs.GetThumbnails(currentPath)) - { - var newThumbPath = fs.GetRelativePath(propertyId, System.IO.Path.GetFileName(thumbPath)); - fs.CopyFile(thumbPath, newThumbPath); - } - isUpdated = true; - } - } - - - } - - if (isUpdated) - { - //need to re-save the copy with the updated path value - sender.Save(e.Copy); - } + return JsonConvert.DeserializeObject(value); + } + catch (Exception ex) + { + if (writeLog) + LogHelper.Error("Could not parse image cropper value \"" + value + "\"", ex); + return null; } } - static void MediaServiceCreated(IMediaService sender, Core.Events.NewEventArgs e) + /// + /// Gets the files that need to be deleted when entities are deleted. + /// + /// The properties that were deleted. + static IEnumerable GetFilesToDelete(IEnumerable properties) { - AutoFillProperties(e.Entity); + 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); + }).WhereNotNull(); } - static void MediaServiceSaving(IMediaService sender, Core.Events.SaveEventArgs e) + /// + /// After a content has been copied, also copy uploaded files. + /// + /// The event sender. + /// The event arguments. + static void ContentServiceCopied(IContentService sender, Core.Events.CopyEventArgs args) { - foreach (var m in e.SavedEntities) + // get the image cropper field properties with a value + var properties = args.Original.Properties.Where(x => IsCropperField(x, true)); + + // copy files + var isUpdated = false; + var fs = MediaHelper.FileSystem; + foreach (var property in properties) { - AutoFillProperties(m); + var jo = GetJObject((string) property.Value, true); + if (jo == null || jo["src"] == null) continue; + + 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); + args.Copy.SetValue(property.Alias, jo.ToString()); + isUpdated = true; } + + // if updated, re-save the copy with the updated value + if (isUpdated) + sender.Save(args.Copy); } - static void AutoFillProperties(IContentBase model) + /// + /// After a media has been created, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void MediaServiceCreated(IMediaService sender, Core.Events.NewEventArgs args) { - foreach (var p in model.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.ImageCropperAlias)) - { - var uploadFieldConfigNode = - UmbracoConfig.For.UmbracoSettings().Content.ImageAutoFillProperties - .FirstOrDefault(x => x.Alias == p.Alias); + AutoFillProperties(args.Entity); + } - if (uploadFieldConfigNode != null) + /// + /// After a media has been saved, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void MediaServiceSaving(IMediaService sender, Core.Events.SaveEventArgs args) + { + foreach (var entity in args.SavedEntities) + AutoFillProperties(entity); + } + + /// + /// After a content item has been saved, auto-fill the properties. + /// + /// The event sender. + /// The event arguments. + static void ContentServiceSaving(IContentService sender, Core.Events.SaveEventArgs args) + { + foreach (var entity in args.SavedEntities) + AutoFillProperties(entity); + } + + /// + /// Auto-fill properties (or clear). + /// + /// The content. + 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); + if (autoFillConfig == null) continue; + + var svalue = property.Value as string; + if (string.IsNullOrWhiteSpace(svalue)) { - if (p.Value != null) - { - JObject json = null; - try - { - json = JObject.Parse((string)p.Value); - } - catch (JsonException) - { - //note: we are swallowing this exception because in some cases a normal string/non json value will be passed in which will just be the - // file path like /media/23454/hello.jpg - // This will happen everytime an image is uploaded via the folder browser and we don't really want to pollute the log since it's not actually - // a problem and we take care of this below. - // see: http://issues.umbraco.org/issue/U4-4756 - } - if (json != null && json["src"] != null) - { - model.PopulateFileMetaDataProperties(uploadFieldConfigNode, json["src"].Value()); - } - else if (p.Value is string) - { - var src = p.Value == null ? string.Empty : p.Value.ToString(); - var config = ApplicationContext.Current.Services.DataTypeService.GetPreValuesByDataTypeId(p.PropertyType.DataTypeDefinitionId).FirstOrDefault(); - var crops = string.IsNullOrEmpty(config) == false ? config : "[]"; - p.Value = "{src: '" + p.Value + "', crops: " + crops + "}"; - //Only provide the source path, not the whole JSON value - model.PopulateFileMetaDataProperties(uploadFieldConfigNode, src); - } - } - else - model.ResetFileMetaDataProperties(uploadFieldConfigNode); + UploadAutoFillProperties.Reset(content, autoFillConfig); + continue; } - } - } - private IDictionary _internalPreValues; - public override IDictionary DefaultPreValues - { - get { return _internalPreValues; } - set { _internalPreValues = value; } + var jo = GetJObject(svalue, false); + string src; + if (jo == null) + { + // so we have a non-empty string value that cannot be parsed into a json object + // see http://issues.umbraco.org/issue/U4-4756 + // it can happen when an image is uploaded via the folder browser, in which case + // the property value will be the file source eg '/media/23454/hello.jpg' and we + // are fixing that anomaly here - does not make any sense at all but... bah... + var config = ApplicationContext.Current.Services.DataTypeService + .GetPreValuesByDataTypeId(property.PropertyType.DataTypeDefinitionId).FirstOrDefault(); + var crops = string.IsNullOrWhiteSpace(config) ? "[]" : config; + src = svalue; + property.Value = "{src: '" + svalue + "', crops: " + crops + "}"; + } + else + { + src = jo["src"] == null ? null : jo["src"].Value(); + } + + if (src == null) + UploadAutoFillProperties.Reset(content, autoFillConfig); + else + UploadAutoFillProperties.Populate(content, autoFillConfig, fs.GetRelativePath(src)); + } } internal class ImageCropperPreValueEditor : PreValueEditor diff --git a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs index 7c84ff7026..aa46bd9843 100644 --- a/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -1,6 +1,7 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.Drawing; using System.Globalization; using System.IO; using System.Linq; @@ -16,145 +17,160 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Web.Models.ContentEditing; +using File = System.IO.File; namespace Umbraco.Web.PropertyEditors { + /// + /// The value editor for the image cropper property editor. + /// internal class ImageCropperPropertyValueEditor : PropertyValueEditorWrapper { public ImageCropperPropertyValueEditor(PropertyValueEditor wrapped) : base(wrapped) - { - - } - + { } /// - /// Overrides the deserialize value so that we can save the file accordingly + /// Converts the value received from the editor into the value can be stored in the database. /// - /// + /// The value received from the editor. + /// The current value of the property + /// The converted value. + /// + /// The is used to re-use the folder, if possible. + /// FIXME this is ?! /// This is value passed in from the editor. We normally don't care what the editorValue.Value is set to because /// we are more interested in the files collection associated with it, however we do care about the value if we /// are clearing files. By default the editorValue.Value will just be set to the name of the file (but again, we /// just ignore this and deal with the file collection in editorValue.AdditionalData.ContainsKey("files") ) - /// - /// - /// The current value persisted for this property. This will allow us to determine if we want to create a new - /// file path or use the existing file path. - /// - /// + /// + /// public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue) { + var fs = MediaHelper.FileSystem; - - string oldFile = string.Empty; - string newFile = string.Empty; - JObject newJson = null; - JObject oldJson = null; - - //get the old src path - if (currentValue != null && string.IsNullOrEmpty(currentValue.ToString()) == false) + // get the current path + var currentPath = string.Empty; + try { - try - { - oldJson = JObject.Parse(currentValue.ToString()); - } - catch (Exception ex) - { - //for some reason the value is invalid so continue as if there was no value there - LogHelper.WarnWithException("Could not parse current db value to a JObject", ex); - } - - if (oldJson != null && oldJson["src"] != null) - { - oldFile = oldJson["src"].Value(); - } + var svalue = currentValue as string; + var currentJson = string.IsNullOrWhiteSpace(svalue) ? null : JObject.Parse(svalue); + if (currentJson != null && currentJson["src"] != null) + currentPath = currentJson["src"].Value(); } + catch (Exception ex) + { + // for some reason the value is invalid so continue as if there was no value there + LogHelper.WarnWithException("Could not parse current db value to a JObject.", ex); + } + if (string.IsNullOrWhiteSpace(currentPath) == false) + currentPath = fs.GetRelativePath(currentPath); - //get the new src path + // get the new json and path + JObject editorJson = null; + var editorFile = string.Empty; if (editorValue.Value != null) { - newJson = editorValue.Value as JObject; - if (newJson != null && newJson["src"] != null) - { - newFile = newJson["src"].Value(); - } + editorJson = editorValue.Value as JObject; + if (editorJson != null && editorJson["src"] != null) + editorFile = editorJson["src"].Value(); } - //compare old and new src path - //if not alike, that means we have a new file, or delete the current one... - if (string.IsNullOrEmpty(newFile) || editorValue.AdditionalData.ContainsKey("files")) + // ensure we have the required guids + if (editorValue.AdditionalData.ContainsKey("cuid") == false // for the content item + || editorValue.AdditionalData.ContainsKey("puid") == false) // and the property type + throw new Exception("Missing cuid/puid additional data."); + var cuido = editorValue.AdditionalData["cuid"]; + var puido = editorValue.AdditionalData["puid"]; + if ((cuido is Guid) == false || (puido is Guid) == false) + throw new Exception("Invalid cuid/puid additional data."); + var cuid = (Guid)cuido; + var puid = (Guid)puido; + if (cuid == Guid.Empty || puid == Guid.Empty) + throw new Exception("Invalid cuid/puid additional data."); + + // editorFile is empty whenever a new file is being uploaded + // or when the file is cleared (in which case editorJson is null) + // else editorFile contains the unchanged value + + var uploads = editorValue.AdditionalData.ContainsKey("files") && editorValue.AdditionalData["files"] is IEnumerable; + var files = uploads ? ((IEnumerable)editorValue.AdditionalData["files"]).ToArray() : new ContentItemFile[0]; + var file = uploads ? files.FirstOrDefault() : null; + + if (file == null) // not uploading a file { - var fs = FileSystemProviderManager.Current.GetFileSystemProvider(); - - //if we have an existing file, delete it - if (string.IsNullOrEmpty(oldFile) == false) - fs.DeleteFile(fs.GetRelativePath(oldFile), true); - else - oldFile = string.Empty; - - //if we have a new file, add it to the media folder and set .src - - if (editorValue.AdditionalData.ContainsKey("files")) + // if editorFile is empty then either there was nothing to begin with, + // or it has been cleared and we need to remove the file - else the + // value is unchanged. + if (string.IsNullOrWhiteSpace(editorFile) && string.IsNullOrWhiteSpace(currentPath) == false) { - var files = editorValue.AdditionalData["files"] as IEnumerable; - if (files != null && files.Any()) - { - var file = files.First(); - - if (UploadFileTypeValidator.ValidateFileExtension(file.FileName)) - { - //create name and folder number - var name = IOHelper.SafeFileName(file.FileName.Substring(file.FileName.LastIndexOf(IOHelper.DirSepChar) + 1, file.FileName.Length - file.FileName.LastIndexOf(IOHelper.DirSepChar) - 1).ToLower()); - - //try to reuse the folder number from the current file - var subfolder = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? oldFile.Replace(fs.GetUrl("/"), "").Split('/')[0] - : oldFile.Substring(oldFile.LastIndexOf("/", StringComparison.Ordinal) + 1).Split('-')[0]; - - //if we dont find one, create a new one - int subfolderId; - var numberedFolder = int.TryParse(subfolder, out subfolderId) - ? subfolderId.ToString(CultureInfo.InvariantCulture) - : MediaSubfolderCounter.Current.Increment().ToString(CultureInfo.InvariantCulture); - - //set a file name or full path - var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories - ? Path.Combine(numberedFolder, name) - : numberedFolder + "-" + name; - - //save file and assign to the json - using (var fileStream = System.IO.File.OpenRead(file.TempFilePath)) - { - var umbracoFile = UmbracoMediaFile.Save(fileStream, fileName); - newJson["src"] = umbracoFile.Url; - - return newJson.ToString(); - } - } - } + fs.DeleteFile(currentPath, true); + return null; // clear } + + return editorJson == null ? null : editorJson.ToString(); // unchanged } - //incase we submit nothing back - if (editorValue.Value == null) + // process the file + var filepath = editorJson == null ? null : ProcessFile(editorValue, file, fs, currentPath, cuid, puid); + + // remove all temp files + foreach (var f in files) + File.Delete(f.TempFilePath); + + // remove current file if replaced + if (currentPath != filepath && string.IsNullOrWhiteSpace(currentPath) == false) + fs.DeleteFile(currentPath, true); + + // update json and return + if (editorJson == null) return null; + editorJson["src"] = filepath == null ? string.Empty : fs.GetUrl(filepath); + return editorJson.ToString(); + } + + private string ProcessFile(ContentPropertyData editorValue, ContentItemFile file, IFileSystem fs, string currentPath, Guid cuid, Guid puid) + { + // process the file + // no file, invalid file, reject change + if (UploadFileTypeValidator.ValidateFileExtension(file.FileName) == false) return null; - return editorValue.Value.ToString(); + // 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 + + using (var filestream = File.OpenRead(file.TempFilePath)) + { + fs.AddFile(filepath, filestream, true); // must overwrite! + + var ext = fs.GetExtension(filepath); + if (ImageHelper.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); + } + + // all related properties (auto-fill) are managed by ImageCropperPropertyEditor + // when the content is saved (through event handlers) + } + + return filepath; } - - public override string ConvertDbToString(Property property, PropertyType propertyType, Core.Services.IDataTypeService dataTypeService) { - if(property.Value == null || string.IsNullOrEmpty(property.Value.ToString())) + if (property.Value == null || string.IsNullOrEmpty(property.Value.ToString())) return null; - //if we dont have a json structure, we will get it from the property type + // if we dont have a json structure, we will get it from the property type var val = property.Value.ToString(); if (val.DetectIsJson()) return val; + // more magic here ;-( var config = dataTypeService.GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId).FirstOrDefault(); - var crops = !string.IsNullOrEmpty(config) ? config : "[]"; + var crops = string.IsNullOrEmpty(config) ? "[]" : config; var newVal = "{src: '" + val + "', crops: " + crops + "}"; return newVal; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 68c7f5c592..d037d79f8c 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -456,7 +456,6 @@ - diff --git a/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs b/src/umbraco.cms/businesslogic/datatype/FileHandlerData.cs index 12f151876d..b80b2892a4 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) - : MediaSubfolderCounter.Current.Increment().ToString(CultureInfo.InvariantCulture); + : MediaHelper.GetNextFolder(); var fileName = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories ? Path.Combine(numberedFolder, name)