3

I have this little problem with downloading my xlsx-file. I am sending my request for the file over jquery Ajax and on the backend the data is correctly collected and assembled to a xlsx-file. So now on its way back to the frontend i am setting all the headers in preparation to force download the file, but the download never starts.

These are the response headers of my request:

Connection      Keep-Alive
Content-Disposition attachment; filename="export.xlsx"
Content-Length  346420
Content-Type    application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
Date            Mon, 23 Nov 2015 13:23:30 GMT
Keep-Alive      timeout=5, max=91
Server          Apache/2.4.16 (Win32) OpenSSL/1.0.1p PHP/5.6.12
Set-Cookie      <cookiesettings>
content-transfer-encoding   binary
x-powered-by    PHP/5.6.12

imho the download should start immediately, but nothing happens.

EDIT: Until now I used a form submit, but the data amount is really big so the time which is needed to assemble the file is also really long and sometimes a couple of minutes or even an hour, so this was no longer possible.

So I built a java-job to build the file and startet an ajax snippet which asks for completion every second or so.

So here is my Code. Frontend: This is called on button-click

download: function (type, maximum) {
        var
            self = this,
            oParams = this.oTable.oApi._fnAjaxParameters(this.oTable.fnSettings()),
            aoPost = [
                { 'name': 'exportType', 'value': type },
                { 'name': 'exportMax', 'value': maximum },
                { 'name': 'handleId', 'value': self.options.handleId }
            ],
            nIFrame, nContentWindow, nForm, nInput, i
        ;

        // Call a self made function to get extra search parameters
        // without call an data update AJAX call.
        self.oTable.fnSettings().addAdditionalSearchData(oParams);

        // Create an IFrame to do the request
        nIFrame = document.createElement('iframe');
        nIFrame.setAttribute('id', 'RemotingIFrame');
        nIFrame.style.border = '0px';
        nIFrame.style.width = '0px';
        nIFrame.style.height = '0px';

        document.body.appendChild(nIFrame);
        nContentWindow = nIFrame.contentWindow;
        nContentWindow.document.open();
        nContentWindow.document.close();

        nForm = nContentWindow.document.createElement('form');
        nForm.className = 'export-table';
        nForm.setAttribute('method', 'post');

        // Add POST data.
        var formData = {};
        for (i = 0; i < aoPost.length; i++) {
            nInput = nContentWindow.document.createElement('input');
            nInput.setAttribute('name', aoPost[ i ].name);
            nInput.setAttribute('type', 'text');
            nInput.value = aoPost[ i ].value;
            nForm.appendChild(nInput);
            formData[aoPost[ i ].name] = aoPost[ i ].value;
        }

        // Add dataTables POST.
        for (i = 0; i < oParams.length; i++) {
            nInput = nContentWindow.document.createElement('input');
            nInput.setAttribute('name', oParams[ i ].name);
            nInput.setAttribute('type', 'text');
            nInput.value = oParams[ i ].value;
            nForm.appendChild(nInput);
            formData[oParams[ i ].name] = oParams[ i ].value;
        }

        nForm.setAttribute('action', '/service/exportTableData');

        // Add the form and the iFrame.
        nContentWindow.document.body.appendChild(nForm);

        // Send the request.
        //nForm.submit();


        // Send the request.
        var form = $(nContentWindow.document.body).find('form.export-table');

        var jobId = 0;

        form.ajaxForm(
            {
                'showMessagesOnSuccess': false
            },
            {
                'getData': function () {
                    return formData;
            }
            }
        ).data('ajaxForm').submit();
    }

The Ajax request on submit:

$.ajax({
    type: 'POST',
    url: self.handler.getServiceUrl(),
    timeout: GLOBALS.AJAX_REQUEST_TIMEOUT,
    cache: false,
    data: (<get the Data>)
    ,
    success: function (response) {
        if (response.success === true) {
            // Check if we have to wait for a result.
            if (response.jobId !== undefined && response.jobId !== 0) {
                self.checkJobStatus(response.jobId);
            } else {
                <success - show some messages>
            }
        } else {
            self.handler.error(response);
        }
    },
    error: function () {
        <Show error Message>
    }
});

The CheckJobStatus:

checkJobStatus: function (jobId) {
    var self = this;
    $.ajax({
        type: 'POST',
        timeout: GLOBALS.AJAX_REQUEST_TIMEOUT,
        cache: false,
        data: { 'jobId': jobId },
        url: self.handler.getServiceUrl(),
        success: function (response) {
            if(response !== null && response.data !== undefined) {
                if (response.data.isFinished === true) {
                    if (response.success === true) {
                        // Check if we have to wait for a result.
                        self.handler.success(response);
                    } else {
                        self.handler.error(response);
                    }
                } else if (response.success === true && response.data !== null) {
                    setTimeout(
                        function () {
                            self.checkJobStatus(jobId);
                        },
                        500
                    );
                } else {
                    Helper.logFrontendError();
                }
            } else if (response !== null && response.success === true) {
                setTimeout(
                    function () {
                        self.checkJobStatus(jobId);
                    },
                    1000
                );
            } else {
                    Helper.logFrontendError();
            }
        },
        error: function (response) {
                Helper.logFrontendError();
        }
    });
}

Backend - php:

(...)
if ($action == 'exportTableData' || $action == 'exportChartData') {
            $responseData = $service->execute();
            if(isset($responseData->data['contentType']) && $responseData->data['contentType'] != null && isset($responseData->data['data'])) {
                $this->sendTextData($responseData->data['contentType'], $responseData->data['data']);
            } else {
                $this->sendJsonData($responseData);
            }
        } else {
            $this->sendJsonData($service->execute());
        }
(...)


private function sendTextData($contentType, $data) {
    $this->set('filename', 'export.xlsx');
    $this->set('data', $data);
    $this->response->type($contentType);
    $this->render('/Layouts/excel', 'excel');
}


(...)
$handlerResult = new HandlerResult();

    if($dataServiceResult == null) {
        $service = new DataService();
            $dataServiceResult = $service->exportTableData(
                    $controller->Auth->User('id'),
                    json_encode($request->data),
                    null
            );
    } else {
        if ($dataServiceResult->header->resultKey == 0) {
            $handlerResult->wsData['data'] = $dataServiceResult->data;
            $handlerResult->wsData['contentType'] = $dataServiceResult->contentType;
        }
    }
    $handlerResult->wsResultHeader = $dataServiceResult->header;
    return $handlerResult; // ++++ this result returns to the first codeblock in this section ++++

Backend - java - This is where the File is assembled:

(...)
if (jobId > 0) {
            FrontendJobStatus status = FrontendJobQueue.getJobStatus(context.userId, jobId);
            this.result = (WSExportTableDataResult) status.getResult();
            logger.info((this.result.data == null) ? "ByteArray is EMPTY" : "ByteArray is NOT EMPTY");
        } else {
            this.jobId = FrontendJobQueue.addJob(this.context.userId, new ExportTableDataJob(this.context, this.postData));
            this.result.header.jobId = this.jobId;
        }
(...)

The Jop:
<Workbook assembly>
ByteArrayOutputStream out = new ByteArrayOutputStream();
wb.write(out);
this.result.data = out.toByteArray();
        this.result.contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
        // this.result.contentType = "application/vnd.ms-excel";

        this.result.setResultHeader(APIConstants.RESULT_SUCCESS);

Layout/excel:

<?php
header('Content-Disposition: attachment; filename="'.$filename.'"');
header('Content-Transfer-Encoding: binary');
ob_clean();
echo $data;

EDIT 2: So I tried to open a new window on success with the Data, and i could start the download, but the file ist no valid xlsx File anymore.

var reader = new FileReader();
    var blob = new Blob([response.responseText], { type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" });
    reader.readAsDataURL(blob);

    reader.onloadend = function (e) {
        window.open(reader.result, 'Excel', 'width=20,height=10,toolbar=0,menubar=0,scrollbars=no', '_blank');
    }

Any Ideas?

Jan Wedel
  • 31
  • 1
  • 5
  • 1
    show us some code, your front end code + jquery ajax request and the back end code which preps the file and then streams it out to the jquery. – Dave Nov 23 '15 at 13:59
  • You cannot download a file over ajax. Just do a normal form submit. As above, going to need to see your code to help any further – Steve Nov 23 '15 at 14:02
  • hint , you cannot force a download, only stream to the browser with the intent to download (the browser may decide to open it instead) aside from that, give us code – Elentriel Nov 23 '15 at 14:11
  • 1
    Possible duplicate of [Handle file download from ajax post](http://stackoverflow.com/questions/16086162/handle-file-download-from-ajax-post) – Rulisp Nov 23 '15 at 14:27
  • This may be a bit of a duplicate, but i updated the question. There is a way to start the download, but the file i get is corrupt and without a prper filename. thats no problem for me but i need to know why this corruption happens. My new hope lies a form which i will feed with the blob data i get from my ajax request. But if this is corrupt there would be no need to even try it. So please consider this new question – Jan Wedel Nov 24 '15 at 07:58
  • So, finally I solved the issue by waiting for the success-message and added a new form to request the data, which is then presented as filedownload. But thanks for these multiple suggestions anyway, it helped me a whole lot. – Jan Wedel Nov 26 '15 at 11:40

2 Answers2

0

After a lot of research i found this site and the essence of its statment is that jquery ajax does not support receiving binary data, but provides a solution for implementing plain xhr request which support blob transfer. The Site: http://www.henryalgus.com/reading-binary-files-using-jquery-ajax/

Jan Wedel
  • 31
  • 1
  • 5
  • Ok, here is how i would do it: keep your polling solution, but when the job is complete, instead of trying to send back binary data via ajax, simply save to a temp file , eg `1234.tmp` then send the file reference back to js. On receiving the file reference, simply call `window.location = url/filereader.php?ref=1234`. `filereader.php` sets the appropriate headers, reads the file then deletes it. I have done this a few times and it works fine even on ancient browsers – Steve Nov 24 '15 at 10:50
  • So this aswer i gave was no option for me in the end, because it would then expect binary responses only, but this ajax-block i used, is used by many other requests also. so i made a workaround which is described in my last comment on the original question. But I marked this as answer, because it answers the originial question best by saying that binary responses for jquery Ajax are not possible by default. – Jan Wedel Nov 26 '15 at 11:42
0

To expand on my comment, instead of trying to send back binary data via ajax, simply save to a temp file , send the file reference back to js. On receiving the file reference, simply set window.location.href to point to a filereading endpoint, passing the file reference. I have done this a few times and it works fine even on ancient browsers:

$('#start').click(function(){
    $.post('/createfile.php', {some:data}, function(response){
        if(response.started){
            pollFile(response.fileId);
        }
    });
);
function pollFile(fileId){
    $.get('/filestatus.php?fileid=' + fileId, function(response){
        if(response.fileCompleted){
            window.location.href = '/downloadfile.php?fileid=' + fileId;
        }else{
            setTimeout('pollFile', 5000, fileId);
        }
    });
}

//createfile.php    
$fileId = uniqid();
SomePersistentStorage::addJob($fileID);
//start file job here, code should run in a seperate process/thread, so
//either use a job queue system, use shell_exec or make an http request,
//then once job is queued/started:
header('Content-Type: application/json');
echo json_encode(['started'=>true, 'fileId'=>$fileId]);

//processjob.php - the file that does the work, could be your java
//program for example, just using php here for consistency
//after file is done
file_put_contents($filepath, $data);
SomePersistentStorage::updateJob($fileID, true);

//filestatus.php
$fileId = $_GET['fileid'];
header('Content-Type: application/json');
echo json_encode(['fileCompleted'=>SomePersistentStorage::isJobCompleted($fileID)]);

//downloadfile.php
$fileId = $_GET['fileid'];
$filepath = 'tmp/' . $fileId . '.tmp';
//correct headers here, then
readfile($filepath);
unlink($filepath);

If you dont want to imediatly delete the file, then you could just run a cron to delete files in the specific folder, that are older than x.

Steve
  • 20,703
  • 5
  • 41
  • 67
  • Thanks! I already considered a solution like this, but I don't like to save the a file because its not best practice here. So I came around with a solution where I store the job-result next to my job-queue in a Hashmap, especially, because there may be several request at a time. So When its stored, I am able to listen to a success by Ajax and than create a hidden form to request the Data. I will report about my progress. – Jan Wedel Nov 24 '15 at 12:12
  • The file storage is just one possable why to do it - this answer is more about highlighting how you can use `window.location.hash` to direct the browser to a file reading endpoint, rather than trying to send binary data over ajax. If you are saving the file data in memory in a java hashmap, then `download.php` simply requests the data from your java service instead of reading from a file. – Steve Nov 24 '15 at 12:16
  • Though i wouldnt say file storage is a bad practice - as long as the file is stored in a non web accessible location, it is secure. And ram is usually a far scarcer resource than disk space – Steve Nov 24 '15 at 12:20
  • And if you are worried about someone being able to guess the fileId and thus access someone elses file, then you can just use session storage as well as / instead of query parameters to pass the fileid about – Steve Nov 24 '15 at 12:23
  • Thanks for that! I solved it in another way but this may be really interesting for further problems. – Jan Wedel Nov 26 '15 at 11:44