130

I downloaded a file as response of ajax. How to get the file name and file type from content-disposition and display thumbnail for it. I got many search results but couldn't find right way.

$(".download_btn").click(function () {
  var uiid = $(this).data("id2");

  $.ajax({
    url: "http://localhost:8080/prj/" + data + "/" + uiid + "/getfile",
    type: "GET",
    error: function (jqXHR, textStatus, errorThrown) {
      console.log(textStatus, errorThrown);
    },
    success: function (response, status, xhr) {
      var header = xhr.getResponseHeader('Content-Disposition');
      console.log(header);     
    }
});

Console output:

inline; filename=demo3.png
Paul P
  • 3,346
  • 2
  • 12
  • 26
Arun Sivan
  • 1,680
  • 2
  • 12
  • 23
  • What does the console say? – Matthew Dec 02 '16 at 19:13
  • Why are you setting `window.location.href ="http://localhost:8080/prj/" + data + "/" + uiid + "/getfile";`? That will cause the browser to leave the page and just show that URL. How do you expect to display a thumbnail for the image if you have left the page? Why do you need the file name that the server suggests you save the file as in order to generate a thumbnail? – Quentin Dec 02 '16 at 19:16
  • 1
    Getting the file name from the content-disposition is one problem. You can't get the file type from it, at least not reliably, that is what the content-type header is for. The thumbnail display would come from the data and is an entirely separate problem. – Quentin Dec 02 '16 at 19:18
  • i need file name to display near thumbnail. – Arun Sivan Dec 02 '16 at 19:18
  • but i could find file type from filename itself `filename.jpg` – Arun Sivan Dec 02 '16 at 19:20
  • if i could get file name i can get its extension and find if it is an image or not. files are stored in server as base64encoded string in text file.content type is always set to `application/x-msdownload` as single api is handling download of all file types. – Arun Sivan Dec 02 '16 at 19:35

11 Answers11

141

Here is how I used it sometime back. I'm assuming you are providing the attachment as a server response.

I set the response header like this from my REST service response.setHeader("Content-Disposition", "attachment;filename=XYZ.csv");

function(response, status, xhr){
    var filename = "";
    var disposition = xhr.getResponseHeader('Content-Disposition');
    if (disposition && disposition.indexOf('attachment') !== -1) {
        var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
        var matches = filenameRegex.exec(disposition);
        if (matches != null && matches[1]) { 
          filename = matches[1].replace(/['"]/g, '');
        }
    }
}

EDIT: Editing the answer to suit your question- use of the word inline instead of attachment

function(response, status, xhr){
    var filename = "";
    var disposition = xhr.getResponseHeader('Content-Disposition');
    if (disposition && disposition.indexOf('inline') !== -1) {
        var filenameRegex = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/;
        var matches = filenameRegex.exec(disposition);
        if (matches != null && matches[1]) { 
          filename = matches[1].replace(/['"]/g, '');
        }
    }
}

More here

Winter Soldier
  • 2,607
  • 3
  • 14
  • 18
  • 1
    what does `filename = matches[1].replace(/['"]/g, '');` do? – edmundpie Sep 07 '18 at 03:17
  • 7
    This solution will not work with cases like this: **attachment; filename*=UTF-8''filename.txt**. Using this Regex, the filename will be **UTF-8filename.txt**. – Felipe Desiderati Jul 10 '19 at 22:01
  • 7
    I can't believe how knarly and ugly JavaScript is at times ... – Jammer Mar 04 '20 at 10:07
  • 5
    To match utf8 pattern like this also: `dis = "attachment; filename*=UTF-8''filename.pdf"` try `/filename\*?=([^']*'')?([^;]*)/.exec(dis)[2]` – ruuter Oct 07 '20 at 16:30
  • 2
    I think it would be better to use `disposition.startsWith("attachment")` instead of `disposition.indexOf('attachment') !== -1`, since the filename may contain an _attachment_ – foske May 21 '21 at 12:41
  • `(filename\*=UTF-8''|filename=)(.+\b)` – doox911 Nov 19 '22 at 08:58
43

This is an improvement on marjon4's answer.

A much simplified way to the selected answer would be to use split like this;

var fileName = xhr.getResponseHeader('content-disposition').split('filename=')[1].split(';')[0];

Note: This solution may not work as expected if your file name itself contains a semi-colon (;)

Shivam Puri
  • 1,578
  • 12
  • 25
  • Wouldn't this fail if the filename as a semicolon in it? – Mathew Alden Dec 30 '20 at 23:14
  • 7
    This is a naive approach, do not recommend. This could return incorrect results if the filename contains semicolons. Missing support for `filename*=UTF-8''`. – Steven Liekens Feb 18 '21 at 14:44
  • 1
    The solution works fine with ```filename*=UTF-8' '``` format The entire format you probably are referring to goes like this ```content-disposition: "attachment; filename=document.pdf; filename*=UTF-8''document.pdf"```. Please try to understand and answer before saying it won't work! And yes, for all technicality, this solution won't work if the file name itself has semi-colons. But in most cases, the file name doesn't include semi-colons so it has worked fine for me with this pretext in mind! (Still adding a note for that in my answer.) – Shivam Puri Mar 03 '21 at 06:11
  • 2
    Nor will it work for en edge-case when the filename itself contains `filename=`. – Aleksei May 19 '21 at 15:03
  • 2
    I would add .replaceAll('"',''), otherwise it seems to fail with spaced names – Inigo EC Jan 30 '22 at 19:24
28

If you want to get the filename and support both those weird url encoded UTF-8 headers and the ascii headers, you can use something like this

public getFileName(disposition: string): string {
    const utf8FilenameRegex = /filename\*=UTF-8''([\w%\-\.]+)(?:; ?|$)/i;
    const asciiFilenameRegex = /^filename=(["']?)(.*?[^\\])\1(?:; ?|$)/i;

    let fileName: string = null;
    if (utf8FilenameRegex.test(disposition)) {
      fileName = decodeURIComponent(utf8FilenameRegex.exec(disposition)[1]);
    } else {
      // prevent ReDos attacks by anchoring the ascii regex to string start and
      //  slicing off everything before 'filename='
      const filenameStart = disposition.toLowerCase().indexOf('filename=');
      if (filenameStart >= 0) {
        const partialDisposition = disposition.slice(filenameStart);
        const matches = asciiFilenameRegex.exec(partialDisposition );
        if (matches != null && matches[2]) {
          fileName = matches[2];
        }
      }
    }
    return fileName;
}

A couple of notes:

  1. this will take the value of the UTF-8 filename, if set, over the ascii name
  2. on download, your browser may further alter the name to replace certain characters, like ", with _ (Chrome)
  3. the ascii pattern works best for quoted file names, but supports unquoted values. In that case it treats all text after the filename= and before the either the next ; or the end of the header value as the file name.
  4. This does not clean up path information. If you are saving the file from a website, that's the browser's job, but if your using this in the context of a node app or something similar, be sure to clean up the path information per the OS and leave just the filename, or a crafted file name might be used to overwrite a system file (think of a file name like ../../../../../../../path/to/system/files/malicious.dll)

MDN Content Disposition Header

J Scott
  • 781
  • 9
  • 12
  • 1
    The ASCII file name doesn't have to be enclosed in single or double quotes. – MoonStom Jul 30 '21 at 00:26
  • @MoonStom That's a good point, I'll update. – J Scott Jul 30 '21 at 17:27
  • 2
    This is a great answer, I just had to make one change and add `i` to the end of the first regex because my header was coming back as `filename*=utf-8` not `UTF-8`. – Coleman Sep 01 '21 at 13:59
  • 1
    The `dot` and `hyphen` do not need to be escaped in a character set in this instance. So use `[.-]` instead of `[\.\-]` – Steven Spungin Jan 19 '22 at 16:15
  • I didn't know that, but I'm also of the opinion that special characters should be escaped even if they are not special characters in that context; it makes the code more readable, which I favor even outside of Stack Overflow – J Scott Jan 19 '22 at 17:17
  • Note that the ASCII version seems to be vulnerable to a ReDos attack according to https://devina.io/redos-checker. – Patrik Affentranger Apr 11 '22 at 10:13
17

In my case the header looks like this:

attachment; filename="test-file3.txt"

Therefore I was able to extract the filename pretty easily with a named group regexp:

const regExpFilename = /filename="(?<filename>.*)"/;

const filename: string | null = regExpFilename.exec(contentDispositionHeader)?.groups?.filename ?? null;

I know I'm slightly off topic here as OP doesn't have the quotes around the filename but still sharing in case someone comes across the same pattern as I just did

maxime1992
  • 22,502
  • 10
  • 80
  • 121
15

Or simply just:

var fileName = xhr.getResponseHeader('Content-Disposition').split("filename=")[1];
marjon4
  • 332
  • 3
  • 9
3

Try this solution:

var contentDisposition = xhr.getResponseHeader('Content-Disposition');
var startIndex = contentDisposition.indexOf("filename=") + 10; // Adjust '+ 10' if filename is not the right one.
var endIndex = contentDisposition.length - 1; //Check if '- 1' is necessary
var filename = contentDisposition.substring(startIndex, endIndex);
console.log("filename: " + filename)
Andrea Ligios
  • 49,480
  • 26
  • 114
  • 243
Osvaldo Cabrera
  • 327
  • 4
  • 4
1

There is an npm package that does the job: content-disposition

haxpanel
  • 4,402
  • 4
  • 43
  • 71
  • 4
    Seems like it does not work on the browser. – UltimaWeapon Jan 28 '21 at 07:52
  • 1
    And the author of [https://github.com/jshttp/content-disposition/issues/32](content-disposition) doesn't seem to want to remove nodejs deps. Pretty much all the npm packages I could find use at least the `path` package from nodejs. – contrebis Jul 01 '21 at 15:54
  • 1
    There is also [content-disposition-header](https://www.npmjs.com/package/content-disposition-header) which works with both browsers and Node.js – fservantdev Feb 07 '22 at 12:31
1

The below also takes into account scenarios where the filename includes unicode characters (i.e.,-, !, (, ), etc.) and hence, comes (utf-8 encoded) in the form of, for instance, filename*=utf-8''Na%C3%AFve%20file.txt (see here for more details). In such cases, the decodeURIComponent() function is used to decode the filename.

const disposition = xhr.getResponseHeader('Content-Disposition');
filename = disposition.split(/;(.+)/)[1].split(/=(.+)/)[1]
if (filename.toLowerCase().startsWith("utf-8''"))
    filename = decodeURIComponent(filename.replace("utf-8''", ''))
else
    filename = filename.replace(/['"]/g, '')

If you are doing a cross-origin request, make sure to add Access-Control-Expose-Headers: Content-Disposition to the response headers on server side (see Access-Control-Expose-Headers), in order to expose the Content-Disposition header; otherwise, the filename won't be accessible on client side through JavaScript. For instance:

headers = {'Access-Control-Expose-Headers': 'Content-Disposition'}
return FileResponse("Naïve file.txt", filename="Naïve file.txt", headers=headers)
Chris
  • 18,724
  • 6
  • 46
  • 80
1

I believe this will help!

let filename = response.headers['content-disposition'].split('filename=')[1].split('.')[0];
let extension = response.headers['content-disposition'].split('.')[1].split(';')[0];
Agostinho Tinho
  • 129
  • 1
  • 3
  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Mar 10 '22 at 20:36
0

There's also library content-disposition-attachment, which can be used in the browser:

npm i -D content-disposition-attachment
import { AxiosResponse } from "axios";
import { parse } from "content-disposition-attachment";

const getFilenameFromHeaders = ({ headers }: AxiosResponse<Blob>) => {
  const defaultName = "untitled";
  try {
    const { attachment, filename } = parse(headers["content-disposition"]);
    return attachment ? filename : defaultName;
  } catch (e) {
    console.error(e);
    return defaultName;
  }
};
keemor
  • 1,149
  • 15
  • 16
-1

If you are not working with multipart body then you can use this function. It extracts the filename from the Content-Disposition header value (string like: inline; filename=demo3.png) and decodes as needed.

const getFileNameFromContentDisposition = disposition => { 
    if (disposition
        && (disposition.startsWith('attachment') || disposition.startsWith('inline'))
    ) {
        let filename = disposition.startsWith('attachment')
            ? disposition.replace("attachment;", "")
            : disposition.replace("inline;", ""); //replaces first match only
        filename = filename.trim();
        if (filename.includes("filename*=") && filename.includes("filename=")) {
            let filenames = filename.split(";"); //we can parse by ";" because all ";"s inside filename are escaped
            if (filenames.length > 1) { //"filename=" or "filename*=" not inside filename
                if (filenames[0].trim().startsWith("filename*=")) { //"filename*=" is preferred
                    filename = filenames[0].trim();
                } else {
                    filename = filenames[1].trim();
                }
            }
        }
        if (filename.startsWith("filename*=")) {
            filename = filename.replace("filename*=", "")
            .split("''").slice(1).join("''"); //remove encoding and ''
            filename = decodeURIComponent(filename);
        } else if (filename.startsWith("filename=")) {
            filename = filename.replace("filename=", "")
            if (filename.startsWith('"') && filename.endsWith('"')) {
                filename = filename.slice(1, filename.length - 1); //remove quotes
            }
        }
        return filename;
    }
}

The result of the function can be split into name and extension as follows:

let name = getFileNameFromContentDisposition("inline; filename=demo.3.png").split(".");
let extension = name[name.length - 1];
name = name.slice(0, name.length - 1).join(".");
console.log(name); // demo.3
console.log(extension); //png

You can display thumbnail, for example, using svg:

let colors = {"png": "red", "jpg": "orange"};
//this is a simple example, you can make something more beautiful
let createSVGThumbnail = extension => `<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" viewBox="0 0 18 20">
    <rect x="0" y="0" width="18" height="20" fill = "#FAFEFF"/>
    <rect x="0" y="7" width="18" height="6" stroke="${colors[extension] || "blue"}" fill = "${colors[extension] || "blue"}"/>
    <text stroke = "white" fill = "white" font-size = "6" x = "0" y = "12.5" textLength = "18">${extension.toUpperCase()}</text>
</svg>`;

...

//You can use it as HTML element background-image
let background = "data:image/svg+xml;base64," + btoa(new TextDecoder().decode(createSVGThumbnail("png"))); 
foske
  • 86
  • 1
  • 3
  • 10
  • 1
    You should check if there are any quotes before you remove them. Even your own example disposition header doesn't use any quotes. Also it's common for the disposition header to include both "filename=" AND "filename=*" for backwards compatibility. – AnorZaken Sep 10 '21 at 09:27
  • Thanks for the comment! Indeed, I did not consider everything. Corrected answer – foske Sep 10 '21 at 17:26