2018-06-29 19:52:40 +02:00
|
|
|
|
using System;
|
|
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
using System.Drawing;
|
|
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.IO;
|
|
|
|
|
|
using System.Linq;
|
|
|
|
|
|
using System.Threading;
|
|
|
|
|
|
using System.Threading.Tasks;
|
|
|
|
|
|
using LightInject;
|
|
|
|
|
|
using Umbraco.Core.Configuration;
|
|
|
|
|
|
using Umbraco.Core.Configuration.UmbracoSettings;
|
|
|
|
|
|
using Umbraco.Core.Composing;
|
|
|
|
|
|
using Umbraco.Core.Exceptions;
|
|
|
|
|
|
using Umbraco.Core.IO.MediaPathSchemes;
|
|
|
|
|
|
using Umbraco.Core.Logging;
|
|
|
|
|
|
using Umbraco.Core.Media;
|
|
|
|
|
|
using Umbraco.Core.Media.Exif;
|
|
|
|
|
|
using Umbraco.Core.Models;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Umbraco.Core.IO
|
|
|
|
|
|
{
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// A custom file system provider for media
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[FileSystemProvider("media")]
|
|
|
|
|
|
public class MediaFileSystem : FileSystemWrapper
|
|
|
|
|
|
{
|
|
|
|
|
|
public MediaFileSystem(IFileSystem wrapped)
|
|
|
|
|
|
: base(wrapped)
|
|
|
|
|
|
{
|
|
|
|
|
|
// due to how FileSystems is written at the moment, the ctor cannot be used to inject
|
|
|
|
|
|
// dependencies, so we have to rely on property injection for anything we might need
|
2018-06-29 14:25:48 +02:00
|
|
|
|
Current.Container.InjectProperties(this);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
MediaPathScheme.Initialize(this);
|
|
|
|
|
|
|
|
|
|
|
|
UploadAutoFillProperties = new UploadAutoFillProperties(this, Logger, ContentConfig);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[Inject]
|
|
|
|
|
|
internal IMediaPathScheme MediaPathScheme { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[Inject]
|
|
|
|
|
|
internal IContentSection ContentConfig { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
[Inject]
|
|
|
|
|
|
internal ILogger Logger { get; set; }
|
|
|
|
|
|
|
|
|
|
|
|
internal UploadAutoFillProperties UploadAutoFillProperties { get; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <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)
|
|
|
|
|
|
{
|
|
|
|
|
|
//ensure duplicates are removed
|
|
|
|
|
|
files = files.Distinct();
|
|
|
|
|
|
|
|
|
|
|
|
var allsuccess = true;
|
|
|
|
|
|
var rootRelativePath = GetRelativePath("/");
|
|
|
|
|
|
|
|
|
|
|
|
Parallel.ForEach(files, file =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (file.IsNullOrWhiteSpace()) return;
|
|
|
|
|
|
|
|
|
|
|
|
var relativeFilePath = GetRelativePath(file);
|
|
|
|
|
|
if (FileExists(relativeFilePath) == false) return;
|
|
|
|
|
|
|
|
|
|
|
|
var parentDirectory = Path.GetDirectoryName(relativeFilePath);
|
|
|
|
|
|
|
|
|
|
|
|
// don't want to delete the media folder if not using directories.
|
|
|
|
|
|
if (ContentConfig.UploadAllowDirectories && parentDirectory != rootRelativePath)
|
|
|
|
|
|
{
|
|
|
|
|
|
//issue U4-771: if there is a parent directory the recursive parameter should be true
|
|
|
|
|
|
DeleteDirectory(parentDirectory, string.IsNullOrEmpty(parentDirectory) == false);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
DeleteFile(file);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
|
|
|
|
|
onError?.Invoke(file, e);
|
|
|
|
|
|
allsuccess = false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return allsuccess;
|
2018-06-20 10:49:12 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
2018-06-29 19:52:40 +02:00
|
|
|
|
public void DeleteMediaFiles(IEnumerable<string> files)
|
|
|
|
|
|
{
|
|
|
|
|
|
files = files.Distinct();
|
|
|
|
|
|
|
|
|
|
|
|
Parallel.ForEach(files, file =>
|
|
|
|
|
|
{
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
if (file.IsNullOrWhiteSpace()) return;
|
|
|
|
|
|
if (FileExists(file) == false) return;
|
|
|
|
|
|
DeleteFile(file);
|
|
|
|
|
|
|
2018-06-20 10:49:12 +02:00
|
|
|
|
var directory = MediaPathScheme.GetDeleteDirectory(file);
|
|
|
|
|
|
if (!directory.IsNullOrWhiteSpace())
|
2018-06-29 19:52:40 +02:00
|
|
|
|
DeleteDirectory(directory, true);
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception e)
|
|
|
|
|
|
{
|
2018-08-16 12:00:12 +01:00
|
|
|
|
Logger.Error<MediaFileSystem>("Failed to delete attached file '{File}'", e, file);
|
2018-06-29 19:52:40 +02:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#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());
|
|
|
|
|
|
|
|
|
|
|
|
return MediaPathScheme.GetFilePath(cuid, puid, filename);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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)
|
|
|
|
|
|
{
|
|
|
|
|
|
filename = Path.GetFileName(filename);
|
|
|
|
|
|
if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename));
|
|
|
|
|
|
filename = IOHelper.SafeFileName(filename.ToLowerInvariant());
|
|
|
|
|
|
|
|
|
|
|
|
return MediaPathScheme.GetFilePath(cuid, puid, filename, prevpath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Associated Media Files
|
|
|
|
|
|
|
|
|
|
|
|
/// <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);
|
|
|
|
|
|
|
|
|
|
|
|
// 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>
|
|
|
|
|
|
/// 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);
|
|
|
|
|
|
return filepath;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// gets or creates a property for a content item.
|
|
|
|
|
|
private static Property GetProperty(IContentBase content, string propertyTypeAlias)
|
|
|
|
|
|
{
|
|
|
|
|
|
var property = content.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias));
|
|
|
|
|
|
if (property != null) return property;
|
|
|
|
|
|
|
|
|
|
|
|
var propertyType = content.GetContentType().CompositionPropertyTypes
|
|
|
|
|
|
.FirstOrDefault(x => x.Alias.InvariantEquals(propertyTypeAlias));
|
|
|
|
|
|
if (propertyType == null)
|
|
|
|
|
|
throw new Exception("No property type exists with alias " + propertyTypeAlias + ".");
|
|
|
|
|
|
|
|
|
|
|
|
property = new Property(propertyType);
|
|
|
|
|
|
content.Properties.Add(property);
|
|
|
|
|
|
return property;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// fixme - what's below belongs to the upload property editor, not the media filesystem!
|
|
|
|
|
|
|
|
|
|
|
|
public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filename, Stream filestream, string culture = null, string segment = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var property = GetProperty(content, propertyTypeAlias);
|
|
|
|
|
|
var oldpath = property.GetValue(culture, segment) is string svalue ? GetRelativePath(svalue) : null;
|
|
|
|
|
|
var filepath = StoreFile(content, property.PropertyType, filename, filestream, oldpath);
|
|
|
|
|
|
property.SetValue(GetUrl(filepath), culture, segment);
|
|
|
|
|
|
SetUploadFile(content, property, filepath, filestream, culture, segment);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public void SetUploadFile(IContentBase content, string propertyTypeAlias, string filepath, string culture = null, string segment = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var property = GetProperty(content, propertyTypeAlias);
|
|
|
|
|
|
// fixme delete?
|
|
|
|
|
|
var oldpath = property.GetValue(culture, segment) is string svalue ? GetRelativePath(svalue) : null;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(oldpath) == false && oldpath != filepath)
|
|
|
|
|
|
DeleteFile(oldpath);
|
|
|
|
|
|
property.SetValue(GetUrl(filepath), culture, segment);
|
|
|
|
|
|
using (var filestream = OpenFile(filepath))
|
|
|
|
|
|
{
|
|
|
|
|
|
SetUploadFile(content, property, filepath, filestream, culture, segment);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// sets a file for the FileUpload property editor
|
|
|
|
|
|
// ie generates thumbnails and populates autofill properties
|
|
|
|
|
|
private void SetUploadFile(IContentBase content, Property property, string filepath, Stream filestream, string culture = null, string segment = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
// will use filepath for extension, and filestream for length
|
|
|
|
|
|
UploadAutoFillProperties.Populate(content, property.Alias, filepath, filestream, culture, segment);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region Image
|
|
|
|
|
|
|
|
|
|
|
|
/// <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
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|