2015-12-08 12:53:11 +01:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
2016-11-03 10:31:44 +01:00
|
|
|
|
using System.Drawing;
|
|
|
|
|
|
using System.Drawing.Drawing2D;
|
|
|
|
|
|
using System.Drawing.Imaging;
|
2013-09-13 18:11:20 +10:00
|
|
|
|
using System.Globalization;
|
2012-11-08 08:27:38 +05:00
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
2016-11-03 10:31:44 +01:00
|
|
|
|
using System.Threading;
|
|
|
|
|
|
using System.Threading.Tasks;
|
2012-11-08 08:27:38 +05:00
|
|
|
|
using Umbraco.Core.Configuration;
|
2013-09-13 18:11:20 +10:00
|
|
|
|
using Umbraco.Core.Configuration.UmbracoSettings;
|
2016-11-03 10:31:44 +01:00
|
|
|
|
using Umbraco.Core.DI;
|
|
|
|
|
|
using Umbraco.Core.Exceptions;
|
2016-04-15 13:25:11 +02:00
|
|
|
|
using Umbraco.Core.Media;
|
2016-11-03 10:31:44 +01:00
|
|
|
|
using Umbraco.Core.Media.Exif;
|
|
|
|
|
|
using Umbraco.Core.Models;
|
|
|
|
|
|
using Umbraco.Core.Services;
|
2012-11-07 09:30:40 +05:00
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Core.IO
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// A custom file system provider for media
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[FileSystemProvider("media")]
|
2013-01-15 01:19:51 +03:00
|
|
|
|
public class MediaFileSystem : FileSystemWrapper
|
2012-11-07 09:30:40 +05:00
|
|
|
|
{
|
2013-09-16 15:52:59 +10:00
|
|
|
|
private readonly IContentSection _contentConfig;
|
2016-11-03 10:31:44 +01:00
|
|
|
|
private readonly ImageHelper _imageHelper;
|
|
|
|
|
|
private readonly UploadAutoFillProperties _uploadAutoFillProperties;
|
|
|
|
|
|
private readonly IDataTypeService _dataTypeService;
|
2013-09-13 18:11:20 +10:00
|
|
|
|
|
2016-11-03 10:31:44 +01:00
|
|
|
|
private readonly object _folderCounterLock = new object();
|
|
|
|
|
|
private long _folderCounter;
|
|
|
|
|
|
private bool _folderCounterInitialized;
|
|
|
|
|
|
|
|
|
|
|
|
private static readonly Dictionary<int, string> DefaultSizes = new Dictionary<int, string>
|
|
|
|
|
|
{
|
|
|
|
|
|
{ 100, "thumb" },
|
|
|
|
|
|
{ 500, "big-thumb" }
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// FIXME
|
|
|
|
|
|
// explain the two ctors
|
|
|
|
|
|
// fix the whole mediaHelper, imageHelper, autofill props mess
|
|
|
|
|
|
// also
|
|
|
|
|
|
// what shannon says: this is an "enhanced filesystem" so it's OK to use it with extra stuff!
|
|
|
|
|
|
|
|
|
|
|
|
public MediaFileSystem(IFileSystem2 wrapped)
|
2013-09-25 19:23:41 +10:00
|
|
|
|
: this(wrapped, UmbracoConfig.For.UmbracoSettings().Content)
|
2016-11-03 10:31:44 +01:00
|
|
|
|
{ }
|
2012-11-08 08:27:38 +05:00
|
|
|
|
|
2016-09-16 16:18:59 +02:00
|
|
|
|
public MediaFileSystem(IFileSystem2 wrapped, IContentSection contentConfig) : base(wrapped)
|
2013-09-13 18:11:20 +10:00
|
|
|
|
{
|
|
|
|
|
|
_contentConfig = contentConfig;
|
2016-11-03 10:31:44 +01:00
|
|
|
|
_imageHelper = new ImageHelper(contentConfig); // FIXME inject or KILL
|
2013-09-13 18:11:20 +10:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-11-03 10:31:44 +01:00
|
|
|
|
// note - this is currently experimental / being developed
|
|
|
|
|
|
//public static bool UseTheNewMediaPathScheme { get; set; }
|
|
|
|
|
|
public const bool UseTheNewMediaPathScheme = false;
|
|
|
|
|
|
|
2016-04-15 13:25:11 +02:00
|
|
|
|
// none of the methods below are used in Core anymore
|
2016-11-03 10:31:44 +01:00
|
|
|
|
// fixme FIX THEM WTF WTF WTF?!
|
2016-04-15 13:25:11 +02:00
|
|
|
|
|
2015-12-08 12:53:11 +01:00
|
|
|
|
[Obsolete("This low-level method should NOT exist.")]
|
|
|
|
|
|
public string GetRelativePath(int propertyId, string fileName)
|
2012-11-08 08:27:38 +05:00
|
|
|
|
{
|
2015-12-08 12:53:11 +01:00
|
|
|
|
var sep = _contentConfig.UploadAllowDirectories
|
2012-11-08 08:27:38 +05:00
|
|
|
|
? Path.DirectorySeparatorChar
|
|
|
|
|
|
: '-';
|
|
|
|
|
|
|
2015-12-08 12:53:11 +01:00
|
|
|
|
return propertyId.ToString(CultureInfo.InvariantCulture) + sep + fileName;
|
2012-11-08 08:27:38 +05:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-04-15 13:25:11 +02:00
|
|
|
|
[Obsolete("This low-level method should NOT exist.", false)]
|
2013-02-19 11:06:00 -01:00
|
|
|
|
public string GetRelativePath(string subfolder, string fileName)
|
|
|
|
|
|
{
|
2015-12-08 12:53:11 +01:00
|
|
|
|
var sep = _contentConfig.UploadAllowDirectories
|
2013-02-19 11:06:00 -01:00
|
|
|
|
? Path.DirectorySeparatorChar
|
|
|
|
|
|
: '-';
|
|
|
|
|
|
|
2015-12-08 12:53:11 +01:00
|
|
|
|
return subfolder + sep + fileName;
|
2013-02-19 11:06:00 -01:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-11-03 10:31:44 +01:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Deletes all files passed in.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="files"></param>
|
|
|
|
|
|
/// <param name="onError"></param>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
internal bool DeleteFiles(IEnumerable<string> files, Action<string, Exception> onError = null)
|
2016-04-15 13:25:11 +02:00
|
|
|
|
{
|
2016-11-03 10:31:44 +01:00
|
|
|
|
//ensure duplicates are removed
|
|
|
|
|
|
files = files.Distinct();
|
2012-11-08 08:27:38 +05:00
|
|
|
|
|
2016-11-03 10:31:44 +01:00
|
|
|
|
var allsuccess = true;
|
|
|
|
|
|
var rootRelativePath = GetRelativePath("/");
|
2012-11-08 08:27:38 +05:00
|
|
|
|
|
2016-11-03 10:31:44 +01:00
|
|
|
|
Parallel.ForEach(files, file =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (file.IsNullOrWhiteSpace()) return;
|
2012-11-08 08:27:38 +05:00
|
|
|
|
|
2016-11-03 10:31:44 +01:00
|
|
|
|
var relativeFilePath = GetRelativePath(file);
|
|
|
|
|
|
if (FileExists(relativeFilePath) == false) return;
|
2012-11-08 08:27:38 +05:00
|
|
|
|
|
2016-11-03 10:31:44 +01:00
|
|
|
|
var parentDirectory = Path.GetDirectoryName(relativeFilePath);
|
2012-11-08 08:27:38 +05:00
|
|
|
|
|
2016-11-03 10:31:44 +01:00
|
|
|
|
// don't want to delete the media folder if not using directories.
|
|
|
|
|
|
if (_contentConfig.UploadAllowDirectories && parentDirectory != rootRelativePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
//issue U4-771: if there is a parent directory the recursive parameter should be true
|
|
|
|
|
|
DeleteDirectory(parentDirectory, string.IsNullOrEmpty(parentDirectory) == false);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
DeleteFile(file, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
onError?.Invoke(file, e);
|
|
|
|
|
|
allsuccess = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return allsuccess;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#region Media Path
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the file path of a media file.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="filename">The file name.</param>
|
|
|
|
|
|
/// <param name="cuid">The unique identifier of the content/media owning the file.</param>
|
|
|
|
|
|
/// <param name="puid">The unique identifier of the property type owning the file.</param>
|
|
|
|
|
|
/// <returns>The filesystem-relative path to the media file.</returns>
|
|
|
|
|
|
/// <remarks>With the old media path scheme, this CREATES a new media path each time it is invoked.</remarks>
|
|
|
|
|
|
public string GetMediaPath(string filename, Guid cuid, Guid puid)
|
|
|
|
|
|
{
|
|
|
|
|
|
filename = Path.GetFileName(filename);
|
|
|
|
|
|
if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename));
|
|
|
|
|
|
filename = IOHelper.SafeFileName(filename.ToLowerInvariant());
|
|
|
|
|
|
|
|
|
|
|
|
string folder;
|
|
|
|
|
|
if (UseTheNewMediaPathScheme == false)
|
|
|
|
|
|
{
|
|
|
|
|
|
// old scheme: filepath is "<int>/<filename>" OR "<int>-<filename>"
|
|
|
|
|
|
// default media filesystem maps to "~/media/<filepath>"
|
|
|
|
|
|
folder = GetNextFolder();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// new scheme: path is "<cuid>-<puid>/<filename>" OR "<cuid>-<puid>-<filename>"
|
|
|
|
|
|
// default media filesystem maps to "~/media/<filepath>"
|
|
|
|
|
|
// assumes that cuid and puid keys can be trusted - and that a single property type
|
|
|
|
|
|
// for a single content cannot store two different files with the same name
|
|
|
|
|
|
folder = Combine(cuid, puid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/...
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var filepath = UmbracoConfig.For.UmbracoSettings().Content.UploadAllowDirectories
|
|
|
|
|
|
? Path.Combine(folder, filename)
|
|
|
|
|
|
: folder + "-" + filename;
|
|
|
|
|
|
|
|
|
|
|
|
return filepath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static byte[] Combine(Guid guid1, Guid guid2)
|
|
|
|
|
|
{
|
|
|
|
|
|
var bytes1 = guid1.ToByteArray();
|
|
|
|
|
|
var bytes2 = guid2.ToByteArray();
|
|
|
|
|
|
var bytes = new byte[bytes1.Length];
|
|
|
|
|
|
for (var i = 0; i < bytes1.Length; i++)
|
|
|
|
|
|
bytes[i] = (byte) (bytes1[i] ^ bytes2[i]);
|
|
|
|
|
|
return bytes;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the file path of a media file.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="filename">The file name.</param>
|
|
|
|
|
|
/// <param name="prevpath">A previous file path.</param>
|
|
|
|
|
|
/// <param name="cuid">The unique identifier of the content/media owning the file.</param>
|
|
|
|
|
|
/// <param name="puid">The unique identifier of the property type owning the file.</param>
|
|
|
|
|
|
/// <returns>The filesystem-relative path to the media file.</returns>
|
|
|
|
|
|
/// <remarks>In the old, legacy, number-based scheme, we try to re-use the media folder
|
|
|
|
|
|
/// specified by <paramref name="prevpath"/>. Else, we CREATE a new one. Each time we are invoked.</remarks>
|
|
|
|
|
|
public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (UseTheNewMediaPathScheme || string.IsNullOrWhiteSpace(prevpath))
|
|
|
|
|
|
return GetMediaPath(filename, cuid, puid);
|
|
|
|
|
|
|
|
|
|
|
|
filename = Path.GetFileName(filename);
|
|
|
|
|
|
if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename));
|
|
|
|
|
|
filename = IOHelper.SafeFileName(filename.ToLowerInvariant());
|
|
|
|
|
|
|
|
|
|
|
|
// old scheme, with a previous path
|
|
|
|
|
|
// prevpath should be "<int>/<filename>" OR "<int>-<filename>"
|
|
|
|
|
|
// and we want to reuse the "<int>" 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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the next media folder in the original number-based scheme.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <returns></returns>
|
|
|
|
|
|
/// <remarks>Should be private, is internal for legacy FileHandlerData which is obsolete.</remarks>
|
|
|
|
|
|
internal string GetNextFolder()
|
|
|
|
|
|
{
|
|
|
|
|
|
EnsureFolderCounterIsInitialized();
|
|
|
|
|
|
return Interlocked.Increment(ref _folderCounter).ToString(CultureInfo.InvariantCulture);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private void EnsureFolderCounterIsInitialized()
|
|
|
|
|
|
{
|
|
|
|
|
|
lock (_folderCounterLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_folderCounterInitialized) return;
|
|
|
|
|
|
|
|
|
|
|
|
_folderCounter = 1000; // seed
|
|
|
|
|
|
var directories = GetDirectories("");
|
|
|
|
|
|
foreach (var directory in directories)
|
|
|
|
|
|
{
|
|
|
|
|
|
long folderNumber;
|
|
|
|
|
|
if (long.TryParse(directory, out folderNumber) && folderNumber > _folderCounter)
|
|
|
|
|
|
_folderCounter = folderNumber;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// note: not multi-domains ie LB safe as another domain could create directories
|
|
|
|
|
|
// while we read and parse them - don't fix, move to new scheme eventually
|
|
|
|
|
|
|
|
|
|
|
|
_folderCounterInitialized = true;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Associated Media Files
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Stores a media file associated to a property of a content item.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="content">The content item owning the media file.</param>
|
|
|
|
|
|
/// <param name="propertyType">The property type owning the media file.</param>
|
|
|
|
|
|
/// <param name="filename">The media file name.</param>
|
|
|
|
|
|
/// <param name="filestream">A stream containing the media bytes.</param>
|
|
|
|
|
|
/// <param name="oldpath">An optional filesystem-relative filepath to the previous media file.</param>
|
|
|
|
|
|
/// <returns>The filesystem-relative filepath to the media file.</returns>
|
|
|
|
|
|
/// <remarks>
|
|
|
|
|
|
/// <para>The file is considered "owned" by the content/propertyType.</para>
|
|
|
|
|
|
/// <para>If an <paramref name="oldpath"/> is provided then that file (and associated thumbnails if any) is deleted
|
|
|
|
|
|
/// before the new file is saved, and depending on the media path scheme, the folder may be reused for the new file.</para>
|
|
|
|
|
|
/// </remarks>
|
|
|
|
|
|
public string StoreFile(IContentBase content, PropertyType propertyType, string filename, Stream filestream, string oldpath)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (content == null) throw new ArgumentNullException(nameof(content));
|
|
|
|
|
|
if (propertyType == null) throw new ArgumentNullException(nameof(propertyType));
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(filename)) throw new ArgumentNullOrEmptyException(nameof(filename));
|
|
|
|
|
|
if (filestream == null) throw new ArgumentNullException(nameof(filestream));
|
|
|
|
|
|
|
|
|
|
|
|
// clear the old file, if any
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(oldpath) == false)
|
|
|
|
|
|
DeleteFile(oldpath, true);
|
|
|
|
|
|
|
|
|
|
|
|
// get the filepath, store the data
|
|
|
|
|
|
// use oldpath as "prevpath" to try and reuse the folder, in original number-based scheme
|
|
|
|
|
|
var filepath = GetMediaPath(filename, oldpath, content.Key, propertyType.Key);
|
|
|
|
|
|
AddFile(filepath, filestream);
|
|
|
|
|
|
return filepath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Clears a media file.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="filepath">The filesystem-relative path to the media file.</param>
|
|
|
|
|
|
public new void DeleteFile(string filepath)
|
|
|
|
|
|
{
|
|
|
|
|
|
DeleteFile(filepath, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Copies a media file as a new media file, associated to a property of a content item.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="content">The content item owning the copy of the media file.</param>
|
|
|
|
|
|
/// <param name="propertyType">The property type owning the copy of the media file.</param>
|
|
|
|
|
|
/// <param name="sourcepath">The filesystem-relative path to the source media file.</param>
|
|
|
|
|
|
/// <returns>The filesystem-relative path to the copy of the media file.</returns>
|
|
|
|
|
|
public string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (content == null) throw new ArgumentNullException(nameof(content));
|
|
|
|
|
|
if (propertyType == null) throw new ArgumentNullException(nameof(propertyType));
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(sourcepath)) throw new ArgumentNullOrEmptyException(nameof(sourcepath));
|
|
|
|
|
|
|
|
|
|
|
|
// ensure we have a file to copy
|
|
|
|
|
|
if (FileExists(sourcepath) == false) return null;
|
|
|
|
|
|
|
|
|
|
|
|
// get the filepath
|
|
|
|
|
|
var filename = Path.GetFileName(sourcepath);
|
|
|
|
|
|
var filepath = GetMediaPath(filename, content.Key, propertyType.Key);
|
|
|
|
|
|
this.CopyFile(sourcepath, filepath);
|
|
|
|
|
|
CopyThumbnails(sourcepath, filepath);
|
|
|
|
|
|
return filepath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// gets or creates a property for a content item.
|
|
|
|
|
|
private static Property GetProperty(IContentBase content, string propertyTypeAlias)
|
|
|
|
|
|
{
|
|
|
|
|
|
var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias));
|
|
|
|
|
|
if (property != null) return property;
|
|
|
|
|
|
|
|
|
|
|
|
var propertyType = content.GetContentType().CompositionPropertyTypes
|
|
|
|
|
|
.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias));
|
|
|
|
|
|
if (propertyType == null)
|
|
|
|
|
|
throw new Exception("No property type exists with alias " + propertyTypeAlias + ".");
|
|
|
|
|
|
|
|
|
|
|
|
property = new Property(propertyType);
|
|
|
|
|
|
content.Properties.Add(property);
|
|
|
|
|
|
return property;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fixme - what's below belongs to the upload property editor, not the media filesystem!
|
|
|
|
|
|
|
|
|
|
|
|
public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream)
|
|
|
|
|
|
{
|
|
|
|
|
|
var property = GetProperty(content, propertyTypeAlias);
|
|
|
|
|
|
var svalue = property.Value as string;
|
|
|
|
|
|
var oldpath = svalue == null ? null : GetRelativePath(svalue);
|
|
|
|
|
|
var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath);
|
|
|
|
|
|
property.Value = GetUrl(filepath);
|
|
|
|
|
|
SetUploadFile(content, property, filepath, filestream);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath)
|
|
|
|
|
|
{
|
|
|
|
|
|
var property = GetProperty(content, propertyTypeAlias);
|
|
|
|
|
|
var svalue = property.Value as string;
|
|
|
|
|
|
var oldpath = svalue == null ? null : GetRelativePath(svalue); // FIXME DELETE?
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath)
|
|
|
|
|
|
DeleteFile(oldpath);
|
|
|
|
|
|
property.Value = GetUrl(filepath);
|
|
|
|
|
|
using (var filestream = OpenFile(filepath))
|
|
|
|
|
|
{
|
|
|
|
|
|
SetUploadFile(content, property, filepath, filestream);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sets a file for the FileUpload property editor
|
|
|
|
|
|
// ie generates thumbnails and populates autofill properties
|
|
|
|
|
|
private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream)
|
|
|
|
|
|
{
|
|
|
|
|
|
// check if file is an image (and supports resizing and thumbnails etc)
|
|
|
|
|
|
var extension = Path.GetExtension(filepath);
|
|
|
|
|
|
var isImage = _imageHelper.IsImageFile(extension);
|
|
|
|
|
|
|
|
|
|
|
|
// specific stuff for images (thumbnails etc)
|
|
|
|
|
|
if (isImage)
|
|
|
|
|
|
{
|
|
|
|
|
|
using (var image = Image.FromStream(filestream))
|
|
|
|
|
|
{
|
|
|
|
|
|
// use one image for all
|
|
|
|
|
|
GenerateThumbnails(image, filepath, property.PropertyType);
|
|
|
|
|
|
_uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, image);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// will use filepath for extension, and filestream for length
|
|
|
|
|
|
_uploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Image
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets a value indicating whether the file extension corresponds to an image.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="extension">The file extension.</param>
|
|
|
|
|
|
/// <returns>A value indicating whether the file extension corresponds to an image.</returns>
|
|
|
|
|
|
public bool IsImageFile(string extension)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (extension == null) return false;
|
|
|
|
|
|
extension = extension.TrimStart('.');
|
|
|
|
|
|
return _contentConfig.ImageFileTypes.InvariantContains(extension);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Gets the dimensions of an image.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
/// <param name="stream">A stream containing the image bytes.</param>
|
|
|
|
|
|
/// <returns>The dimension of the image.</returns>
|
|
|
|
|
|
/// <remarks>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.</remarks>
|
|
|
|
|
|
public Size GetDimensions(Stream stream)
|
|
|
|
|
|
{
|
|
|
|
|
|
//Try to load with exif
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var jpgInfo = ImageFile.FromStream(stream);
|
|
|
|
|
|
|
|
|
|
|
|
if (jpgInfo.Format != ImageFileFormat.Unknown
|
|
|
|
|
|
&& jpgInfo.Properties.ContainsKey(ExifTag.PixelYDimension)
|
|
|
|
|
|
&& jpgInfo.Properties.ContainsKey(ExifTag.PixelXDimension))
|
|
|
|
|
|
{
|
|
|
|
|
|
var height = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelYDimension].Value);
|
|
|
|
|
|
var width = Convert.ToInt32(jpgInfo.Properties[ExifTag.PixelXDimension].Value);
|
|
|
|
|
|
if (height > 0 && width > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
return new Size(width, height);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception)
|
|
|
|
|
|
{
|
|
|
|
|
|
//We will just swallow, just means we can't read exif data, we don't want to log an error either
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
//we have no choice but to try to read in via GDI
|
|
|
|
|
|
using (var image = Image.FromStream(stream))
|
|
|
|
|
|
{
|
|
|
|
|
|
|
|
|
|
|
|
var fileWidth = image.Width;
|
|
|
|
|
|
var fileHeight = image.Height;
|
|
|
|
|
|
return new Size(fileWidth, fileHeight);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Manage thumbnails
|
|
|
|
|
|
|
|
|
|
|
|
// note: this does not find 'custom' thumbnails?
|
|
|
|
|
|
// will find _thumb and _big-thumb but NOT _custom?
|
|
|
|
|
|
public IEnumerable<string> GetThumbnails(string path)
|
|
|
|
|
|
{
|
|
|
|
|
|
var parentDirectory = Path.GetDirectoryName(path);
|
|
|
|
|
|
var extension = Path.GetExtension(path);
|
|
|
|
|
|
|
|
|
|
|
|
return GetFiles(parentDirectory)
|
|
|
|
|
|
.Where(x => x.StartsWith(path.TrimEnd(extension) + "_thumb") || x.StartsWith(path.TrimEnd(extension) + "_big-thumb"))
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
}
|
2012-11-08 08:27:38 +05:00
|
|
|
|
|
2016-01-26 14:18:30 +01:00
|
|
|
|
public void DeleteFile(string path, bool deleteThumbnails)
|
2016-11-03 10:31:44 +01:00
|
|
|
|
{
|
|
|
|
|
|
DeleteFile(path);
|
|
|
|
|
|
|
|
|
|
|
|
if (deleteThumbnails == false)
|
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
|
|
DeleteThumbnails(path);
|
|
|
|
|
|
}
|
2012-11-08 08:27:38 +05:00
|
|
|
|
|
2016-01-26 14:18:30 +01:00
|
|
|
|
public void DeleteThumbnails(string path)
|
2016-11-03 10:31:44 +01:00
|
|
|
|
{
|
|
|
|
|
|
GetThumbnails(path)
|
|
|
|
|
|
.ForEach(DeleteFile);
|
|
|
|
|
|
}
|
2015-12-08 12:53:11 +01:00
|
|
|
|
|
2016-01-26 14:18:30 +01:00
|
|
|
|
public void CopyThumbnails(string sourcePath, string targetPath)
|
2016-11-03 10:31:44 +01:00
|
|
|
|
{
|
|
|
|
|
|
var targetPathBase = Path.GetDirectoryName(targetPath) ?? "";
|
|
|
|
|
|
foreach (var sourceThumbPath in GetThumbnails(sourcePath))
|
|
|
|
|
|
{
|
|
|
|
|
|
var sourceThumbFilename = Path.GetFileName(sourceThumbPath) ?? "";
|
|
|
|
|
|
var targetThumbPath = Path.Combine(targetPathBase, sourceThumbFilename);
|
|
|
|
|
|
this.CopyFile(sourceThumbPath, targetThumbPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region GenerateThumbnails
|
|
|
|
|
|
|
|
|
|
|
|
public IEnumerable<ResizedImage> GenerateThumbnails(Image image, string filepath, string preValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(preValue))
|
|
|
|
|
|
return GenerateThumbnails(image, filepath);
|
|
|
|
|
|
|
|
|
|
|
|
var additionalSizes = new List<int>();
|
|
|
|
|
|
var sep = preValue.Contains(",") ? "," : ";";
|
|
|
|
|
|
var values = preValue.Split(new[] { sep }, StringSplitOptions.RemoveEmptyEntries);
|
|
|
|
|
|
foreach (var value in values)
|
|
|
|
|
|
{
|
|
|
|
|
|
int size;
|
|
|
|
|
|
if (int.TryParse(value, out size))
|
|
|
|
|
|
additionalSizes.Add(size);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return GenerateThumbnails(image, filepath, additionalSizes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IEnumerable<ResizedImage> GenerateThumbnails(Image image, string filepath, IEnumerable<int> additionalSizes = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var w = image.Width;
|
|
|
|
|
|
var h = image.Height;
|
|
|
|
|
|
|
|
|
|
|
|
var sizes = additionalSizes == null ? DefaultSizes.Keys : DefaultSizes.Keys.Concat(additionalSizes);
|
|
|
|
|
|
|
|
|
|
|
|
// start with default sizes,
|
|
|
|
|
|
// add additional sizes,
|
|
|
|
|
|
// filter out duplicates,
|
|
|
|
|
|
// filter out those that would be larger that the original image
|
|
|
|
|
|
// and create the thumbnail
|
|
|
|
|
|
return sizes
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.Where(x => w >= x && h >= x)
|
|
|
|
|
|
.Select(x => GenerateResized(image, filepath, DefaultSizes.ContainsKey(x) ? DefaultSizes[x] : "", x))
|
|
|
|
|
|
.ToList(); // now
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IEnumerable<ResizedImage> GenerateThumbnails(Stream filestream, string filepath, PropertyType propertyType)
|
|
|
|
|
|
{
|
|
|
|
|
|
// get the original image from the original stream
|
|
|
|
|
|
if (filestream.CanSeek) filestream.Seek(0, 0); // fixme - what if we cannot seek?
|
|
|
|
|
|
using (var image = Image.FromStream(filestream))
|
|
|
|
|
|
{
|
|
|
|
|
|
return GenerateThumbnails(image, filepath, propertyType);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public IEnumerable<ResizedImage> GenerateThumbnails(Image image, string filepath, PropertyType propertyType)
|
|
|
|
|
|
{
|
|
|
|
|
|
// if the editor is an upload field, check for additional thumbnail sizes
|
|
|
|
|
|
// that can be defined in the prevalue for the property data type. otherwise,
|
|
|
|
|
|
// just use the default sizes.
|
|
|
|
|
|
var sizes = propertyType.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias
|
|
|
|
|
|
? _dataTypeService
|
|
|
|
|
|
.GetPreValuesByDataTypeId(propertyType.DataTypeDefinitionId)
|
|
|
|
|
|
.FirstOrDefault()
|
|
|
|
|
|
: string.Empty;
|
|
|
|
|
|
|
|
|
|
|
|
return GenerateThumbnails(image, filepath, sizes);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region GenerateResized - Generate at resized filepath derived from origin filepath
|
|
|
|
|
|
|
|
|
|
|
|
public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
return GenerateResized(originImage, originFilepath, sizeName, maxWidthHeight, -1, -1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int fixedWidth, int fixedHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
return GenerateResized(originImage, originFilepath, sizeName, -1, fixedWidth, fixedHeight);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ResizedImage GenerateResized(Image originImage, string originFilepath, string sizeName, int maxWidthHeight, int fixedWidth, int fixedHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(sizeName))
|
|
|
|
|
|
sizeName = "UMBRACOSYSTHUMBNAIL";
|
|
|
|
|
|
var extension = Path.GetExtension(originFilepath) ?? string.Empty;
|
|
|
|
|
|
var filebase = originFilepath.TrimEnd(extension);
|
|
|
|
|
|
var resizedFilepath = filebase + "_" + sizeName + extension;
|
|
|
|
|
|
|
|
|
|
|
|
return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, fixedWidth, fixedHeight);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region GenerateResizedAt - Generate at specified resized filepath
|
|
|
|
|
|
|
|
|
|
|
|
public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
return GenerateResizedAt(originImage, resizedFilepath, maxWidthHeight, -1, -1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ResizedImage GenerateResizedAt(Image originImage, int fixedWidth, int fixedHeight, string resizedFilepath)
|
|
|
|
|
|
{
|
|
|
|
|
|
return GenerateResizedAt(originImage, resizedFilepath, -1, fixedWidth, fixedHeight);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public ResizedImage GenerateResizedAt(Image originImage, string resizedFilepath, int maxWidthHeight, int fixedWidth, int fixedHeight)
|
|
|
|
|
|
{
|
|
|
|
|
|
// target dimensions
|
|
|
|
|
|
int width, height;
|
|
|
|
|
|
|
|
|
|
|
|
// if maxWidthHeight then get ratio
|
|
|
|
|
|
if (maxWidthHeight > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
var fx = (float)originImage.Size.Width / maxWidthHeight;
|
|
|
|
|
|
var fy = (float)originImage.Size.Height / maxWidthHeight;
|
|
|
|
|
|
var f = Math.Max(fx, fy); // fit in thumbnail size
|
|
|
|
|
|
width = (int)Math.Round(originImage.Size.Width / f);
|
|
|
|
|
|
height = (int)Math.Round(originImage.Size.Height / f);
|
|
|
|
|
|
if (width == 0) width = 1;
|
|
|
|
|
|
if (height == 0) height = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
else if (fixedWidth > 0 && fixedHeight > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
width = fixedWidth;
|
|
|
|
|
|
height = fixedHeight;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
width = height = 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// create new image with best quality settings
|
|
|
|
|
|
using (var bitmap = new Bitmap(width, height))
|
|
|
|
|
|
using (var graphics = Graphics.FromImage(bitmap))
|
|
|
|
|
|
{
|
|
|
|
|
|
// if the image size is rather large we cannot use the best quality interpolation mode
|
|
|
|
|
|
// because we'll get out of mem exceptions. So we detect how big the image is and use
|
|
|
|
|
|
// the mid quality interpolation mode when the image size exceeds our max limit.
|
|
|
|
|
|
graphics.InterpolationMode = originImage.Width > 5000 || originImage.Height > 5000
|
|
|
|
|
|
? InterpolationMode.Bilinear // mid quality
|
|
|
|
|
|
: InterpolationMode.HighQualityBicubic; // best quality
|
|
|
|
|
|
|
|
|
|
|
|
// everything else is best-quality
|
|
|
|
|
|
graphics.SmoothingMode = SmoothingMode.HighQuality;
|
|
|
|
|
|
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
|
|
|
|
|
|
graphics.CompositingQuality = CompositingQuality.HighQuality;
|
|
|
|
|
|
|
|
|
|
|
|
// copy the old image to the new and resize
|
|
|
|
|
|
var rect = new Rectangle(0, 0, width, height);
|
|
|
|
|
|
graphics.DrawImage(originImage, rect, 0, 0, originImage.Width, originImage.Height, GraphicsUnit.Pixel);
|
|
|
|
|
|
|
|
|
|
|
|
// copy metadata
|
|
|
|
|
|
// fixme - er... no?
|
|
|
|
|
|
|
|
|
|
|
|
// get an encoder - based upon the file type
|
|
|
|
|
|
var extension = (Path.GetExtension(resizedFilepath) ?? "").TrimStart('.').ToLowerInvariant();
|
|
|
|
|
|
var encoders = ImageCodecInfo.GetImageEncoders();
|
|
|
|
|
|
ImageCodecInfo encoder;
|
|
|
|
|
|
switch (extension)
|
|
|
|
|
|
{
|
|
|
|
|
|
case "png":
|
|
|
|
|
|
encoder = encoders.Single(t => t.MimeType.Equals("image/png"));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "gif":
|
|
|
|
|
|
encoder = encoders.Single(t => t.MimeType.Equals("image/gif"));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "tif":
|
|
|
|
|
|
case "tiff":
|
|
|
|
|
|
encoder = encoders.Single(t => t.MimeType.Equals("image/tiff"));
|
|
|
|
|
|
break;
|
|
|
|
|
|
case "bmp":
|
|
|
|
|
|
encoder = encoders.Single(t => t.MimeType.Equals("image/bmp"));
|
|
|
|
|
|
break;
|
|
|
|
|
|
// TODO: this is dirty, defaulting to jpg but the return value of this thing is used all over the
|
|
|
|
|
|
// place so left it here, but it needs to not set a codec if it doesn't know which one to pick
|
|
|
|
|
|
// Note: when fixing this: both .jpg and .jpeg should be handled as extensions
|
|
|
|
|
|
default:
|
|
|
|
|
|
encoder = encoders.Single(t => t.MimeType.Equals("image/jpeg"));
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// set compresion ratio to 90%
|
|
|
|
|
|
var encoderParams = new EncoderParameters();
|
|
|
|
|
|
encoderParams.Param[0] = new EncoderParameter(Encoder.Quality, 90L);
|
|
|
|
|
|
|
|
|
|
|
|
// save the new image
|
|
|
|
|
|
using (var stream = new MemoryStream())
|
|
|
|
|
|
{
|
|
|
|
|
|
bitmap.Save(stream, encoder, encoderParams);
|
|
|
|
|
|
stream.Seek(0, 0);
|
|
|
|
|
|
if (resizedFilepath.Contains("UMBRACOSYSTHUMBNAIL"))
|
|
|
|
|
|
{
|
|
|
|
|
|
var filepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", maxWidthHeight.ToInvariantString());
|
|
|
|
|
|
AddFile(filepath, stream);
|
|
|
|
|
|
if (extension != "jpg")
|
|
|
|
|
|
{
|
|
|
|
|
|
filepath = filepath.TrimEnd(extension) + "jpg";
|
|
|
|
|
|
stream.Seek(0, 0);
|
|
|
|
|
|
AddFile(filepath, stream);
|
|
|
|
|
|
}
|
|
|
|
|
|
// TODO: Remove this, this is ONLY here for backwards compatibility but it is essentially completely unusable see U4-5385
|
|
|
|
|
|
stream.Seek(0, 0);
|
|
|
|
|
|
resizedFilepath = resizedFilepath.Replace("UMBRACOSYSTHUMBNAIL", width + "x" + height);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
AddFile(resizedFilepath, stream);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return new ResizedImage(resizedFilepath, width, height);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Inner classes
|
|
|
|
|
|
|
|
|
|
|
|
public class ResizedImage
|
|
|
|
|
|
{
|
|
|
|
|
|
public ResizedImage()
|
|
|
|
|
|
{ }
|
|
|
|
|
|
|
|
|
|
|
|
public ResizedImage(string filepath, int width, int height)
|
|
|
|
|
|
{
|
|
|
|
|
|
Filepath = filepath;
|
|
|
|
|
|
Width = width;
|
|
|
|
|
|
Height = height;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public string Filepath { get; set; }
|
|
|
|
|
|
public int Width { get; set; }
|
|
|
|
|
|
public int Height { get; set; }
|
2015-12-08 12:53:11 +01:00
|
|
|
|
}
|
2016-11-03 10:31:44 +01:00
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
}
|
2012-11-07 09:30:40 +05:00
|
|
|
|
}
|