- Node Cookbook(Second Edition)
- David Mark Clements
- 1615字
- 2021-07-16 12:04:29
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.
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:

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.
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.
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.
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>
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.
- 深度實踐OpenStack:基于Python的OpenStack組件開發
- Spring 5.0 Microservices(Second Edition)
- Java高并發核心編程(卷2):多線程、鎖、JMM、JUC、高并發設計模式
- Maven Build Customization
- 算法基礎:打開程序設計之門
- PostgreSQL技術內幕:事務處理深度探索
- Julia機器學習核心編程:人人可用的高性能科學計算
- Mastering Scientific Computing with R
- Apache Mesos Essentials
- C程序設計案例教程
- iOS編程基礎:Swift、Xcode和Cocoa入門指南
- ServiceNow:Building Powerful Workflows
- Getting Started with React Native
- Raspberry Pi Robotic Blueprints
- Learning iOS Security