使用腾讯云 SCF 云函数压缩 COS 对象存储文件
歡迎大家前往騰訊云技術社區,獲取更多騰訊海量技術實踐干貨哦~
作者:騰訊云Serverless團隊
在使用騰訊云 COS 對象存儲的過程中,我們經常有想要把整個 Bucket 打包下載的需求,但是 COS 并沒有提供整個 Bucket 打包下載的能力。這時,我們可以利用騰訊云的 SCF 無服務器云函數,完成 COS Bucket 的打包,并重新保存壓縮后的文件到 COS 中,然后通過 COS 提供的文件訪問鏈接下載文件。
但是在使用 SCF 云函數進行 COS Bucket 打包的過程中,偶爾會碰到這樣的問題:我期望將某個 COS Bucket 內的文件全部下載下來然后打包壓縮,把壓縮文件再上傳到 COS 中進行備份;但是在這個過程中,COS Bucket 內的文件可能數量多體積大,而 SCF 云函數的運行環境,實際只有 512MB 的 /tmp 目錄是可以讀寫的。這樣算上下載的文件,和生成的 ZIP 包,可能僅支持一定體積的文件處理,滿足不了所需。怎么辦?
在這種情況下,可能有的同學會想到使用內存,將內存轉變為文件系統,即內存文件系統,或者直接讀取文件并放置在內存中,或者在內存中生成文件。這種方法能解決一部分問題,但同時也帶來了些其他問題:
我們在這里嘗試了一種流式文件處理的方式,通過單個文件壓縮后數據立即提交 COS 寫的方法,一次處理一個文件,使得被壓縮文件無需在 SCF 的緩存空間內堆積,壓縮文件也無需放在緩存或內存中,而是直接寫入 COS。在這里,我們實際利用了兩種特性:ZIP 文件的數據結構特性和 COS 的分片上傳特性。
zip 文件的數據結構
在官方文檔中給出的 zip 文件格式如下:
Overall .ZIP file format:[local file header 1][file data 1][data descriptor 1]. ..[local file header n][file data n][data descriptor n][archive decryption header] (EFS)[archive extra data record] (EFS)[central directory][zip64 end of central directory record][zip64 end of central directory locator] [end of central directory record]可以看到,實際的 zip 文件格式基本是[文件頭+文件數據+數據描述符]{此處可重復n次}+核心目錄+目錄結束標識
組成的,壓縮文件的文件數據和壓縮數據是在文件頭部,相關的目錄結構,zip文件信息存儲在文件尾部。這樣的結構,為我們后續 COS 分片上傳寫入帶來了方便,可以先寫入壓縮數據內容,再寫入最終文件信息。
COS 分片上傳
COS 分片上傳按照如下操作即可進行:
在上傳過程中,還隨時可以查詢已上傳分片,或結束取消分片上傳。
文件壓縮處理流程設計
利用 zip 文件數據結構中文件壓縮數據在前目錄和額外標識在后的特性,和 COS 支持分片上傳的特性,我們可以利用流式文件處理方式來依次處理文件,并且做到處理完成一個文件壓縮就上傳處理后的壓縮數據分片。這種處理流程可以簡化為如下說明:
1. 初始化 zip 文件數據結構,并將數據結構保存在內存中。
2. 初始化 COS 分片上傳文件,保存好分片上傳 ID。
3. 下載要放入壓縮包的文件至本地,使用 zip 算法,生成壓縮文件的數據內容并保存在內存中,并根據目錄格式,更新zip數據格式中的目錄標識。
4. 將壓縮后的文件數據使用 COS 上傳分片,上傳至 COS 中。
5. 清理刪除下載至本地的需壓縮文件。
6. 根據需要,重復 3~5 步驟,增加壓縮包內的文件。
7. 在壓縮文件處理完成后,使用分片上傳,將內存中的 zip 文件數據結構最后的目錄結構部分上傳至 COS。
8. 通知 COS 結束上傳,完成最終 zip 文件的自動拼接。
在這個處理流程中,一次只處理一個文件,對本地緩存和內存使用都只這一個文件的占用,相比下載全部文件再處理,大大減小了本地緩存占用和內存占用,這種情況下,使用少量緩存和內存就可以完成 COS 中大量文件的壓縮打包處理。
使用SCF進行 COS 文件壓縮處理實現
流式壓縮文件庫 archiver
我們這里使用 node.js 開發語言來實現 COS 文件壓縮處理。我們這里使用了 cos-nodejs-sdk-v5 sdk 和 archiver 模塊。其中 archiver 模塊是實現zip和tar包壓縮的流式處理庫,能夠通過 append 輸入欲壓縮文件,通過 stream 輸出壓縮后的文件流。archiver的簡單用法如下:
// require modules var fs = require('fs'); var archiver = require('archiver');// create a file to stream archive data to. var output = fs.createWriteStream(__dirname + '/example.zip'); var archive = archiver('zip', {zlib: { level: 9 } // Sets the compression level. });// pipe archive data to the file archive.pipe(output);// append a file from stream var file1 = __dirname + '/file1.txt'; archive.append(fs.createReadStream(file1), { name: 'file1.txt' });// append a file archive.file('file1.txt', { name: 'file4.txt' });// finalize the archive (ie we are done appending files but streams have to finish yet) // 'close', 'end' or 'finish' may be fired right after calling this method so register to them beforehand archive.finalize();archiver 將會在每次 append 文件的時候,將文件的壓縮數據輸出到 pipe 指定的輸出流上。因此,我們在這里可以通過實現我們自身的 WriteStream,獲取到 archiver 的寫請求,并把寫入內容轉移到 COS 模塊的分片上傳能力上。在這里,我們實現的 WriteStream 為:
var Writable = require('stream').Writable; var util = require('util');module.exports = TempWriteStream;let handlerBuffer;function TempWriteStream(options) {if (!(this instanceof TempWriteStream))return new TempWriteStream(options);if (!options) options = {};options.objectMode = true;handlerBuffer = options.handlerBuffer;Writable.call(this, options); }util.inherits(TempWriteStream, Writable);TempWriteStream.prototype._write = function write(doc, encoding, next) {handlerBuffer(doc);process.nextTick(next) };通過集成 nodejs 中的 Writable stream,我們可以將寫操作轉移到我們所需的 handle 上去,handle 可以對接 COS 的分片上傳功能。
COS 分片上傳
COS 分片上傳功能的實現如下,我們將其封裝為 Upload 模塊:
const cos = require('./cos')let Duplex = require('stream').Duplex; function bufferToStream(buffer) {let stream = new Duplex();stream.push(buffer);stream.push(null);return stream; }// 大于4M上傳 const sliceSize = 4 * 1024 * 1024function Upload(cosParams) {this.cosParams = cosParams;this.partNumber = 1;this.uploadedSize = 0;this.Parts = []this.tempSize = 0;this.tempBuffer = new Buffer('') }Upload.prototype.init = function (next) {const _this = this;cos.multipartInit(this.cosParams, function (err, data) {_this.UploadId = data.UploadIdnext()}); } Upload.prototype.upload = function(partNumber, buffer) {const _this = this;const params = Object.assign({Body: bufferToStream(buffer),PartNumber: partNumber,UploadId: this.UploadId,ContentLength: buffer.length}, this.cosParams);cos.multipartUpload(params, function (err, data) {if (err) {console.log(err)} else {_this.afterUpload(data.ETag, buffer, partNumber)}}); }Upload.prototype.sendData = function (buffer) {this.tempSize += buffer.length;if (this.tempSize >= sliceSize) {this.upload(this.partNumber, Buffer.concat([this.tempBuffer, buffer]))this.partNumber++;this.tempSize = 0;this.tempBuffer = new Buffer('')} else {this.tempBuffer = Buffer.concat([this.tempBuffer, buffer]);} }Upload.prototype.afterUpload = function (etag, buffer, partNumber) {this.uploadedSize += buffer.lengththis.Parts.push({ ETag: etag, PartNumber: partNumber })if (this.uploadedSize == this.total) {this.complete();} }Upload.prototype.complete = function () {this.Parts.sort((part1, part2) => {return part1.PartNumber - part2.PartNumber});const params = Object.assign({UploadId: this.UploadId,Parts: this.Parts,}, this.cosParams);cos.multipartComplete(params, function (err, data) {if (err) {console.log(err)} else {console.log('Success!')}}); }Upload.prototype.sendDataFinish = function (total) {this.total = total;this.upload(this.partNumber, this.tempBuffer); }module.exports = Upload;對于 COS 本身已經提供的 SDK,我們在其基礎上封裝了相關查詢,分片上傳初始化,分片上傳等功能如下:
const COS = require('cos-nodejs-sdk-v5');const cos = new COS({AppId: '125xxxx227',SecretId: 'AKIDutrojxxxxxxx5898Lmciu',SecretKey: '96VJ5tnlxxxxxxxl5To6Md2', }); const getObject = (event, callback) => {const Bucket = event.Bucket;const Key = event.Key;const Region = event.Regionconst params = {Region,Bucket,Key};cos.getObject(params, function (err, data) {if (err) {const message = `Error getting object ${Key} from bucket ${Bucket}.`;callback(message);} else {callback(null, data);}}); };const multipartUpload = (config, callback) => {cos.multipartUpload(config, function (err, data) {if (err) {console.log(err);}callback && callback(err, data);}); };const multipartInit = (config, callback) => {cos.multipartInit(config, function (err, data) {if (err) {console.log(err);}callback && callback(err, data);}); };const multipartComplete = (config, callback) => {cos.multipartComplete(config, function (err, data) {if (err) {console.log(err);}callback && callback(err, data);}); };const getBucket = (config, callback) => {cos.getBucket(config, function (err, data) {if (err) {console.log(err);}callback && callback(err, data);}); };module.exports = {getObject,multipartUpload,multipartInit,multipartComplete,getBucket };在具體使用時,需要將文件中 COS 相關登錄信息的APPId,SecretId,SecretKey等替換為自身可用的真實內容。
功能入口實現函數
我們在最終入口函數 index.js 中使用各個組件來完成最終的目錄檢索,文件壓縮打包上傳。在這里,我們利用函數入參來確定要訪問的 bucket 名稱和所屬地域,期望壓縮的文件夾和最終壓縮后文件名。云函數入口函數仍然為 main_handler。
// require modules const fs = require('fs'); const archiver = require('archiver');const cos = require('./cos');const Upload = require('./Upload')const TempWriteStream = require('./TempWriteStream')const start = new Date();const getDirFileList = (region, bucket, dir, next) => {const cosParams = {Bucket: bucket,Region: region,}const params = Object.assign({ Prefix: dir }, cosParams);cos.getBucket(params, function (err, data) {if (err) {console.log(err)} else {let fileList = [];data.Contents.forEach(function (item) {if (!item.Key.endsWith('/')) {fileList.push(item.Key)}});next && next(fileList)}}) }const handler = (region, bucket, source, target) => {const cosParams = {Bucket: bucket,Region: region,}const multipartUpload = new Upload(Object.assign({ Key: target}, cosParams));const output = TempWriteStream({ handlerBuffer: multipartUpload.sendData.bind(multipartUpload) })var archive = archiver('zip', {zlib: { level: 9 } // Sets the compression level.});output.on('finish', function () {multipartUpload.sendDataFinish(archive.pointer());});output.on('error', function (error) {console.log(error);});archive.on('error', function (err) {console.log(err)});archive.pipe(output);multipartUpload.init(function () {getDirFileList(region, bucket, source, function(fileList) {let count = 0;const total = fileList.length;for (let fileName of fileList) {((fileName) => {let getParams = Object.assign({ Key: fileName }, cosParams)cos.getObject(getParams, (err, data) => {if (err) {console.log(err)return}var buffer = data.Body;console.log("download file "+fileName);archive.append(buffer, { name: fileName.split('/').pop() });console.log("zip file "+fileName);count++;if (count == total) {console.log("finish zip "+count+" files")archive.finalize();}})})(fileName)}})}) }exports.main_handler = (event, context, callback) => {var region = event["region"];var bucket = event["bucket"];var source = event["source"];var zipfile = event["zipfile"];//handler('ap-guangzhou', 'testzip', 'pic/', 'pic.zip');handler(region, bucket, source, zipfile) }測試及輸出
最終我們將如上的代碼文件及相關依賴庫打包為zip代碼包,創建函數并上傳代碼包。同時我們準備好一個 COS Bucket命名為 testzip, 在其中創建 pic 文件夾,并在文件夾中傳入若干文件。通過函數頁面的測試功能,我們使用如下模版測試函數:
{ "region":"ap-guangzhou", "bucket":"testzip", "source":"pic/", "zipfile":"pic.zip" }函數輸出日志為:
... 2017-10-13T12:18:18.579Z 9643c683-b010-11e7-a4ea-5254001df6c6 download file pic/DSC_3739.JPG 2017-10-13T12:18:18.579Z 9643c683-b010-11e7-a4ea-5254001df6c6 zip file pic/DSC_3739.JPG 2017-10-13T12:18:18.689Z 9643c683-b010-11e7-a4ea-5254001df6c6 download file pic/DSC_3775.JPG 2017-10-13T12:18:18.690Z 9643c683-b010-11e7-a4ea-5254001df6c6 zip file pic/DSC_3775.JPG 2017-10-13T12:18:18.739Z 9643c683-b010-11e7-a4ea-5254001df6c6 download file pic/DSC_3813.JPG 2017-10-13T12:18:18.739Z 9643c683-b010-11e7-a4ea-5254001df6c6 finish zip 93 files 2017-10-13T12:18:56.887Z 9643c683-b010-11e7-a4ea-5254001df6c6 Success!可以看到函數執行成功,并從 COS Bucket 根目錄看到新增加的 pic.zip 文件。
項目源代碼及改進方向
目前項目所有源代碼已經放置在 Github 上,路徑為 https://github.com/qcloud-scf/demo-scf-compress-cos。可以通過下載或 git clone 項目,獲取到項目源代碼,根據自身帳號信息,修改 cos 文件內的帳號 APPId、SecretId、SecretKey這些認證信息,然后將根目錄下所有文件打包至 zip 壓縮包后,通過 SCF 創建函數并通過 zip 文件上傳代碼來完成函數創建,根據上面所屬的“測試及輸出”步驟來測試函數的可用性。
函數在此提供的仍然只是個demo代碼,更多的是為大家帶來一種新的思路及使用騰訊云 SCF 無服務器云函數和 COS 對象存儲。基于此思路,Demo本身后續還有很多可以改進的方法,或根據業務進行變化的思路:
后續對于此 Demo 如果有更多疑問,想法,或改進需求,歡迎大家提交 git pr 或 issue。項目地址:https://github.com/qcloud-scf/demo-scf-compress-cos
相關閱讀
使用騰訊云 CDN 、COS 以及萬象優圖實現HTTP/2樣例
如何利用云對象存儲 COS 免費托管靜態網站
Serverless 初探
此文已由作者授權騰訊云技術社區發布,轉載請注明文章出處
原文鏈接:https://cloud.tencent.com/community/article/810260
總結
以上是生活随笔為你收集整理的使用腾讯云 SCF 云函数压缩 COS 对象存储文件的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Android播放音效
- 下一篇: 什么是BPOS