socketio-file-upload

Uploads files to a Node.JS server using Socket.IO

Socket.IO File Upload

This module provides functionality to upload files from a browser to a Node.JS server that runs Socket.IO. Throughout the process, if their browser supports WebSockets, the user will not submit a single HTTP request. Supports Socket.IO 0.9 and 1.0.

The intended audience are single-page web apps, but other types of Node.JS projects may benefit from this library.

Since version 0.4, this module also supports monitoring file upload progress.

The module is released under the X11 open-source license.

Navigate to your project directory and run:

$ npm install --save socketio-file-upload

In your Express app, add the router like this (if you don't use Express, read the docs below):

var siofu = require("socketio-file-upload");
var app = express()
    .use(siofu.router)
    .listen(8000);

On a server-side socket connection, do this:

io.on("connection", function(socket){
    var uploader = new siofu();
    uploader.dir = "/path/to/save/uploads";
    uploader.listen(socket);
});

The client-side script is served at /siofu/client.js. Include it like this:

<script src="/siofu/client.js"></script>

The module also supports AMD; see the docs below for more information.

Then, in your client side app, with this HTML:

<input type="file" id="siofu_input" />

Just do this in JavaScript:

var socket = io.connect();
var uploader = new SocketIOFileUpload(socket);
uploader.listenOnInput(document.getElementById("siofu_input"));

That's all you need to get started. For the detailed API, continue reading below. A longer example is available at the bottom of the readme.

The client-side interface is inside the SocketIOFileUpload namespace. Include it with:

<script src="/siofu/client.js"></script>

If you're awesome and you use AMD/RequireJS, set up your paths config like this:

requirejs.config({
    paths: {
        "SocketIOFileUpload": "/siofu/client",
        // ... 
    }
});

and then include it in your app like this:

define("app", ["SocketIOFileUpload"], function(SocketIOFileUpload){
    // ... 
});

When instantiating an instance of the SocketIOFileUpload, pass a reference to your socket.

var instance = new SocketIOFileUpload(socket);

When the user selects a file or files in the specified HTML Input Element, the library will begin to upload that file or those files.

JavaScript:

instance.listenOnInput(document.getElementById("file_input"));

HTML:

<label>Upload File: <input type="file" id="file_input" /></label>

All browsers tested support this method.

When the user drags and drops a file or files onto the specified HTML Element, the library will begin to upload that file or those files.

JavaScript:

instance.listenOnDrop(document.getElementById("file_drop"));

HTML:

<div id="file_drop">Drop Files Here</div>

In order to work, this method requires a browser that supports the HTML5 drag-and-drop interface.

Like instance.listenOnInput(input), except instead of listening for the "change" event on the input element, listen for the "click" event of a button.

JavaScript:

instance.listenOnSubmit(document.getElementById("my_button"), document.getElementById("file_input"));

HTML:

<label>Upload File: <input type="file" id="file_input" /></label>
<button id="my_button">Upload File</button>

A shorthand for running instance.listenOnSubmit(submitButton, input) repeatedly over multiple file input elements. Accepts an array of file input elements as the second argument.

When this method is called, the user will be prompted to choose a file to upload.

JavaScript:

document.getElementById("file_button").addEventListener("click", instance.prompt, false);

HTML:

<button id="file_button">Upload File</button>

Unfortunately, this method does not work in Firefox for security reasons. Read the code comments for more information.

Call this method to manually submit a FileList object to be uploaded over the socket. The argument is of type FileList.

Unbinds all events and DOM elements created by this instance of SIOFU.

Important Memory Note: In order to remove the instance of SIOFU from memory, you need to do at least three things:

  1. Remove all siofu.prompt event listeners and then
  2. Call this function and then
  3. Set this reference (and all references) to the instance to null

For example, if you created an instance like this:

// ...
var instance = new SocketIOFileUpload(socket);
myBtn.addEventListener("click", instance.prompt, false);
// ...

then you can remove it from memory like this:

myBtn.removeEventListener("click", instance.prompt, false);
instance.destroy();
instance = null;

Will cancel any attempt by the user to upload a file larger than this number of bytes. An "error" event with code 1 will be emitted if such an attempt is made. Defaults to a value of null, which does not enforce a file size limit.

To tell the client when they have tried to upload a file that is too large, you can use the following code:

siofu.addEventListener("error", function(data){
    if (data.code === 1) {
        alert("Don't upload such a big file");
    }
});

For maximum security, if you set a maximum file size on the client side, you should also do so on the server side.

The size of the file "chunks" to be loaded at a time. This enables you to monitor the upload progress with a progress bar and the "progress" event (see below).

The default value is 100 KiB, which is specified as

instance.chunkSize = 1024 * 100;

Setting this parameter to 0 disables chunking of files.

Defaults to false, which reads files as an octet array. This is necessary for binary-type files, like images.

Set to true to read and transmit files as plain text instead. This will save bandwidth if you expect to transmit only text files. If you choose this option, it is recommended that you perform a filter by returning false to a start event if the file does not have a desired extension.

Starting with Socket.IO 1.0, binary data may now be transmitted through the Web Socket. Begining with SIOFU version 0.3.2 (December 17, 2014), this option is enabled by default. To support older versions of Socket.IO (e.g. version 0.9.x), set this option to false, which transmits files as base 64-encoded strings.

Advantages of enabling this option:

  • Less overhead in the socket, since base 64 increases overhead by approximately 33%.
  • No serialization and deserialization into and out of base 64 is required on the client and server side.

Disadvantages of enabling this option:

  • Transmitting buffer types through a WebSocket is not supported in older browsers.
  • This option is relatively new in both Socket.IO and Socket.IO File Upload and has not been rigorously tested.

As you use this option, please leave feedback.

This method is experimental, and has been deprecated in Socket.IO File Upload as of version 0.3 in favor of instance.useBuffer.

Defaults to false, which transmits binary files as Base 64 data (with a 33% overhead).

Set to true to instead transmit the data as a serialized octet array. This will result in an overhead of over 1000% (not recommended for production applications).

Note: This option is not supported by Firefox.

Instances of the SocketIOFileUpload object implement the W3C EventTarget interface. This means that you can do:

  • instance.addEventListener("type", callback)
  • instance.removeEventListener("type", callback)
  • instance.dispatchEvent(event)

The events are documented below.

The user has chosen files to upload, through any of the channels you have implemented. If you want to cancel the upload, make your callback return false.

  • event.files an instance of a W3C FileList object

This event is fired immediately following the choose event, but once per file. If you want to cancel the upload for this individual file, make your callback return false.

  • event.file an instance of a W3C File object

Part of the file has been loaded from the file system and ready to be transmitted via Socket.IO. This event can be used to make an upload progress bar.

You can compute the percent progress via event.bytesLoaded / event.file.size

TODO: A better implementation would be for the server to confirm receipt of a chunk before emitting this event. Feel free to submit a PR if the current implementation does not fit your needs.

  • event.file an instance of a W3C File object
  • event.bytesLoaded the number of bytes that have been loaded into memory
  • event.name the filename to which the server saved the file

A file has been loaded into an instance of the HTML5 FileReader object and has been transmitted through Socket.IO. We are awaiting a response from the server about whether the upload was successful; when we receive this response, a complete event will be dispatched.

  • event.file an instance of a W3C File object
  • event.reader an instance of a W3C FileReader object
  • event.name the filename to which the server saved the file

The server has received our file.

  • event.file an instance of a W3C File object
  • event.success true if the server-side implementation ran without error; false otherwise
  • event.detail The value of file.clientDetail on the server side. Properties may be added to this object literal during any event on the server side.

The server encountered an error.

  • event.file an instance of a W3C File object
  • event.message the error message
  • event.code the error code, if available

The server-side interface is contained within an NPM module. Require it with:

var SocketIOFileUpload = require("socketio-file-upload");

If you are using an HTTP server in Node, pass it into this method in order for the client-side JavaScript file to be served.

var app = http.createServer( /* your configurations here */ ).listen(80);
SocketIOFileUpload.listen(app);

If you are using Connect-based middleware like Express, pass this value into the middleware.

var app = express()
            .use(SocketIOFileUpload.router)
            .use( /* your other middleware here */ )
            .listen(80);

Listen for uploads occuring on this Socket.IO socket.

io.sockets.on("connection", function(socket){
    var uploader = new SocketIOFileUpload();
    uploader.listen(socket);
});

If specified, the module will attempt to save uploaded files in this directory. The module will inteligently suffix numbers to the uploaded filenames until name conflicts are resolved. It will also sanitize the filename to help prevent attacks.

The last-modified time of the file might be retained from the upload. If this is of high importance to you, I recommend performing some tests, and if it does not meet your needs, submit an issue or a pull request.

Use these UNIX permissions when saving the uploaded file. Defaults to 0666.

The maximum file size, in bytes, to write to the disk. If file data is received from the client that exceeds this bound, the data will not be written to the disk and an "error" event will be thrown. Defaults to null, in which no maximum file size is enforced.

Note that the other events like "progress", "complete", and "saved" will still be emitted even if the file's maximum allowed size had been exceeded. However, in those events, event.file.success will be false.

Instances of SocketIOFileUpload implement Node's EventEmitter interface. This means that you can do:

  • instance.on("type", callback)
  • instance.removeListener("type", callback)
  • instance.emit("type", event)
  • et cetera.

The events are documented below.

The client has started the upload process.

  • event.file An object containing the file's name, mtime, encoding, meta, success, bytesLoaded, and id. Note: encoding is either "text" if the file is being transmitted as plain text or "octet" if it is being transmitted using an ArrayBuffer. Note: In the "progress", "complete", "saved", and "error" events, if you are letting the module save the file for you, the file object will contain two additional properties: base, the new base name given to the file, and pathName, the full path at which the uploaded file was saved.

Data has been received from the client.

  • event.file The same file object that would have been passed during the start event earlier.
  • event.buffer A buffer containing the data received from the client

The transmission of a file is complete.

  • event.file The same file object that would have been passed during the start event earlier.
  • event.interrupt true if the client said that the data was interrupted (not completely sent); false otherwise

A file has been saved. It is recommended that you check event.file.success to tell whether or not the file was saved without errors.

  • event.file The same file object that would have been passed during the start event earlier.

An error was encountered in the saving of the file.

  • event.file The same file object that would have been passed during the start event earlier.
  • event.error The I/O error that was encountered.

It is sometimes useful to add metadata to a file prior to uploading the file. You may add metadata to a file on the client side by setting the file.meta property on the File object during the "choose" or "start" events. You may also add metadata to a file on the server side by setting the file.clientDetail property on the fileInfo object during any of the server-side events.

To add meta data to an individual file, you can listen on the "start" event as shown below.

// client side 
siofu.addEventListener("start", function(event){
    event.file.meta.hello = "world";
});

The data is then available on the server side as follows.

// server side 
uploader.on("saved", function(event){
    console.log(event.file.meta.hello);
});

You can also refer back to your meta data at any time on the client side by referencing the same event.file.meta object literal.

You can add meta data on the server. The meta data will be available to the client on the "complete" event on the client as shown below.

// server side 
siofuServer.on("saved", function(event){
    event.file.clientDetail.hello = "world";
});

The information saved in event.file.clientDetail will be available in event.detail on the client side.

// client side 
siofu.addEventListener("complete", function(event){
    console.log(event.detail.hello);
});

This example assumes that you are running your application via the Connect middleware, including Express. If you are using a middleware that is not Connect-based or Node-HTTP-based, download the client.js file from the project repository and serve it on the path /siofu/client.js. Alternatively, you may contribute an adapter for your middleware to this project and submit a pull request.

// Require the libraries:
var SocketIOFileUpload = require('socketio-file-upload'),
    socketio = require('socket.io'),
    express = require('express');

// Make your Express server:
var app = express()
    .use(SocketIOFileUpload.router)
    .use(express.static(__dirname + "/public"))
    .listen(80);

// Start up Socket.IO:
var io = socketio.listen(app);
io.sockets.on("connection", function(socket){

    // Make an instance of SocketIOFileUpload and listen on this socket:
    var uploader = new SocketIOFileUpload();
    uploader.dir = "/srv/uploads";
    uploader.listen(socket);

    // Do something when a file is saved:
    uploader.on("saved", function(event){
        console.log(event.file);
    });

    // Error handler:
    uploader.on("error", function(event){
        console.log("Error from uploader", event);
    });
});
<!DOCTYPE html>
<html>
<head>
<title>Upload Files</title>
<script src="/siofu/client.js"></script>
<script src="/socket.io/socket.io.js"></script>

<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function(){

    // Initialize instances:
    var socket = io.connect();
    var siofu = new SocketIOFileUpload(socket);

    // Configure the three ways that SocketIOFileUpload can read files:
    document.getElementById("upload_btn").addEventListener("click", siofu.prompt, false);
    siofu.listenOnInput(document.getElementById("upload_input"));
    siofu.listenOnDrop(document.getElementById("file_drop"));

    // Do something when a file is uploaded:
    siofu.addEventListener("complete", function(event){
        console.log(event.success);
        console.log(event.file);
    });

}, false);
</script>

</head>
<body>

<p><button id="upload_btn">Prompt for File</button></p>
<p><label>Choose File: <input type="file" id="upload_input"/></label></p>
<div id="file_drop" dropzone="copy" title="drop files for upload">Drop File</div>

</body>
</html>

I hope to one day see this project implement the following features.

  • Upload Progress. There are examples of this on the net, so it should be feasible to implement.
  • Allow input of a file URL rather than uploading a file from your computer or mobile device.