Ошибка при загрузке Excel в Google Cloud Storage

Я использую библиотеку exceljs. Он отлично работает на моем локальном сервере node. Теперь я пытаюсь использовать функции Firebase для загрузки файла excel в облачное хранилище Google.

Это весь код, который я использую:

'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
const ExcelJS = require('exceljs');

admin.initializeApp();

var workbook = new ExcelJS.Workbook();
var worksheet = workbook.addWorksheet('Relatório Consolidado');



function startExcel(){

  worksheet.columns = [
    { header: 'Empresa', key: 'empresa', width: 25 },
    { header: 'Data criação', key: 'data_criacao', width: 25 },
    { header: 'Responsável agendamento', key: 'agendador', width: 25 },
    { header: 'Colaborador', key: 'colaborador', width: 25 },
    { header: 'Endereço', key: 'endereco', width: 25 },
    { header: 'CPF', key: 'cpf', width: 25 },
    { header: 'CTPS', key: 'ctps', width: 25 },
    { header: 'Função', key: 'funcao', width: 25 },

    { header: 'Data agendado', key: 'nome_subtipo_produto', width: 25 },
    { header: 'Data atendimento médico', key: 'nome_subtipo_produto', width: 25 },
    { header: 'Data inicio atendimento', key: 'nome_subtipo_produto', width: 25 },
    { header: 'Data inicio exames', key: 'nome_subtipo_produto', width: 25 },
    { header: 'Tipo de exame', key: 'valor_produto', width: 25 },
    { header: 'Exames realizados', key: 'valor_produto', width: 25 },
    { header: 'Status atendimento', key: 'tipoPagamento', width: 25 },
    { header: 'Status exames', key: 'centroCustoStr', width: 25 }
  ];        
}

function salvaExcel(){

  return new Promise(function(resolve, reject){

      let filename = `/tmp/Relatorio.xlsx`
      let bucketName = 'gs://xxx.appspot.com/Relatorios'
      const bucket = admin.storage().bucket(bucketName);      

      workbook.xlsx.writeFile(filename)
      .then(() => {

      console.log('Excel criado com sucesso! Enviando upload do arquivo: ' + filename)          

        const metadata = {
          contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        };

        bucket.upload(filename, metadata)

        .then(() => {
          const theFile = bucket.file(filename);
          theFile.getSignedURL(signedUrlOptions)

          .then((signedUrl) => {
            resolve(signedUrl)
          });

        })
        .catch((error) => {
          reject('Erro ao realizar upload: ' + error)
        })                      
      })  

      .catch((error) => {
        reject('Erro ao realizar upload: ' + error)
      })         

  })    
}

startExcel()


/**********************************
 * Relatórios
 ********************************/

function relatorios(change, context){

  return new Promise((resolve, reject) => {

    const snapshot = change.after
    const data = snapshot.val()  

    verificaRelatorioAgendamentos(change)

    .then(() => {
      resolve()

    })

    .catch((error => {                
      reject(error)

    }))

  })           
}


function verificaRelatorioAgendamentos(change, context){

  return new Promise((resolve, reject) => {

    const snapshot = change.after
    const data = snapshot.val()      
    const dataInicial = data.dataInicial    
    const year = moment(dataInicial).format('YYYY')
    const month = moment(dataInicial).format('MM')
    const state = 'DF'    
    let path = "/agendamentos/" + state + "/" +  year + "/" + month

    const relatorios = admin.database().ref(path).once('value');

    return Promise.all([relatorios])

      .then(results => {                

        let valores = results[0]
        criaRelatorioAgendamentos(valores)

        .then(() => {
          resolve()

        })

        .catch((error => {          
          reject(error)

        }))

    })

  })       

}


function criaRelatorioAgendamentos(results){

  return new Promise((resolve, reject) => {

    let promises = []

    results.forEach(element => {

      let promise = new Promise(function(resolveExcel){ 

        let data = element.val()        

        worksheet.addRow({
          id: 1, 
          empresa: data.agendador.company, 
          data_criacao: data.dataCriacao, 
          agendador: data.agendador.nome, 
          colaborador: data.colaborador.nome,
          cpf: data.colaborador.cpf, 
          ctps: data.colaborador.ctps, 
          funcao: data.colaborador.funcao, 
          data_agendado: data.data, 
          data_atendimento_medico: data.dataAtendimento, 
          data_inicio_atendimento: data.dataInicio, 
          data_inicio_exames: data.dataInicioExames, 
          tipo_exame: data.tipoExame, 
          exames: data.exames[0].nome, 
          status_atendimento: data.status, 
          status_exames: data.statusExames

        })

        resolveExcel()

      })      

      promises.push(promise)

    })

    Promise.all(promises)

      .then(() => {          
        salvaExcel()

        .then((url) => {

          console.log('Salvar URL' + url) 

          resolve(url)

        })

        .catch((error => {
          reject(error)

        }))


    })


  })       

}


exports.relatorios = functions.database.ref('/relatorios/{state}/{year}/{month}/{relatoriosId}')
    .onWrite((change, context) => {      
      return relatorios(change, context)
});

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

введите здесь описание изображения

Что я делаю неправильно? Я ценю любую помощь.

Спасибо!


person Diego Desenvolvedor    schedule 19.02.2020    source источник
comment
Поскольку вы не показываете здесь весь свой код, это невозможно сказать. Объявление функции отсутствует. Кроме того, каждый раз, когда я вижу return new Promise в JavaScript с большим количеством промисов внутри, это поднимает флаги, что промисы не обрабатываются должным образом. Не похоже, что вы делаете здесь что-то, что требует new Promise.   -  person Doug Stevenson    schedule 19.02.2020
comment
@DougStevenson Я обновил весь код   -  person Diego Desenvolvedor    schedule 19.02.2020


Ответы (1)


Сообщение об ошибке, которое вы получаете, возникает из-за попытки получить подписанный URL-адрес несуществующего файла.

Когда вы вызываете bucket.upload(filename, metadata), вы загружаете файл /tmp/Relatorio.xlsx, который создает в вашей корзине файл с именем Relatorio.xlsx. В следующей строке вы вызываете bucket.file(filename);, который неправильно ассоциирует себя с /tmp/Relatorio.xlsx вместо Relatorio.xlsx.

Чтобы исправить это, вы должны использовать объект File, который разрешен из bucket.upload(), вместо того, чтобы создавать его самостоятельно:

bucket.upload(filename, metadata)
    .then((file) => file.getSignedURL())
    .then((url) => {
        console.log('Salvar URL' + url)
    })

Другие примечания и исправления

Ваш код также содержит много ненужных new Promise((resolve, reject) => { ... }) вызовов. Это называется антишаблон конструктора промисов, и большинство из них можно удалить, правильно связав промисы в цепочку. Этот сообщение в блоге представляет собой хороший ускоренный курс по промисам и их правильному использованию. .

Что касается исходного кода вашей функции, поскольку файл index.js для функций будет содержать несколько определений функций, вам не следует определять переменные в верхней части вашего файла index.js, если они не являются общими для всех ваших функций, и они не имеют состояния в случае, если функция вызывается несколько раз. Это особенно важно при работе с вводом-выводом или ресурсоемкими ресурсами памяти, такими как файлы.

С вашим текущим кодом, если функция relatorios была вызвана дважды за короткий период, сохраненный файл будет содержать как старые данные из первого вызова, так и новые данные из текущего вызова, что приведет как к недопустимому файлу, так и к потенциальной утечке памяти.

Если удалить лишние вызовы обещаний и сделать так, чтобы ваш код exceljs можно было повторно запустить без повреждения каких-либо данных, в результате получится следующий файл index.js:

'use strict';
const functions = require('firebase-functions');
const admin = require('firebase-admin');
// 'exceljs' is required on-demand in MyExcelSheetHelper

admin.initializeApp();

/* HELPER CLASS */

/**
 * A helper class used to create reuseable functions that won't
 * conflict with each other
 */
class MyExcelSheetHelper {

  constructor() {
    const ExcelJS = require('exceljs');

    this.workbook = new ExcelJS.Workbook();
    this.worksheet = this.workbook.addWorksheet('Relatório Consolidado');

    this.worksheet.columns = [
      { header: 'Empresa', key: 'empresa', width: 25 },
      { header: 'Data criação', key: 'data_criacao', width: 25 },
      { header: 'Responsável agendamento', key: 'agendador', width: 25 },
      { header: 'Colaborador', key: 'colaborador', width: 25 },
      { header: 'Endereço', key: 'endereco', width: 25 },
      { header: 'CPF', key: 'cpf', width: 25 },
      { header: 'CTPS', key: 'ctps', width: 25 },
      { header: 'Função', key: 'funcao', width: 25 },

      { header: 'Data agendado', key: 'nome_subtipo_produto', width: 25 },
      { header: 'Data atendimento médico', key: 'nome_subtipo_produto', width: 25 },
      { header: 'Data inicio atendimento', key: 'nome_subtipo_produto', width: 25 },
      { header: 'Data inicio exames', key: 'nome_subtipo_produto', width: 25 },
      { header: 'Tipo de exame', key: 'valor_produto', width: 25 },
      { header: 'Exames realizados', key: 'valor_produto', width: 25 },
      { header: 'Status atendimento', key: 'tipoPagamento', width: 25 },
      { header: 'Status exames', key: 'centroCustoStr', width: 25 }
    ];
  }

  /**
   * Streams this workbook to Cloud Storage
   * @param storageFilepath - the relative path where the file is uploaded to Cloud Storage
   * @returns the signed URL for the file
   */
  salva(storageFilepath) {
    if (!storageFilepath) {
      return Promise.reject(new Error('storageFilepath is required'));
    }

    const bucket = admin.storage().bucket();

    const storageFile = bucket.file(storageFilepath);

    const uploadFilePromise = new Promise((resolve, reject) => {
      try {
        const stream = storageFile.createWriteStream({
          contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
        });

        stream.on('finish', () => {
          resolve();
        });

        stream.on('error', error => {
          reject(error);
        });

        this.workbook.xlsx.write(stream)
          .then(() => {
            stream.end();
          });

      } catch (e) { // catches errors from createWriteStream
        reject(e);
      }
    })

    return uploadFilePromise
      .then(() => {
        var CONFIG = {                                                                      
          action: 'read',                                                               
          expires: '03-01-2500',                                                        
        };    

        bucket.file(storageFilepath).getSignedUrl(CONFIG)
        .then((signedUrl) => {

          return signedUrl
        })
      })

  }
}

/* FUNCTIONS CODE */

function criaRelatorioAgendamentos(path, querySnapshot) {
  const excelFileHelper = new MyExcelSheetHelper();
  const worksheet = excelFile.worksheet;

  // this forEach loop is synchronous, so no Promises are needed here
  querySnapshot.forEach(entrySnapshot => {
    const data = entrySnapshot.val();

    worksheet.addRow({
      id: 1,
      empresa: data.agendador.company,
      data_criacao: data.dataCriacao,
      agendador: data.agendador.nome,
      colaborador: data.colaborador.nome,
      cpf: data.colaborador.cpf,
      ctps: data.colaborador.ctps,
      funcao: data.colaborador.funcao,
      data_agendado: data.data,
      data_atendimento_medico: data.dataAtendimento,
      data_inicio_atendimento: data.dataInicio,
      data_inicio_exames: data.dataInicioExames,
      tipo_exame: data.tipoExame,
      exames: data.exames[0].nome,
      status_atendimento: data.status,
      status_exames: data.statusExames
    });
  });

  return excelFileHelper.salva(path + '/Relatorio.xlsx');
}

exports.relatorios = functions.database.ref('/relatorios/{state}/{year}/{month}/{relatoriosId}')
    .onWrite((change, context) => {

    // Verificar relatorio agendamentos

    const snapshot = change.after;
    const data = snapshot.val();
    const dataInicial = data.dataInicial;
    const year = moment(dataInicial).format('YYYY');
    const month = moment(dataInicial).format('MM');
    const state = 'DF';
    const path = "/agendamentos/" + state + "/" +  year + "/" + month;

    return admin.database().ref(path).once('value')
      .then(valores => {
        return criaRelatorioAgendamentos(path, valores);
      });
});
person samthecodingman    schedule 19.02.2020
comment
Спасибо за подробное объяснение, очень помогло! Я изменил class MyExcelSheetHelper() { на class MyExcelSheetHelper {, чтобы развернуть функции. Кроме того, я изменил this.worksheet = workbook.addWorksheet на this.worksheet = this.workbook.addWorksheet. Теперь файл создается в хранилище, но функция возвращает ошибка.. ReferenceError: storageFile не определен Я уже изменил переменную storageFile, пытаясь установить 'scope', но безуспешно.. - person Diego Desenvolvedor; 19.02.2020
comment
Хорошо, я понял... изменение на ** return Bucket.file(storageFilepath).getSignedUrl(CONFIG) ** сработало как шарм... Кроме того, мне пришлось включить API управления идентификацией и доступом (IAM)< /b> и установите роль ** Service Account Token Creator **, чтобы все заработало. @samthecodingman БОЛЬШОЕ СПАСИБО!!! - person Diego Desenvolvedor; 19.02.2020