将 Excel 上传到 Google 云存储时出错

Erro uploading Excel to Google Cloud Storage

我正在使用 'exceljs' 库。它在我的本地节点服务器上运行良好。现在我正在尝试使用 Firebase Functions 将 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)
});

在 Functions 控制台上,日志显示 excel 文件已成功创建。但是上传的时候,弹出一个很奇怪的错误:

我做错了什么?我很感激任何帮助。

谢谢!

您收到的错误消息来自于尝试获取一个不存在的文件的已签名 URL。

当您调用 bucket.upload(filename, metadata) 时,您正在上传文件 /tmp/Relatorio.xlsx,这会在您的存储桶中创建一个名为 Relatorio.xlsx 的文件。在下一行你调用 bucket.file(filename); 错误地将自己与 /tmp/Relatorio.xlsx 而不是 Relatorio.xlsx.

相关联

要解决此问题,您应该使用从 bucket.upload() 解析的 File 对象,而不是自己创建它:

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

其他注释和修复

您的代码还包含很多不必要的 new Promise((resolve, reject) => { ... }) 调用。这称为 Promise 构造函数反模式,其中大部分可以通过正确链接 Promise 来移除。 blog post 是关于 Promises 以及如何正确使用它们的很好的速成课程。

关于您的函数的源代码,由于函数的 index.js 文件将包含多个函数定义,因此您不应在 index.js 文件的顶部定义变量,除非它们由所有函数共享你的函数,它们是无状态的,以防一个函数被多次调用。这在处理 I/O 或文件等内存密集型资源时尤为重要。

使用您当前的代码,如果 relatorios 函数在短时间内被调用两次,保存的文件将包含第一次调用的旧数据和当前调用的新数据,从而导致文件无效和潜在的内存泄漏。

删除过多的 promise 调用并进行调用,以便您的 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);
      });
});

这是我用来保存由 exceljs 生成的 excel 文件的版本 及其各自的库版本

// @google-cloud/storage   --> 5.3.0
// exceljs  --> 4.3.0
// moment  --> 2.29.1     
const moment = require('moment');
const ExcelJS = require('exceljs');

class Excel {

constructor(nameSheet) {
    this.workbook = new ExcelJS.Workbook();
    this.workbook.creator = 'User...';
    this.workSheet = this.workbook.addWorksheet(nameSheet);
}

// ..... other code....

saveFile = async (path, filename) =>
{
    
    const storage = new Storage();
    const bucket = storage.bucket('name_bucket...');
    const storageFile = bucket.file(`${path}/${filename}`);
    const blobStream = storageFile.createWriteStream({
        contentType: 'application/ms-excel',
    });
    
    await this.workbook.xlsx.write(blobStream);
    
    blobStream.end();
    
    const config = {
        action: 'read',
        expires: moment().add(30, 'minutes').format(),
    };
    
    return await storageFile.getSignedUrl(config)
}

}