Beispiel #1
0
        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);
        }
Beispiel #2
0
 public MediaPathData(MediaPathData pathData)
 {
     Node   = pathData.Node;
     _name  = pathData.FileName;
     _title = pathData._title;
     _ext   = pathData._ext;
     _mime  = pathData._mime;
 }
Beispiel #3
0
 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) + '\"')
     });
 }
Beispiel #4
0
        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);
        }
Beispiel #5
0
        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);
        }
Beispiel #6
0
        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);
        }
Beispiel #7
0
        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);
            }
        }
Beispiel #8
0
        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);
            }
        }