背景起,有奏樂:
有偉人曰:學習技能的最好途徑莫過於理論與實踐相結合。
初學Node這貨時,每每讀教程必會Fall asleep。
當真要開發系統時,頓覺精神百倍,即便踩坑無數也不失斗志。
因為同團隊的小伙伴們都在辛勤工作,正是因為他們的工作,
才讓我有足夠的時間拖着我疲軟的智商來研究Node和AWS這些貨。
系統完成,雖不盡完善,但不敢怠慢,迅速記錄,免遺忘。
為后續更新和開發做一參考。
這就是人生。只要努力,便美美噠。
標題略長,其實這系統要做的事只三件:
1. 從本地上傳文件到我們自己的服務器,並存儲。
2. 將文件上傳到七牛雲存儲。
3. 將文件上傳到亞馬遜的AWS S3存儲。
幾處說明:
1. 用Node的好處是寫服務端代碼也不用糾結語法問題了:
系統的開發用Node完成。寫前后端都是JS,免去了語法的困擾。
不僅回憶起數日之前寫Scala時對語法的糾結和困惑,一身冷汗。
2. Plupload是個好東東:
Client端的File Select用Plupload完成。
有了Plupload這貨,再不糾結<input type='file'>的難看樣式的兼容問題不好把控了。
Plupload雖然對File做了封裝,但也提供了如 getNative 等的接口供我們訪問原生。
十分體貼。
3. AWS的Upload在前端完成:
真相只有一個:在Node服務端的AWS的Upload我還沒跑通……
請盡情的鄙視我吧T_T
好在路路通羅馬。我繞路從前端趕到了羅馬。
服務端請求的Block在這里:
從服務端向AWS上傳文件時,其文件的Body以流方式被分塊上傳。
測試后發現,上傳完成,也只傳了部分,導致文件無法正常訪問。
而在前端上傳時,直接用原生File對象即可實現上傳。
遂成功抵達羅馬。
關於在服務端的上傳問題,有待繼續研究。
學海無涯0_0
4. 七牛的上傳在服務端完成:
七牛的上傳也可以在前端完成,只不過七牛自己的JS-SDK包裹了Plupload。
由於我的上傳邏輯是由自己的Plupload來觸發七牛和亞馬遜(或其他第三方上傳),
因此不在前端再New一個Plupload來做七牛的上傳了。
New兩個同樣的東西實在是太二了好么。
設計的理念是,所有第三方上傳都必須在我們的服務器Trigger之后才發生。
就醬任性。
—————— 我是冬季里顫巍巍的分割線 ——————
主要邏輯和部分代碼:
1. 主程序和框架:
使用Express框架和Jade渲染引擎。
主程序app.js只做服務器的創建和監聽,
涉及業務邏輯的請求和處理,都寫在二級目錄(./routes)的模塊里。
app.js 的部分內容如下:
3 var express = require('express');
4 var favicon = require('serve-favicon'); 5 var bodyParser = require('body-parser'); 6 var debug = require('debug')('express:server'); 7 var http = require('http'); 8 var port = normalizePort(process.env.PORT || '3038'); 9 var app = express(); 10 var server = http.createServer(app); 11 var index = require('./routes/index'); // 業務邏輯在這里 12 13 app.set('port', port); 14 server.on('error', serverOnError); 15 server.on('listening', serverOnListening); 16 server.on('connection', serverOnConnecting); 17 server.listen(port); 18 19 app.set('views', path.join(__dirname, 'views')); 20 app.set('view engine', 'jade'); 21 app.use(favicon(path.join(__dirname, 'public/lib', 'favicon.ico'))); 22 app.use(bodyParser.json()); 23 app.use(bodyParser.urlencoded({ extended: true })); 24 app.use(express.static(path.join(__dirname, 'public'))); 25 26 app.use('/', index);
1 /* ====================================================== */
2 module.exports = app;
2. POST請求將文件上傳並存儲在本地服務器:
需要注意的是,這里的POST請求用到了中間件:
1 var multipart = require('connect-multiparty'); 3 var multipartMiddleware = multipart(); 5 var express = require('express'); 7 var router = express.Router(); 9 router.post( ‘/saveInLocalServer’, multipartMiddleware, function(req, res){ 。。。});
這個請求接收的是從前端的Plupload上傳的File,
神秘的中間件會在服務器生成臨時文件,但不會刪除它們。
因此在處理的最后要手動刪除臨時文件req.files。How to?
收到請求后,處理文件的部分代碼如下:
1 var file = req.files.file; 2 var tempPath = file.path, 3 fileName = file.name, 4 fileType = file.type, 5 fileSize = file.size; 6 var uploadDirName = dirName.DirName; // 生成目錄的模塊,每月一生 7 var filenameWithMd5 = MD5( new Date().getTime() ) + '-' + fileName; 8 var filenameForCloud = fileRename.FileRename(fileName); 9 // 保存到本地服務器的文件,使用MD5重命名文件 10 // 上傳到雲存儲的文件,使用自定義的模塊重命名 11 var targetPath = path.resolve('./' + uploadDirName + '/' + filenameWithMd5); 12 // Save file in our local server: 13 fs.rename(tempPath, targetPath, function(err, data){ 14 if( err ){ 15 var result = 'error'; 16 res.status( result ).send(); 17 } else { 18 var result = 'ok'; 19 var uploadInfos = { ... }; // AWS的config信息定義在服務端,由模塊引入並發送到前端,供JS接口調用: 20 res.status( result ).send( uploadInfos ); 21 // Next do Qi Niu Upload ... blah blah blah 22 } 23 });
針對上述代碼的幾處說明:
a:關於在本地服務器生成目錄:
我們的需求是,每月首次觸發上傳動作時,在服務器創建一只新目錄。
該月內的其余上傳文件,都存儲在這一目錄里。
所有的文件會按上傳時間,以自然月為目錄而分類。
按月創建目錄的邏輯,我寫了一枚小小模塊,如下:
var fs = require('fs'); var _d = new Date(); var _year = _d.getFullYear(); var _month = (_d.getMonth() + 1 < 10)?('0' + (_d.getMonth() + 1)):(_d.getMonth() + 1); // 為整齊,月份都顯示為兩位數,因此1-9月前面加0 var dir = _year + '-' + _month + '-alex_upload'; if (!fs.existsSync(dir)){ fs.mkdirSync(dir); } exports.DirName = dir; // 輸出模塊名為DirName // ============================ // 假設這個文件名為makeDirName.js,則在業務邏輯中引入並應用要這樣: var d_name = require('../routes/makeDirName'); var someName = d_name.DirName; // 輸出的模塊名在這里被這樣引用
b:關於文件重命名:
我們的需求是,存在本地服務器的文件,使用MD5重命名。
上傳到雲存儲的文件,使用時間戳和隨機字符串共同重命名。
重命名文件的模塊是醬紫寫的:
1 function rename( filename ) { 2 var name = ''; 3 var possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 4 var length = 6; // 隨機字符串的長度,暫用6 5 for(var i = 0; i < length; i++){ 6 name += possible.charAt( Math.floor(Math.random() * possible.length) ); 7 } 8 var timestamp = new Date().getTime(); 9 name = timestamp + '-' + name + '-' + filename; 10 return name; 11 }; 12 13 exports.FileRename = rename;
c:關於res.status( 200 ).send( data ):
每個請求的response必須Call一下res.end(),
以此來告訴服務器這個請求的header和body都已發送,
並且這個請求已經完成。
如果不告訴服務器,呆萌的服務器是永遠不會知道的。
瀏覽器會一直在請求狀態中,標題欄的小圈圈一直在轉啊轉,
表示請求一直在持續啊持續。
在Call了res.end()之后,res.finished 的值為true,否則是false。
res.send() 會Call res.end(),因此不需重復Call。
3:在前端請求AWS S3
在發送剛才所提到的POST請求之前,
前端先new一個plupload的Uploader,部分代碼如下:
1 var _myUploader = new plupload.Uploader({ 2 runtimes: 'html5,flash,silverlight,html4', 3 file_data_name: 'file', 4 container: _SCOPE.containerId, 5 browse_button: _SCOPE.filePickerId, 6 uptoken_url: _ELE.fileUptoken.innerHTML, 7 url: _ELE.fileLocalSave.innerHTML, 8 flash_swf_url: _SCOPE.swfUrl, 9 silverlight_xap_url:_SCOPE.xapUrl, 10 filters: { 11 max_file_size: _SCOPE.maxFileSize, 12 mime_types: [ 13 {title: 'Image files', extensions: 'jpg,png,gif'}, 14 {title: 'Zip files', extensions: 'zip'} 15 ] 16 }, 17 init: {...} 18 });
這里的 _SCOPE 和 _ELE 定義在全局作用域,或指定頁面模塊作用域下。
目的是從服務端接收相關的配置參數,在頁面發送請求時調用。
這里遵循了一個高端大氣上檔次的寫碼原則,即:
常量參數的配置,
如Domain地址、取token之通信接口、
even 賬戶的accessKey&accessToken blah blah blah……
都在服務端某指定模塊內統一配置。
當前端需要某參數時,由頁面渲染res.render() 傳遞到頁面元素HTML屬性里,
但是不可以將Key等賬戶密鑰渲染在頁面結構里。
也可以通過前后端通信將參數傳遞給前端頁面,
例如剛才所述的POST接口里的uploadInfos。
這樣做,在一處定義,其余皆調用。
當值有更新時,只在定義處更新其值即可。
避免多處賦值,更新時丟三落四陷入混亂。
嗯咳,所有工程師都知道的好么!我說多了……
……繼續說上傳:
使用Plupload,在其FileUploaded 的回調里,
即可執行向AWS S3發送請求了。
FileUploaded是在Plupload的文件上傳成功后才會觸發。
前端請求AWS S3的簡要方法如下:
(這里的file是從FileUploaded的方法里用getNative獲取到的原生file對象)
1 function doAWSUpload( rename, file, info ) { 2 var file_name = file.name, 3 file_type = file.type, 4 file_size = file.size; 5 var bucket = new AWS.S3(); 6 var uniqueName = rename; 7 bucket.config.update({ // 配置信息,在服務端傳來的info里 8 accessKeyId: info.accessKeyId, 9 secretAccessKey: info.secretAccessKey 10 }); 11 bucket.config.region = info.region; 12 var params = { 13 Bucket: info.bucket, // 賬戶指定的bucket名 14 Key: uniqueName, 15 ContentType: file_type, 16 Body: file,
ACL: 'public-read', // 設置文件訪問權限 17 ServerSideEncryption: info.ServerSideEncryption 18 }; 19 bucket.putObject(params, function(err, data){ // 此賬戶必須要有putObject的操作權限才能調用 20 if(err){ 21 var errText = ' ' + file_name + ' failed in uploading to AWS! ' + err; 22 _ELE.fileConsole.innerHTML += errText; 23 }else{ 24 var url = 'https://s3.amazonaws.com/' + info.bucket + '/' + uniqueName;26 _ELE.fileConsole.innerHTML += ' AWS upload succeeded! ' + url; 27 } 28 }).on('httpUploadProgress', function(progress){ 29 console.log( 'AWS uploading...', Math.round(progress.loaded / progress.total * 100) ); 30 }); 31 };
執行這個方法的前提是前端頁面調用了JS-SDK,
並且,……最重要的是並且:
對應賬戶在AWS的Console管理后台的相關配置要正確。
最討厭各種相關配置了,
配來配去一百年才成功一次……
4:AWS的賬戶在Console管理后台的相關配置
首先注冊一枚高大上的AWS賬戶。
如果你經常在Amazon上買買買,也可以用你的Retail賬戶。
開通AWS服務,需要驗證,其過程要填寫Payment賬戶信息。
我十分Naive的填了自己的Credit Card信息,結果直接被扣掉1刀勒。
嚇尿之后,立刻刪。
大約因為作為Retail賬戶時我曾做過快捷支付神馬的腦殘設置吧。
總之,1美元而已,這已不是重點……
有了一枚飄逸的AWS賬戶后,登錄 https://console.aws.amazon.com
選擇S3服務,進來后無視一切,先Create Bucket,
點擊這個新的Bucket,選擇Properties,
在Permissions里,再選擇 “Edit CORS Configuration”,
一個較為典型的CORS Configuration可以長這個樣子:
1 <?xml version="1.0" encoding="UTF-8"?> 2 <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> 3 <CORSRule> 4 <AllowedOrigin>http://localhost:3038</AllowedOrigin> //本地測試入口 5 <AllowedOrigin>http://shaojing.wang</AllowedOrigin> //線上測試入口 6 <AllowedMethod>PUT</AllowedMethod> //可執行的方法 7 <AllowedMethod>DELETE</AllowedMethod> //可執行的方法 8 <MaxAgeSeconds>3000</MaxAgeSeconds> 9 <ExposeHeader>x-amz-server-side-encryption</ExposeHeader> 10 <ExposeHeader>x-amz-request-id</ExposeHeader> 11 <ExposeHeader>x-amz-id-2</ExposeHeader> 12 <AllowedHeader>*</AllowedHeader> 13 </CORSRule> 14 </CORSConfiguration>
這里的CORS Configuration即對跨域請求所做限制,
只有“AllowedOrigin”里指定的端口才能向AWS發出請求,
而只有“AllowedHeader”里指定的端口才能接收請求(訪問文件)。
上傳成功后,可通過這樣的URI訪問到文件:
https://s3.amazonaws.com/myBucketName/1452581386878-hPp8Mc-test.png
附:AWS的文檔在這里:http://docs.aws.amazon.com/
關於如何在服務端進行AWS S3的上傳,下次再寫文章分享。
下面該講什么了……
5:在服務端實現向七牛雲存儲上傳文件
該七牛了。
請八牛、九牛和十牛再耐心等一等。
六牛你不要鬧,你已經謝世了好么。
從服務器向七牛雲發送請求之前,需要獲取授權,
請求授權之前,需要設置賬戶信息。
設置賬戶信息之前,你得先有一枚賬戶。
有了賬戶就有了AccessKey & SecretKey。
還是剛才講的,在統一配置參數的模塊里,配置好這些Key們的信息,
然后在服務端將發送請求之前,做賦值:
1 var qiniu = require('qiniu'); 2 var qnConf = require('../config/qiniu_config'); 3 4 /* Prepare Qiniu config, we make Qiniu upload in Node Server not in browser*/ 5 qiniu.conf.ACCESS_KEY = qnConf.QiniuConfig.ACCESS_KEY; 6 qiniu.conf.SECRET_KEY = qnConf.QiniuConfig.SECRET_KEY;
賦值之后,就可以開心的去請求upToken了!
寫一只孤零零的單獨小模塊,用來生成upToken,代碼長這樣:
1 var qiniu = require('qiniu'); 2 3 function uptoken(bucketname) { // 指定一個bucket傳名字進來 4 var putPolicy = new qiniu.rs.PutPolicy(bucketname); 5 return putPolicy.token(); 6 } 7 8 exports.Uptoken = uptoken;
拿到upToken就可以華麗麗麗麗的開始上傳了。
可以在剛才本地存儲的POST請求成功后的回調里做。
代碼就像醬紫:
1 // Do Qiniu upload in here: 2 var targetPath = path.resolve('./' + uploadDirName + '/' + filenameWithMd5); //接剛才的POST里的處理 3 var qiniu_uptoken = generateUptoken.Uptoken(qnConf.QiniuConfig.Bucket_Name); 4 var extra = null; // 放額外信息,先寫null 5 fs.readFile(targetPath, function(error, data){ 6 qiniu.io.put(qiniu_uptoken, uploadDirName + '/' + filenameForCloud, data, extra, function(err, ret){ 7 if(err){ 8 console.log('Something is wrong with Qiniu upload! ', err); 9 }else{ 10 console.log('qiniu: ', ret); 11 console.log('Qiniu URL = ', qnConf.QiniuConfig.Domain + uploadDirName + '/' + filenameForCloud); //手動拼結果URL 12 } 13 }); 14 });
至此,七牛的上傳也OK鳥!
撒花~~樂隊起~~
5:后記
本文所述內容,僅限於最主要最基本的邏輯,
未涉及頁面的交互和部分異常響應的處理。
僅供參考。表扔雞蛋。