不管喜欢与否,javascript无处不在。 我们可以在客户端的前台应用中找到它,也可以在大量的框架、类库中找到它,而且可以在服务器端的后台应用中找到它。
近年来, Javascript越来越流行,这似乎是由于 Javascript 生态系统正在帮助提高生产率、减少入门所需的时间。 在我的 第一篇文章中,我介绍了使用 ASP.NET Web 后端 API 实现 G级文件上传,发表完这篇文章后,我决定试一下使用 Node.js 能否达到同样的效果。 这意味着我需要实现 UploadChunk和 MergeAll方法,在 Node.js中我发表的 最后一篇文章谈到了这一点。
开发环境
我们将使用 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.js中穿件G级别文件上传的后端代码。下面我需要用npm安装这个项目需要的一些依赖:
Express - Node.js网页应用框架,用于构建单页面、多页面以及混合网络应用
Formidable - 用于解析表单数据,特别是文件上传的Node.js模块
fs-extra - 文件系统交互模块
模块安装完成后,我们可以从解决方案资源管理器中看到它们。
图6 解决方案资源管理器显示已安装模块
下一步我们需要在解决方案资源管理器新建一个 "Scripts" 文件夹并且添加 "workeruploadchunk.js" 和 "workerprocessfile.js" 到该文件夹。我们还需要下载 jQuery 2.x 和 SparkMD5 库并添加到"Scripts"文件夹。 最后还需要添加 "Default.html" 页面。这些都在我之前的 post 中介绍过。
首先我们需要用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 style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-removed: initial; background-repeat: initial; background-size: initial; color: #000066; font-family: Consolas; font-size: 9pt;">var</span><span style="background-attachment: initial; background-clip: initial; background-image: initial; background-origin: initial; background-removed: initial; background-repeat: initial; background-size: initial; font-family: Consolas; font-size: 9pt;"> app <span style="color: #339933;">=</span> express<span style="color: #009900;">()</span><span style="color: #339933;">;</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 // 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; } ;
评论删除后,数据将无法恢复
评论(16)
引用来自“nant”的评论
开发nodejs,visual studio好还是idea好?引用来自“MingjunYang”的评论
用vs写node,感觉好醉人。。。。。。