您好,欢迎访问一九零五行业门户网

在Node.js中使用HTTP上传文件的方法_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包管理器 (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校验和的方法。
// 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,然后让应用等待连接。
// 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 0)) { for (var i = 0; i = 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 资源库上找到
其它类似信息

推荐信息