首先我们需要用Node.js的"require()"函数来导入在后台上传G级文件的模块。注意我也导入了"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');
下一行代码就是见证奇迹的时刻。
<span>var</span><span> app <span>=</span> express<span>()</span><span>;</span></span>
这行代码是用来创建express应用的。express应用是一个封装了Node.js底层功能的中间件。如果你还记得那个由Blank Node.js Web应用模板创建的"Hello World" 程序,你会发现我导入了"http"模块,然后调用了"http.CreateServer()"方法创建了 "Hello World" web应用。我们刚刚创建的express应用内建了所有的功能。
现在我们已经创建了一个express应用,我们让它呈现之前创建的"Default.html",然后让应用等待连接。
// 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。
// 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()方法,它使用来检查临时目录是否存在。如果目录不存在则创建一个。注意我们使用的是该方法的同步版本。
// 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写入文件中。
// 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();
});
}
二进制文件块的上传是通过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"请求的。这个方法实现了之前描述过的功能。
// 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实现的。
// String padding left code taken from
// 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;
} ;