Quantcast
Channel: Node.jsタグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 8862

Cloud Functions for Firebaseを使ってExcelファイル←→ Cloud Firestore を読み書きするサンプルコード

$
0
0

WEBアプリからExcelファイルを操作(アップロード・ダウンロードなど)する必要があったため、そのときに調べた内容の備忘メモ。

イントロ

Cloud Functions for Firebase を使って、Excelファイル内のデータをCloud Firestoreへ投入したり、FirestoreのデータをExcelファイルとしてダウンロードしたりするサンプルコードです。

以下のことができるようになります。

  • Excelデータを読み込んで、Firestoreへ保存
  • Firestoreデータを読み出して、Excelへ流し込んでダウンロード
  • Excelファイルを、Storageへアップロード(上記で用いるExcelテンプレートをアップロード)

それぞれ、図的には下記のようになります。

Excelデータを読み込んで、Firestoreへ保存

ローカルのExcelファイルを、Cloud Functionsへアップロード。FunctionsはExcelファイルを読み込んでJSONデータを生成し、Firestoreにデータを書き込みます。

upload.png

Firestoreデータを読み出して、Excelへ流し込んでダウンロード

Cloud Functionsを呼び出すとFunctionsがFirestoreからデータを取得。またCloud Storageに置いたテンプレートExcelファイルを取り出してそこにデータを書き込み、Excelファイルをダウンロードします。

download.png

Excelファイルを、Storageへアップロード(上記で用いるExcelテンプレートをアップロード)

ついでに、テンプレートのExcelをCloud Functions経由で、Cloud Storageへアップロードします。

templateUpload.png

前提、事前準備(メモ)

Node.js はインストールされてる前提で、firebase-toolsのインストールから。

$node --versionv10.18.1

$npm i -g firebase-tools

+ firebase-tools@7.12.1
added 516 packages from 325 contributors in 20.769s

$firebase --version7.12.1

続いてFirebaseへのログイン。下記のコマンドを実行するとブラウザが起動するので、そちらでFirebaseにログインしておきます。

$firebase login
✔  Success! Logged in as xxxx@example.com

今回のサンプルのコードをGitHubからダウンロードして、使用するFirebaseのプロジェクトを設定しておきます。

$git clone https://github.com/masatomix/excel2firestore.git
$cd excel2firestore/
$firebase use --add? Which project do you want to add? slackapp-sample
? What alias do you want to use for this project? (e.g. staging) default

Created alias default for slackapp-sample.
Now using alias default (slackapp-sample)
$

その他Firebase上で

  • Cloud Functions for Firebase が利用可能な状態
  • Cloud Storage が利用可能な状態
  • Cloud Firestore が利用可能な状態

にしておきましょう1

環境設定

サービスアカウント設定

FunctionsからFirestoreへ読み書きを行うために「サービスアカウントJSONファイル」が必要です。
Firebaseのプロジェクトの左メニューの歯車アイコンから「プロジェクトの設定」を選択 >> サービスアカウント 画面でJSONファイルを生成・取得しておいてください。

0002.png

その後、ソースコード上の ./functions/src/firebase-adminsdk.jsonという名前で配置しておいてください。

Storageの設定

StorageのURLを指定します。Firebaseのプロジェクトの左メニュー >> Storage を選択。

0001.png

gs://slackapp-sample.appspot.comがStorageのURLなのでそれを設定します。

$cd functions/
$cat ./src/firebaseConfig.ts
export default {
  apiKey: '',
  authDomain: '',
  databaseURL: 'https://slackapp-sample.firebaseio.com', ←今回使いません
  projectId: 'slackapp-sample',          ←今回使いません
  storageBucket: 'slackapp-sample.appspot.com',    ← 正しいStorage名に。
  messagingSenderId: '',
  appId: ''
}

以上で準備は完了です。

Functionsを起動し、実行する

$npm i
...
found 0 vulnerabilities

$npm run serve
>functions@0.0.6-SNAPSHOT serve /Users/xxx/excel2firestore/functions
>npm run build && firebase serve --only functions
>functions@0.0.6-SNAPSHOT build /Users/xxx/excel2firestore/functions
>tsc
⚠  Your requested "node" version "8" doesn't match your global version "10"
✔  functions: Emulator started at http://localhost:5000
i  functions: Watching "/Users/xxx/excel2firestore/functions" for Cloud Functions...
✔  functions[api]: http function initialized (http://localhost:5000/slackapp-sample/us-central1/api).

起動したので、別のターミナルから。。

$pwd/Users/xxx/excel2firestore/functions
$
  • Excelデータを、Firestoreへ
    • $ curl http://localhost:5000/slackapp-sample/us-central1/api/samples/upload -F file=@samples.xlsx -X POST
  • Firestoreデータを、整形されたExcelへ
    • $ curl http://localhost:5000/slackapp-sample/us-central1/api/samples/download -o result.xlsx
  • Excelファイルを、Storageへ
    • $ curl http://localhost:5000/slackapp-sample/us-central1/api/samples/templateUpload -F file=@samples.xlsx -X POST

コード説明

基本的なFunctionsのコード(Expressを使った部分とか)は省略します。興味があればCloneしたコードをご確認ください:-)

「Excelデータを読み込んで、Firestoreへ保存」のサンプルコード

HTTPでFormからアップロードされてくるデータを取り扱うための「busboy」を用いてファイルのデータを取得し、一旦ファイルとして書き出します。次のそのファイルから「xlsx-populate-wrapper」を使ってExcelファイルを読み込み、Firestore へデータを書き込んでいます。内容的には Google Cloud内のドキュメント#マルチパートデータの内容ほぼそのままですね。

また xlsx-populate-wrapper は「xlsx-populate」のWrapperですが、ファイルの読み書きで変更したい箇所があったので、forkしてすこしだけ改変させてもらいました。

オリジナル: https://github.com/juniorCitizen/xlsx-populate-wrapper

upload.ts
import{Request,Response}from'express'import*asadminfrom'firebase-admin'import{excel2Sample4}from'./sample4'import*aspathfrom'path'import*asosfrom'os'import*asBusboyfrom'busboy'import*asfsfrom'fs'constSAMPLE4:string='sample4'exportconstupload=async(request:Request,response:Response)=>{// https://cloud.google.com/functions/docs/writing/http?hl=ja// https://qiita.com/rubytomato@github/items/11c7f3fcaf60f5ce3365console.log('start.')// Node.js doesn't have a built-in multipart/form-data parsing library.// Instead, we can use the 'busboy' library from NPM to parse these requests.constbusboy=newBusboy({headers:request.headers})consttmpdir=os.tmpdir()// This object will accumulate all the uploaded files, keyed by their name.constuploads:{[key:string]:string}={}constfileWrites:Array<Promise<any>>=[]busboy.on('file',(fieldname,file,filename,encoding,mimetype)=>{// file: NodeJS.ReadableStreamconsole.log('busboy.on.file start.')console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding} , mimetype: ${mimetype}`)// Note: os.tmpdir() points to an in-memory file system on GCF// Thus, any files in it must fit in the instance's memory.constfilepath=path.join(tmpdir,filename)uploads[fieldname]=filepathconstwriteStream=fs.createWriteStream(filepath)file.pipe(writeStream)// File was processed by Busboy; wait for it to be written to disk.constpromise=newPromise((resolve,reject)=>{file.on('end',()=>{writeStream.end()excel2Sample4(filepath).then((datas:any[])=>{for(constinstanceofdatas){admin.firestore().doc(`${SAMPLE4}/${instance.operationId}`).set(instance)}resolve(datas)}).catch(err=>reject(err))})// writeStream.on('finish', resolve)// writeStream.on('error', reject)})fileWrites.push(promise)})// Triggered once all uploaded files are processed by Busboy.// We still need to wait for the disk writes (saves) to complete.busboy.on('finish',async()=>{console.log('busboy.on.finish start.')constresults:any[]=awaitPromise.all(fileWrites)for(constfileofObject.values(uploads)){fs.unlinkSync(file)}constlength=results.map(result=>result.length).reduce((acc,value)=>acc+value)// response.status(200).send(`${Object.keys(uploads).length} file executed.`)response.status(200).send(`${length}件処理しました。`)})constreqex:any=requestbusboy.end(reqex.rawBody)}

下記では、Excelから取得した行データを、Firestoreに書き込む前にJSONデータにしています。JSON生成をゴニョゴニョやってますが、開発してるWEBアプリ向けのデータ構造に変換しているだけで、記事観点での本質的な意味はありません。

Excel上日付のデータについては、Excelのシリアル値(number)が取得されるので、Date型への変換などを行っています。

sample4.ts(抜粋)#excel2Sample4
import*asadminfrom'firebase-admin'import{xlsx2json,dateFromSn,toBoolean}from'./commonUtils'constSAMPLE1:string='sample1'constSAMPLE4:string='sample4'exportconstexcel2Sample4=(path:string):Promise<Array<any>>=>{constformat_func=(instance:any):any=>{constnow=admin.firestore.Timestamp.now()constdata:any={operationId:instance.operationId,driver:{ref:admin.firestore().doc(`${SAMPLE1}/${instance.driverId}`)},opeType:String(instance.opeType),opeDateFrom:dateFromSn(instance.opeDateFrom),opeDateTo:dateFromSn(instance.opeDateTo),opeStatus:String(instance.opeStatus),destinationDate:dateFromSn(instance.destinationDate),isUnplanned:toBoolean(instance.isUnplanned),createdAt:now,updatedAt:now,}returndata}returnxlsx2json(path,SAMPLE4,format_func)}

下記は、実際にExcelファイルから行データを生成する処理です。

commonUtils.ts(抜粋)#xlsx2json
importxPopWrapper=require('xlsx-populate-wrapper')/**
 * Excelファイルを読み込み、各行をデータとして配列で返すメソッド。
 * @param path Excelファイルパス
 * @param sheet シート名
 * @param format_func フォーマット関数。instanceは各行データが入ってくるので、任意に整形して返せばよい
 */exportconstxlsx2json=asyncfunction(path:string,sheet:string,format_func?:(instance:any)=>any):Promise<Array<any>>{constworkbook=newxPopWrapper(path)awaitworkbook.init()constinstances:Array<any>=workbook.getData(sheet)if(format_func){returninstances.map(instance=>format_func(instance))}returninstances}/**
 * Excelのシリアル値を、Dateへ変換します。
 * @param serialNumber シリアル値
 */exportconstdateFromSn=(serialNumber:number):Date=>{returnXlsxPopulate.numberToDate(serialNumber)}exportconsttoBoolean=function(boolStr:string|boolean):boolean{if(typeofboolStr==='boolean'){returnboolStr}returnboolStr.toLowerCase()==='true'}

「Firestoreデータを読み出して、Excelへ流し込んでダウンロード」のサンプルコード

Cloud StorageからテンプレートとなるExcelファイルを取得します。またFirestoreからはExcelに書き込むデータを取得し、再び「xlsx-populate-wrapper」を使ってExcelファイルへデータを書き込んで、ユーザへのResponseへExcelデータとして返却します。データをExcelへ書き込みつつ、ある程度の書式設定・罫線の描画も行っています。

download.ts
import{Request,Response}from'express'import*asadminfrom'firebase-admin'import{getSample4Promise}from'./sample4'import*aspathfrom'path'import*asosfrom'os'constSAMPLE4:string='sample4'importxPopWrapper=require('xlsx-populate-wrapper')exportconstdownload=async(request:Request,response:Response)=>{constbucket=admin.storage().bucket()constfileName='output.xlsx'constfullPath=path.join(os.tmpdir(),fileName)try{awaitbucket.file(fileName).download({destination:fullPath,})// ファイル読み込みconsole.log(fullPath)constworkbook=newxPopWrapper(fullPath)awaitworkbook.init()constrowCount=awaitaddRow(workbook)applyStyles(workbook,rowCount)constnewFileName='download.xlsx'constnewFilePath=path.join(os.tmpdir(),newFileName)// 書き込んだファイルを保存awaitworkbook.commit(newFilePath)console.log(newFilePath)response.download(newFilePath,newFileName)}catch(error){console.log(error)response.status(500).send(error)}}constaddRow=async(workbook:any):Promise<number>=>{constdatas=awaitgetSample4Promise()constconvertedDatas=datas.map(data=>Object.assign(data,{isUnplanned:String(data.isUnplanned)// Booleanだけは、Excelでfalseが表示出来ず。文字列化することにした。}))workbook.update(SAMPLE4,convertedDatas)// 更新returndatas.length}// https://www.npmjs.com/package/xlsx-populate#style-reference// https://support.office.com/en-us/article/Number-format-codes-5026bbd6-04bc-48cd-bf33-80f18b4eae68?ui=en-US&rs=en-US&ad=US// https://www.tipsfound.com/vba/07015constapplyStyles=(workbook:any,rowCount:number)=>{constsheet=workbook.getWorkbook().sheet(SAMPLE4)sheet.range(`D2:D${rowCount+1}`).style('numberFormat','@')// 書式: 文字(コレをやらないと、見かけ上文字だが、F2で抜けると数字になっちゃう)sheet.range(`G2:G${rowCount+1}`).style('numberFormat','@')// 書式: 文字(コレをやらないと、見かけ上文字だが、F2で抜けると数字になっちゃう)sheet.range(`E2:F${rowCount+1}`).style('numberFormat','yyyy/mm/dd')// 書式: 日付sheet.range(`H2:H${rowCount+1}`).style('numberFormat','yyyy/mm/dd hh:mm')// 書式: 日付+時刻// データのある行に、罫線を引くsheet.range(`A2:I${rowCount+1}`).style('border',{top:{style:'thin'},left:{style:'thin'},bottom:{style:'thin'},right:{style:'thin'}})}
sample4.ts(抜粋)#getSample4Promise
import*asadminfrom'firebase-admin'constSAMPLE4:string='sample4'typeQuerySnapshot=admin.firestore.QuerySnapshottypeDocumentSnapshot=admin.firestore.DocumentSnapshotexportconstgetSample4Promise=async():Promise<Array<any>>=>{constreturnArray:any=[]constsnapshot:QuerySnapshot=awaitadmin.firestore().collection(SAMPLE4).get()snapshot.forEach((docref:DocumentSnapshot)=>{constorgData=docref.data()!// nullはない、と仮定// プロパティを再定義。constdata=Object.assign(orgData,{opeDateFrom:orgData.opeDateFrom.toDate(),opeDateTo:orgData.opeDateTo.toDate(),destinationDate:orgData.destinationDate.toDate(),createdAt:orgData.createdAt.toDate(),updatedAt:orgData.updatedAt.toDate(),driverId:orgData.driver.ref.id,driver:orgData.driver.ref,})})returnreturnArray}

「Excelファイルを、Storageへアップロード(上記で用いるExcelテンプレートをアップロード)」のサンプルコード

登り電文のExcelファイルを受けとるのは、先ほどもでてきた「busboy」で。先ほどはファイルとして一時的に書き出しましたが、今回は受けとったデータをそのまま、Cloud Storage へ保存しています。

templateUploader.ts
import{Request,Response}from'express'import*asadminfrom'firebase-admin'import*asBusboyfrom'busboy'exportconsttemplateUpload=async(request:Request,response:Response)=>{// https://qiita.com/rubytomato@github/items/11c7f3fcaf60f5ce3365console.log('start.')constbusboy=newBusboy({headers:request.headers})constbucket=admin.storage().bucket()// This object will accumulate all the uploaded files, keyed by their name.constuploads:{[key:string]:string}={}busboy.on('file',(fieldname,file,filename,encoding,mimetype)=>{console.log('busboy.on.file start.')console.log(`File [${fieldname}]: filename: ${filename}, encoding: ${encoding} , mimetype: ${mimetype}`)uploads[fieldname]=filenamefile.on('data',async(data)=>{console.log(`File [${fieldname}] got ${data.length} bytes`)try{awaitbucket.file(filename).save(data,{contentType:'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'})}catch(error){console.log(error)response.status(500).send(error)}})file.on('end',()=>{console.log('file.on.end start.')console.log(`File [${fieldname}]: filename: ${filename} Finished.`)})})// Triggered once all uploaded files are processed by Busboy.// We still need to wait for the disk writes (saves) to complete.busboy.on('finish',()=>{console.log('busboy.on.finish start.')response.status(200).send(`${Object.keys(uploads).length} file(s) uploaded.`)})constreqex:any=requestbusboy.end(reqex.rawBody)}

細かい説明は省略してしまいましたが、だいたいこんな感じです。。

以上、おつかれさまでしたー。

関連リンク


  1. Functionsからの処理なので、Security Rules の設定は影響がない、はず。もちろん本運用時は適切な設定で。 


Viewing all articles
Browse latest Browse all 8862

Trending Articles