前言
雖然實作過檔案之間的傳送或編寫/覆蓋新檔案,但發現自己對底層實作的原理完全沒有概念,加上這次在研究GCS的實作,因此有了這篇文章的誕生。學習程式之後察覺,每個部分都可以越挖越深,越看越細。
Stream - 流
什麼是Stream流? I/O的傳輸=>資料輸入輸出,有資料從一端流向另一端的感覺。HTTP請求、檔案傳輸都會使用到Stream的概念。
有四種不同的流:
- Readable streams 創造讀取的流
- Writable streams 創造寫入的流
- Duplex streams 可讀可寫流
- Transform streams 基於Duplex,在讀寫過程可以調整數據的流
在實作上我們常用的fs套件就是base on 原生的stream再包裝,所以在fs裡面也可以看到很多stream的使用方法。
Buffer - 緩衝區
二進制緩衝區,和String可以互相做轉換。Stream的傳輸會將數據儲存到內部的緩衝區(buffer)中,buffer容量滿時再流向另一端,這麼做可以減少stream直接對磁碟頻繁的操作。而緩衝器的容量大小也可以藉由highWaterMark
來做設定。
附上fs操作檔案的實作
1 2 3 4 5
| const fs = require('fs'); const read = fs.readFileSync('./test1.txt');
const readstream = fs.createReadStream('./test1.txt');
|
如果使用createreadstream
做傳輸的話,會使用pipe()來做搭配,資料在流動的時候需要有一個管道讓它進行傳輸
而有趣的是,read & write的用法是有對應的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const readable = fs.createReadStream('test1.txt'); const writeable = fs.createWriteStream('test2.txt')
readable .on('error',(err)=>{ }) .on('data',(chunk)=>{ console.log(chunk) }) .on('end',()=>{ console.log('there will be no more data.') })
writeable .on('pipe',(stream)=>{ console.log(stream) }) .on('finish',()=>{ console.log('數據寫入新檔案完成會觸發') })
readable.pipe(writeable);
|
以上的執行順序會是
- writeable打開pipe管道,開始接收readable的stream
- readable的data事件觸發,開始將數據轉換成buffer
- readable的end事件觸發,數據已全數轉換完畢
- writeable的finish事件觸發,數據全數完成寫入新檔案
還有其他的事件
可以到官方文件看看
因為使用方法很多,這裡我是想自己記錄另外一些使用方法,
自行將檔案轉成buffer再透過可寫流寫入檔案,
也就是使用上面的readFileSync方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const fs = require('fs'); const readbuffer = fs.readFileSync('./test1.txt'); const writeable = fs.createWriteStream(); writeable.write(readbuffer,()=>{ console.log('write successfully') });
const fs = require('fs'); const readbuffer = fs.readFileSync('./test1.txt'); const writeable = fs.createWriteStream(); writeable .on('finish',()=>{ console.log('完成') }) .end(readbuffer,()=>{ console.log('最後一個想寫入的東西,寫入完成') })
|
Google Storage實作檔案傳輸
google storage,google提供的雲端空間,用來存放檔案或圖片,和AWS的S3是相同功能,實務上用來將網頁上傳的檔案跟圖片存放到這邊來,成功之後就可以返回一個url提供我們下載自己上傳的檔案&呈現在自己的網頁上。
- 必須先npm
@google-cloud/storage
才能連接google storage空間
- 必須先到GCP取得憑證,利用憑證才能進入google storage空間
- 提供GCS bucket name => 也就是你在google storage中開創的空間名稱
完成以上才會開始寫程式
*這邊沒有寫太多GCS說明,主要注重在傳輸檔案的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| const { keyFilename } = require('./config'); const { Storage } = require('@google-cloud/storage'); const storage = new Storage({ keyFilename });
let bucket = storage.bucket('your_bucket_name')
const uploadFromBuffer = (buffer, destination) => new Promise((resolve, reject)=>{ const file = bucket.file(destination) const ws = file.createWriteStream(); ws .on('error',(err)=>{ reject(err); }) .on('finish',async()=>{ resolve(await file.publicUrl()); }) .end(buffer); });
|
會看到@google-cloud/storage
的使用也有createReadStream
, createWriteStream
的方法,表示他們也有使用stream再包裝~
前後端上傳檔案實作
以上是單純上傳到google storage的程式碼,這部分通常都會寫在後端,現在想實作從前端串接後端API再上傳至GCS上,前端到後端API的這段我也研究了一些時間,原因是因為跟postman的不熟悉?以及傳送檔案這樣的實物和傳送JSON感覺上不同而導致在操作上會有很多疑惑,許多不確定經過一試再試的精神才總算完成了!!
之前也有實作過傳送檔案的方法,當時使用的方式是google搜尋後比較常見的方法,是使用FormData在前端抓取到資料,而他的實作順序是
- 使用者上傳檔案,前端接收到data訊息
- 打後端API傳送formData,檔案被傳送到專案的資料夾內,檔案相關資料用Multer接收
- 後端API用stream傳送資料到GCS上
完成前到後的流程,如果想看相關教學可以看這裡
2. 前端直接傳送buffer到後端
用buffer傳到後端是這次新的學習,這邊紀錄一下。
後端的Buffer在前端是概括了ArrayBuffer、TypeBuffer、viewData,
ArrayBuffer是一大塊內存,只能唯讀,若想對ArrayBuffer暫存器做操作,要使用TypeBuffer的種類才可以,而TypeBuffer種類有Uint8array, Uint16array…等等,而此次前端的資料流就使用了Uint8array來傳送到Server端。
- 前端使用file API - FileReader操作,將檔案讀取成ArrayBuffer格式
- fetch給server時 body的data要將ArrayBuffer轉成Uint8array才可傳送
- server接收到Uint8array後再轉成Buffer才可傳至gcs
upload.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99
| <!DOCTYPE html> <html lang="en">
<head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head>
<body> <input type="file" accept="" id="file-uploader"> <input type="button" value="案 下去" id="btn"> <script> const fileUploader = document.getElementById('file-uploader'); const btn = document.getElementById('btn') let file; let filename; fileUploader.addEventListener('change', handleFileUpload); btn.addEventListener('click', upload); async function upload() { try { const beforeUploadCheck = await beforeUpload(file); if (!beforeUploadCheck.isValid) throw beforeUploadCheck.errorMessages;
const arrayBuffer = await getArrayBuffer(file); await uploadFileAJAX(arrayBuffer, filename); console.log("File Uploaded Success");
} catch (err) { console.log(err) } } async function handleFileUpload(e) { file = e.target.files[0] filename = e.target.files[0].name console.log(file) console.log(filename) if (!file) return; }
function beforeUpload(fileobj) { return new Promise((resolve) => { const validFileTypes = ["image/jpeg", "image/png", "xlsx"]; const isValidFileType = validFileTypes.includes(fileobj.type) let errorMessages = [];
if (!isValidFileType) errorMessages.push('You can only upload JPG or PNG file!');
const isValidFileSize = fileobj.size / 1024 / 1024 < 2; if (!isValidFileSize) errorMessages.push('Image must smaller than 2MB!'); resolve({ isValid: isValidFileType && isValidFileSize, errorMessages: errorMessages.join("\n") }); }); };
function getArrayBuffer(fileobj) { return new Promise((resolve, reject) => { const reader = new FileReader() reader.addEventListener('load', () => { console.log(reader) resolve(reader.result) }); reader.addEventListener('error', () => { reject('error occurred in getArrayBuffer'); }); reader.readAsArrayBuffer(fileobj) }); };
function uploadFileAJAX(arrayBuffer, filename) { return fetch("/upload/buffer", { headers: { "content-type": "application/json", }, method: "POST", body: JSON.stringify({ uint8array: new Uint8Array(arrayBuffer), filename: `test/${filename}`
}) }) .then(res => { return res.json() }) .then(data => console.log(data)) .catch(err => console.log(err)) }; </script> </body>
</html>
|
express接收部分
1 2 3 4 5 6 7 8 9
| app.post('/upload/buffer', async (req, res) => { const _uint8array = req.body.uint8array const _filename = req.body.filename const uint8array = Uint8Array.from(Object.values(_uint8array)) const buffer = Buffer.from(uint8array) const g_url = await GCS.uploadFromBuffer(buffer, _filename) return res.json({ status: "OK", data: g_url }) })
|
參考這篇的範例程式碼
心得:
邊整理資料邊整理思緒,想了一遍流程後思路也更清晰一些,但寫文章關於架構的整理,想著要怎麼寫才能清楚一點紀錄整個過程還是挺花時間的就是。以上就是近日來了解的部分,若說要研究肯定還可以再看更多細節QQ,只能多看看文件,相信會跟它們熟一點~以上
資料來源:
认识node核心模块–从Buffer、Stream到fs
瞭解Node.js中的Stream
Node.js流
Streams and Buffers in Node.js
官方文件
官方文件中文
google storage SDK文件
Node.js 大檔案上傳
[WebAPIs] 檔案上傳 Input File, File Upload, and FileList
nodejs 处理文件上传(express)
Author:
Chi Lin
Permalink:
https://chiderlin.github.io/2021/12/07/buffer-stream/
License:
Copyright (c) 2019 CC-BY-NC-4.0 LICENSE
Slogan:
Do you believe in DESTINY?