eligrey / FileSaver.js

An HTML5 saveAs() FileSaver implementation
https://eligrey.com/blog/saving-generated-files-on-the-client-side/
Other
21.42k stars 4.38k forks source link

Can someone show me an reliable solution to download files? #709

Open Paul6552 opened 3 years ago

Paul6552 commented 3 years ago

Can someone please show me a reliable alternative (pure code or a framework (please supported)) with which I can download data under React. There a a ton of frameworks, most of them not really supported anymore (e.g. js-file-download)

I'm a little desperate because in 2021 it will still be difficult to reliably download a file in all browsers.

This framework does not work e.g. in the iMac Apple and I think that the owner of this repository no longer has any desire to maintain this framework.

Right now I am doing workaround over workaround to get the file downloaded:

function getPdfFile( response: MyAxiosResponse ): void {

        if ( response.data.messages.length === 0 ) {
            const binaryString = window.atob( response.data.response.balanceSheetPdf );
            const len = binaryString.length;
            const bytes = new Uint8Array( len );
            for ( let i = 0; i < len; i++ ) {
                bytes[i] = binaryString.charCodeAt( i );
            }
            const file = new Blob( [bytes.buffer], { type: 'application/pdf' } );
            const fileURL = URL.createObjectURL( file );

            if(isIOS()){
                //window.open not working on Apple. Don´t know why. Therefore download the file
                FileSaver.saveAs( file, 'BalanceSheet.pdf' );
            }else{
                window.open( fileURL );
            }
        }
}

I thank you for any reliable solution.

jimmywarting commented 3 years ago

AxiosResponse? You should be using server techniques if possible

https://github.com/eligrey/FileSaver.js/wiki/Saving-a-remote-file#using-http-header

Content-Type: 'application/octet-stream; charset=utf-8'
Content-Disposition: attachment; filename="filename.jpg"; filename*="filename.jpg"
Content-Length: <size in bytes>

window.atob( Do you fetch a pdf as base64 in a json payload? That is not so smart, JSON is not meant to handle binary data

Anyway i would have converted the base64 to a blob using fetch instead... https://stackoverflow.com/a/36183085/1008999 I would also have ditched Axios entirely for only using the fetch api

Paul6552 commented 3 years ago

@jimmywarting Thank you for your quick feedback.

When I saw the Stackoverflow article, I just thought "Hello my old friend" :-D My solution is from Jeremy of this article.

I know it's not very smart to abuse json, but it was the quickest and easiest way to do that back then.

The solution would be with the fetch api -> blob -> FileSaver.saveAs (blob, 'BalanceSheet.pdf') ? Does this work reliably on apple devices as well?

Thanks

jimmywarting commented 3 years ago

It's a reliably way of getting a blob in binary format, saving it can be an entirely different story

Fetch is supported in more newer places such as web workers, Deno, and Node (with node-fetch atm) Axios use either node:http or window.XMLHttpRequest, so it dose not work in Deno or in WebWorkers. And you can't stream data using XMLHttpRequest... + i think it's too bloated and not that cross compatible

Paul6552 commented 3 years ago

@jimmywarting Thank you for your input. Took hours for me to change everything X) But I changed my restendpoint to "Application/octed-stream" and axios to fetch.

Sadly, saving in an iMac Browser is only working with a workaround. This is my code now:

myFetch( 'report/getBalanceSheet',
    {year: balanceSheetYear},
    {
        method: 'get',
        headers: {
            'Authorization': 'Bearer ' + keycloak.token
        }
    })
    .then(async res => {
        // check for error response
        if (!res.ok) {
            // get error message from body or default to response statusText
            const error = res.status + " " + res.statusText;
            return Promise.reject(error);
        }

        const blob = await res.blob();

        setwaitForResponseWithOverlay( false );

        const fileURL = URL.createObjectURL( blob );

        if(safariWindow != null){
            //With FileSaver it is not working, only workaround safariWindow.location.replace
            //FileSaver.saveAs( file, 'BalanceSheet.pdf' );
            safariWindow.location.replace(fileURL);
        }else{
            window.open( fileURL );
        }
    })
    .catch( ( error ) => {
        console.log("There was an error: " + error.toString());
    } );

Any suggestions?

jimmywarting commented 3 years ago

Hmm, so Authorization header is the reason why you are using ajax... the file is protected. with a simple cookie you could just navigate to the file to download it.

Any suggestions?

Was a long time since i had a look at the source of FileSaver, have forgotten how every quirk works, lots of PR seems to have happen since the last time i looked. I always try to steer ppl who tries to save something from the server and have access to it, to use the server to save file rather than using filesaver, or any other downloader lib (like streamsaver or file system access).

have you consider generating a uniq one-time / expiring download link instead? or posting the Authorization token in some other method that don't involve sending it as a header (like ?token= or using a <form>)

Paul6552 commented 3 years ago

Sorry, but that was programmatically too high for me.

  1. Yes, the file is protected. The token refreshes itself every 3 seconds, so a cookie would not work. If I understood you, you would save the token in a cookie.

  2. "Do you consider generating a uniq one-time / expiring download link instead? or posting the Authorization token in some other method that don't involve sending it as a header (like? token = or using a

    ) " Unfortunately that is too high for me. Do you have a Stackoverflow article so that I can read it here? thanks

jimmywarting commented 3 years ago

Unfortunately that is too high for me. Do you have a Stackoverflow article so that I can read it here? thanks

Basically what james are mentioning here: https://stackoverflow.com/a/59363326/1008999 (you also have the vanila js solution from https://stackoverflow.com/a/66078703/1008999) but instead of a jwt token you post the bearer token

you would also have to change the way you authenticate on the backend to. Parse the token from a <form> submission instead of looking for some request header

makc commented 3 years ago

@Paul6552 what about this - I used it few times without complaints

Paul6552 commented 3 years ago

@jimmywarting Thank you, but changing the way how I authenticate is a little bit an overkill for me. But thanks a lot for the suggestion.

@makc I will try your solution, because my solution is not relaible under Apple MAC devices.

Right now my solution is this (not tested on apple devices. I havent a mac or iphone, therefore I have everytime to ask someone else)

function getBalanceSheet(): void {

    const link = document.createElement( 'a' );

    myFetch( 'report/getBalanceSheet',
        {year: balanceSheetYear},
        {
            method: 'get',
            headers: {
                'Authorization': 'Bearer ' + keycloak.token,
                'Content-Type': 'application/json'
            }
        })
        .then(async res => {
            // check for error response
            if ( !res.ok ) {
                // get error message from body or default to response statusText
                const error = res.status + " " + res.statusText;
                return Promise.reject( error );
            }

            const blob = await res.blob();

            if(blob.size === 0){
                printout("Nothing to show");
                return;
            }

            const fileURL = URL.createObjectURL( blob );

            if(isIOS()){
                if ( link.href ) {
                    URL.revokeObjectURL( link.href );
                }

                link.href = URL.createObjectURL( blob );
                link.download = "Balance sheet" || 'data.json';
                link.dispatchEvent( new MouseEvent( 'click' ) );
            } else {
                window.open( fileURL );
            }
        })
        .catch( ( error ) => {
            console.log("There was an error: " + error.toString());
        } );
}

I'll tell you again how it works with Apple devices. (Hope X))