這篇文章主要介紹了在Node.js中使用HTTP上傳文件的方法,作者以windows下的visual studio作為操作node的環境,推薦閱讀!需要的朋友可以參考下
開發環境
我們將使用 Visual Studio Express 2013 for Web 作為開發環境, 不過它還不能被用來做 Node.js 開發。為此我們需要安裝 Node.js Tools for Visual Studio。 裝好後 Visual Studio Express 2013 for Web 就會轉變成一個 Node.js IDE 環境,提供創建這個應用所需要的所有東西.。而基於這裡提供的指導,我們需要:
下載安裝 Node.js Windows 版,選擇適用你系統平台的版本, 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
現在我們已經對安裝進行了驗證,我們現在就可以准備開始創建支持GB級文件上傳的Node.js後台程序了. 開始我們先創建一個新的項目,並選擇一個空的 Node.js Web應用程序模板.
Figure 2 New project using the Blank Node.js Web Application template
項目創建好以後,我們應該會看到一個叫做 server.js 的文件,還有解決方案浏覽器裡面的Node包管理器 (npm).
圖3 解決方案管理器裡面的 Node.js 應用程序
server.js 文件裡面有需要使用Node.js來創建一個基礎的hello world應用程序的代碼.
Figure 4 The Hello World application
我現在繼續把這段代碼從 server.js 中刪除,然後在Node.js中穿件G級別文件上傳的後端代碼。下面我需要用npm安裝這個項目需要的一些依賴:
Express - Node.js網頁應用框架,用於構建單頁面、多頁面以及混合網絡應用
Formidable - 用於解析表單數據,特別是文件上傳的Node.js模塊
fs-extra - 文件系統交互模塊
圖5 使用npm安裝所需模塊
模塊安裝完成後,我們可以從解決方案資源管理器中看到它們。
圖6 解決方案資源管理器顯示已安裝模塊
下一步我們需要在解決方案資源管理器新建一個 "Scripts" 文件夾並且添加 "workeruploadchunk.js" 和 "workerprocessfile.js" 到該文件夾。我們還需要下載jQuery 2.x 和 SparkMD5 庫並添加到"Scripts"文件夾。 最後還需要添加 "Default.html" 頁面。
創建Node.js後台
首先我們需要用Node.js的"require()"函數來導入在後台上傳G級文件的模塊。注意我也導入了"path"以及"crypto" 模塊。"path"模塊提供了生成上傳文件塊的文件名的方法。"crypto" 模塊提供了生成上傳文件的MD5校驗和的方法。
?
1
2
3
4
5
6// The required modules
var express = require('express');
var formidable = require('formidable');
var fs = require('fs-extra');
var path = require('path');
var crypto = require('crypto');
下一行代碼就是見證奇跡的時刻。
復制代碼 代碼如下:
var app = express();
這行代碼是用來創建express應用的。express應用是一個封裝了Node.js底層功能的中間件。如果你還記得那個由Blank Node.js Web應用模板創建的"Hello World" 程序,你會發現我導入了"http"模塊,然後調用了"http.CreateServer()"方法創建了 "Hello World" web應用。我們剛剛創建的express應用內建了所有的功能。
現在我們已經創建了一個express應用,我們讓它呈現之前創建的"Default.html",然後讓應用等待連接。
?
1
2
3
4
5
6
7
8// 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應用有app.VERB()方法,它提供了路由的功能。我們將使用app.post()方法來處理"UploadChunk" 請求。在app.post()方法裡我們做的第一件事是檢查我們是否在處理POST請求。接下去檢查Content-Type是否是mutipart/form-data,然後檢查上傳的文件塊大小不能大於51MB。
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18// 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;
}
一旦我們成功通過了所有的檢查,我們將把上傳的文件塊作為一個單獨分開的文件並將它按順序數字命名。下面最重要的代碼是調用fs.ensureDirSync()方法,它使用來檢查臨時目錄是否存在。如果目錄不存在則創建一個。注意我們使用的是該方法的同步版本。
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 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);
}
正如我之前提出的,我們可以通過兩種方式上傳文件到後端服務器。第一種方式是在web浏覽器中使用FormData,然後把文件塊作為二進制數據發送,另一種方式是把文件塊轉換成base64編碼的字符串,然後創建一個手工的multipart/form-data encoded請求,然後發送到後端服務器。
所以我們需要檢查一下是否在上傳的是一個手工multipart/form-data encoded請求,通過檢查"CelerFT-Encoded"頭部信息,如果這個頭部存在,我們創建一個buffer並使用request的ondata時間把數據拷貝到buffer中。
在request的onend事件中通過將buffer呈現為字符串並按CRLF分開,從而從 multipart/form-data encoded請求中提取base64字符串。base64編碼的文件塊可以在數組的第四個索引中找到。
通過創建一個新的buffer來將base64編碼的數據重現轉換為二進制。隨後調用fs.outputFileSync()方法將buffer寫入文件中。
?
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// 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('rn');
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();
});
}
二進制文件塊的上傳是通過formidable模塊來處理的。我們使用formidable.IncomingForm()方法得到multipart/form-data encoded請求。formidable模塊將把上傳的文件塊保存為一個單獨的文件並保存到臨時目錄。我們需要做的是在formidable的onend事件中將上傳的文件塊保存為裡一個名字。
?
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
55else {
// 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"請求的。這個方法實現了之前描述過的功能。
?
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// 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實現的。
?
1
2
3
4
5
6
7
8
9
10
11// 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;
} ;
其中一件事是,發表上篇文章後我繼續研究是為了通過域名碎片實現並行上傳到CeleFT功能。域名碎片的原理是訪問一個web站點時,讓web浏覽器建立更多的超過正常允許范圍的並發連接。 域名碎片可以通過使用不同的域名(如web1.example.com,web2.example.com)或者不同的端口號(如8000, 8001)托管web站點的方式實現。
示例中,我們使用不同端口號托管web站點的方式。
我們使用 iisnode 把 Node.js集成到 IIS( Microsoft Internet Information Services)實現這一點。 下載兼容你操作系統的版本 iisnode (x86) 或者 iisnode (x64)。 下載 IIS URL重寫包。
一旦安裝完成(假定windows版Node.js已安裝),到IIS管理器中創建6個新網站。將第一個網站命名為CelerFTJS並且將偵聽端口配置為8000。
圖片7在IIS管理器中創建一個新網站
然後創建其他的網站。我為每一個網站都創建了一個應用池,並且給應用池“LocalSystem”級別的權限。所有網站的本地路徑是C:inetpubwwwrootCelerFTNodeJS。
圖片8 文件夾層級
我在Release模式下編譯了Node.js應用,然後我拷貝了server.js文件、Script文件夾以及node_modules文件夾到那個目錄下。
要讓包含 iisnode 的Node.js的應用工作,我們需要創建一個web.config文件,並在其中添加如下得內容。
?
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
web.config中各項的意思是讓iisnode處理所有得*.js文件,由server.js 處理任何匹配"/*"的URL。
如果你正確的做完了所有的工作,你就可以通過http://localhost:8000浏覽網站,並進入CelerFT "Default.html"頁面。
下面的web.config項可以改善 iisnode中Node.js的性能。
復制代碼 代碼如下:
node_env="production" debuggingEnabled="false" devErrorsEnabled="false" nodeProcessCountPerApplication="0" maxRequestBufferSize="52428800" />
並行上傳
為了使用域名碎片來實現並行上傳,我不得不給Node.js應用做些修改。我第一個要修改的是讓Node.js應用支持跨域資源共享。我不得不這樣做是因為使用域碎片實際上是讓一個請求分到不同的域並且同源策略會限制我的這個請求。
好消息是XMLttPRequest 標准2規范允許我這麼做,如果網站已經把跨域資源共享打開,更好的是我不用為了實現這個而變更在"workeruploadchunk.js"裡的上傳方法。
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// 使用跨域資源共享 // 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();
}
} ;
// 在表達式中使用跨域資源共享
app. use ( enableCORS ) ;
為了使server.js文件中得CORS可用,我創建了一個函數,該函數會創建必要的頭以表明Node.js應用支持CORS。另一件事是我還需要表明CORS支持兩種請求,他們是:
簡單請求:
1、只用GET,HEAD或POST。如果使用POST向服務器發送數據,那麼發送給服務器的HTTP POST請求的Content-Type應是application/x-www-form-urlencoded, multipart/form-data, 或 text/plain其中的一個。
2、HTTP請求中不要設置自定義的頭(例如X-Modified等)
預檢請求:
1、使用GET,HEAD或POST以外的方法。假設使用POST發送請求,那麼Content-Type不能是application/x-www-form-urlencoded, multipart/form-data, or text/plain,例如假設POST請求向服務器發送了XML有效載荷使用了application/xml or text/xml,那麼這個請求就是預檢的。
2、在請求中設置自定義頭(比如請求使用X-PINGOTHER頭)。
在我們的例子中,我們用的是簡單請求,所以我們不需要做其他得工作以使例子能夠工作。
在 "workeruploadchunk.js" 文件中,我向 self.onmessage 事件添加了對進行並行文件數據塊上傳的支持.
?
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// 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 頁面我對當前的URL進行了保存,因為我准備把這些信息發送給文件上傳的工作程序. 只所以這樣做是因為:
我想要利用這個信息增加端口數量
做了 CORS 請求,我需要把完整的 URL 發送給 XMLHttpRequest 對象.
復制代碼 代碼如下:
// Save current protocol and host for parallel uploads
"font-family: 'Lucida Console'; font-size: 8pt;">var currentProtocol = window.location.protocol;
"font-family: 'Lucida Console'; font-size: 8pt;">var currentHostandPort = window.location.host;
"font-family: 'Lucida Console'; font-size: 8pt;">var currentLocation = currentProtocol + "//" + currentHostandPort;
The code below shows the modification made to the upload message.
// Send and upload message to the webworker
"background-color: #ffff99; font-family: 'Lucida Console'; font-size: 8pt;">case 'upload':
// Check to see if backend supports parallel uploads
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 資源庫上找到