How do I save files to users fs automatically?



My Electron app does only one thing - well meant to.

User inputs a date range then clicks a button that posts a request to a web service which responds with any amount of xml files. These files need to be saved to the user’s file system. (thats it - no more)

Currently I am using the fs WriteFile which is fine - but have to first make the post request on a user onclick then another onclick to save to user’s fs.

What I need is for all functionality in be in one button click and no file dialogs showing during file download process.

The idea is the user can go off and do what ever else they need to do and let the app get on with retreiving data.

I am not sure about this createWriteStream. I have tried this but it is saving to the actual application as well is the users fs which is really silly.

The downLoadIItem module is not an option as it expects a url as a parameter.

Or am I not seeing the wood for the trees?

Any help appreciated:/


I may be missing the point somewhere, but surely instead of opening a Save As dialog so they can choose where to save it, you simply use the standard NodeJS file writing functions to write to *.xml files on a calculated file path.

If it needs to be in one of the special user folders then you can use Electron’s app.getPath() function to know the path for it.

Of course if the user still needs to be able to control which folder the files go in then you can do that through a configuration file or UI input etc.


Thank for that. I will try this out. I have had too much tunnel vision.


Thanks, I got that working in the renderer - like this.
const path = require('path');
const remote = require('electron').remote
const app =;

var xmlData = app.getPath('downloads');
this.path = path.join(xmlData +'/'+'downloads/'+ startDate +'_'+ endDate +'P'+ pageNumber + '.xml');
fs.writeFile(this.path, body);

body is the POST request response


Looks good to me. Correct me if I’m talking nonsense, but my understanding of the path.join() function is that it determines the OS specific path separators automatically for you. So usually you would do it with multiple parameters like this:

this.path = path.join(xmlData, 'downloads', startDate +'_'+ endDate +'P'+ pageNumber + '.xml');

Also I’m not sure why you would create a “downloads” folder inside the user’s Downloads folder, but that’s your business. For tidier (more anal) semantics I would have named the subfolder after your app name or whatever is appropriate for the data they are getting.

Anyway, congrats on figuring it out! :slight_smile:


Yes I thought this is strange but I was getting an error (NaN) if I left ‘downloads’ and ‘/’ out for some reason. This does not create a further downloads folder inside downloads. mmme.

Yes you are right about the subfolder - I haven’t decided really what this will be.

The file name is very important as it is based on the user date range selection and will be distinct from other file names as it will have the page number added.

Basically the app calls an API that uses pagination. The first page starts at 0 and all pages return a maximum result set of 300 results.

If the user changes the date range the page number resets to 0.


I’ve seen weirdness like that before, but I couldn’t replicate it in your example. When it happened to me before it was because I was using an expression that made JS try to implicitly convert a string into number, resulting in NaN because the string didn’t contain a number.

If startDate is a number then perhaps converting it to string will help:

this.path = path.join(xmlData, startDate.toString() + '_' + endDate + 'P' + pageNumber + '.xml');

Or a shorter and lazier method which might work is to put an empty string at the beginning of the expression:

this.path = path.join(xmlData, '' + startDate + '_' + endDate + 'P' + pageNumber + '.xml');


Thanks. Have tried your suggestions but still get error. The dates are strings. There is validation (calculations) and formatting with moment.js prior to sending of the POST soap request - so perhaps the issue lies there somewhere. I will keep working on it. By the way what is the best method to put the POST request in a loop. There will be automatic calls to the web service (I will have to keep sending the soap request envelope with parameters) until the response data is less than a certain length.


It’s difficult to say without knowing it precisely, but it sounds like I would create a function which looks at the current state of things and issues another POST request as appropriate.

function download_processor()
    if(current_request_finished === false)return;
    if(process_finished === false)do_next_request();
    else stop_processing();

If this is taking place in HTML JS (i.e. not preloaded) then you’ll have to use setTimeout() or setInterval() to keep triggering the function until the job is done. If this is in Node’s JS or in window’s preloaded JS then you can use Node’s setImmediate() and clearImmediate() to trigger it on each cycle of Node’s event loop - Node also does the timeout and interval variants if you prefer those.

Which way you do it depends on whether it makes sense to put the workload on the Node process or the Chromium one. If doing it in the Node process then it might be worth still running the web requests through Chromium with Electron’s net API in case the user has internet access through a proxy etc.

Best to keep everything asynchronous in the processing functions.


Thanks for that. I have it working in the sandbox environment so far with the following. I think your code is more robust though, so will also try this way.
... });//res.on(data)ends res.on('end', () => { document.getElementById('response').innerHTML = " "; var getNextPage = setTimeout(myFunction, 3000); if(total_length <= 486){ //empty soap envelope in bytes alert('No more data in this date range. Choose another date range', 'Get Data'); clearTimeout(getNextPage); } ...


Looks fine to me, but you can try different ways to see what works best for you.

The only change I would make to that is to move the setTimeout to the if statement so you don’t need to set it and then immediately clear it:

var getNextPage;
if(total_length <= 486){ //empty soap envelope in bytes
alert('No more data in this date range. Choose another date range', 'Get Data');
else getNextPage = setTimeout(myFunction, 3000);

If no other line is referencing the timeout then you could get rid of the variable:

if(total_length <= 486){ //empty soap envelope in bytes
alert('No more data in this date range. Choose another date range', 'Get Data');
else setTimeout(myFunction, 3000);


Ah yes, much easier to read. And yes I may try some other way as I have tried in production for first time. Downloaded 10 pages from one date range at about 400,000 to 410,000 bytes each, took 8 mins (including writing to a file) - a bit too long.