Как заставить клиента загружать очень большой файл, который генерируется на лету
У меня есть функция экспорта, которая читает всю базу данных и создает файл.xls со всеми записями. Затем файл отправляется клиенту.
Конечно, время экспорта полной базы данных требует много времени, и вскоре запрос завершится ошибкой тайм-аута.
Каково лучшее решение для этого случая?
Я слышал кое-что о создании очереди с Redis, например, но для этого потребуется два запроса: один для запуска задания, которое сгенерирует файл, и второй для загрузки сгенерированного файла.
Возможно ли это с помощью одного запроса от клиента?
4 ответа
Экспорт в Excel:
Используйте потоки. Ниже приводится приблизительное представление о том, что может быть сделано:
Используйте модуль exceljs. Потому что у него есть потоковый API, нацеленный на эту проблему.
var Excel = require('exceljs')
Так как мы пытаемся начать загрузку. Напишите соответствующие заголовки для ответа.
res.status(200); res.setHeader('Content-disposition', 'attachment; filename=db_dump.xls'); res.setHeader('Content-type', 'application/vnd.ms-excel');
Создайте рабочую книгу с поддержкой Streaming Excel Writer. Поток, предоставленный писателю, является ответом сервера.
var options = { stream: res, // write to server response useStyles: false, useSharedStrings: false }; var workbook = new Excel.stream.xlsx.WorkbookWriter(options);
Теперь поток вывода на выход настроен. для потоковой передачи входных данных выберите драйвер БД, который выдает результаты запроса / курсор в виде потока.
Определите асинхронную функцию, которая выводит 1 таблицу на 1 рабочий лист.
var tableToSheet = function (name, done) { var str = dbDriver.query('SELECT * FROM ' + name).stream(); var sheet = workbook.addWorksheet(name); str.on('data', function (d) { sheet.addRow(d).commit(); // format object if required }); str.on('end', function () { sheet.commit(); done(); }); str.on('error', function (err) { done(err); }); }
Теперь давайте экспортируем некоторые таблицы БД, используя mapSeries модуля async:
async.mapSeries(['cars','planes','trucks'],tableToSheet,function(err){ if(err){ // log error } res.end(); })
CSV Export:
Для экспорта в CSV одного модуля таблицы / коллекции можно использовать fast-csv:
// response headers as usual
res.status(200);
res.setHeader('Content-disposition', 'attachment; filename=mytable_dump.csv');
res.setHeader('Content-type', 'text/csv');
// create csv stream
var csv = require('fast-csv');
var csvStr = csv.createWriteStream({headers: true});
// open database stream
var dbStr = dbDriver.query('SELECT * from mytable').stream();
// connect the streams
dbStr.pipe(csvStr).pipe(res);
Теперь вы передаете данные из базы данных в ответ HTTP, конвертируя их в формат xls/csv на лету. Нет необходимости буферизовать или хранить все данные в памяти или в файле.
Вам не нужно отправлять весь файл один раз, вы можете отправить этот файл кусками (построчно, например), просто используйте res.write(chunk)
а также res.end()
в конце, чтобы отметить его как завершенный.
Вы можете отправить информацию о файле в виде потока, посылая каждый отдельный блок по мере его создания res.write(chunk)
или, если отправка фрагмента файла по фрагменту не является вариантом, и вам нужно подождать весь файл, прежде чем отправлять какую-либо информацию, вы всегда можете оставить соединение открытым, установив длительность тайм-аута в бесконечность или любое значение, которое, по вашему мнению, будет достаточно высоко, чтобы позволить файлу быть созданным. Затем настройте функцию, которая создает файл.xls, и либо:
1) Принимает обратный вызов, который получает выходные данные в виде аргумента после их готовности, отправляет эти данные и затем закрывает соединение, или;
2) Возвращает обещание, которое разрешается с выводом данных, как только оно будет готово, позволяя вам отправить разрешенное значение и закрыть соединение, как в версии обратного вызова.
Это будет выглядеть примерно так:
function xlsRouteHandler(req, res){
res.setTimeout(Infinity) || res.socket.setTimeout(Infinity)
//callback version
createXLSFile(...fileCreationArguments, function(finishedFile){
res.end(finishedFile)
})
//promise version
createXLSFile(...fileCreationArguments)
.then(finishedFile => res.end(finishedFile))
}
Если вы все еще беспокоитесь о тайм-ауте, вы всегда можете установить интервал таймера для отправки случайных res.write()
сообщение, чтобы предотвратить тайм-аут соединения с сервером, а затем отменить этот интервал, как только готовый файл готов к отправке.
Ссылка на эту ссылку, которая использует jedis (клиент Java Redis). Ключом к этому является команда LPOPRPUSH.
https://blog.logentries.com/2016/05/queuing-tasks-with-redis/