服務端文件上傳

上一篇談到了小程序端從選擇文件到文件的上傳下載整個流程。但是文件上傳服務器的真正操作實際上是在服務器實現。本篇文章主要談談服務端如何實現文件上傳到服務器並返回可支持訪問的url。首先,我們可以先考慮下業務邏輯。我給出的方案一是這樣一個簡單邏輯:將上傳文件分成圖片上傳和文件上傳兩部分邏輯。爲什麼要區分兩部分邏輯呢?因爲我們假設一個業務場景:商品上架功能需要上傳商品主圖,輪播圖等一系列圖片,我們如果一次只能上傳一張圖片,則得調用多次接口,會造成服務器帶寬和資源的浪費。所以我們處理圖片上傳我們可以設置圖片數組放置需上傳的圖片。那對於非圖片的文件呢?比如我們要上傳一個視頻,可能幾十M,我們同時上傳十個八個,這時候客戶端遲遲得不到響應,用戶體驗會很差,所以我們在處理非圖片文件時一般需要一個一個文件進行上傳。接下來我們來看下服務端如何實現文件上傳。


用過Node的人應該都知道,Node實現文件上傳一般都需要使用multiparty庫,我們首先需要生成multiparty對象並配置文件最終上傳的路徑:
//生成multiparty對象,並配置上傳目標路徑var form = new multiparty.Form({uploadDir: (mainPath + '/picTemp/')});

我們生成multiparty對象後,就可以使用multiparty.from().parse(req, callback)進行文件上傳。文件上傳成功實際上就會上傳到我們剛纔定義的上傳目錄中,然後返回files。我們可以看下文件上傳效果:


這時候有人說文件上傳解決了,當然沒那麼簡單。我們文件上傳看似解決了,但是還需要考慮各種各樣的bug場景,簡單舉幾個例子:服務器設置文件上傳最大爲25M,我上傳一個50M的文件,這時候服務器肯定返回413狀態碼標識文件太大。再比如我們需要限制文件最大上傳數量等等邏輯。所以接下來我們開始慢慢優化這個文件上傳功能。首先我們需要先對參數做限制,一個變量名只能對應一個文件,比如我上傳兩個文件,文件名都用mp4_url,這時候肯定不允許,這時候我們需要報錯並刪除已上傳圖片:
//查看圖片是否超過限制        var picNum = 0;        par.picNames = Object.keys(files);        var picSizeArr = [];        for(var picKey in files){            if(files[picKey].length > 1){//每個名字只能帶一張圖片                delPicsWithFiles(files);                return cb('文件參數有誤', 400);            }
par[picKey] = files[picKey][0].path; picNum += files[picKey].length; picSizeArr.push(parseInt(files[picKey][0].size)); } //根據上傳來的files表單刪除圖片 function delPicsWithFiles(files) { //圖片超過限制,刪除上傳來的圖片 for (var key in files) { files[key].forEach(function (picObj) { var uploadedPath = picObj.path; fs.unlink(uploadedPath, function () { }); }); } }

第一步校驗通過了,下一步就是針對圖片和非圖片做不同的操作。圖片允許多圖同時上傳,所以我們需要判斷上傳的圖片是否超過我們限制的最大張數,如果圖片張數超限,則刪除所有已上傳圖片:

if(picNum > maxPic){   //圖片超過限制,刪除上傳來的圖片  delPicsWithFiles(files);  return cb('圖片個數超過限制', 400);}

並且需要判斷圖片文件大小是否符合規範,一般大小要和服務器配置一致,防止文件大小超過服務器限制大小。

if(picSizeArr[0] > 4000000) {   delPicsWithFiles(files);   return cb('圖片過大,請重新選圖!',400);}
一般上傳功能會有業務邏輯操作,比如上傳成功保存數據庫。所以我們得對參數進行校驗,比如參數不全的情況就得刪除所有已上傳圖片:
//檢驗參數是否正確,包括圖片命名,不正確的話去刪除上傳的圖片,並且返回錯誤            checkParFunc(par,function (err,errCode) {
if(!err){//驗證正確,去重命名 par.files = files; picHelp.renamePics(par,pathDir,isNeedUid,function (err,errCode,param) { if(err){ cb(err, errCode, param); delPicsWithFiles(files); return; }
cb(null, 0, param); });
return; }
//驗證不正確,刪除上傳來的圖片 delPicsWithFiles(files); cb(err,errCode); }); function checkParFunc(par, cb) { if (!par.banner1 || !par.shopTitle1 || !par.price1 || !par.score1 || !par.linkUrl1) { return cb('參數不全', 400); } cb(null, 0, par); }

如果到這裏檢驗通過一般來說我們圖片上傳業務邏輯沒問題了。但是我們還是可以繼續優化,剛纔上傳成功的截圖我們可以看到文件上傳後文件名都是隨機字符串,我們很多時候都是需要對文件上傳做分類纔可以維護數據。所以下一步我們通過分割時間戳按照時間來將上傳的圖片轉移到新的文件夾存儲,並且我們移動到真正存儲的文件夾時,通過fs.readFile()取到文件後綴名,然後將文件重命名成按時間戳進行命名,最終移動文件夾返回文件所在的地址,文件上傳邏輯大功告成:

//給上傳的圖片重命名 //par:參數 picType:路徑名picHelp.renamePics = function (par,picType,isNeedUid,cb) {    if(!par.files){        cb('參數有誤',400);        return;    }    //構造路徑    var uid = 0;    if(par.userInfo){        uid = par.userInfo.main_userInfo ? par.userInfo.main_userInfo.uid : par.userInfo.uid;    }    var date = new Date();    var userPath = '/' + picType;    userPath += '/' + date.getFullYear();    userPath += '/' + (date.getMonth()+1);    userPath += '/' + date.getDate();    if(isNeedUid == true) {        userPath += '/' + parseInt(uid / 100);        userPath += '/' + uid;    }    mkdirs((mainPath + userPath),function (err) {//創建目錄        if(err){            cb(err,400);            return;        }
userPath += '/' + date.getHours() + date.getMinutes() + date.getSeconds() + date.getMilliseconds(); changeDir(par, 0, userPath, function (err, par) {
if (err) { cb(err, 400,par); return; } cb(null, 0, par); }); });}
//遞歸創建目錄 異步方法function mkdirs(dirname, callback) { fs.exists(dirname, function (exists) { if (exists) { callback(null); } else { mkdirs(path.dirname(dirname), function () { fs.mkdir(dirname, callback); }); } });}
//更新圖片路徑function changeDir(par,index,userPath,callback) { var keyArr = Object.keys(par.files);
if(keyArr.length < 1){ callback(null,par); return; } var picObj = par.files[keyArr[index]][0]; var uploadedPath = picObj.path;
fs.readFile(uploadedPath, function (err,bytesRead) { if (err) { callback(err,par); return; } var info = imageInfo(bytesRead); var type; if(!info || !info.format){ type = '.jpg'; }else { type = imageInfoFileType(info.format); if (!type) { callback('上傳圖片格式有誤', par); return; } } //參數正確 更換圖片路徑 var picPach = userPath + type; var dstPath = mainPath + picPach; checkDirs(dstPath,function (exits) { if(exits){ picPach = userPath + (keyArr.length + index) + type; dstPath = mainPath + picPach; } //重命名爲真實文件名 fs.rename(uploadedPath,dstPath,function (err) { if(err){ callback(err,par); return; } par[picObj.fieldName] = picPach; if(index < (keyArr.length-1)){ changeDir(par,index+1,userPath,callback); }else { callback(null,par); } }); }); });}


講完了圖片上傳功能,那針對非圖片上傳如何實現呢?實際上非文件上傳我們可以設置一次只允許上傳一個文件,然後判斷文件大小是否超過限制,然後一樣驗證參數是否又出現參數不全等情況,最後一樣進行按時間戳分割移動到當天文件夾下存放並進行重命名成按時間戳命名並返回圖片路徑。邏輯和剛纔圖片處理類似所以 我們直接看看代碼:
if(par.mp4_url) {   if(!files.mp4_url || !files.mp4_url[0] || !files.mp4_url[0].size || files.mp4_url[0].size == 0) {     cb('視頻上傳時發生錯誤!', 400);        return;     }
if(files.mp4_url[0].size > 8000000) { fs.unlink(par.mp4_url, function () {}); cb('文件文件過大',400); return;     }
delete files['mp4_url']; delPicsWithFiles(files); pathDir = 'bbs_mp4';
checkParFunc(par, function (err,errCode) { if(err){ fs.unlink(par.mp4_url, function () {}); return cb(err, errCode); } picHelp.renameVideo(par,pathDir,isNeedUid,function (err,errCode,param) { if(err){ cb(err,errCode,param); fs.unlink(par.mp4_url, function () {});             return; } cb(null, 0, param); });        return;     });}
//文件上傳picHelp.renameVideo = function (par,picType,isNeedUid,cb) { var uid = 0; if(par.userInfo){ uid = par.userInfo.main_userInfo ? par.userInfo.main_userInfo.uid : par.userInfo.uid; } var date = new Date(); var userPath = '/' + picType; userPath += '/' + date.getFullYear(); userPath += '/' + (date.getMonth()+1); userPath += '/' + date.getDate(); if(isNeedUid == true) { userPath += '/' + parseInt(uid / 100); userPath += '/' + uid; } mkdirs((mainPath + userPath),function (err) {//創建目錄 if(err){ cb(err,400); return; } userPath += '/' + date.getHours() + date.getMinutes() + date.getSeconds() + date.getMilliseconds(); var uploadedPath = par.mp4_url; fs.readFile(uploadedPath, function (err) { if (err) { cb(err, 400); return; } var type = par.mp4_url.split('.')[1]; var picPach = userPath + '.' + type; par.mp4_name = picPach; var dstPath = mainPath + picPach; checkDirs(dstPath,function (exits) { if(exits){ picPach = userPath + type; dstPath = mainPath + picPach; } fs.rename(uploadedPath,dstPath,function (err) { if(err){ cb(err,400); return; } par.mp4_url = par.mp4_name; cb(null, 0, par); }); }); });    });}


到這裏我們可以測試下不同文件的上傳效果可以看到測試多種不同格式最後全部成功上傳:


目前博客小程序前後端已開源於碼雲,歡迎來一個star。源碼地址:
https://gitee.com/mqzuimeng_admin/wx_blog.git
歡迎體驗小程序,如果有修改建議可以在小程序提交意見反饋或加入技術羣諮詢。

歡迎關注公衆號:程序猿周先森。查看更多精彩文章。

本文分享自微信公衆號 - 程序猿周先森(zhanyue_org)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章