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

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
主站蜘蛛池模板: 白水县| 乌苏市| 江津市| 呼伦贝尔市| 梅河口市| 富宁县| 大埔县| 当涂县| 天津市| 白沙| 行唐县| 斗六市| 西乌珠穆沁旗| 黄平县| 墨竹工卡县| 汪清县| 锡林郭勒盟| 颍上县| 湘乡市| 衡山县| 古田县| 克什克腾旗| 垦利县| 西丰县| 运城市| 万山特区| 塔城市| 抚宁县| 周宁县| 泉州市| 临西县| 石门县| 无为县| 楚雄市| 溧水县| 射洪县| 辽源市| 织金县| 沾化县| 海兴县| 社旗县|