0

I have an API-Server who responds to requests like this:

http://localhost:8080/slim3/public/api/v1/files/Test1.jpg
http://localhost:8080/slim3/public/api/v1/files/Test2.txt
...

If I put such URL into my browser I can get the download prompt. Now I'm struggeling to process the download of a file via jQuery / Ajax. Every thread I found here on Stackoverflow tells me to send back the actual download url and open it via window.location. I don't understand how this is possible when my server already has the file downloaded for me and I just need to "grab" it somehow on the client-side?

It is clear to me that I can't force the download dialog via jQuery / Javascript. I read this in multiple threads here. But the same threads don't tell me how I can get the direct download url. Or do I mix things up here unfortunately?

Here is what I have:

Client (jQuery)

$(document).ready(function(){
   $(document).on('click', '#file', function(e){   
        e.preventDefault();
        var filename = $(this).data('url'); 
        $.ajax({
            type : "GET",
            cache: false,
            url : "http://localhost:8080/slim3/public/api/v1/files/" + filename,   
            success : function(data) {
                console.log(data) // the console writes nothing 
                //window.location = "data:application/octet-stream," + encodeURIComponent(data); // not working            
                //var downloadUrl = data.url; // not working
                //window.location = downloadUrl; // // not working
            },
            error : function(data) {}
        });
   });
});

Server (PHP)

public function show($request, $response, $args)
{               
    $file = 'C:\xampp\htdocs\slim3\storage\Test1.jpg';

    $res = $response->withHeader('Content-Description', 'File Transfer')
               ->withHeader('Content-Type', 'application/octet-stream')
               ->withHeader('Content-Disposition', 'attachment;filename="'.basename($file).'"')
               ->withHeader('Expires', '0')
               ->withHeader('Cache-Control', 'must-revalidate')
               ->withHeader('Pragma', 'public')
               ->withHeader('Content-Length', filesize($file));
    readfile($file);
    return $res;
}

Solution:

Rob pointed me in the right direction. I actually don't need to do an GET Ajax request. So the final jQuery function looks exacty like this and works:

   $(document).on('click', '#file', function(e){   
        e.preventDefault();
        var filename = $(this).data('url'); 
        window.location = "http://localhost:80/slimmi/public/api/v1/files/" + filename;        
   });
Magiranu
  • 299
  • 4
  • 27
  • can you explain what you actually want to do? i don't really get what you're asking. – malifa Aug 10 '17 at 20:55
  • @lexith I'm trying to get the "Save as" dialog on my client which is written in HTML / jQuery. – Magiranu Aug 10 '17 at 21:04
  • Okay but then what's wrong with the suggested solutions that you mentioned? You said that you know you can't do it with ajax. – malifa Aug 10 '17 at 21:06
  • What's wrong is that it doesn't work on the client, meaning there won't come the "Save as" dialog. I probably mixed things up because not it works via the solution provided by Rob. Thanks anyway lexith! – Magiranu Aug 10 '17 at 21:15

3 Answers3

2

In Client,filename variable will be wrong. It should be Test1.jpgor Test2.txt. I think that $(this).data('url'); returns the current url instead of Test1.jpgor Test2.txtnames. Do you try to substract the file name by using:

var url = $(this).data('url');
var filename = url.substring(url.lastIndexOf("/") + 1, url.length);
cjavierbp
  • 36
  • 5
1

Your server just sends back the actual file requested by name in the URL right?

It looks to me like you just need to replace all of the ajax code with

document.location = "http://localhost:8080/slim3/public/api/v1/files/" + filename;

The headers that you set in the PHP will determine whether the browser shows the save dialog or attempts to display the file - those look right.

Rob Ruchte
  • 3,569
  • 1
  • 16
  • 18
1

What you could do if the files are generated on demand is have PHP encode your file in Base64 - like this, setting the appropriate type - and return that to the client. Convert the Base64 to a Blob - you can put Base64 in an anchor's href but IE has a prohibitively small URI size - then create a URL object from that Blob. Among other things this ensures that the data is URL safe. Finally, create an "invisible" anchor tab and click it.

$.ajax({
        type: "GET",
        url: target,
        success: function (response) {
            // create a download anchor tag
            var downloadLink = document.createElement('a');
            downloadLink.target = '_blank';
            downloadLink.download = 'your-file-name-here';

            // convert Base64 to Blob - don't forget to set content type!
            var blob = b64toBlob(response, [file type here]);

            // create an object URL from the Blob
            var URL = window.URL || window.webkitURL;
            var downloadUrl = URL.createObjectURL(blob);

            // set object URL as the anchor's href
            downloadLink.href = downloadUrl;

            // append the anchor to document body
            document.body.appendChild(downloadLink);

            // fire a click event on the anchor
            downloadLink.click();

            // cleanup: remove element and revoke object URL
            document.body.removeChild(downloadLink);
            URL.revokeObjectURL(downloadUrl);
        }
    });

Convert the Base64 to Blob like this - source.

function b64toBlob(b64Data, contentType, sliceSize) {
    contentType = contentType || '';
    sliceSize = sliceSize || 512;

    var byteCharacters = atob(b64Data);
    var byteArrays = [];

    for (var offset = 0; offset < byteCharacters.length; offset += sliceSize) {
        var slice = byteCharacters.slice(offset, offset + sliceSize);

        var byteNumbers = new Array(slice.length);
        for (var i = 0; i < slice.length; i++) {
            byteNumbers[i] = slice.charCodeAt(i);
        }

        var byteArray = new Uint8Array(byteNumbers);

        byteArrays.push(byteArray);
    }

    var blob = new Blob(byteArrays, {type: contentType});
    return blob;
}

This is what I use to download PDF's generated on demand by our Django server and she seems to run pretty darn well.

Addendum

The reason why our website does it this way instead of just returning the file name for a subsequent call is because it's a bit easier on the server I/O. The solution that was chosen means that the requested file has to exist somewhere on the server - most likely on disk. (One might be able to keep the generated file in memory using PHP's tmpfile() but my knowledge of PHP is limited so I do not know how you would keep that file around between HTTP calls).

My project makes big honking PDF's - possibly hundreds of pages. I really, really don't want to have to make an actual file object out of this data, save it to disk, and then almost immediately read it back off the disk (I am aware that that isn't exactly how the server is doing it, but anyway you slice it it's doing more work than necessary). The server has the PDF made, it's in memory, why not just ... give it back to the client?

Returning files like this means that one doesn't need to do any extra clean up work - once the Base64 has left the building, that's it. There's no file on disk so there's nothing that has to be dealt with later (good or bad depending on your needs).

Snowie
  • 223
  • 1
  • 3
  • 8
  • Why do you do that instead of handing out the pdfs directly? i mean besides causing extra traffic. – malifa Aug 10 '17 at 21:19