protected internal virtual async Task <bool> CheckUniqueFileName(MediaPathData pathData) { // (perf) First make fast check var exists = await _db.MediaFiles.AnyAsync(x => x.Name == pathData.FileName && x.FolderId == pathData.Folder.Id); if (!exists) { return(false); } var q = new MediaSearchQuery { FolderId = pathData.Folder.Id, Term = string.Concat(pathData.FileTitle, "*.", pathData.Extension), Deleted = null }; var query = _searcher.PrepareQuery(q, MediaLoadFlags.AsNoTracking).Select(x => x.Name); var files = new HashSet <string>(await query.ToListAsync(), StringComparer.CurrentCultureIgnoreCase); if (_helper.CheckUniqueFileName(pathData.FileTitle, pathData.Extension, files, out var uniqueName)) { pathData.FileName = uniqueName; return(true); } return(false); }
public MediaPathData(MediaPathData pathData) { Node = pathData.Node; _name = pathData.FileName; _title = pathData._title; _ext = pathData._ext; _mime = pathData._mime; }
private static FileStreamResult CreateFileResult(IFile file, MediaPathData pathData) { return(new FileStreamResult(file.OpenRead(), pathData.MimeType) { EnableRangeProcessing = true, // INFO: (core)(perf)I think ETag is sufficient and ignoring this reduces header comparison by one item. //LastModified = file.LastModified, EntityTag = new EntityTagHeaderValue('\"' + ETagUtility.GenerateETag(file) + '\"') }); }
public bool TokenizePath(string path, bool normalizeFileName, out MediaPathData data) { data = null; if (path.IsEmpty()) { return(false); } var dir = Path.GetDirectoryName(path); if (dir.HasValue()) { var node = _folderService.GetNodeByPath(dir); if (node != null) { data = new MediaPathData(node, path.Substring(dir.Length + 1), normalizeFileName); return(true); } } return(false); }
private MediaPathData CreateDestinationPathData(MediaFile file, string destinationFileName) { if (!_helper.TokenizePath(destinationFileName, true, out var pathData)) { // Passed path is NOT a path, but a file name if (IsPath(destinationFileName)) { // ...but file name includes path chars, which is not allowed throw new ArgumentException( T("Admin.Media.Exception.InvalidPath", Path.GetDirectoryName(destinationFileName)), nameof(destinationFileName)); } if (file.FolderId == null) { throw new NotSupportedException(T("Admin.Media.Exception.FolderAssignment")); } pathData = new MediaPathData(_folderService.GetNodeById(file.FolderId.Value), destinationFileName); } return(pathData); }
private async Task <(MediaFile Copy, bool IsDupe)> InternalCopyFile( MediaFile file, MediaPathData destPathData, bool copyData, DuplicateEntryHandling dupeEntryHandling, Func <Task <MediaFile> > dupeFileSelector, Func <MediaPathData, Task> uniqueFileNameChecker) { // Find dupe and handle var isDupe = false; var dupe = await dupeFileSelector(); if (dupe != null) { switch (dupeEntryHandling) { case DuplicateEntryHandling.Skip: await uniqueFileNameChecker(destPathData); return(dupe, true); case DuplicateEntryHandling.ThrowError: var fullPath = destPathData.FullPath; await uniqueFileNameChecker(destPathData); throw _exceptionFactory.DuplicateFile(fullPath, ConvertMediaFile(dupe), destPathData.FullPath); case DuplicateEntryHandling.Rename: await uniqueFileNameChecker(destPathData); dupe = null; break; case DuplicateEntryHandling.Overwrite: if (file.FolderId == destPathData.Folder.Id) { throw new IOException(T("Admin.Media.Exception.Overwrite")); } break; } } isDupe = dupe != null; var copy = dupe ?? new MediaFile(); // Simple clone MapMediaFile(file, copy); // Set folder id copy.FolderId = destPathData.Folder.Id; // A copied file cannot stay in deleted state copy.Deleted = false; // Set name stuff if (!copy.Name.EqualsNoCase(destPathData.FileName)) { copy.Name = destPathData.FileName; copy.Extension = destPathData.Extension; copy.MimeType = destPathData.MimeType; } // Save to DB if (isDupe) { _db.TryUpdate(copy); } else { _db.MediaFiles.Add(copy); } // Copy data: blob, alt, title etc. if (copyData) { await InternalCopyFileData(file, copy); } return(copy, isDupe); }
protected async Task <(MediaStorageItem StorageItem, MediaFile File)> ProcessFile( MediaFile file, MediaPathData pathData, Stream inStream, bool isTransient = true, DuplicateFileHandling dupeFileHandling = DuplicateFileHandling.ThrowError, MimeValidationType mediaValidationType = MimeValidationType.MimeTypeMustMatch) { if (file != null) { if (dupeFileHandling == DuplicateFileHandling.ThrowError) { var fullPath = pathData.FullPath; await CheckUniqueFileName(pathData); throw _exceptionFactory.DuplicateFile(fullPath, ConvertMediaFile(file, pathData.Folder), pathData.FullPath); } else if (dupeFileHandling == DuplicateFileHandling.Rename) { if (await CheckUniqueFileName(pathData)) { file = null; } } } if (file != null && mediaValidationType != MimeValidationType.NoValidation) { if (mediaValidationType == MimeValidationType.MimeTypeMustMatch) { ValidateMimeTypes("Save", file.MimeType, pathData.MimeType); } else if (mediaValidationType == MimeValidationType.MediaTypeMustMatch) { ValidateMediaTypes("Save", _typeResolver.Resolve(pathData.Extension), file.MediaType); } // Restore file if soft-deleted file.Deleted = false; // Delete thumbnail await _imageCache.DeleteAsync(file); } file ??= new MediaFile { IsTransient = isTransient, FolderId = pathData.Node.Value.Id }; // Untrackable folders can never contain transient files. if (!pathData.Folder.CanDetectTracks) { file.IsTransient = false; } var name = pathData.FileName; if (name != pathData.FileName) { pathData.FileName = name; } file.Name = pathData.FileName; file.Extension = pathData.Extension; file.MimeType = pathData.MimeType; if (file.MediaType == null) { file.MediaType = _typeResolver.Resolve(pathData.Extension, pathData.MimeType); } // Process image if (inStream != null && inStream.Length > 0 && file.MediaType == MediaType.Image && (await ProcessImage(file, inStream)).Out(out var outImage)) { file.Width = outImage.Width; file.Height = outImage.Height; file.PixelSize = outImage.Width * outImage.Height; return(MediaStorageItem.FromImage(outImage), file); } else { file.RefreshMetadata(inStream, _imageProcessor.Factory); return(MediaStorageItem.FromStream(inStream), file); } }
public async Task Invoke( HttpContext context, IMediaService mediaService, IFolderService folderService, IPermissionService permissionService, IWorkContext workContext, MediaSettings mediaSettings, MediaHelper mediaHelper, Lazy <IEnumerable <IMediaHandler> > mediaHandlers, ILogger <MediaMiddleware> logger) { var mediaFileId = context.GetRouteValueAs <int>("id"); var path = context.GetRouteValueAs <string>("path"); if (context.Request.Method != HttpMethods.Get && context.Request.Method != HttpMethods.Head) { await NotFound(null); return; } var method = context.Request.Method; MediaFileInfo mediaFile = null; MediaPathData pathData = null; if (mediaFileId == 0) { // This is most likely a request for a default placeholder image pathData = new MediaPathData(path); } else if (!mediaHelper.TokenizePath(path, false, out pathData)) { // Missing or malformed Uri: get file metadata from DB by id, but only when current user has media manage rights if (!(await permissionService.AuthorizeAsync(Permissions.Media.Update))) { await NotFound(null); return; } mediaFile = await mediaService.GetFileByIdAsync(mediaFileId, MediaLoadFlags.AsNoTracking); if (mediaFile == null || mediaFile.FolderId == null || mediaFile.Deleted) { await NotFound(mediaFile?.MimeType); return; } pathData = new MediaPathData(folderService.GetNodeById(mediaFile.FolderId.Value), mediaFile.Name) { Extension = mediaFile.Extension, MimeType = mediaFile.MimeType }; } var q = await CreateImageQuery(context, pathData.MimeType, pathData.Extension); // Security: check allowed thumnail sizes and return 404 if disallowed. var thumbMaxWidth = q.MaxWidth; var thumbMaxHeight = q.MaxHeight; var thumbSizeAllowed = IsThumbnailSizeAllowed(thumbMaxWidth) && (thumbMaxHeight == thumbMaxWidth || IsThumbnailSizeAllowed(thumbMaxHeight)); if (!thumbSizeAllowed) { await NotFound(pathData.MimeType); return; } // Create the handler context var handlerContext = new MediaHandlerContext { HttpContext = context, CurrentCustomer = workContext.CurrentCustomer, PermissionService = permissionService, MediaFileId = mediaFileId, RawPath = path, MediaService = mediaService, PathData = pathData, ImageQuery = q }; handlerContext.SetSourceFile(mediaFile); var handlers = mediaHandlers.Value.OrderBy(x => x.Order).ToArray(); // Run every registered media handler to obtain a thumbnail for the requested media file IMediaHandler currentHandler; for (var i = 0; i < handlers.Length; i++) { currentHandler = handlers[i]; // Execute handler await currentHandler.ExecuteAsync(handlerContext); if (handlerContext.Exception != null) { var isThumbExtractFail = handlerContext.Exception is ExtractThumbnailException; var statusCode = isThumbExtractFail ? StatusCodes.Status204NoContent : StatusCodes.Status500InternalServerError; var statusMessage = isThumbExtractFail ? handlerContext.Exception.InnerException?.Message.EmptyNull() : handlerContext.Exception.Message; await SendStatus(statusCode, statusMessage); return; } if (handlerContext.Executed || handlerContext.ResultFile != null) { // Get out if the handler produced a result file or has been executed in any way break; } } try { var responseFile = handlerContext.ResultFile ?? await handlerContext.GetSourceFileAsync(); if (responseFile == null || !responseFile.Exists) { await NotFound(pathData.MimeType); return; } if (string.Equals(responseFile.Extension, "." + pathData.Extension, StringComparison.CurrentCultureIgnoreCase)) { pathData.MimeType = MimeTypes.MapNameToMimeType(responseFile.Extension); } // Create FileStreamResult object var fileResult = CreateFileResult(responseFile, pathData); // Cache control ApplyResponseCaching(context, mediaSettings); // INFO: Although we are outside of the MVC pipeline we gonna use ActionContext anyway, because "FileStreamResult" // does everything we need (ByteRange, ETag etc.), so wo we gonna use it instead of reinventing the wheel. // A look at the MVC source code reveals that HttpContext is the only property that gets accessed, therefore we can omit // all the other stuff like ActionDescriptor or ModelState (which we cannot access or create from a middleware anyway). await fileResult.ExecuteResultAsync(new ActionContext { HttpContext = context, RouteData = context.GetRouteData() }); } finally { var imageProcessor = context.RequestServices.GetRequiredService <IImageProcessor>(); logger.Debug("ImageProcessor TOTAL: {0} ms.", imageProcessor.TotalProcessingTimeMs); } #region Functions bool IsThumbnailSizeAllowed(int?size) { return(size.GetValueOrDefault() == 0 || mediaSettings.IsAllowedThumbnailSize(size.Value) || permissionService.Authorize(Permissions.Media.Update, workContext.CurrentCustomer)); } async Task NotFound(string mime) { context.Response.ContentType = mime.NullEmpty() ?? "text/html"; context.Response.StatusCode = 404; await context.Response.WriteAsync("404: Not Found"); } async Task SendStatus(int code, string message) { context.Response.StatusCode = code; await context.Response.WriteAsync(message); } #endregion }
private async Task <MediaFolderInfo> InternalCopyFolder( DbContextScope scope, TreeNode <MediaFolderNode> sourceNode, string destPath, DuplicateEntryHandling dupeEntryHandling, IList <DuplicateFileInfo> dupeFiles, CancellationToken cancelToken = default) { // Get dest node var destNode = _folderService.GetNodeByPath(destPath); // Dupe handling if (destNode != null && dupeEntryHandling == DuplicateEntryHandling.ThrowError) { throw _exceptionFactory.DuplicateFolder(sourceNode.Value.Path, destNode.Value); } var doDupeCheck = destNode != null; // Create dest folder if (destNode == null) { destNode = await CreateFolderAsync(destPath); } // INFO: we gonna change file name during the files loop later. var destPathData = new MediaPathData(destNode, "placeholder.txt"); // Get all source files in one go var files = await _searcher.SearchFiles( new MediaSearchQuery { FolderId = sourceNode.Value.Id }, MediaLoadFlags.AsNoTracking | MediaLoadFlags.WithTags).LoadAsync(); IDictionary <string, MediaFile> destFiles = null; HashSet <string> destNames = null; if (doDupeCheck) { // Get all files in destination folder for faster dupe selection destFiles = (await _searcher .SearchFiles(new MediaSearchQuery { FolderId = destNode.Value.Id }, MediaLoadFlags.None).LoadAsync()) .ToDictionarySafe(x => x.Name); // Make a HashSet from all file names in the destination folder for faster unique file name lookups destNames = new HashSet <string>(destFiles.Keys, StringComparer.CurrentCultureIgnoreCase); } // Holds source and copy together, 'cause we perform a two-pass copy (file first, then data) var tuples = new List <(MediaFile, MediaFile)>(500); // Copy files batched foreach (var batch in files.Slice(500)) { if (cancelToken.IsCancellationRequested) { break; } foreach (var file in batch) { if (cancelToken.IsCancellationRequested) { break; } destPathData.FileName = file.Name; // >>> Do copy var copyResult = await InternalCopyFile( file, destPathData, false /* copyData */, dupeEntryHandling, () => Task.FromResult(destFiles?.Get(file.Name)), p => UniqueFileNameChecker(p)); if (copyResult.Copy != null) { if (copyResult.IsDupe) { dupeFiles.Add(new DuplicateFileInfo { SourceFile = ConvertMediaFile(file, sourceNode.Value), DestinationFile = ConvertMediaFile(copyResult.Copy, destNode.Value), UniquePath = destPathData.FullPath }); } if (!copyResult.IsDupe || dupeEntryHandling != DuplicateEntryHandling.Skip) { // When dupe: add to processing queue only if file was NOT skipped tuples.Add((file, copyResult.Copy)); } } } if (!cancelToken.IsCancellationRequested) { // Save batch to DB (1st pass) await scope.CommitAsync(cancelToken); // Now copy file data foreach (var op in tuples) { await InternalCopyFileData(op.Item1, op.Item2); } // Save batch to DB (2nd pass) await scope.CommitAsync(cancelToken); } _db.DetachEntities <MediaFolder>(); _db.DetachEntities <MediaFile>(); tuples.Clear(); } // Copy folders foreach (var node in sourceNode.Children) { if (cancelToken.IsCancellationRequested) { break; } destPath = destNode.Value.Path + "/" + node.Value.Name; await InternalCopyFolder(scope, node, destPath, dupeEntryHandling, dupeFiles, cancelToken); } return(new MediaFolderInfo(destNode)); Task UniqueFileNameChecker(MediaPathData pathData) { if (destNames != null && _helper.CheckUniqueFileName(pathData.FileTitle, pathData.Extension, destNames, out var uniqueName)) { pathData.FileName = uniqueName; } return(Task.CompletedTask); } }