Arun Sivan
Arun Sivan

Reputation: 1810

How to get file name from content-disposition

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

Upvotes: 142

Views: 220949

Answers (12)

cryss
cryss

Reputation: 4509

Somehow I found all the other answers either too complex or not prioritizing the UTF-8 file name. So here is my answer:

function extractFileNameFromContentDisposition(disposition) {
  const props = disposition.split(';').map(prop => prop.trim().split('='));
  
  const encodedFileName = props.find(prop => prop[0] === 'filename*');
  const encodingMark = "utf-8''";

  if (encodedFileName && encodedFileName[1].toLowerCase().startsWith(encodingMark)) {
    return decodeURIComponent(encodedFileName[1].substring(encodingMark.length));
  }
  
  const regularFileName = props.find(prop => prop[0] === 'filename');
  
  if (regularFileName) {
    return regularFileName[1].replace(/['"]/g, '');
  }
  
  return null;
}

Upvotes: 0

Chris
Chris

Reputation: 34560

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 and this related answer 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''/i, ''));
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)

Upvotes: 2

Simon Schürg
Simon Schürg

Reputation: 2263

A JavaScript/TypeScript solution that should cover most edge cases. Prefers filename*= over filename= by sorting.

/**
 * https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition
 */
parseFilenameFromContentDispositionHeader(contentDisposition) {
    const filenameField = contentDisposition
        .split(';')
        .map((x) => x.trim())
        .filter((x) => x.startsWith('filename'))
        .sort()?.[0];
    const rightPart = filenameField?.split('=')?.at(-1).replaceAll('"', '');
    if (filenameField?.startsWith('filename=')) {
        return rightPart;
    } else if (filenameField?.startsWith('filename*=')) {
        return decodeURIComponent(rightPart?.split("'")?.at(-1));
    }
    return null;
}

Upvotes: 0

keemor
keemor

Reputation: 1279

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;
  }
};

Upvotes: 2

J Scott
J Scott

Reputation: 1029

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

Upvotes: 39

Agostinho Tinho
Agostinho Tinho

Reputation: 159

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];

Upvotes: 1

foske
foske

Reputation: 108

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"))); 

Upvotes: -1

Shivam Puri
Shivam Puri

Reputation: 1666

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 (;)

Upvotes: 49

maxime1992
maxime1992

Reputation: 23813

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

Upvotes: 19

marjon4
marjon4

Reputation: 358

Or simply just:

var fileName = xhr.getResponseHeader('Content-Disposition').split("filename=")[1];

Upvotes: 14

Osvaldo Cabrera
Osvaldo Cabrera

Reputation: 323

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)

Upvotes: 1

Winter Soldier
Winter Soldier

Reputation: 2695

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

Upvotes: 149

Related Questions