使用 HTTP 上传 G 级的文件之 Node.js 版本 已翻译 100%

oschina 投递于 2014/11/02 08:16 (共 16 段, 翻译完成于 11-04)
阅读 8171
收藏 187
14
加载中

不管喜欢与否,javascript无处不在。 我们可以在客户端的前台应用中找到它,也可以在大量的框架、类库中找到它,而且可以在服务器端的后台应用中找到它。

近年来, Javascript越来越流行,这似乎是由于 Javascript 生态系统正在帮助提高生产率、减少入门所需的时间。 在我的 第一篇文章中,我介绍了使用 ASP.NET Web 后端 API 实现 G级文件上传,发表完这篇文章后,我决定试一下使用 Node.js 能否达到同样的效果。  这意味着我需要实现 UploadChunk和 MergeAll方法,在 Node.js中我发表的 最后一篇文章谈到了这一点。

daxiang
daxiang
翻译于 2014/11/02 09:00
1

开发环境
我们将使用 Visual Studio Express 2013 for Web 作为开发环境, 不过它还不能被用来做 Node.js 开发。为此我们需要安装 Node.js Tools for Visual Studio。  装好后 Visual Studio Express 2013 for Web 就会转变成一个 Node.js IDE 环境,提供创建这个应用所需要的所有东西.。而基于这里提供的指导,我们需要:

LeoXu
LeoXu
翻译于 2014/11/02 10:16
1

安装完成后我们就会运行 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

LeoXu
LeoXu
翻译于 2014/11/02 10:21
1

项目创建好以后,我们应该会看到一个叫做 server.js 的文件,还有解决方案浏览器里面的Node包管理器 (npm). 


图3 解决方案管理器里面的 Node.js 应用程序

server.js 文件里面有需要使用Node.js来创建一个基础的hello world应用程序的代码.


Figure 4 The Hello World application

LeoXu
LeoXu
翻译于 2014/11/02 10:24
1

我现在继续把这段代码从 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" 页面。这些都在我之前的 post 中介绍过。

Cath
Cath
翻译于 2014/11/02 13:56
1

创建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');

下一行代码就是见证奇迹的时刻。

<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应用内建了所有的功能。

开源中国七里香
开源中国七里香
翻译于 2014/11/02 21:24
1

现在我们已经创建了一个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;      
        }

LeoG0816
LeoG0816
翻译于 2014/11/02 22:06
1

一旦我们成功通过了所有的检查,我们将把上传的文件块作为一个单独分开的文件并将它按顺序数字命名。下面最重要的代码是调用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请求,然后发送到后端服务器。  

LeoG0816
LeoG0816
翻译于 2014/11/02 22:14
1

所以我们需要检查一下是否在上传的是一个手工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();      
    });  
}

LeoG0816
LeoG0816
翻译于 2014/11/02 22:24
1

二进制文件块的上传是通过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;  
}   ;
LeoG0816
LeoG0816
翻译于 2014/11/02 22:32
1
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接。
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
加载中

评论(16)

qwfys
qwfys
+1
牛仔豆
牛仔豆
这是windows9吗?
leonooo
leonooo
webstorm开发node,用过后就再也不想换其它的IDE了
平果
平果
好牛B的样子
Raphael_goh
Raphael_goh

引用来自“nant”的评论

开发nodejs,visual studio好还是idea好?
当然是用webstorm了 没必要用idea(虽然差不多,但webstorm足够了),更加没必要用VS
酷酷的就
酷酷的就
代码里边的注释为何不翻译, 是故意的么?
Lambert_lin
Lambert_lin
好厉害
nant
nant
开发nodejs,visual studio好还是idea好?
Lambert_lin
Lambert_lin
李惟
李惟

引用来自“MingjunYang”的评论

用vs写node,感觉好醉人。。。。。。
我也是醉了
返回顶部
顶部