├── .editorconfig
├── .eslintignore
├── .eslintrc.yml
├── .github
└── workflows
│ ├── build-test.yml
│ └── publish.yml
├── .gitignore
├── README.md
├── client.js
├── client.min.js
├── demo
├── .snyk
├── app.js
├── package.json
└── public_html
│ ├── config.js
│ └── index.html
├── package-lock.json
├── package.json
├── server.js
└── test
├── .eslintrc.yml
├── assets
├── mandrill.png
└── sonnet18.txt
├── browser-phantom.js
├── serve
├── browser-file-transfer.js
└── index.html
├── setup-server.js
├── test-serves-client-js.js
└── test-transfer.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | # IDE Configuration
2 | # See if you need a plugin => https://editorconfig.org/#download
3 |
4 | root = true
5 |
6 | # Default config
7 |
8 | [*]
9 | charset = utf-8
10 | indent_style = tab
11 | insert_final_newline = true
12 | trim_trailing_whitespace = true
13 |
14 | # Specific config
15 |
16 | [*.{json,yml,yaml}]
17 | indent_style = space
18 | indent_size = 1
19 |
20 | [*.md]
21 | trim_trailing_whitespace = false
22 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | demo/node_modules/
2 | test/serve/bundle.js
3 | client.min.js
4 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | env:
2 | es6: true
3 | browser: true
4 | extends: 'eslint:recommended'
5 | parserOptions:
6 | ecmaVersion: 5
7 | rules:
8 | indent:
9 | - error
10 | - tab
11 | - SwitchCase: 1
12 | linebreak-style:
13 | - error
14 | - unix
15 | quotes:
16 | - error
17 | - double
18 | semi:
19 | - error
20 | - always
21 |
--------------------------------------------------------------------------------
/.github/workflows/build-test.yml:
--------------------------------------------------------------------------------
1 | name: Node.js CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | - uses: actions/setup-node@v1
11 | with:
12 | node-version: 10.x
13 | - name: Install
14 | run: npm ci
15 | - name: Test
16 | run: npm test
17 | env:
18 | CI: true
19 | X_USE_PHANTOM: 1
20 | - name: Lint
21 | run: npm run lint
22 | - name: Check client.min.js
23 | run: |
24 | npm run minify
25 | git update-index --refresh
26 | git diff-index HEAD
27 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish Release
2 | on:
3 | push:
4 | branches:
5 | - master
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | steps:
10 | - uses: actions/checkout@v1
11 | - uses: actions/setup-node@v1
12 | with:
13 | node-version: 10.x
14 | - run: npm ci
15 | - run: npm test
16 | - uses: JS-DevTools/npm-publish@v1
17 | name: Publish to npm
18 | id: publish
19 | with:
20 | token: ${{ secrets.NPM_AUTH_TOKEN }}
21 | - if: steps.publish.outputs.type != 'none'
22 | name: Create GitHub Release
23 | id: create_release
24 | uses: actions/create-release@v1
25 | env:
26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
27 | with:
28 | tag_name: "v${{ steps.publish.outputs.version }}"
29 | release_name: Release ${{ steps.publish.outputs.version }}
30 | draft: true
31 | prerelease: false
32 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | demo/node_modules/
3 | demo/uploads/
4 | test/serve/bundle.js
5 | .idea
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Socket.IO File Upload
2 | =====================
3 |
4 | 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 higher.
5 |
6 | The intended audience are single-page web apps, but other types of Node.JS projects may benefit from this library.
7 |
8 | Since version 0.4, this module also supports monitoring file upload progress.
9 |
10 | The module is released under the X11 open-source license.
11 |
12 | [](https://github.com/sffc/socketio-file-upload/actions)
13 | [](https://snyk.io/test/github/sffc/socketio-file-upload)
14 | [](https://npmjs.org/package/socketio-file-upload "View this project on npm")
15 |
16 |
17 | ## Quick Start
18 |
19 | Navigate to your project directory and run:
20 |
21 | $ npm install --save socketio-file-upload
22 |
23 | In your Express app, add the router like this (if you don't use Express, read the docs below):
24 |
25 | ```javascript
26 | var siofu = require("socketio-file-upload");
27 | var app = express()
28 | .use(siofu.router)
29 | .listen(8000);
30 | ```
31 |
32 | On a server-side socket connection, do this:
33 |
34 | ```javascript
35 | io.on("connection", function(socket){
36 | var uploader = new siofu();
37 | uploader.dir = "/path/to/save/uploads";
38 | uploader.listen(socket);
39 | });
40 | ```
41 |
42 | The client-side script is served at `/siofu/client.js`. Include it like this:
43 |
44 | ```html
45 |
46 | ```
47 |
48 | If you use browserify, just require it like this:
49 |
50 | ```javascript
51 | var SocketIOFileUpload = require('socketio-file-upload');
52 | ```
53 |
54 | The module also supports AMD; see the docs below for more information.
55 |
56 | Then, in your client side app, with this HTML:
57 |
58 | ```html
59 |
60 | ```
61 |
62 | Just do this in JavaScript:
63 |
64 | ```javascript
65 | var socket = io.connect();
66 | var uploader = new SocketIOFileUpload(socket);
67 | uploader.listenOnInput(document.getElementById("siofu_input"));
68 | ```
69 |
70 | 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.
71 |
72 | ## Table of Contents
73 |
74 | - [Client-Side API](#client-side-api)
75 | - [instance.listenOnInput(input)](#instancelistenoninputinput)
76 | - [instance.listenOnDrop(element)](#instancelistenondropelement)
77 | - [instance.listenOnSubmit(submitButton, input)](#instancelistenonsubmitsubmitbutton-input)
78 | - [instance.listenOnArraySubmit(submitButton, input[])](#instancelistenonarraysubmitsubmitbutton-input)
79 | - [instance.prompt()](#instanceprompt)
80 | - [instance.submitFiles(files)](#instancesubmitfilesfiles)
81 | - [instance.destroy()](#instancedestroy)
82 | - [instance.resetFileInputs = true](#instanceresetFileInputs--true)
83 | - [instance.maxFileSize = null](#instancemaxfilesize--null)
84 | - [instance.chunkSize = 100 KiB](#instancechunksize--100-kib)
85 | - [instance.useText = false](#instanceusetext--false)
86 | - [instance.useBuffer = true](#instanceusebuffer--true)
87 | - [instance.serializeOctets = false](#instanceserializeoctets--false)
88 | - [instance.topicName = "siofu"](#instancetopicname--siofu)
89 | - [instance.wrapData = false](#instancewrapdata--false)
90 | - [instance.exposePrivateFunction = false](#instanceexposeprivatefunction--false)
91 | - [Client-Side Events](#events)
92 | - [choose](#choose)
93 | - [start](#start)
94 | - [progress](#progress)
95 | - [load](#load)
96 | - [complete](#complete)
97 | - [error](#error)
98 | - [Server-Side API](#server-side-api)
99 | - [SocketIOFileUpload.listen(app)](#socketiofileuploadlistenapp)
100 | - [SocketIOFileUpload.router](#socketiofileuploadrouter)
101 | - [instance.listen(socket)](#instancelistensocket)
102 | - [instance.abort(id, socket)](#instanceabortid-socket)
103 | - [instance.dir = "/path/to/upload/directory"](#instancedir--pathtouploaddirectory)
104 | - [instance.mode = "0666"](#instancemode--0666)
105 | - [instance.maxFileSize = null](#instancemaxfilesize--null-1)
106 | - [instance.emitChunkFail = false](#instanceemitchunkfail--false)
107 | - [instance.uploadValidator(event, callback)](#instanceuploadvalidatorevent-callback)
108 | - instance.topicName = "siofu" (see [client](#instancetopicname--siofu))
109 | - instance.wrapData = false (see [client](#instancewrapdata--false))
110 | - instance.exposePrivateFunction = false (see [client](#instanceexposeprivatefunction--false))
111 | - [Server-Side Events](#events-1)
112 | - [start](#start-1)
113 | - [progress](#progress-1)
114 | - [complete](#complete-1)
115 | - [saved](#saved)
116 | - [error](#error)
117 | - [Adding Meta Data](#adding-meta-data)
118 | - [Client to Server](#client-to-server-meta-data)
119 | - [Server to Client](#server-to-client-meta-data)
120 | - [Example](#example)
121 |
122 | ## Client-Side API
123 |
124 | The client-side interface is inside the `SocketIOFileUpload` namespace. Include it with:
125 |
126 | ```html
127 |
128 | ```
129 |
130 | If you're awesome and you use AMD/RequireJS, set up your paths config like this:
131 |
132 | ```javascript
133 | requirejs.config({
134 | paths: {
135 | "SocketIOFileUpload": "/siofu/client",
136 | // ...
137 | }
138 | });
139 | ```
140 |
141 | and then include it in your app like this:
142 |
143 | ```javascript
144 | define("app", ["SocketIOFileUpload"], function(SocketIOFileUpload){
145 | // ...
146 | });
147 | ```
148 |
149 | When instantiating an instance of the `SocketIOFileUpload`, pass a reference to your socket.
150 |
151 | ```javascript
152 | var instance = new SocketIOFileUpload(socket);
153 | ```
154 |
155 | ### Public Properties and Methods
156 |
157 | Each public property can be set up in an object passing at second parameter of the Siofu constructor:
158 |
159 | ```javascript
160 | var instance = new SocketIOFileUpload(socket);
161 | instance.chunkSize = 1024 * 1000
162 | // is the same that
163 | var instance = new SocketIOFileUpload(socket, {
164 | chunkSize: 1024 * 1000
165 | });
166 | ```
167 |
168 |
169 | #### instance.listenOnInput(input)
170 |
171 | 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.
172 |
173 | JavaScript:
174 |
175 | ```javascript
176 | instance.listenOnInput(document.getElementById("file_input"));
177 | ```
178 |
179 | HTML:
180 |
181 | ```html
182 |
183 | ```
184 |
185 | All browsers tested support this method.
186 |
187 | #### instance.listenOnDrop(element)
188 |
189 | 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.
190 |
191 | JavaScript:
192 |
193 | ```javascript
194 | instance.listenOnDrop(document.getElementById("file_drop"));
195 | ```
196 |
197 | HTML:
198 |
199 | ```html
200 |
Drop Files Here
201 | ```
202 |
203 | In order to work, this method requires a browser that supports the HTML5 drag-and-drop interface.
204 |
205 | #### instance.listenOnSubmit(submitButton, input)
206 |
207 | Like `instance.listenOnInput(input)`, except instead of listening for the "change" event on the input element, listen for the "click" event of a button.
208 |
209 | JavaScript:
210 |
211 | ```javascript
212 | instance.listenOnSubmit(document.getElementById("my_button"), document.getElementById("file_input"));
213 | ```
214 |
215 | HTML:
216 |
217 | ```html
218 |
219 |
220 | ```
221 |
222 | #### instance.listenOnArraySubmit(submitButton, input[])
223 |
224 | 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.
225 |
226 | #### instance.prompt()
227 |
228 | When this method is called, the user will be prompted to choose a file to upload.
229 |
230 | JavaScript:
231 |
232 | ```javascript
233 | document.getElementById("file_button").addEventListener("click", instance.prompt, false);
234 | ```
235 |
236 | HTML:
237 |
238 | ```html
239 |
240 | ```
241 |
242 | Unfortunately, this method does not work in Firefox for security reasons. Read the code comments for more information.
243 |
244 | #### instance.submitFiles(files)
245 |
246 | Call this method to manually submit an array of files. The argument can be either a [FileList](https://developer.mozilla.org/en-US/docs/Web/API/FileList) or an array of [File](https://developer.mozilla.org/en-US/docs/Web/API/File) objects.
247 |
248 | #### instance.destroy()
249 |
250 | Unbinds all events and DOM elements created by this instance of SIOFU.
251 |
252 | **Important Memory Note:** In order to remove the instance of SIOFU from memory, you need to do at least three things:
253 |
254 | 1. Remove all `siofu.prompt` event listeners *and then*
255 | 2. Call this function *and then*
256 | 3. Set this reference (and all references) to the instance to `null`
257 |
258 | For example, if you created an instance like this:
259 |
260 | ```javascript
261 | // ...
262 | var instance = new SocketIOFileUpload(socket);
263 | myBtn.addEventListener("click", instance.prompt, false);
264 | // ...
265 | ```
266 |
267 | then you can remove it from memory like this:
268 |
269 | ```javascript
270 | myBtn.removeEventListener("click", instance.prompt, false);
271 | instance.destroy();
272 | instance = null;
273 | ```
274 |
275 | #### instance.resetFileInputs = true
276 |
277 | Defaults to `true`, which resets file input elements to their empty state after the user selects a file. If you do not reset the file input elements, if the user selects a file with the same name as the previous file, then the second file may not be uploaded.
278 |
279 | #### instance.maxFileSize = null
280 |
281 | 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.
282 |
283 | To tell the client when they have tried to upload a file that is too large, you can use the following code:
284 |
285 | ```javascript
286 | siofu.addEventListener("error", function(data){
287 | if (data.code === 1) {
288 | alert("Don't upload such a big file");
289 | }
290 | });
291 | ```
292 |
293 | For maximum security, if you set a maximum file size on the client side, you should also do so on the server side.
294 |
295 | #### instance.chunkSize = 100 KiB
296 |
297 | 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).
298 |
299 | The default value is 100 KiB, which is specified as
300 |
301 | `instance.chunkSize = 1024 * 100;`
302 |
303 | Setting this parameter to 0 disables chunking of files.
304 |
305 | #### instance.useText = false
306 |
307 | Defaults to `false`, which reads files as an octet array. This is necessary for binary-type files, like images.
308 |
309 | 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.
310 |
311 | #### instance.useBuffer = true
312 |
313 | 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.
314 |
315 | Advantages of enabling this option:
316 |
317 | - Less overhead in the socket, since base 64 increases overhead by approximately 33%.
318 | - No serialization and deserialization into and out of base 64 is required on the client and server side.
319 |
320 | Disadvantages of enabling this option:
321 |
322 | - Transmitting buffer types through a WebSocket is not supported in older browsers.
323 | - This option is relatively new in both Socket.IO and Socket.IO File Upload and has not been rigorously tested.
324 |
325 | As you use this option, [please leave feedback](https://github.com/vote539/socketio-file-upload/issues/16).
326 |
327 | #### instance.serializeOctets = false
328 |
329 | *This method is experimental, and has been deprecated in Socket.IO File Upload as of version 0.3 in favor of instance.useBuffer.*
330 |
331 | Defaults to `false`, which transmits binary files as Base 64 data (with a 33% overhead).
332 |
333 | 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).
334 |
335 | *Note:* This option is not supported by Firefox.
336 |
337 | #### instance.topicName = "siofu"
338 |
339 | Customize the name of the topic where Siofu emit message. Need to be the same that the one specified in the server options.
340 |
341 | Can be used in team with instance.wrapData and instance.exposePrivateFunction to use a topic already used for something else.
342 |
343 | #### instance.wrapData = false
344 |
345 | By default Siofu client sends data the server on a different topic depending of the progress of the upload:
346 |
347 | ```
348 | siofu_start
349 | siofu_progress
350 | siofu_done
351 | ```
352 |
353 | And events received from the server to the client:
354 |
355 | ```
356 | siofu_ready
357 | siofu_chunk
358 | siofu_complete
359 | siofu_error
360 | ```
361 |
362 | If wrapData is set to true, Siofu will use only one topic specified by instance.topicName and wrap the data into a parent message.
363 |
364 | The following examples are example settings for the client. :warning: IF YOU USE `wrapData` ON THE CLIENT, YOU MUST ALSO USE IT ON THE SERVER. :warning:
365 |
366 | ex:
367 |
368 | ```javascript
369 | // wrapData false:
370 | {
371 | id: id,
372 | success: success,
373 | detail: fileInfo.clientDetail
374 | }
375 | // wrapData true
376 | {
377 | action: 'complete',
378 | message: {
379 | id: id,
380 | success: success,
381 | detail: fileInfo.clientDetail
382 | }
383 | }
384 | ```
385 |
386 | You can personalise the 'action' and 'message' key by passing a object to wrapData instance. The settings on the server should be the inverse of the settings on the client. For example, if the client has wrapData.wrapKey.message = "data", then the server should have wrapData.unwrapKey.message = "data".
387 |
388 | ```javascript
389 | instance.wrapData = {
390 | wrapKey: {
391 | action: 'actionType',
392 | message: 'data'
393 | },
394 | unwrapKey: {
395 | action: 'actionType',
396 | message: 'message'
397 | }
398 | }
399 | // Send a message like this:
400 | {
401 | actionType: 'complete',
402 | data: {
403 | id: id,
404 | success: success,
405 | detail: fileInfo.clientDetail
406 | }
407 | }
408 | // Expect message like this from the server:
409 | {
410 | actionType: 'complete',
411 | message: {
412 | id: id,
413 | success: success,
414 | detail: fileInfo.clientDetail
415 | }
416 | }
417 | ```
418 |
419 | It's also possible to add additional data (for strongly typed topic or secure pipeline or acknowledgement):
420 | ```javascript
421 | instance.wrapData = {
422 | adtionalData: {
423 | userId: '123456',
424 | },
425 | }
426 | // Send a message like this:
427 | {
428 | userId: '123456',
429 | action: 'complete',
430 | message: {
431 | id: id,
432 | success: success,
433 | detail: fileInfo.clientDetail
434 | }
435 | }
436 | ```
437 |
438 | #### instance.exposePrivateFunction = false
439 |
440 | If true this will expose some functions used in intern to personalize action on the topic. This is used alongside with wrapData to add custom check or logic before process the file upload.
441 | If true you will have access to:
442 | ```
443 | instance.chunckCallback
444 | instance.readyCallback
445 | instance.completCallback
446 | instance.errorCallback
447 | ```
448 |
449 | ### Events
450 |
451 | Instances of the `SocketIOFileUpload` object implement the [W3C `EventTarget` interface](http://www.w3.org/wiki/DOM/domcore/EventTarget). This means that you can do:
452 |
453 | * `instance.addEventListener("type", callback)`
454 | * `instance.removeEventListener("type", callback)`
455 | * `instance.dispatchEvent(event)`
456 |
457 | The events are documented below.
458 |
459 | #### choose
460 |
461 | 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`.
462 |
463 | ##### Event Properties
464 |
465 | * `event.files` an instance of a W3C FileList object
466 |
467 | #### start
468 |
469 | 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`.
470 |
471 | ##### Event Properties
472 |
473 | * `event.file` an instance of a W3C File object
474 |
475 | #### progress
476 |
477 | 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.
478 |
479 | You can compute the percent progress via `event.bytesLoaded / event.file.size`
480 |
481 | ##### Event Properties
482 |
483 | * `event.file` an instance of a W3C File object
484 | * `event.bytesLoaded` the number of bytes that have been loaded into memory
485 | * `event.name` the filename to which the server saved the file
486 |
487 | #### load
488 |
489 | 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.
490 |
491 | ##### Event Properties
492 |
493 | * `event.file` an instance of a W3C File object
494 | * `event.reader` an instance of a W3C FileReader object
495 | * `event.name` the filename to which the server saved the file
496 |
497 | #### complete
498 |
499 | The server has received our file.
500 |
501 | ##### Event Properties
502 |
503 | * `event.file` an instance of a W3C File object
504 | * `event.success` true if the server-side implementation ran without error; false otherwise
505 | * `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.
506 |
507 | #### error
508 |
509 | The server encountered an error.
510 |
511 | ##### Event Properties
512 |
513 | * `event.file` an instance of a W3C File object
514 | * `event.message` the error message
515 | * `event.code` the error code, if available
516 |
517 | ## Server-Side API
518 |
519 | The server-side interface is contained within an NPM module. Require it with:
520 |
521 | ```javascript
522 | var SocketIOFileUpload = require("socketio-file-upload");
523 | ```
524 |
525 | ### Static Properties and Methods
526 |
527 | #### SocketIOFileUpload.listen(app)
528 |
529 | 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.
530 |
531 | ```javascript
532 | var app = http.createServer( /* your configurations here */ ).listen(80);
533 | SocketIOFileUpload.listen(app);
534 | ```
535 |
536 | #### SocketIOFileUpload.router
537 |
538 | If you are using Connect-based middleware like Express, pass this value into the middleware.
539 |
540 | ```javascript
541 | var app = express()
542 | .use(SocketIOFileUpload.router)
543 | .use( /* your other middleware here */ )
544 | .listen(80);
545 | ```
546 |
547 | ### Public Properties and Methods
548 |
549 | #### instance.listen(socket)
550 |
551 | Listen for uploads occuring on this Socket.IO socket.
552 |
553 | ```javascript
554 | io.sockets.on("connection", function(socket){
555 | var uploader = new SocketIOFileUpload();
556 | uploader.listen(socket);
557 | });
558 | ```
559 |
560 | #### instance.abort(id, socket)
561 |
562 | Aborts an upload that is in progress. Example use case:
563 |
564 | ```javascript
565 | uploader.on("start", function(event){
566 | if (/\.exe$/.test(event.file.name)) {
567 | uploader.abort(event.file.id, socket);
568 | }
569 | });
570 | ```
571 |
572 | #### instance.dir = "/path/to/upload/directory"
573 |
574 | If specified, the module will attempt to save uploaded files in this directory. The module will intelligently suffix numbers to the uploaded filenames until name conflicts are resolved. It will also sanitize the filename to help prevent attacks.
575 |
576 | 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.
577 |
578 | #### instance.mode = "0666"
579 |
580 | Use these UNIX permissions when saving the uploaded file. Defaults to `0666`.
581 |
582 | #### instance.maxFileSize = null
583 |
584 | 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.
585 |
586 | 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.
587 |
588 | #### instance.emitChunkFail = false
589 |
590 | Whether or not to emit an error event if a progress chunk fails to finish writing. In most cases, the failure is a harmless notification that the file is larger than the internal buffer size, but it could also mean that the file upload triggered an ENOSPC error. It may be useful to enable this error event if you are concerned about uploads running out of space.
591 |
592 | #### instance.uploadValidator(event, callback)
593 |
594 | Can be overridden to enable async validation and preparing.
595 |
596 | ```javascript
597 | uploader.uploadValidator = function(event, callback){
598 | // asynchronous operations allowed here; when done,
599 | if (/* success */) {
600 | callback(true);
601 | } else {
602 | callback(false);
603 | }
604 | };
605 | ```
606 |
607 | ### Events
608 |
609 | Instances of `SocketIOFileUpload` implement [Node's `EventEmitter` interface](http://nodejs.org/api/events.html#events_class_events_eventemitter). This means that you can do:
610 |
611 | * `instance.on("type", callback)`
612 | * `instance.removeListener("type", callback)`
613 | * `instance.emit("type", event)`
614 | * et cetera.
615 |
616 | The events are documented below.
617 |
618 | #### start
619 |
620 | The client has started the upload process, and the server is now processing the request.
621 |
622 | ##### Event Properties
623 |
624 | * `event.file` An object containing the file's `name`, `mtime`, `encoding`, `meta`, `success`, `bytesLoaded`, and `id`.
625 | *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.
626 |
627 | #### progress
628 |
629 | Data has been received from the client.
630 |
631 | ##### Event Properties
632 |
633 | * `event.file` The same file object that would have been passed during the `start` event earlier.
634 | * `event.buffer` A buffer containing the data received from the client
635 |
636 | #### complete
637 |
638 | The transmission of a file is complete.
639 |
640 | ##### Event Properties
641 |
642 | * `event.file` The same file object that would have been passed during the `start` event earlier.
643 | * `event.interrupt` true if the client said that the data was interrupted (not completely sent); false otherwise
644 |
645 | #### saved
646 |
647 | 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.
648 |
649 | In this event, you can safely move the saved file to a new location.
650 |
651 | ##### Event Properties
652 |
653 | * `event.file` The same file object that would have been passed during the `start` event earlier.
654 |
655 | #### error
656 |
657 | An error was encountered in the saving of the file.
658 |
659 | ##### Event Properties
660 |
661 | * `event.file` The same file object that would have been passed during the `start` event earlier.
662 | * `event.error` The I/O error that was encountered.
663 |
664 | ## Adding Meta Data
665 |
666 | 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.
667 |
668 | ### Client to Server Meta Data
669 |
670 | To add meta data to an individual file, you can listen on the "start" event as shown below.
671 |
672 | ```javascript
673 | // client side
674 | siofu.addEventListener("start", function(event){
675 | event.file.meta.hello = "world";
676 | });
677 | ```
678 |
679 | The data is then available on the server side as follows.
680 |
681 | ```javascript
682 | // server side
683 | uploader.on("saved", function(event){
684 | console.log(event.file.meta.hello);
685 | });
686 | ```
687 |
688 | 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.
689 |
690 | ### Server to Client Meta Data
691 |
692 | 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.
693 |
694 | ```javascript
695 | // server side
696 | siofuServer.on("saved", function(event){
697 | event.file.clientDetail.hello = "world";
698 | });
699 | ```
700 |
701 | The information saved in `event.file.clientDetail` will be available in `event.detail` on the client side.
702 |
703 | ```javascript
704 | // client side
705 | siofu.addEventListener("complete", function(event){
706 | console.log(event.detail.hello);
707 | });
708 | ```
709 |
710 | ## Example
711 |
712 | 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.
713 |
714 | ### Server Code: app.js
715 |
716 | ```javascript
717 | // Require the libraries:
718 | var SocketIOFileUpload = require('socketio-file-upload'),
719 | socketio = require('socket.io'),
720 | express = require('express');
721 |
722 | // Make your Express server:
723 | var app = express()
724 | .use(SocketIOFileUpload.router)
725 | .use(express.static(__dirname + "/public"))
726 | .listen(80);
727 |
728 | // Start up Socket.IO:
729 | var io = socketio.listen(app);
730 | io.sockets.on("connection", function(socket){
731 |
732 | // Make an instance of SocketIOFileUpload and listen on this socket:
733 | var uploader = new SocketIOFileUpload();
734 | uploader.dir = "/srv/uploads";
735 | uploader.listen(socket);
736 |
737 | // Do something when a file is saved:
738 | uploader.on("saved", function(event){
739 | console.log(event.file);
740 | });
741 |
742 | // Error handler:
743 | uploader.on("error", function(event){
744 | console.log("Error from uploader", event);
745 | });
746 | });
747 | ```
748 |
749 | ### Client Code: public/index.html
750 |
751 | ```html
752 |
753 |
754 |
755 | Upload Files
756 |
757 |
758 |
759 |
785 |
786 |
787 |
788 |
789 |
790 |
791 |
Drop File
792 |
793 |
794 |
795 | ```
796 |
797 | ## Future Work
798 |
799 | First, I'm aware that this module currently lacks unit tests (mocha, etc). This is a problem that should be solved. I'm willing to accept PRs that add unit tests, or else one of these days when I have extra time I'll see if I can add them myself.
800 |
801 | In addition, the following features would be useful for the module to support.
802 |
803 | 1. Allow input of a file URL rather than uploading a file from your computer or mobile device.
804 |
805 | As always PRs are welcome.
806 |
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2015 Shane Carr and others
3 | * X11 License
4 | *
5 | * Permission is hereby granted, free of charge, to any person obtaining a
6 | * copy of this software and associated documentation files (the "Software"),
7 | * to deal in the Software without restriction, including without limitation
8 | * the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 | * and/or sell copies of the Software, and to permit persons to whom the
10 | * Software is furnished to do so, subject to the following conditions:
11 | *
12 | * The above copyright notice and this permission notice shall be included in
13 | * all copies or substantial portions of the Software.
14 | *
15 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 | * DEALINGS IN THE SOFTWARE.
22 | *
23 | * Except as contained in this notice, the names of the authors or copyright
24 | * holders shall not be used in advertising or otherwise to promote the sale,
25 | * use or other dealings in this Software without prior written authorization
26 | * from the authors or copyright holders.
27 | */
28 |
29 | // Do not check function indentation because this is intentionally ignored in order to preserve history in git.
30 | /* eslint-disable indent */
31 |
32 | /*
33 | * A client-side JavaScript object to handle file uploads to a Node.JS server
34 | * via Socket.IO.
35 | * @implements EventTarget
36 | * @param {SocketIO} socket The current Socket.IO connection.
37 | */
38 | (function (scope, name, factory) {
39 | /* eslint-disable no-undef */
40 | if (typeof define === "function" && define.amd) {
41 | define([], factory);
42 | }
43 | else if (typeof module === "object" && module.exports) {
44 | module.exports = factory();
45 | }
46 | else {
47 | scope[name] = factory();
48 | }
49 | /* eslint-enable no-undef */
50 | }(this, "SocketIOFileUpload", function () {
51 | return function (socket, options) {
52 | "use strict";
53 |
54 | var self = this; // avoids context issues
55 |
56 | // Check for compatibility
57 | if (!window.File || !window.FileReader) {
58 | throw new Error("Socket.IO File Upload: Browser Not Supported");
59 | }
60 |
61 | if ( !window.siofu_global ) {
62 | window.siofu_global = {
63 | instances: 0,
64 | downloads: 0
65 | };
66 | }
67 |
68 | // Private and Public Variables
69 | var callbacks = {},
70 | uploadedFiles = {},
71 | chunkCallbacks = {},
72 | readyCallbacks = {},
73 | communicators = {};
74 |
75 | var _getOption = function (key, defaultValue) {
76 | if(!options) {
77 | return defaultValue;
78 | }
79 | return options[key] || defaultValue;
80 | };
81 |
82 | self.fileInputElementId = "siofu_input_"+window.siofu_global.instances++;
83 | self.resetFileInputs = true;
84 | self.useText = _getOption("useText", false);
85 | self.serializedOctets = _getOption("serializedOctets", false);
86 | self.useBuffer = _getOption("useBuffer", true);
87 | self.chunkSize = _getOption("chunkSize", 1024 * 100); // 100kb default chunk size
88 | self.topicName = _getOption("topicName", "siofu");
89 |
90 | /**
91 | * WrapData allow you to wrap the Siofu messages into a predefined format.
92 | * You can then easily use Siofu packages even in strongly typed topic.
93 | * wrapData can be a boolean or an object. It is false by default.
94 | * If wrapData is true it will allow you to send all the messages to only one topic by wrapping the siofu actions and messages.
95 | *
96 | * ex:
97 | {
98 | action: 'complete',
99 | message: {
100 | id: id,
101 | success: success,
102 | detail: fileInfo.clientDetail
103 | }
104 | }
105 | *
106 | * If wrapData is an object constituted of two mandatory key and one optional:
107 | * wrapKey and unwrapKey (mandatory): Corresponding to the key used to wrap the siofu data and message
108 | * additionalData (optional): Corresponding to the data to send along with file data
109 | *
110 | * ex:
111 | * if wrapData = {
112 | wrapKey: {
113 | action: 'actionType',
114 | message: 'data'
115 | },
116 | unwrapKey: {
117 | action: 'actionType',
118 | message: 'message'
119 | },
120 | additionalData: {
121 | acknowledgement: true
122 | }
123 | }
124 | * When Siofu will send for example a complete message this will send:
125 | *
126 | {
127 | acknowledgement: true,
128 | actionType: 'complete',
129 | data: {
130 | id: id,
131 | success: success,
132 | detail: fileInfo.clientDetail
133 | }
134 | }
135 | * and it's waiting from client data formatted like this:
136 | *
137 | {
138 | actionType: '...',
139 | message: {...}
140 | }
141 | * /!\ If wrapData is wrong configured is interpreted as false /!\
142 | */
143 | self.wrapData = _getOption("wrapData", false);
144 |
145 | var _isWrapDataWellConfigured = function () {
146 | if (typeof self.wrapData === "boolean") {
147 | return true;
148 | }
149 | if (typeof self.wrapData !== "object" || Array.isArray(self.wrapData)) {
150 | return false;
151 | }
152 |
153 | if(!self.wrapData.wrapKey || typeof self.wrapData.wrapKey.action !== "string" || typeof self.wrapData.wrapKey.message !== "string" ||
154 | !self.wrapData.unwrapKey || typeof self.wrapData.unwrapKey.action !== "string" || typeof self.wrapData.unwrapKey.message !== "string") {
155 | return false;
156 | }
157 |
158 | return true;
159 | };
160 |
161 |
162 | /**
163 | * Allow user to access to some private function to customize message reception.
164 | * This is used if you specified wrapOptions on the client side and have to manually bind message to callback.
165 | */
166 | self.exposePrivateFunction = _getOption("exposePrivateFunction", false);
167 |
168 | var _getTopicName = function (topicExtension) {
169 | if (self.wrapData) {
170 | return self.topicName;
171 | }
172 |
173 | return self.topicName + topicExtension;
174 | };
175 |
176 | var _wrapData = function (data, action) {
177 | if(!_isWrapDataWellConfigured() || !self.wrapData) {
178 | return data;
179 | }
180 | var dataWrapped = {};
181 | if(self.wrapData.additionalData) {
182 | Object.assign(dataWrapped, self.wrapData.additionalData);
183 | }
184 |
185 | var actionKey = self.wrapData.wrapKey && typeof self.wrapData.wrapKey.action === "string" ? self.wrapData.wrapKey.action : "action";
186 | var messageKey = self.wrapData.wrapKey && typeof self.wrapData.wrapKey.message === "string" ? self.wrapData.wrapKey.message : "message";
187 |
188 | dataWrapped[actionKey] = action;
189 | dataWrapped[messageKey] = data;
190 | return dataWrapped;
191 | };
192 |
193 | /**
194 | * Private method to dispatch a custom event on the instance.
195 | * @param {string} eventName Name for which listeners can listen.
196 | * @param {object} properties An object literal with additional properties
197 | * to be attached to the event object.
198 | * @return {boolean} false if any callback returned false; true otherwise
199 | */
200 | var _dispatch = function (eventName, properties) {
201 | var evnt = document.createEvent("Event");
202 | evnt.initEvent(eventName, false, false);
203 | for (var prop in properties) {
204 | if (properties.hasOwnProperty(prop)) {
205 | evnt[prop] = properties[prop];
206 | }
207 | }
208 | return self.dispatchEvent(evnt);
209 | };
210 |
211 | /**
212 | * Private method to bind an event listener. Useful to ensure that all
213 | * events have been unbound. Inspired by Backbone.js.
214 | */
215 | var _listenedReferences = [];
216 | var _listenTo = function (object, eventName, callback, bubble) {
217 | object.addEventListener(eventName, callback, bubble);
218 | _listenedReferences.push(arguments);
219 | };
220 | var _stopListeningTo = function (object, eventName, callback, bubble) {
221 | if (object.removeEventListener) {
222 | object.removeEventListener(eventName, callback, bubble);
223 | }
224 | };
225 | var _stopListening = function () {
226 | for (var i = _listenedReferences.length - 1; i >= 0; i--) {
227 | _stopListeningTo.apply(this, _listenedReferences[i]);
228 | }
229 | _listenedReferences = [];
230 | };
231 |
232 | /**
233 | * Private closure for the _load function.
234 | * @param {File} file A W3C File object
235 | * @return {void}
236 | */
237 | var _loadOne = function (file) {
238 | // First check for file size
239 | if (self.maxFileSize !== null && file.size > self.maxFileSize) {
240 | _dispatch("error", {
241 | file: file,
242 | message: "Attempt by client to upload file exceeding the maximum file size",
243 | code: 1
244 | });
245 | return;
246 | }
247 |
248 | // Dispatch an event to listeners and stop now if they don't want
249 | // this file to be uploaded.
250 | var evntResult = _dispatch("start", {
251 | file: file
252 | });
253 | if (!evntResult) return;
254 |
255 | // Scope variables
256 | var reader = new FileReader(),
257 | id = window.siofu_global.downloads++,
258 | uploadComplete = false,
259 | useText = self.useText,
260 | offset = 0,
261 | newName;
262 | if (reader._realReader) reader = reader._realReader; // Support Android Crosswalk
263 | uploadedFiles[id] = file;
264 |
265 | // An object for the outside to use to communicate with us
266 | var communicator = { id: id };
267 |
268 | // Calculate chunk size
269 | var chunkSize = self.chunkSize;
270 | if (chunkSize >= file.size || chunkSize <= 0) chunkSize = file.size;
271 |
272 | // Private function to handle transmission of file data
273 | var transmitPart = function (start, end, content) {
274 | var isBase64 = false;
275 | if (!useText) {
276 | try {
277 | var uintArr = new Uint8Array(content);
278 |
279 | // Support the transmission of serialized ArrayBuffers
280 | // for experimental purposes, but default to encoding the
281 | // transmission in Base 64.
282 | if (self.serializedOctets) {
283 | content = uintArr;
284 | }
285 | else if (self.useBuffer) {
286 | content = uintArr.buffer;
287 | }
288 | else {
289 | isBase64 = true;
290 | content = _uint8ArrayToBase64(uintArr);
291 | }
292 | }
293 | catch (error) {
294 | socket.emit(_getTopicName("_done"), _wrapData({
295 | id: id,
296 | interrupt: true
297 | }, "done"));
298 | return;
299 | }
300 | }
301 |
302 | // TODO override the send data
303 | socket.emit(_getTopicName("_progress"), _wrapData({
304 | id: id,
305 | size: file.size,
306 | start: start,
307 | end: end,
308 | content: content,
309 | base64: isBase64
310 | }, "progress"));
311 | };
312 |
313 | // Callback when tranmission is complete.
314 | var transmitDone = function () {
315 | socket.emit(_getTopicName("_done"), _wrapData({
316 | id: id
317 | }, "done"));
318 | };
319 |
320 | // Load a "chunk" of the file from offset to offset+chunkSize.
321 | //
322 | // Note that FileReader has its own "progress" event. However,
323 | // it has not proven to be reliable enough for production. See
324 | // Stack Overflow question #16713386.
325 | //
326 | // To compensate, we will manually load the file in chunks of a
327 | // size specified by the user in the uploader.chunkSize property.
328 | var processChunk = function () {
329 | // Abort if we are told to do so.
330 | if (communicator.abort) return;
331 |
332 | var chunk = file.slice(offset, Math.min(offset+chunkSize, file.size));
333 | if (useText) {
334 | reader.readAsText(chunk);
335 | }
336 | else {
337 | reader.readAsArrayBuffer(chunk);
338 | }
339 | };
340 |
341 | // Callback for when the reader has completed a load event.
342 | var loadCb = function (event) {
343 | // Abort if we are told to do so.
344 | if (communicator.abort) return;
345 |
346 | // Transmit the newly loaded data to the server and emit a client event
347 | var bytesLoaded = Math.min(offset+chunkSize, file.size);
348 | transmitPart(offset, bytesLoaded, event.target.result);
349 | _dispatch("progress", {
350 | file: file,
351 | bytesLoaded: bytesLoaded,
352 | name: newName
353 | });
354 |
355 | // Get ready to send the next chunk
356 | offset += chunkSize;
357 | if (offset >= file.size) {
358 | // All done!
359 | transmitDone();
360 | _dispatch("load", {
361 | file: file,
362 | reader: reader,
363 | name: newName
364 | });
365 | uploadComplete = true;
366 | }
367 | };
368 | _listenTo(reader, "load", loadCb);
369 |
370 | // Listen for an "error" event. Stop the transmission if one is received.
371 | _listenTo(reader, "error", function () {
372 | socket.emit(_getTopicName("_done"), _wrapData({
373 | id: id,
374 | interrupt: true
375 | }, "done"));
376 | _stopListeningTo(reader, "load", loadCb);
377 | });
378 |
379 | // Do the same for the "abort" event.
380 | _listenTo(reader, "abort", function () {
381 | socket.emit(_getTopicName("_done"), _wrapData({
382 | id: id,
383 | interrupt: true
384 | }, "done"));
385 | _stopListeningTo(reader, "load", loadCb);
386 | });
387 |
388 | // Transmit the "start" message to the server.
389 | socket.emit(_getTopicName("_start"), _wrapData({
390 | name: file.name,
391 | mtime: file.lastModified,
392 | meta: file.meta,
393 | size: file.size,
394 | encoding: useText ? "text" : "octet",
395 | id: id
396 | }, "start"));
397 |
398 | // To avoid a race condition, we don't want to start transmitting to the
399 | // server until the server says it is ready.
400 | var readyCallback = function (_newName) {
401 | newName = _newName;
402 | processChunk();
403 | };
404 | var chunkCallback = function(){
405 | if ( !uploadComplete )
406 | processChunk();
407 | };
408 | readyCallbacks[id] = readyCallback;
409 | chunkCallbacks[id] = chunkCallback;
410 |
411 | return communicator;
412 | };
413 |
414 | /**
415 | * Private function to load the file into memory using the HTML5 FileReader object
416 | * and then transmit that file through Socket.IO.
417 | *
418 | * @param {FileList} files An array of files
419 | * @return {void}
420 | */
421 | var _load = function (files) {
422 | // Iterate through the array of files.
423 | for (var i = 0; i < files.length; i++) {
424 | // Evaluate each file in a closure, because we will need a new
425 | // instance of FileReader for each file.
426 | var communicator = _loadOne(files[i]);
427 | communicators[communicator.id] = communicator;
428 | }
429 | };
430 |
431 | /**
432 | * Private function to fetch an HTMLInputElement instance that can be used
433 | * during the file selection process.
434 | * @return {void}
435 | */
436 | var _getInputElement = function () {
437 | var inpt = document.getElementById(self.fileInputElementId);
438 | if (!inpt) {
439 | inpt = document.createElement("input");
440 | inpt.setAttribute("type", "file");
441 | inpt.setAttribute("id", self.fileInputElementId);
442 | inpt.style.display = "none";
443 | document.body.appendChild(inpt);
444 | }
445 | return inpt;
446 | };
447 |
448 | /**
449 | * Private function to remove an HTMLInputElement created by this instance
450 | * of SIOFU.
451 | *
452 | * @return {void}
453 | */
454 | var _removeInputElement = function () {
455 | var inpt = document.getElementById(self.fileInputElementId);
456 | if (inpt) {
457 | inpt.parentNode.removeChild(inpt);
458 | }
459 | };
460 |
461 | var _baseFileSelectCallback = function (files) {
462 | if (files.length === 0) return;
463 |
464 | // Ensure existence of meta property on each file
465 | for (var i = 0; i < files.length; i++) {
466 | if(!files[i].meta) files[i].meta = {};
467 | }
468 |
469 | // Dispatch the "choose" event
470 | var evntResult = _dispatch("choose", {
471 | files: files
472 | });
473 |
474 | // If the callback didn't return false, continue with the upload
475 | if (evntResult) {
476 | _load(files);
477 | }
478 | };
479 |
480 | /**
481 | * Private function that serves as a callback on file input.
482 | * @param {Event} event The file input change event
483 | * @return {void}
484 | */
485 | var _fileSelectCallback = function (event) {
486 | var files = event.target.files || event.dataTransfer.files;
487 | event.preventDefault();
488 | _baseFileSelectCallback(files);
489 |
490 | if (self.resetFileInputs) {
491 | try {
492 | event.target.value = ""; //for IE11, latest Chrome/Firefox/Opera...
493 | } catch(err) {
494 | // ignore
495 | }
496 | if (event.target.value) { //for IE5 ~ IE10
497 | var form = document.createElement("form"),
498 | parentNode = event.target.parentNode, ref = event.target.nextSibling;
499 | form.appendChild(event.target);
500 | form.reset();
501 | parentNode.insertBefore(event.target, ref);
502 | }
503 | }
504 | };
505 |
506 |
507 | /**
508 | * Submit files at arbitrary time
509 | * @param {FileList} files Files received form the input element.
510 | * @return {void}
511 | */
512 | this.submitFiles = function (files) {
513 | if (files) {
514 | _baseFileSelectCallback(files);
515 | }
516 | };
517 |
518 | /**
519 | * Use a submitButton to upload files from the field given
520 | * @param {HTMLInputElement} submitButton the button that the user has to
521 | * click to start the upload
522 | * @param {HTMLInputElement} input the field with the data to upload
523 | *
524 | * @return {void}
525 | */
526 | this.listenOnSubmit = function (submitButton, input) {
527 | if (!input.files) return;
528 | _listenTo(submitButton, "click", function () {
529 | _baseFileSelectCallback(input.files);
530 | }, false);
531 | };
532 |
533 | /**
534 | * Use a submitButton to upload files from the field given
535 | * @param {HTMLInputElement} submitButton the button that the user has to
536 | * click to start the upload
537 | * @param {Array} array an array of fields with the files to upload
538 | *
539 | * @return {void}
540 | */
541 | this.listenOnArraySubmit = function (submitButton, array) {
542 | for (var index in array) {
543 | this.listenOnSubmit(submitButton, array[index]);
544 | }
545 | };
546 |
547 | /**
548 | * Use a file input to activate this instance of the file uploader.
549 | * @param {HTMLInputElement} inpt The input element (e.g., as returned by
550 | * document.getElementById("yourId"))
551 | * @return {void}
552 | */
553 | this.listenOnInput = function (inpt) {
554 | if (!inpt.files) return;
555 | _listenTo(inpt, "change", _fileSelectCallback, false);
556 | };
557 |
558 | /**
559 | * Accept files dropped on an element and upload them using this instance
560 | * of the file uploader.
561 | * @param {HTMLELement} div Any HTML element. When the user drags a file
562 | * or files onto this element, those files will
563 | * be processed by the instance.
564 | * @return {void}
565 | */
566 | this.listenOnDrop = function (div) {
567 | // We need to preventDefault on the dragover event in order for the
568 | // drag-and-drop operation to work.
569 | _listenTo(div, "dragover", function (event) {
570 | event.preventDefault();
571 | }, false);
572 |
573 | _listenTo(div, "drop", _fileSelectCallback);
574 | };
575 |
576 | /**
577 | * Display a dialog box for the user to select a file. The file will then
578 | * be uploaded using this instance of SocketIOFileUpload.
579 | *
580 | * This method works in all current browsers except Firefox, though Opera
581 | * requires that the input element be visible.
582 | *
583 | * @return {void}
584 | */
585 | this.prompt = function () {
586 | var inpt = _getInputElement();
587 |
588 | // Listen for the "change" event on the file input element.
589 | _listenTo(inpt, "change", _fileSelectCallback, false);
590 |
591 | // Fire a click event on the input element. Firefox does not allow
592 | // programatic clicks on input elements, but the other browsers do.
593 | // Note that Opera requires that the element be visible when "clicked".
594 | var evnt = document.createEvent("MouseEvents");
595 | evnt.initMouseEvent("click", true, true, window,
596 | 0, 0, 0, 0, 0, false, false, false, false, 0, null);
597 | inpt.dispatchEvent(evnt);
598 | };
599 |
600 | /**
601 | * Destroy an instance of Socket.IO file upload (i.e., unbind events and
602 | * relieve memory).
603 | *
604 | * IMPORTANT: To finish the memory relief process, set all external
605 | * references to this instance of SIOFU (including the reference used to
606 | * call this destroy function) to null.
607 | *
608 | * @return {void}
609 | */
610 | this.destroy = function () {
611 | _stopListening();
612 | _removeInputElement();
613 | for (var id in communicators) {
614 | if (communicators.hasOwnProperty(id)) {
615 | communicators[id].abort = true;
616 | }
617 | }
618 | callbacks = null, uploadedFiles = null, readyCallbacks = null, communicators = null;
619 | };
620 |
621 | /**
622 | * Registers an event listener. If the callback function returns false,
623 | * the file uploader will stop uploading the current file.
624 | * @param {string} eventName Type of event for which to listen.
625 | * @param {Function} callback Listener function. Will be passed the
626 | * event as an argument when the event occurs.
627 | * @return {void}
628 | */
629 | this.addEventListener = function (eventName, callback) {
630 | if (!callbacks[eventName]) callbacks[eventName] = [];
631 | callbacks[eventName].push(callback);
632 | };
633 |
634 | /**
635 | * Removes an event listener.
636 | * @param {string} eventName Type of event.
637 | * @param {Function} callback Listener function to remove.
638 | * @return {boolean} true if callback removed; false otherwise
639 | */
640 | this.removeEventListener = function (eventName, callback) {
641 | if (!callbacks[eventName]) return false;
642 | for (var i = 0; i < callbacks[eventName].length; i++) {
643 | if (callbacks[eventName][i] === callback) {
644 | callbacks[eventName].splice(i, 1);
645 | return true;
646 | }
647 | }
648 | return false;
649 | };
650 |
651 | /**
652 | * Dispatches an event into this instance's event model.
653 | * @param {Event} evnt The event to dispatch.
654 | * @return {boolean} false if any callback returned false; true otherwise
655 | */
656 | this.dispatchEvent = function (evnt) {
657 | var eventCallbacks = callbacks[evnt.type];
658 | if (!eventCallbacks) return true;
659 | var retVal = true;
660 | for (var i = 0; i < eventCallbacks.length; i++) {
661 | var callbackResult = eventCallbacks[i](evnt);
662 | if (callbackResult === false) {
663 | retVal = false;
664 | }
665 | }
666 | return retVal;
667 | };
668 |
669 | // OTHER LIBRARIES
670 | /*
671 | * base64-arraybuffer
672 | * https://github.com/niklasvh/base64-arraybuffer
673 | *
674 | * Copyright (c) 2012 Niklas von Hertzen
675 | * Licensed under the MIT license.
676 | *
677 | * Adapted for SocketIOFileUpload.
678 | */
679 | var _uint8ArrayToBase64 = function (bytes) {
680 | var i, len = bytes.buffer.byteLength, base64 = "",
681 | chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
682 |
683 | for (i = 0; i < len; i += 3) {
684 | base64 += chars[bytes[i] >> 2];
685 | base64 += chars[((bytes[i] & 3) << 4) | (bytes[i + 1] >> 4)];
686 | base64 += chars[((bytes[i + 1] & 15) << 2) | (bytes[i + 2] >> 6)];
687 | base64 += chars[bytes[i + 2] & 63];
688 | }
689 |
690 | if ((len % 3) === 2) {
691 | base64 = base64.substring(0, base64.length - 1) + "=";
692 | }
693 | else if (len % 3 === 1) {
694 | base64 = base64.substring(0, base64.length - 2) + "==";
695 | }
696 |
697 | return base64;
698 | };
699 | // END OTHER LIBRARIES
700 | var _chunckCallback = function(data) {
701 | if ( chunkCallbacks[data.id] )
702 | chunkCallbacks[data.id]();
703 | };
704 |
705 | var _readyCallback = function (data) {
706 | if (readyCallbacks[data.id])
707 | readyCallbacks[data.id](data.name);
708 | };
709 |
710 | var _completCallback = function (data) {
711 | if (uploadedFiles[data.id]) {
712 | _dispatch("complete", {
713 | file: uploadedFiles[data.id],
714 | detail: data.detail,
715 | success: data.success
716 | });
717 | }
718 | };
719 |
720 | var _errorCallback = function (data) {
721 | if ( uploadedFiles[data.id] ) {
722 | _dispatch("error", {
723 | file: uploadedFiles[data.id],
724 | message: data.message,
725 | code: 0
726 | });
727 | if (communicators) communicators[data.id].abort = true;
728 | }
729 | };
730 |
731 | // CONSTRUCTOR: Listen to the "complete", "ready", and "error" messages
732 | // on the socket.
733 | if (_isWrapDataWellConfigured() && self.wrapData) {
734 | var mapActionToCallback = {
735 | chunk: _chunckCallback,
736 | ready: _readyCallback,
737 | complete: _completCallback,
738 | error: _errorCallback
739 | };
740 |
741 | _listenTo(socket, _getTopicName(), function (message) {
742 | if (typeof message !== "object") {
743 | console.log("SocketIOFileUploadClient Error: You choose to wrap your data so the message from the server need to be an object"); // eslint-disable-line no-console
744 | return;
745 | }
746 | var actionKey = self.wrapData.unwrapKey && typeof self.wrapData.unwrapKey.action === "string" ? self.wrapData.unwrapKey.action : "action";
747 | var messageKey = self.wrapData.unwrapKey && typeof self.wrapData.unwrapKey.message === "string" ? self.wrapData.unwrapKey.message : "message";
748 |
749 | var action = message[actionKey];
750 | var data = message[messageKey];
751 | if (!action || !data || !mapActionToCallback[action]) {
752 | console.log("SocketIOFileUploadClient Error: You choose to wrap your data but the message from the server is wrong configured. Check the message and your wrapData option"); // eslint-disable-line no-console
753 | return;
754 | }
755 | mapActionToCallback[action](data);
756 | });
757 | } else {
758 | _listenTo(socket, _getTopicName("_chunk"), _chunckCallback);
759 | _listenTo(socket, _getTopicName("_ready"), _readyCallback);
760 | _listenTo(socket, _getTopicName("_complete"), _completCallback);
761 | _listenTo(socket, _getTopicName("_error"), _errorCallback);
762 | }
763 |
764 | if (this.exposePrivateFunction) {
765 | this.chunckCallback = _chunckCallback;
766 | this.readyCallback = _readyCallback;
767 | this.completCallback = _completCallback;
768 | this.errorCallback = _errorCallback;
769 | }
770 | };
771 | }));
772 |
--------------------------------------------------------------------------------
/client.min.js:
--------------------------------------------------------------------------------
1 | /* Socket IO File Upload Client-Side Library
2 | * Copyright (C) 2015 Shane Carr and others
3 | * Released under the X11 License
4 | * For more information, visit: https://github.com/sffc/socketio-file-upload
5 | */
6 | var $jscomp=$jscomp||{};$jscomp.scope={};$jscomp.owns=function(c,f){return Object.prototype.hasOwnProperty.call(c,f)};$jscomp.assign="function"==typeof Object.assign?Object.assign:function(c,f){for(var b=1;bb.maxFileSize)v("error",{file:a,message:"Attempt by client to upload file exceeding the maximum file size",
13 | code:1});else if(v("start",{file:a})){var g=new FileReader,d=window.siofu_global.downloads++,f=!1,h=b.useText,l=0,u;g._realReader&&(g=g._realReader);e[d]=a;var w={id:d},y=b.chunkSize;if(y>=a.size||0>=y)y=a.size;var p=function(){if(!w.abort){var b=a.slice(l,Math.min(l+y,a.size));h?g.readAsText(b):g.readAsArrayBuffer(b)}},q=function(e){if(!w.abort){var F=Math.min(l+y,a.size);a:{var k=l;e=e.target.result;var q=!1;if(!h)try{var r=new Uint8Array(e);if(b.serializedOctets)e=r;else if(b.useBuffer)e=r.buffer;
14 | else{q=!0;var t,p=r.buffer.byteLength,n="";for(t=0;t