在Node.js中使用HTTP上傳文件的方法
開發(fā)環(huán)境
我們將使用 Visual Studio Express 2013 for Web 作為開發(fā)環(huán)境, 不過它還不能被用來做 Node.js 開發(fā)。為此我們需要安裝 Node.js Tools for Visual Studio。 裝好后 Visual Studio Express 2013 for Web 就會轉(zhuǎn)變成一個 Node.js IDE 環(huán)境,提供創(chuàng)建這個應(yīng)用所需要的所有東西.。而基于這里提供的指導(dǎo),我們需要:
- 下載安裝 Node.js Windows 版,選擇適用你系統(tǒng)平臺的版本, Node.js (x86) 或者Node.js (x64) 。
- 下載并安裝 Node.js 的 Visual Studio 工具。
安裝完成后我們就會運行 Visual Studio Express 2013 for Web, 并使用 Node.js 的交互窗口來驗證安裝. Node.js 的交互窗口可以再 View->Other Windows->Node.js Interactive Window 下找到. Node.js 交互窗口運行后我們要輸入一些命令檢查是否一切OK.
Figure 1 Node.js Interactive Window
現(xiàn)在我們已經(jīng)對安裝進(jìn)行了驗證,我們現(xiàn)在就可以準(zhǔn)備開始創(chuàng)建支持GB級文件上傳的Node.js后臺程序了. 開始我們先創(chuàng)建一個新的項目,并選擇一個空的 Node.js Web應(yīng)用程序模板.
Figure 2 New project using the Blank Node.js Web Application template
項目創(chuàng)建好以后,我們應(yīng)該會看到一個叫做 server.js 的文件,還有解決方案瀏覽器里面的Node包管理器 (npm).
圖3 解決方案管理器里面的 Node.js 應(yīng)用程序
server.js 文件里面有需要使用Node.js來創(chuàng)建一個基礎(chǔ)的hello world應(yīng)用程序的代碼.
Figure 4 The Hello World application
我現(xiàn)在繼續(xù)把這段代碼從 server.js 中刪除,然后在Node.js中穿件G級別文件上傳的后端代碼。下面我需要用npm安裝這個項目需要的一些依賴:
- Express - Node.js網(wǎng)頁應(yīng)用框架,用于構(gòu)建單頁面、多頁面以及混合網(wǎng)絡(luò)應(yīng)用
- Formidable - 用于解析表單數(shù)據(jù),特別是文件上傳的Node.js模塊
- fs-extra - 文件系統(tǒng)交互模塊
圖5 使用npm安裝所需模塊
模塊安裝完成后,我們可以從解決方案資源管理器中看到它們。
圖6 解決方案資源管理器顯示已安裝模塊
下一步我們需要在解決方案資源管理器新建一個 "Scripts" 文件夾并且添加 "workeruploadchunk.js" 和 "workerprocessfile.js" 到該文件夾。我們還需要下載jQuery 2.x 和 SparkMD5 庫并添加到"Scripts"文件夾。 最后還需要添加 "Default.html" 頁面。
創(chuàng)建Node.js后臺
首先我們需要用Node.js的"require()"函數(shù)來導(dǎo)入在后臺上傳G級文件的模塊。注意我也導(dǎo)入了"path"以及"crypto" 模塊。"path"模塊提供了生成上傳文件塊的文件名的方法。"crypto" 模塊提供了生成上傳文件的MD5校驗和的方法。
// The required modules var express = require('express'); var formidable = require('formidable'); var fs = require('fs-extra'); var path = require('path'); var crypto = require('crypto');
下一行代碼就是見證奇跡的時刻。
這行代碼是用來創(chuàng)建express應(yīng)用的。express應(yīng)用是一個封裝了Node.js底層功能的中間件。如果你還記得那個由Blank Node.js Web應(yīng)用模板創(chuàng)建的"Hello World" 程序,你會發(fā)現(xiàn)我導(dǎo)入了"http"模塊,然后調(diào)用了"http.CreateServer()"方法創(chuàng)建了 "Hello World" web應(yīng)用。我們剛剛創(chuàng)建的express應(yīng)用內(nèi)建了所有的功能。
現(xiàn)在我們已經(jīng)創(chuàng)建了一個express應(yīng)用,我們讓它呈現(xiàn)之前創(chuàng)建的"Default.html",然后讓應(yīng)用等待連接。
// Serve up the Default.html page app.use(express.static(__dirname, { index: 'Default.html' })); // Startup the express.js application app.listen(process.env.PORT || 1337); // Path to save the files var uploadpath = 'C:/Uploads/CelerFT/';
express應(yīng)用有app.VERB()方法,它提供了路由的功能。我們將使用app.post()方法來處理"UploadChunk" 請求。在app.post()方法里我們做的第一件事是檢查我們是否在處理POST請求。接下去檢查Content-Type是否是mutipart/form-data,然后檢查上傳的文件塊大小不能大于51MB。
// Use the post method for express.js to respond to posts to the uploadchunk urls and // save each file chunk as a separate file app.post('*/api/CelerFTFileUpload/UploadChunk*', function(request,response) { if (request.method === 'POST') { // Check Content-Type if (!(request.is('multipart/form-data'))){ response.status(415).send('Unsupported media type'); return; } // Check that we have not exceeded the maximum chunk upload size var maxuploadsize =51 * 1024 * 1024; if (request.headers['content-length']> maxuploadsize){ response.status(413).send('Maximum upload chunk size exceeded'); return; }
一旦我們成功通過了所有的檢查,我們將把上傳的文件塊作為一個單獨分開的文件并將它按順序數(shù)字命名。下面最重要的代碼是調(diào)用fs.ensureDirSync()方法,它使用來檢查臨時目錄是否存在。如果目錄不存在則創(chuàng)建一個。注意我們使用的是該方法的同步版本。
// Get the extension from the file name var extension =path.extname(request.param('filename')); // Get the base file name var baseFilename =path.basename(request.param('filename'), extension); // Create the temporary file name for the chunk var tempfilename =baseFilename + '.'+ request.param('chunkNumber').toString().padLeft('0', 16) + extension + ".tmp"; // Create the temporary directory to store the file chunk // The temporary directory will be based on the file name var tempdir =uploadpath + request.param('directoryname')+ '/' + baseFilename; // The path to save the file chunk var localfilepath =tempdir + '/'+ tempfilename; if (fs.ensureDirSync(tempdir)) { console.log('Created directory ' +tempdir); }
正如我之前提出的,我們可以通過兩種方式上傳文件到后端服務(wù)器。第一種方式是在web瀏覽器中使用FormData,然后把文件塊作為二進(jìn)制數(shù)據(jù)發(fā)送,另一種方式是把文件塊轉(zhuǎn)換成base64編碼的字符串,然后創(chuàng)建一個手工的multipart/form-data encoded請求,然后發(fā)送到后端服務(wù)器。
所以我們需要檢查一下是否在上傳的是一個手工multipart/form-data encoded請求,通過檢查"CelerFT-Encoded"頭部信息,如果這個頭部存在,我們創(chuàng)建一個buffer并使用request的ondata時間把數(shù)據(jù)拷貝到buffer中。
在request的onend事件中通過將buffer呈現(xiàn)為字符串并按CRLF分開,從而從 multipart/form-data encoded請求中提取base64字符串。base64編碼的文件塊可以在數(shù)組的第四個索引中找到。
通過創(chuàng)建一個新的buffer來將base64編碼的數(shù)據(jù)重現(xiàn)轉(zhuǎn)換為二進(jìn)制。隨后調(diào)用fs.outputFileSync()方法將buffer寫入文件中。
// Check if we have uploaded a hand crafted multipart/form-data request // If we have done so then the data is sent as a base64 string // and we need to extract the base64 string and save it if (request.headers['celerft-encoded']=== 'base64') { var fileSlice = newBuffer(+request.headers['content-length']); var bufferOffset = 0; // Get the data from the request request.on('data', function (chunk) { chunk.copy(fileSlice , bufferOffset); bufferOffset += chunk.length; }).on('end', function() { // Convert the data from base64 string to binary // base64 data in 4th index of the array var base64data = fileSlice.toString().split('\r\n'); var fileData = newBuffer(base64data[4].toString(), 'base64'); fs.outputFileSync(localfilepath,fileData); console.log('Saved file to ' +localfilepath); // Send back a sucessful response with the file name response.status(200).send(localfilepath); response.end(); }); }
二進(jìn)制文件塊的上傳是通過formidable模塊來處理的。我們使用formidable.IncomingForm()方法得到multipart/form-data encoded請求。formidable模塊將把上傳的文件塊保存為一個單獨的文件并保存到臨時目錄。我們需要做的是在formidable的onend事件中將上傳的文件塊保存為里一個名字。
else { // The data is uploaded as binary data. // We will use formidable to extract the data and save it var form = new formidable.IncomingForm(); form.keepExtensions = true; form.uploadDir = tempdir; // Parse the form and save the file chunks to the // default location form.parse(request, function (err, fields, files) { if (err){ response.status(500).send(err); return; } //console.log({ fields: fields, files: files }); }); // Use the filebegin event to save the file with the naming convention /*form.on('fileBegin', function (name, file) { file.path = localfilepath; });*/ form.on('error', function (err) { if (err){ response.status(500).send(err); return; } }); // After the files have been saved to the temporary name // move them to the to teh correct file name form.on('end', function (fields,files) { // Temporary location of our uploaded file var temp_path = this.openedFiles[0].path; fs.move(temp_path , localfilepath,function (err){ if (err) { response.status(500).send(err); return; } else { // Send back a sucessful response with the file name response.status(200).send(localfilepath); response.end(); } }); }); // Send back a sucessful response with the file name //response.status(200).send(localfilepath); //response.end(); } }
app.get()方法使用來處理"MergeAll"請求的。這個方法實現(xiàn)了之前描述過的功能。
// Request to merge all of the file chunks into one file app.get('*/api/CelerFTFileUpload/MergeAll*', function(request,response) { if (request.method === 'GET') { // Get the extension from the file name var extension =path.extname(request.param('filename')); // Get the base file name var baseFilename =path.basename(request.param('filename'), extension); var localFilePath =uploadpath + request.param('directoryname')+ '/' + baseFilename; // Check if all of the file chunks have be uploaded // Note we only wnat the files with a *.tmp extension var files =getfilesWithExtensionName(localFilePath, 'tmp') /*if (err) { response.status(500).send(err); return; }*/ if (files.length !=request.param('numberOfChunks')){ response.status(400).send('Number of file chunks less than total count'); return; } var filename =localFilePath + '/'+ baseFilename +extension; var outputFile =fs.createWriteStream(filename); // Done writing the file // Move it to top level directory // and create MD5 hash outputFile.on('finish', function (){ console.log('file has been written'); // New name for the file var newfilename = uploadpath +request.param('directoryname')+ '/' + baseFilename + extension; // Check if file exists at top level if it does delete it //if (fs.ensureFileSync(newfilename)) { fs.removeSync(newfilename); //} // Move the file fs.move(filename, newfilename ,function (err) { if (err) { response.status(500).send(err); return; } else { // Delete the temporary directory fs.removeSync(localFilePath); varhash = crypto.createHash('md5'), hashstream = fs.createReadStream(newfilename); hashstream.on('data', function (data) { hash.update(data) }); hashstream.on('end', function (){ var md5results =hash.digest('hex'); // Send back a sucessful response with the file name response.status(200).send('Sucessfully merged file ' + filename + ", " + md5results.toUpperCase()); response.end(); }); } }); }); // Loop through the file chunks and write them to the file // files[index] retunrs the name of the file. // we need to add put in the full path to the file for (var index infiles) { console.log(files[index]); var data = fs.readFileSync(localFilePath +'/' +files[index]); outputFile.write(data); fs.removeSync(localFilePath + '/' + files[index]); } outputFile.end(); } }) ;
注意Node.js并沒有提供String.padLeft()方法,這是通過擴展String實現(xiàn)的。
// String padding left code taken from // http://www.lm-tech.it/Blog/post/2012/12/01/String-Padding-in-Javascript.aspx String.prototype.padLeft = function (paddingChar, length) { var s = new String(this); if ((this.length< length)&& (paddingChar.toString().length > 0)) { for (var i = 0; i < (length - this.length) ; i++) { s = paddingChar.toString().charAt(0).concat(s); } } return s; } ;
其中一件事是,發(fā)表上篇文章后我繼續(xù)研究是為了通過域名碎片實現(xiàn)并行上傳到CeleFT功能。域名碎片的原理是訪問一個web站點時,讓web瀏覽器建立更多的超過正常允許范圍的并發(fā)連接。 域名碎片可以通過使用不同的域名(如web1.example.com,web2.example.com)或者不同的端口號(如8000, 8001)托管web站點的方式實現(xiàn)。
示例中,我們使用不同端口號托管web站點的方式。
我們使用 iisnode 把 Node.js集成到 IIS( Microsoft Internet Information Services)實現(xiàn)這一點。 下載兼容你操作系統(tǒng)的版本 iisnode (x86) 或者 iisnode (x64)。 下載 IIS URL重寫包。
一旦安裝完成(假定windows版Node.js已安裝),到IIS管理器中創(chuàng)建6個新網(wǎng)站。將第一個網(wǎng)站命名為CelerFTJS并且將偵聽端口配置為8000。
圖片7在IIS管理器中創(chuàng)建一個新網(wǎng)站
然后創(chuàng)建其他的網(wǎng)站。我為每一個網(wǎng)站都創(chuàng)建了一個應(yīng)用池,并且給應(yīng)用池“LocalSystem”級別的權(quán)限。所有網(wǎng)站的本地路徑是C:\inetpub\wwwroot\CelerFTNodeJS。
圖片8 文件夾層級
我在Release模式下編譯了Node.js應(yīng)用,然后我拷貝了server.js文件、Script文件夾以及node_modules文件夾到那個目錄下。
要讓包含 iisnode 的Node.js的應(yīng)用工作,我們需要創(chuàng)建一個web.config文件,并在其中添加如下得內(nèi)容。
<defaultDocument> <files> <add value="server.js" /> </files> </defaultDocument> <handlers> <!-- indicates that the server.js file is a node.js application to be handled by the iisnode module --> <add name="iisnode" path="*.js" verb="*" modules="iisnode" /> </handlers> <rewrite> <rules> <rule name="CelerFTJS"> <match url="/*" /> <action type="Rewrite" url="server.js" /> </rule> <!-- Don't interfere with requests for node-inspector debugging --> <rule name="NodeInspector" patternSyntax="ECMAScript" stopProcessing="true"> <match url="^server.js\/debug[\/]?" /> </rule> </rules> </rewrite>
web.config中各項的意思是讓iisnode處理所有得*.js文件,由server.js 處理任何匹配"/*"的URL。
如果你正確的做完了所有的工作,你就可以通過http://localhost:8000瀏覽網(wǎng)站,并進(jìn)入CelerFT "Default.html"頁面。
下面的web.config項可以改善 iisnode中Node.js的性能。
并行上傳
為了使用域名碎片來實現(xiàn)并行上傳,我不得不給Node.js應(yīng)用做些修改。我第一個要修改的是讓Node.js應(yīng)用支持跨域資源共享。我不得不這樣做是因為使用域碎片實際上是讓一個請求分到不同的域并且同源策略會限制我的這個請求。
好消息是XMLttPRequest 標(biāo)準(zhǔn)2規(guī)范允許我這么做,如果網(wǎng)站已經(jīng)把跨域資源共享打開,更好的是我不用為了實現(xiàn)這個而變更在"workeruploadchunk.js"里的上傳方法。
// 使用跨域資源共享 // Taken from http://bannockburn.io/2013/09/cross-origin-resource-sharing-cors-with-a-node-js-express-js-and-sencha-touch-app/ var enableCORS = function(request,response, next){ response.header('Access-Control-Allow-Origin', '*'); response.header('Access-Control-Allow-Methods', 'GET,POST,OPTIONS'); response.header('Access-Control-Allow-Headers', 'Content-Type, Authorization, Content- Length, X-Requested-With' ) ; // 攔截OPTIONS方法 if ('OPTIONS' ==request.method){ response.send(204); } else { next(); } } ; // 在表達(dá)式中使用跨域資源共享 app. use ( enableCORS ) ;
為了使server.js文件中得CORS可用,我創(chuàng)建了一個函數(shù),該函數(shù)會創(chuàng)建必要的頭以表明Node.js應(yīng)用支持CORS。另一件事是我還需要表明CORS支持兩種請求,他們是:
簡單請求:
1、只用GET,HEAD或POST。如果使用POST向服務(wù)器發(fā)送數(shù)據(jù),那么發(fā)送給服務(wù)器的HTTP POST請求的Content-Type應(yīng)是application/x-www-form-urlencoded, multipart/form-data, 或 text/plain其中的一個。
2、HTTP請求中不要設(shè)置自定義的頭(例如X-Modified等)
預(yù)檢請求:
1、使用GET,HEAD或POST以外的方法。假設(shè)使用POST發(fā)送請求,那么Content-Type不能是application/x-www-form-urlencoded, multipart/form-data, or text/plain,例如假設(shè)POST請求向服務(wù)器發(fā)送了XML有效載荷使用了application/xml or text/xml,那么這個請求就是預(yù)檢的。
2、在請求中設(shè)置自定義頭(比如請求使用X-PINGOTHER頭)。
在我們的例子中,我們用的是簡單請求,所以我們不需要做其他得工作以使例子能夠工作。
在 "workeruploadchunk.js" 文件中,我向 self.onmessage 事件添加了對進(jìn)行并行文件數(shù)據(jù)塊上傳的支持.
// We are going to upload to a backend that supports parallel uploads. // Parallel uploads is supported by publishng the web site on different ports // The backen must implement CORS for this to work else if(workerdata.chunk!= null&& workerdata.paralleluploads ==true){ if (urlnumber >= 6) { urlnumber = 0; } if (urlcount >= 6) { urlcount = 0; } if (urlcount == 0) { uploadurl = workerdata.currentlocation +webapiUrl + urlnumber; } else { // Increment the port numbers, e.g 8000, 8001, 8002, 8003, 8004, 8005 uploadurl = workerdata.currentlocation.slice(0, -1) + urlcount +webapiUrl + urlnumber; } upload(workerdata.chunk,workerdata.filename,workerdata.chunkCount, uploadurl, workerdata.asyncstate); urlcount++; urlnumber++; }
在 Default.html 頁面我對當(dāng)前的URL進(jìn)行了保存,因為我準(zhǔn)備把這些信息發(fā)送給文件上傳的工作程序. 只所以這樣做是因為:
- 我想要利用這個信息增加端口數(shù)量
- 做了 CORS 請求,我需要把完整的 URL 發(fā)送給 XMLHttpRequest 對象.
<span style="font-family: "Lucida Console"; font-size: 8.0pt;">// Save current protocol and host for parallel uploads</span></div>
<div class="MsoNoSpacing" style="background: #FFFF99;">
"font-family: 'Lucida Console'; font-size: 8pt;"><span style="color: #000066;">var</span><span style="font-family: 'Lucida Console'; font-size: 8pt;"> currentProtocol </span><span style="color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">= </span><span style="font-family: 'Lucida Console'; font-size: 8pt;">window.</span><span style="color: #660066; font-family: 'Lucida Console'; font-size: 8pt;">location</span><span style="font-family: 'Lucida Console'; font-size: 8pt;">.</span><span style="color: #660066; font-family: 'Lucida Console'; font-size: 8pt;">protocol</span><span style="color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">;</span></div>
<div class="MsoNoSpacing" style="background: #FFFF99;">
"font-family: 'Lucida Console'; font-size: 8pt;"><span style="color: #000066;">var</span><span style="font-family: 'Lucida Console'; font-size: 8pt;"> currentHostandPort </span><span style="color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">=</span><span style="font-family: 'Lucida Console'; font-size: 8pt;"> window.</span><span style="color: #660066; font-family: 'Lucida Console'; font-size: 8pt;">location</span><span style="font-family: 'Lucida Console'; font-size: 8pt;">.</span><span style="color: #660066; font-family: 'Lucida Console'; font-size: 8pt;">host</span><span style="color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">;</span></div>
<div class="MsoNoSpacing" style="background: #FFFF99;">
"font-family: 'Lucida Console'; font-size: 8pt;"><span style="color: #000066;">var</span><span style="font-family: 'Lucida Console'; font-size: 8pt;"> currentLocation </span><span style="color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">= </span><span style="font-family: 'Lucida Console'; font-size: 8pt;">currentProtocol </span><span style="color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">+</span><span style="font-family: 'Lucida Console'; font-size: 8pt;"> </span><span style="color: #3366cc; font-family: 'Lucida Console'; font-size: 8pt;">"http://"</span><span style="font-family: 'Lucida Console'; font-size: 8pt;"> </span><span style="color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">+</span><span style="font-family: 'Lucida Console'; font-size: 8pt;"> currentHostandPort</span><span style="color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">;</span></div>
<span style="font-family: "Calibri","sans-serif"; font-size: 11.0pt; mso-ascii-theme-font: minor-latin; mso-bidi-font-family: "Times New Roman"; mso-bidi-theme-font: minor-bidi; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin;">
</span>
<span style="font-family: "Calibri","sans-serif"; font-size: 11.0pt; mso-ascii-theme-font: minor-latin; mso-bidi-font-family: "Times New Roman"; mso-bidi-theme-font: minor-bidi; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin;">The code below shows the modification made to the upload message.</span><span style="color: #006600; mso-bidi-font-style: italic;"><o:p></o:p></span>
<span style="font-family: "Calibri","sans-serif"; font-size: 11.0pt; mso-ascii-theme-font: minor-latin; mso-bidi-font-family: "Times New Roman"; mso-bidi-theme-font: minor-bidi; mso-fareast-font-family: Calibri; mso-fareast-theme-font: minor-latin; mso-hansi-theme-font: minor-latin;">
</span>
<span style="background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;">// Send and upload message to the webworker</span>
"background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;"><span style="color: #000066;">case</span><span style="background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;"> </span><span style="background-color: #ffff99; color: #3366cc; font-family: 'Lucida Console'; font-size: 8pt;">'upload'</span><span style="background-color: #ffff99; color: #339933; font-family: 'Lucida Console'; font-size: 8pt;">:</span>
<span style="background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;">// </span><span style="background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;">Check to see if backend supports parallel uploads</span>
var paralleluploads =false;
if ($('#select_parallelupload').prop('checked')) {
paralleluploads = true;
}
uploadworkers[data.id].postMessage({ 'chunk': data.blob, 'filename':data.filename,
'directory': $("#select_directory").val(), 'chunkCount':data.chunkCount,
'asyncstate':data.asyncstate,'paralleluploads':paralleluploads, 'currentlocation':
currentLocation, 'id': data.id });
break;
最后修改了 CelerFT 接口來支持并行上傳.
帶有并行上傳的CelerFT
這個項目的代碼可以再我的 github 資源庫上找到
相關(guān)文章
nodejs+websocket實時聊天系統(tǒng)改進(jìn)版
這篇文章主要介紹了nodejs+websocket實時聊天系統(tǒng)的改進(jìn)版,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-05-05nodejs不用electron實現(xiàn)打開文件資源管理器并選擇文件
最近在開發(fā)一些小腳本,用 nodejs 實現(xiàn),其中很多功能需要選擇一個/多個文件,或者是選擇一個文件夾,這種情況下網(wǎng)上給出的解決方案都是 electron,但是我一個小腳本用 electron 屬實有點夸張了,后來轉(zhuǎn)念一想可以通過 powershell 來實現(xiàn)類似的功能,需要的朋友可以參考下2024-01-01