Joel Baxter
Joel Baxter

Reputation: 43

Chrome extension doing a download can't always specify file extension?

I'm trying to make a Chrome extension that thru chrome.downloads.download can initiate a download and specify the filename of the resulting local file -- in particular, specifying the file extension of the downloaded result.

If the server advertises a content-type of application/octet-stream for this file, this works fine.

However if the server advertises some other content-type (for example application/zip), then the download object is set to have that MIME type, and the downloaded file name is forced to have a related extension (for example ".zip") instead of the one I specified.

I've tried using onHeadersReceived to change the content-type in the incoming headers, forcing it to application/octet-stream, but the download object still ends up having the original MIME type and the file extension is still forced.

Using chrome.downloads.onDeterminingFilename to "suggest" my desired filename+extension also does not help prevent the forced extension. I've disabled all other extensions and also checked to see if onActionIgnored ever fires (it does not). I finally added logging for all other webRequest callbacks to see if there's any other behavior that could explain the difference between the successful and failed cases, but this content-type/mimetype issue seems to be the culprit.

This is with an unpacked extension loaded into Chrome 84.0.4147.89 on Linux (elementary OS, an Ubuntu variant). I'll get into more details about the extension code below, but I'm thinking there's probably just something about the flow of how download objects are created which either a) makes doing this impossible or b) means that I need to do it in some other way.

Thanks for any help!

Here's the manifest of my test extension:

{
    "name": "Test DL Rename",
    "version": "1.0",
    "manifest_version": 2,
    "description": "Add a file extension to the name of a download.",
    "background": { 
      "scripts": ["background.js"],
      "persistent": true
    },
    "permissions": [
      "contextMenus",
      "downloads",
      "webRequest",
      "webRequestBlocking",
      "<all_urls>"
    ]
}

Here's the background.js with all the debugging chattiness stripped out:

const CONTEXT_MENU_ID = "TEST_DL_RENAME";

// add our thing to the context menu for links
chrome.contextMenus.create(
  {
    id: CONTEXT_MENU_ID,
    title: "Test DL Rename",
    contexts: ["link"]
  }
);

// test download-renaming by adding ".qz" extension to downloaded file
chrome.contextMenus.onClicked.addListener(
  function(info, tab) {
    if (info.menuItemId !== CONTEXT_MENU_ID) {
      return;
    }
    filename = info.linkUrl.substring(info.linkUrl.lastIndexOf('/') + 1);
    qzFilename = filename + ".qz";
    console.log("specifying name for download: " + qzFilename);
    chrome.downloads.download(
      {
        url: info.linkUrl,
        filename: qzFilename,
        conflictAction: "uniquify"
      },
      function(downloadId) {
        console.log("started download ID " + downloadId);
      }
    );
  }
);

// test setting content-type on a received download
chrome.webRequest.onHeadersReceived.addListener(
  function(details) {
    // if doing this for real, we'd track which URLs we actually want to change
    // for now just change anything that is a zipfile
    if (details.url.split('.').pop() != "zip") {
      return {};
    }
    for (var i = 0; i < details.responseHeaders.length; i++) {
      if (details.responseHeaders[i].name.toLowerCase() == "content-type"){
        console.log("forcing content-type to application/octet-stream");
        details.responseHeaders[i].value = "application/octet-stream";
        break;
      }
    }
    return {
      responseHeaders: details.responseHeaders
    };
  },
  {
    urls: ["<all_urls>"]
  },
  ["blocking", "responseHeaders"]
);

And these are the files I'm using to run my tests currently. If I right-click on "test with octet-stream MIME type" and choosing "Test DL Rename", that results in a download named "test.zip.qz" as desired. Right-clicking on "test with zip MIME type" results in "test-mime.zip.zip" rather than "test-mime.zip.qz".

Upvotes: 4

Views: 2301

Answers (1)

woxxom
woxxom

Reputation: 73566

Looks like an intended restriction in Chrome's handling of downloads to ensure "safety", which you can contest on https://crbug.com by advocating your use case.

Meanwhile, download the blob yourself and change its type:

chrome.contextMenus.onClicked.addListener(async ({linkUrl: url}) => {
  const blob = await (await fetch(url)).blob();
  const typedBlob = blob.type === 'application/octet-stream' ? blob :
    new Blob([blob], {type: 'application/octet-stream'});
  chrome.downloads.download({
    url: URL.createObjectURL(typedBlob),
    filename: url.substring(url.lastIndexOf('/') + 1) + '.qz',
    conflictAction: 'uniquify',
  });
});

P.S. Now that you don't need webRequest API you can use "persistent": false in manifest.json (FWIW there's a way though to use both at the same time by putting webRequest into optional_permissions, see the documentation).

Upvotes: 2

Related Questions