Export StorageArea.get() using downloads.download()

Hi,

I know I can use downloads.download() to provide users with a way to “export” addon settings in the StorageArea to their local filesystem. But I’m unable to find any examples or see how to do this.

Any suggestions?

thanks,
eric

Hey Eric,

I copied a piece of code i use in VDH: https://pastebin.com/90BJWHsY

In my case, i create an empty file because i am only interested in getting the full path and reserving the unique file name, but you can change the line:

var file = new File(new Array(),fileName);

to put some actual content in your file.
Note that this code is a bit more complicated than what you need if you only want to support Firefox. Chrome does things differently.

Hope this helps.
/mig

EDIT: this just addresses exporting the data to a file, not getting data from the storage area which should be simpler.

Mig, thanks! Hope you are well.

Of course I would like to support Chrome, too, so thank you for the complete example. The call to browser.downloads.search() is strange. Why do that?

I call browser.downloads.search because browser.downloads.download returns, through a Promise, a downloadItemId, but what i’m really interested in is the field filename of the downloadItem which contains the full path to the chosen file.

It is interesting to note that on Firefox, browser.downloads.download resolves after the user has chosen the file (assuming field saveas is true), so you need to call browser.downloads.search to get the details. On Chrome, browser.downloads.download resolves immediately, and you get a notification with downloadItem details through the listener when the file name is chosen by the user.

You should also keep in mind that on Firefox, if the user cancels the SaveAs dialog, browser.downloads.download rejects the promise, while in Chrome, you receive an event with an error.

Those differences make the code more complicated than what it should be. If you are only interested in resolving the promise once the file has been written, you can simplify the whole thing by just installing a downloads.onChanged listener and wait for a change where your downloadItem state is “complete” (details.state && details.state.current === "complete")

Something like this should do it (both Chrome & Firefox):


function SaveFile(fileName, content) {
  return new Promise((resolve, reject) => {
    var file = new File(new Array(content), fileName);
    browser.downloads.download({
      url: URL.createObjectURL(file),
      conflictAction: "uniquify",
      filename: fileName,
      saveAs: true,
    })
      .then((downloadId) => {
        function Listener(details) {
          if (details.id == downloadId)
            if (details.error)
              resolve(false);
            else if (details.state) {
              if (details.state.current != "in_progress") {
                browser.downloads.onChanged.removeListener(Listener);
                resolve(details.state.current == "complete");
              }
            }
        }
        browser.downloads.onChanged.addListener(Listener);
      })
      .catch((err) => {
        resolve(false);
      });
  });
}

SaveFile("test.txt", "Toto a faim")
  .then((result) => {
    console.info("SaveFile ok", result);
  })
  .catch((err) => {
    console.info("SaveFile ko", err);
  });

Check one of my addons with Export feature (e.g Edit, FoxyImage, Historia, etc)

I have a modular code that I copy/paste into all of them. :wink:

Thank you! I get this error on the Blob constructor:

TypeError: Argument 1 of Blob.constructor can’t be converted to a sequence

Here is my code:

getAllSettings().then((settings) => {
  let blob = new Blob(JSON.stringify([settings], null, 2), {type : 'text/json'});
  let filename = "some-settings.json";
  chrome.downloads.download({
    url: URL.createObjectURL(blob),
    filename,
    saveAs: true,
    conflictAction: 'uniquify'
  });
});

Here is the output of JSON.stringify([settings], null, 2).

I’ve used database blobs before but not webAPI blobs before. Any ideas what I’m doing wrong? I’m also slightly confused at the syntax for filename. I thought you’d need a keyName/colon prefix like for url, saveAs, and conflictAction.

Thanks,
Eric

I believe the first argument of new Blob(...) should be an array, so you should try:

let blob = new Blob([JSON.stringify([settings], null, 2)], {type : 'text/json'});

Indeed, you are right. I made the 1st arg to JSON.stringify an array instead. But now I get a different error. chrome.downloads is not defined; yeah, i know that. Ok, so I change chrome to browser but then get browser.downloads is undefined. This is Firefox 58 (nightly). I know browser is defined in this module because i’m using it elsewhere in the same module. It’s an options module. What gives?

You should use the webtension-polyfill library, then you can use browser everywhere. All Chrome API calls like chrome.api.fnt(...args,callback) should then be written browser.api.fnt(...args) with the function returning a Promise instead of invoking the callback.

OK, but I dont think this is related to polyfill use. I’m in Firefox and using the browser object, not Google chrome or the chrome object. Right now, this is strictly a Firefox-only addon.

Sorry, i misread your issue.

Your problem may be that you did not include downloads in the manifest.json permissions.

The error is here. You are passing a string while the blob wants array.

let blob = new Blob(JSON.stringify([settings], null, 2),.......

Try this:

getAllSettings().then((settings) => {
  
  let data = JSON.stringify(settings, null, 2); 
  let blob = new Blob([data], {type : 'text/json'});
  let filename = 'some-settings.json';
  chrome.downloads.download({
    url: URL.createObjectURL(blob),
    filename,
    saveAs: true,
    conflictAction: 'uniquify'
  });
});

Or if you prefer to combine them:

getAllSettings().then((settings) => {

  let blob = new Blob([JSON.stringify(settings, null, 2)], {type : 'text/json'});
  let filename = 'some-settings.json';
  chrome.downloads.download({
    url: URL.createObjectURL(blob),
    filename,
    saveAs: true,
    conflictAction: 'uniquify'
  });
});

I also had to find this by trial and error at the time. :wink:

Have you added “downloads” to permissions?

Where are you running the code? The API is not available in content scripts.

Yes! Thank you so much.