官术网_书友最值得收藏!

Handling file uploads

We cannot process an uploaded file in the same way we process other POST data. When a file input is submitted in a form, the browser processes the file into a multipart message.

Multipart was originally developed as an e-mail format allowing multiple pieces of mixed content to be combined into one message. If we attempted to receive the upload as a stream and write it to a file, we would have a file filled with multipart data instead of the file or files themselves. We need a multipart parser, the writing of which is more than a recipe can cover. So, we'll be using the well-known and battle-tested formidable module to convert our upload data into files.

Getting ready

Let's create a new uploads directory to store the uploaded files and get ready to make modifications to our server.js file from the previous recipe.

Note

If the owner of the Node process (for example, the user of the system that runs the node executable) isn't also the creator (and therefore owner) of the uploads directory, then on some systems (such as Linux and Mac OS X) we'd also have to run chown and/or chmod, and chgrp.

We'll also need to install formidable by executing the following command:

npm install formidable@1.x.x

Note

Notice how we can control the version we install from npm by using an at (@) symbol, and specifying a version range using the character x as a wildcard to specify the latest subversion number. In this case, we're installing formidable major Version 1, the latest minor version, and the latest patch number.

We run the preceding command in the folder that contains the uploads directory.

Finally, we'll make some changes to our form.html file from the last recipe, as follows:

<form method=POST enctype=multipart/form-data>
  <input type=file name=userfile1><br>
  <input type=file name=userfile2><br>
  <input type=submit>
</form>

We included an enctype attribute of multipart/form-data to signify to the browser that the form will contain upload data and we've replaced the text inputs with file inputs.

How to do it...

Let's see what happens when we use our modified form to upload a file to the server from the last recipe. Let's upload form.html itself as our file, as shown in the following screenshot:

How to do it...

Our POST server simply logs the raw HTTP message body to the console, which in this case is multipart data. We had two file inputs on the form. Though we only uploaded one file, the second input is still included in the multipart request. Each file is separated by a predefined boundary that is set in a secondary attribute of the content-type HTTP headers. We'll need to use formidable to parse this data, extracting each file contained therein. We'll do this with the help of the following code:

var http = require('http');
var formidable = require('formidable');
var form = require('fs').readFileSync('form.html');

http.createServer(function (request, response) {
  if (request.method === "POST") {
 var incoming = new formidable.IncomingForm();
 incoming.uploadDir = 'uploads';
 incoming.on('file', function (field, file) {
 if (!file.size) { return; }
 response.write(file.name + ' received\n');
 }).on('end', function () {
 response.end('All files received');
 });
 incoming.parse(request);
  }
  if (request.method === "GET") {
    response.writeHead(200, {'Content-Type': 'text/html'});
    response.end(form);
  }
}).listen(8080);

Our POST server has now become an upload server.

How it works...

We create a new instance of the formidable IncomingForm class and tell it where to upload files. In order to provide feedback to the user, we can listen to our incoming instance. The IncomingForm class emits its own higher level events, so rather than listening to the request object for events and processing data as it comes, we wait for formidable to parse the files out of the multipart message and then notify us through its custom file event.

The file event callback provides us with two parameters: field and file. We don't use the field parameter in our recipe; each field parameter holds the name of the file input element in our HTML done. The file parameter is an object that contains information about the uploaded file. We use this to filter out empty files (usually caused by empty input fields) and grab the filename, which we show to users as confirmation. When formidable has finished parsing the multipart message, it sends an end event in which we end the response.

There's more...

We can POST more than simple form fields and values from a browser. Let's take a look at transferring files from the browser to server.

Using formidable to accept all POST data

Formidable doesn't just handle uploaded files, it will also process general POST data. All we have to do is add a listener for the field event to process forms that contain both files and user data, as follows:

incoming.on('file', function (field, file) {
  response.write(file.name + ' received\n');
}).on('field', function (field, value) {
 response.write(field + ' : ' + value + '\n');
}).on('end', function () {
  response.end('All files received');
});

There's no need to manually implement field data size limits as formidable takes care of this for us, although we can change the defaults with incoming.maxFieldsSize, which allows us to limit the total byte count for the sum of all fields. This limit doesn't apply to file uploads.

Preserving filenames with formidable

When formidable places our files in the uploads directory, it assigns them a name that consists of a randomly generated hexadecimal number. This prevents files of the same name from being overwritten. However, what if we want to know which files are which and still retain the unique filename advantage? We can alter the way formidable names each file during its fileBegin event with the help of the following code:

if (request.method === "POST") {
  var incoming = new formidable.IncomingForm();
  incoming.uploadDir = 'uploads';
 incoming.on('fileBegin', function (field, file) {
 if (file.name){
 file.path += "-" + file.name;
 } //...rest of the code
  }).on('file', function (field, file) {
//...rest of the code

We've appended the original filename onto the end of the random filename assigned by formidable, separating them with a dash. Now we can easily identify our files, though for many scenarios this may not be necessary as we would likely be outputting file information to a database and cross-referencing it to randomly generated names.

Uploading files via PUT

It's also possible to upload files via an HTTP PUT request. While we can only send one file per request, we don't need to do any parsing on the server side since the file will simply stream directly to our server, which means less server-side processing overhead. It would be magnificent if we could achieve this by changing our form's method attribute from POST to PUT, but alas, no. However, thanks to the up and coming XMLHttpRequest Level 2 (xhr2) request, we can now transfer binary data via JavaScript in some browsers (see http://caniuse.com). We grab a file pointer using a change event listener on the input file element, and then we open a PUT request and send the file. The following is for use in our form.html file, which we'll save as put_upload_form.html:

<form id=frm>
  <input type=file id=userfile name=userfile><br>
  <input type=submit>
</form>
<script>
(function () {
  var userfile = document.getElementById('userfile'),
  frm = document.getElementById('frm'),
  file;
  userfile.addEventListener('change', function () {
    file = this.files[0];
  });
  frm.addEventListener('submit', function (e) {
    e.preventDefault();
    if (file) {
      var xhr = new XMLHttpRequest();
      xhr.file = file;
      xhr.open('put', window.location, true);
      xhr.setRequestHeader("x-uploadedfilename", file.fileName || file.name);
      xhr.send(file);
      file = '';
      frm.reset();
    }
  });
}());
</script>

Note

The id attribute is added to the form and the file input. method and enctype attributes have been removed. We're using just one file element because we can only send one file per request, although the example could be extended to asynchronously stream multiple files to our server at once.

Our script attaches a change listener to the file input element. When the user selects a file, we are able to capture a pointer to the file. As the form is submitted, we prevent default behavior, check whether a file is selected, initialize an xhr object, open a PUT request to our server, and set a custom header (which we'll be calling X-UPLOADEDFILENAME), so we can grab the filename later and send the file to our server. Our server code looks like the following:

var http = require('http');
var fs = require('fs');
var form = fs.readFileSync('put_upload.html');
http.createServer(function (request, response) {
  if (request.method === "PUT") {
    var fileData = new Buffer(+request.headers['content-length']);
    var bufferOffset = 0;
    request.on('data', function(chunk) {
      chunk.copy(fileData, bufferOffset);
      bufferOffset += chunk.length;
    }).on('end', function() {
      var rand = (Math.random()*Math.random())
      .toString(16).replace('.','');
      var to = 'uploads/' + rand + "-" +
      request.headers['x-uploadedfilename'];

      fs.writeFile(to, fileData, function(err) {
        if (err) { throw err; }
        console.log('Saved file to ' + to);
        response.end();
      });
    });
  }
  if (request.method === "GET") {
    response.writeHead(200, {'Content-Type': 'text/html'});
    response.end(form);
  }
}).listen(8080);

Our PUT server follows a similar pattern to the simple POST server in the Processing POST data recipe; we listen to the data event and piece the chunks together. However, rather than a string concatenating our data, we must pass our chunks into a buffer because a buffer can handle any data type including binary, whereas a String object will always coerce non-string data into string format—this changes the underlying binary, resulting in corrupted files. Once the end event is triggered, we generate a random filename similar to the naming convention of formidable and write the file to our uploads folder.

This uploading via PUT demonstration will not work in older browsers, so an alternative fallback should be provided in a production environment. Browsers that will support this method are IE10 and above, Firefox, Chrome, Safari, iOS 5+ Safari, and Android browsers. However, due to browser vendors' differing implementations of the same functionality, the example may need some tweaking for cross-browser compatibility.

See also

  • The Sending e-mail recipe discussed in Chapter 9, Integrating Network Paradigms
  • The Using Node as an HTTP client recipe
  • Chapter 5, Employing Streams
主站蜘蛛池模板: 涞水县| 潼南县| 承德县| 东丽区| 额济纳旗| 桦甸市| 木里| 栾城县| 府谷县| 武义县| 武汉市| 石楼县| 吴川市| 花莲市| 宁远县| 望谟县| 本溪市| 西乡县| 丰原市| 锡林郭勒盟| 鹤岗市| 黄骅市| 衡阳县| 湄潭县| 磴口县| 宁德市| 宜丰县| 北流市| 武威市| 威宁| 建始县| 洛扎县| 岳普湖县| 收藏| 浙江省| 上蔡县| 南召县| 石台县| 无棣县| 穆棱市| 行唐县|