Как загрузить файл размером более 2 ГБ с помощью сервера node express и реагировать на клиента - PullRequest
0 голосов
/ 17 июня 2020

Буферы в javascript кажутся ограниченными 2 ГБ для 64-битных систем.

Использование express помощника res.download найдено здесь мой сервер вылетает при попытке загрузить файл что превышает те же 2 ГБ.

Думаю, решением будет потоковая передача файла, из того, что я обнаружил, это можно сделать, используя:

var readStream = fileSystem.createReadStream('/tmp/outputs.zip');
readStream.pipe(res);

Но когда я это сделаю, мой топор ios клиент никогда не получает ответ на запрос GET.

Я не могу использовать кнопку загрузки в этом сценарии, потому что данные, которые я пытаюсь загрузить, защищены аутентификацией JWT.

Любая помощь приветствуется.

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

Node.js сервер:

async function getOutputs(req, res, next) {
    console.log("invoked getOutputs");
    const request = req.params.request;
    const uuid = req.params.uuid;
    const { user } = await getRequestUser(req);
    s3Service.getOutputs(req, res, request, uuid, user)
    .then(
        body => {
            if(body.code !== 202) {
                res.status(body.code).json(body.data);
            }
        }
    )
    .catch(err => next(err));
}

async function getOutputs(req, res, requestId, uuid, user) {
  let canceled = false;
  req.on('close', function (err){
    canceled = true;
  });

  const requestModel = await Request.findOne({where: {id: requestId}});
  const m = moment.utc(requestModel.dataValues.timestamp);
  m.tz('Europe/Madrid');
  const filePath = 'root/' + user.emailaddress + '/' + m.format("YYYYMMDDHHmmss") + '/output/results.zip';
  const s3HeadParams = {
    Bucket: bucket,
    Key: filePath,
  };
  try{
    /*
     * S3 Max download is 2GB at a time
     * Javascript Buffers cap at 2GB
     * App crashes trying to download file above 2GB
     */
    const part = 2*1000*1000*1000;
    const s3head = await s3.headObject(s3HeadParams).promise();
    const size = s3head.ContentLength;
    const ranges = size / part;
    const results = [];
    let totalBytes = 0;
    let prevProgress = 0;
    for(let i = 0; i < ranges; i++) {
      const s3params = {
        Bucket: bucket,
        Key: filePath,
        Range: "bytes=" + (i * part) + "-" + ((i+1) * part -1),
      };
      const s3request = s3.getObject(s3params).on('httpData', function(chunk){
        if(canceled) {
          s3request.abort();
        }
        totalBytes += chunk.length;
        let progress = Math.round(totalBytes / size * 100);
        if(progress > prevProgress) {
          emitProgress(uuid, progress);
          prevProgress = progress;
        }
      });
      results.push(await s3request.promise());
    }
    fs.writeFileSync('/tmp/outputs.zip', '');
    for(let i = 0; i < results.length; i++) {
      fs.appendFileSync('/tmp/outputs.zip', results[i].Body);
    }

    res.setHeader('Content-disposition', 'attachment; filename=outputs.zip');
    res.setHeader('Content-type', 'application/zip');

    var readStream = fileSystem.createReadStream('/tmp/outputs.zip');
    readStream.pipe(res);
    return {
      code: 202,
    };
  } catch(error) {
    return {
      code: 400,
      data: ['Results not yet available, please try again later'],
    };
  }
}

React client:

async function getOutputs(request, callback, setRequesting, uuid, setProgress) {
  try{
    const response = await Axios.get(ADDRESS + '/s3/getOutputs/' + request + '/' + uuid, {
      cancelToken: source.token,
    });
    var file = new Blob([toArrayBuffer(response.data.data)], {type: 'application/zip'});
    if (window.navigator && window.navigator.msSaveOrOpenBlob) {
      window.navigator.msSaveOrOpenBlob(file, 'Flomics-Request-' + request + '-outputs.zip');  
    }
    else {
      var url = URL.createObjectURL(file);
      const link = document.createElement('a');
      link.href = url;
      link.download = 'Request-' + request + '-outputs.zip';
      document.body.appendChild(link);
      link.click();
      document.body.removeChild(link);
      URL.revokeObjectURL(url);
      setRequesting(false);
      setProgress(0);
    }
  } catch(error) {
    if(Axios.isCancel(error)) {
      source = CancelToken.source();
    } else if(error.response && error.response.data && error.response.data[0]) {
      callback('warning', error.response.data[0]);
    } else {
      callback('error', 'Unexpected error');
    }
    setRequesting(false);
    setProgress(0);
  }
}
...