using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Umbraco.Core.Composing; using Umbraco.Core.Configuration.UmbracoSettings; using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Media; using Umbraco.Core.Models; namespace Umbraco.Core.IO { /// /// A custom file system provider for media /// [FileSystemProvider("media")] public class MediaFileSystem : FileSystemWrapper { public MediaFileSystem(IFileSystem wrapped) : base(wrapped) { ContentConfig = Current.Container.GetInstance(); Logger = Current.Container.GetInstance(); MediaPathScheme = Current.Container.GetInstance(); MediaPathScheme.Initialize(this); } private IMediaPathScheme MediaPathScheme { get; } private IContentSection ContentConfig { get; } private ILogger Logger { get; } /// /// Deletes all files passed in. /// /// /// /// internal bool DeleteFiles(IEnumerable files, Action 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; } public void DeleteMediaFiles(IEnumerable files) { files = files.Distinct(); Parallel.ForEach(files, file => { try { if (file.IsNullOrWhiteSpace()) return; if (FileExists(file) == false) return; DeleteFile(file); var directory = MediaPathScheme.GetDeleteDirectory(file); if (!directory.IsNullOrWhiteSpace()) DeleteDirectory(directory, true); } catch (Exception e) { Logger.Error(e, "Failed to delete attached file '{File}'", file); } }); } #region Media Path /// /// Gets the file path of a media file. /// /// The file name. /// The unique identifier of the content/media owning the file. /// The unique identifier of the property type owning the file. /// The filesystem-relative path to the media file. /// With the old media path scheme, this CREATES a new media path each time it is invoked. public string GetMediaPath(string filename, Guid cuid, Guid puid) { filename = Path.GetFileName(filename); if (filename == null) throw new ArgumentException("Cannot become a safe filename.", nameof(filename)); filename = IOHelper.SafeFileName(filename.ToLowerInvariant()); return MediaPathScheme.GetFilePath(cuid, puid, filename); } /// /// Gets the file path of a media file. /// /// The file name. /// A previous file path. /// The unique identifier of the content/media owning the file. /// The unique identifier of the property type owning the file. /// The filesystem-relative path to the media file. /// In the old, legacy, number-based scheme, we try to re-use the media folder /// specified by . Else, we CREATE a new one. Each time we are invoked. public string GetMediaPath(string filename, string prevpath, Guid cuid, Guid puid) { 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 /// /// Stores a media file associated to a property of a content item. /// /// 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 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. /// 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; } /// /// Copies a media file as a new media file, associated to a property of a content item. /// /// The content item owning the copy of the media file. /// The property type owning the copy of the media file. /// The filesystem-relative path to the source media file. /// The filesystem-relative path to the copy of the media file. public string CopyFile(IContentBase content, PropertyType propertyType, string sourcepath) { if (content == null) throw new ArgumentNullException(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; } #endregion } }