8

I store user uploaded files in Media Library, which can later be downloaded by admins. When saving files, I change the file names to guarantee uniqueness, but later when admins download them I want them to see the original or user friendly file name.

This is the line of code behind the download button:

HttpContext.Current.Response.Redirect(MediaManager.GetMediaUrl(mediaItem));

Sitecore uses media item name and the extension stored in Extension property to construct the default file name. It does not use the display name, nor the title or any other field.

I know that the file name is reflected in the content-disposition header and looks like attachment;FileName.doc. I want to know if there is any way I can modify that header in the response of the file download so I can have a chance to include my own logic.

I know, I can create my own custom download handler, but my specific question is regarding intercepting media download requests in Sitecore, which is the preferable method.

4

It used to be possible to add a processor in the getMediaStream pipeline and then set Content-Disposition=true header in earlier versions of Sitecore but there have been some changes in the latest versions meaning that technique no longer works as expected.

Instead, you should add a handler to media:request event instead and handle setting the correct headers and sending the file response back there.

public class DownloadProcessor
{
    public void OnMediaRequest(object sender, EventArgs args)
    {
        if (!Sitecore.MainUtil.GetBool(Sitecore.Web.WebUtil.GetQueryString("download"), false))
            return;

        ForceMediaDownload(request);
    }

    private void ForceMediaDownload(MediaRequest request)
    {
        var mediaItem = MediaManager.GetMedia(request.MediaUri).MediaData.MediaItem;

        var response = request.InnerRequest.RequestContext.HttpContext.Response;
        response.Clear();
        response.ContentType = mediaItem.MimeType;
        response.Headers.Set("Content-Disposition", "attachment; filename=" + GetFileName(mediaItem));
        response.StatusCode = (int)HttpStatusCode.OK;
        response.BufferOutput = true;
        mediaItem.GetMediaStream().CopyTo(response.OutputStream);
        response.Flush();
        response.End();
    }

    private static string GetFileName(MediaItem mi)
    {
        // or whatever logic you need to determine the file name
        return ("{0}.{1}".FormatWith(mi.Name, mi.Extension)).Replace(" ", "-");
    }
}

And then patch the handler in.

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <events>
      <event name="media:request">
        <handler type="Sitecore.Custom.Pipelines.DownloadProcessor, Sitecore.Custom" method="OnMediaRequest"/>
      </event>
    </events>
  </sitecore>
</configuration>

Now to force download of any media item, ensure that the URL includes the parameter download=1.

From your code snippet in your question, you can use this:

string mediaURL = WebUtil.AddQueryString(MediaManager.GetMediaUrl(mediaItem), "download", "1");
HttpContext.Current.Response.Redirect(mediaURL);

I have previously written a blog post on forcing download of media items which contains more details and expanded code sample, including error checking: https://jammykam.wordpress.com/2015/11/16/force-download-of-media-files/

  • 2
    Thanks, your solution definitely works, but the downside is that it is bypassing the checks and logic implemented in Sitecore to handle file download. For example, resuming download or multi-thread download using HTTP range headers. At first, I tried to use your code, but only inject the Content-Disposition header. It did not work since after my handler ran, Sitecore added the header again. So it made me dive deeper and find the code that actually raises that event, which is in MediaRequestHandler as suggested by @Richard. I ended up inheriting from that class to override the behavior. – Maziar Rezaei Sep 28 '16 at 14:42
4

Here is the my final solution after the leads from other answers here. I use the media:request event along with the lesser known AddOnSendingHeaders method on Request object that basically gives me a final chance to change headers before IIS sends the request to the client.

public class ChangeFileName
{
    public void OnMediaRequest(object sender, EventArgs args)
    {
        SitecoreEventArgs eventArgs = args as SitecoreEventArgs;

        MediaRequest mediaRequest = eventArgs?.Parameters?[0] as MediaRequest;
        if (mediaRequest == null)
        {
            return;
        }

        MediaItem mediaItem = MediaManager.GetMedia(mediaRequest.MediaUri).MediaData.MediaItem;
        if(mediaItem == null)
        {
            return;
        }

        HttpRequest request = mediaRequest.InnerRequest;

        request.RequestContext.HttpContext.Response.AddOnSendingHeaders(context =>
        {
            if (CheckFileType(mediaItem))
                context.Response.Headers["Content-Disposition"] = FixFileName(context.Response.Headers["Content-Disposition"]));
        });
    }
}

Then of course, I had to add the event handler to the config:

<event name="media:request">
    <handler type="MyAssembly.ChangeFileName, MyAssembly" method="OnMediaRequest"/>
</event>
  • Nice. Much cleaner than the original answer you provided. – jammykam Sep 28 '16 at 20:26
2

If you wanted to do that in a pipeline, you should look at the getMediaStream pipeline. This processes the media for the Sitecore media handler. You could add a new processor in there to set the content-disposition header.

Sitecore currently sets headers in a protected method called SendMediaHeaders in the MediaRequestHandler. This method:

/// <summary>Sets the headers.</summary>
/// <param name="stream">The stream.</param>
/// <param name="context">The context.</param>
protected virtual void SendStreamHeaders(MediaStream stream, HttpContext context)
{
  stream.Headers.CopyTo(context.Response);
}

copies the headers from the MediaStream to the response. So in your processor, you should be able to add a header to the GetMediaStreamPipelineArgs.OutputStream and they will be added to your response.

Bare in mind tho, this will not just affect download links, but also anywhere that a media item is downloaded to the browser (images etc...)

  • Thanks for your answer, however that pipeline does not appear to be executed when downloading a non-image file, a PDF file in my test. Looking at the MediaRequestHandler, I cannot ascertain if that handler is being triggered at all. – Maziar Rezaei Sep 26 '16 at 19:47
  • The media URL is: /-/media/files/filename.pdf – Maziar Rezaei Sep 26 '16 at 20:02
  • Another thing that I found is that for the non-image files that I was using, the SendMediaHeaders method did not add any headers and basically handed off all the work to a helper class. – Maziar Rezaei Sep 28 '16 at 14:39
0

If I understand you correctly, you're trying to set the filename when you click on the Download link in Media field on an item in the media library?

Media item with download link

Let me know if I've misunderstood, and what you're after is somewhere else.

If this is the case, the URL of the download link is in the format http://yoursite/sitecore/shell/download.aspx?file=A17A048DD4214081A5591B061E1E433A

The relevant code is <your_install>\sitecore\shell\download.aspx and you'll want to decompile and have a look at Sitecore.Shell.DownloadPage in Sitecore.Client.dll.

Unfortunately it doesn't look like this is using any pipelines that you can hook into, so you will have to replace the download.aspx page with your own, that uses your own custom class behind the scenes. If your class inherits from Sitecore.Shell.DownloadPage, you can overwrite the WriteMediaItem method to customise your file name.

The default (decompiled) version of this (SC8.1) looks like:

protected void WriteMediaItem(MediaItem mediaItem)
{
  Assert.ArgumentNotNull((object) mediaItem, "mediaItem");
  Stream mediaStream = mediaItem.GetMediaStream();
  string str = mediaItem.Extension;
  if (!str.StartsWith(".", StringComparison.InvariantCulture))
    str = "." + str;
  this.WriteCacheHeaders(mediaItem.Name + str, mediaStream.Length);
  WebUtil.TransmitStream(mediaStream, this.Response, Settings.Media.StreamBufferSize);
}

Where you can see it uses the mediaItem.Name concatenated with the mediaItem.Extension to build the attachment filename (used in the private WriteCacheHeaders method).

You can replace this protected method with your own logic. As this is a Sitecore file, you should take care when upgrading to ensure that your solution still works and that you track your changes (since the upgrade may overwrite or delete your Sitecore files).

  • Thanks Jason for the answer, but I am constructing the URLs using MediaManager.GetMediaUrl() which returns something like /-/media/files/filename.pdf to use within the website and it does not use download.aspx – Maziar Rezaei Sep 28 '16 at 14:29

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.