Как заставить клиента загружать очень большой файл, который генерируется на лету

У меня есть функция экспорта, которая читает всю базу данных и создает файл.xls со всеми записями. Затем файл отправляется клиенту.

Конечно, время экспорта полной базы данных требует много времени, и вскоре запрос завершится ошибкой тайм-аута.

Каково лучшее решение для этого случая?

Я слышал кое-что о создании очереди с Redis, например, но для этого потребуется два запроса: один для запуска задания, которое сгенерирует файл, и второй для загрузки сгенерированного файла.

Возможно ли это с помощью одного запроса от клиента?

4 ответа

Решение

Экспорт в Excel:

Используйте потоки. Ниже приводится приблизительное представление о том, что может быть сделано:

  1. Используйте модуль exceljs. Потому что у него есть потоковый API, нацеленный на эту проблему.

    var Excel = require('exceljs')
    
  2. Так как мы пытаемся начать загрузку. Напишите соответствующие заголовки для ответа.

    res.status(200);
    res.setHeader('Content-disposition', 'attachment; filename=db_dump.xls');
    res.setHeader('Content-type', 'application/vnd.ms-excel');
    
  3. Создайте рабочую книгу с поддержкой Streaming Excel Writer. Поток, предоставленный писателю, является ответом сервера.

    var options = {
        stream: res, // write to server response
        useStyles: false,
        useSharedStrings: false
    };
    
    var workbook = new Excel.stream.xlsx.WorkbookWriter(options);
    
  4. Теперь поток вывода на выход настроен. для потоковой передачи входных данных выберите драйвер БД, который выдает результаты запроса / курсор в виде потока.

  5. Определите асинхронную функцию, которая выводит 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);
        });
    }
    
  6. Теперь давайте экспортируем некоторые таблицы БД, используя 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/

Другие вопросы по тегам