EPiServer – How to Intercept the Download of a Media File without using Download Media

EPiServer – How to Intercept the Download of a Media File without using Download Media

While researching about how to intercept the download of a media file we found several posts, like this one, that mentioned the use of DownloadMediaRouter.DownloadSegment to get the file name and send it as part of the content disposition header of the response. Unfortunately, in our project it did not work as expected in all scenarios. This post, will explain what was the error, a possible reason for it and how we solved it

epi_server_logo_detail

So, lets begin !!!!

First, we will show the code for the interceptor we borrowed from the post mentioned above, with some slight modifications.


    [TemplateDescriptor(Inherited = true, TemplateTypeCategory = TemplateTypeCategories.HttpHandler)]
    public class MediaDownloadHttpHandler : BlobHttpHandler, IRenderTemplate<DocumentMetadata>
    {
        private readonly Injected<IContentRepository> ContentRepository;

        public new bool IsReusable
        {
            get { return false; }
        }

        protected override Blob GetBlob(HttpContextBase context)
        {
            var routeHelper = ServiceLocator.Current.GetInstance<IContentRouteHelper>();

            //Get file content
            var content = routeHelper.Content;

            if (content == null)
            {
                throw new HttpException(404, "Not Found.");
            }

            // Get binary Storable
            var binaryStorable = content as IBinaryStorable;

            //ACCESS CHECK OR DOWNLOAD TRACKING IMPLEMENTED HERE

            //Set caching policy - No cache for this scenario
            context.Response.Cache.SetCacheability(HttpCacheability.NoCache);

            //Get name of file
            var downLoadFileName = context.Request.RequestContext.GetCustomRouteData(DownloadMediaRouter.DownloadSegment);

            if (!string.IsNullOrEmpty(downLoadFileName))
            {
                //Ensure that the file is being downloaded
                context.Response.AppendHeader("Content-Disposition",
                    string.Format("attachment; filename=\"{0}\"", downLoadFileName));
            }

            return binaryStorable?.BinaryData;
        }
    }

The interceptor runs when a document metadata media file is trying to be downloaded, this includes pdf, xlsx, xls, csv and zip extensions. The code for the document metadata type is below.


namespace Data.Models.Media
{
    using System.ComponentModel.DataAnnotations;
    using EPiServer.Core;
    using EPiServer.DataAbstraction;
    using EPiServer.DataAnnotations;
    using EPiServer.Framework.DataAnnotations;
    using EPiServer.Web;

    [ContentType(GUID = "3014e7d1-53f3-49ca-94ff-29d3da5101a4")]
    [MediaDescriptor(ExtensionString = "pdf,xlsx,xls,csv,zip")]
    public class DocumentMetadata : MediaData
    {
        [CultureSpecific]
        [Display(GroupName = SystemTabNames.Content, Order = 100)]
        public virtual string DocumentTitle { get; set; }
        
        [CultureSpecific]
        [UIHint(UIHint.Textarea)]
        [Display(GroupName = SystemTabNames.Content, Order = 200)]
        public virtual string Teaser { get; set; }

        [CultureSpecific]
        [UIHint(UIHint.Textarea)]
        [Display(GroupName = SystemTabNames.Content, Order = 300)]
        public virtual string Description { get; set; }
    }
}

The error appeared when the user tried to download some xls files, several of them were downloaded successfully, but some of them were download without file extension and corrupted. Debugging the code we identified that the line using DownloadMediaRouter.DownloadSegment was causing the issue . The method sometimes was not returning the file name successfully and as consequence the content disposition header was not set with a valid value. Using Visual Studio, we realized that the code had a summary comment stating this “Unsupported INTERNAL API! Not covered by semantic versioning; might change without notice”. Therefore, it seems like that API is not safe to use, so we decided to implement our own code to solve the issue.

Our implementation use a method to read the extension from the binary data and it joins the extension with the file name so we can use it to set the content disposition value. To return the result we use a class called SourceFile described below


namespace Data.Models.Entities
{
    public class SourceFile
    {
        public string Name { get; set; }
        public string Extension { get; set; }
        public Byte[] FileBytes { get; set; }
    }
}

Using the new class and a the new method GetSourceFileFromBlobData we replace the code was causing the issue with this new code


namespace Business.Initialization
{
    [TemplateDescriptor(Inherited = true, TemplateTypeCategory = TemplateTypeCategories.HttpHandler)]
    public class MediaDownloadHttpHandler : BlobHttpHandler, IRenderTemplate<DocumentMetadata>
    {
        private readonly Injected ContentRepository;

        public new bool IsReusable
        {
            get { return false; }
        }

        protected override Blob GetBlob(HttpContextBase context)
        {
            var routeHelper = ServiceLocator.Current.GetInstance<IContentRouteHelper>();

            //Get file content
            var content = routeHelper.Content;

            if (content == null)
            {
                throw new HttpException(404, "Not Found.");
            }

            // Get model content
            var document = ContentRepository.Service.Get<DocumentMetadata>(content.ContentLink);
            if (document == null)
            {
                throw new HttpException(404, "Not Found.");
            }

            // Get binary Storable
            var binaryStorable = content as IBinaryStorable;

            //ACCESS CHECK OR DOWNLOAD TRACKING IMPLEMENTED HERE

            //Set caching policy - No cache for this scenario
            context.Response.Cache.SetCacheability(HttpCacheability.NoCache);

            //Get name of file
            var sourceFile = GetSourceFileFromBlobData(document.Name, null, binaryStorable);
            var downLoadFileName = sourceFile.Name;

            if (!string.IsNullOrEmpty(downLoadFileName))
            {
                //Ensure that the file is being downloaded
                context.Response.AppendHeader("Content-Disposition",
                    string.Format("attachment; filename=\"{0}\"", downLoadFileName));
            }

            return binaryStorable?.BinaryData;
        }

        private SourceFile GetSourceFileFromBlobData(string name, byte[] sourceFileBytes, IBinaryStorable binaryStorable)
        {
            var sourceFile = new SourceFile
            {
                Name = name,
                FileBytes = sourceFileBytes
            };

            var blobData = binaryStorable.BinaryData.ToString();
            var idx = blobData.LastIndexOf("/", StringComparison.Ordinal);

            if (idx != -1)
            {
                var ext = blobData.Substring(idx + 1);
                var extIdx = ext.LastIndexOf(".", StringComparison.Ordinal);
                if (extIdx != -1)
                {
                    sourceFile.Extension = ext.Substring(extIdx + 1);
                    if (!sourceFile.Name.Substring(sourceFile.Name.Length - 5).Contains("."))
                    {
                        sourceFile.Name = sourceFile.Name + "." + sourceFile.Extension;
                    }
                }
            }

            return sourceFile;
        }
    }
}

And that is all. With these changes we did not have any more problems downloading files. If you have any questions let me know. I hope it will help someone and as always keep learning !!!

Written by:

Jorge Cardenas

Developer with several years of experience who is passionate about technology and how to solve problems through it.

View All Posts

Leave a Reply