diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..dc6cb8bb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + + +[*] + +# Change these settings to your own preference +indent_style = space +indent_size = 2 + +# We recommend you to keep these unchanged +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.gitignore b/.gitignore index 88c10166..785f8f55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,22 @@ *~ samples/Node.js/node_modules/ + +# Eclipse +.classpath +.project +.settings/ + +# Intellij +.idea/ +*.iml +*.iws + +# Mac +.DS_Store + +# Maven +log/ +target/ + +# Gradle +.gradle \ No newline at end of file diff --git a/MIT-LICENSE b/MIT-LICENSE index 9d1da89a..05e40704 100644 --- a/MIT-LICENSE +++ b/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2011, 23, http://www.23developer.com +Copyright (c) 2011, 23, https://www.twentythree.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index 7c3f107c..746b5486 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ ## What is Resumable.js -Resumable.js is a JavaScript library providing multiple simultaneous, stable and resumable uploads via the HTML5 File API. +Resumable.js is a JavaScript library providing multiple simultaneous, stable and resumable uploads via the [`HTML5 File API`](http://www.w3.org/TR/FileAPI/). The library is designed to introduce fault-tolerance into the upload of large files through HTTP. This is done by splitting each file into small chunks. Then, whenever the upload of a chunk fails, uploading is retried until the procedure completes. This allows uploads to automatically resume uploading after a network connection is lost either locally or to the server. Additionally, it allows for users to pause, resume and even recover uploads without losing state because only the currently uploading chunks will be aborted, not the entire upload. -Resumable.js does not have any external dependencies other than the `HTML5 File API`. This is relied on for the ability to chunk files into smaller pieces. Currently, this means that support is limited to Firefox 4+, Chrome 11+ and Safari 6+. +Resumable.js does not have any external dependencies other than the `HTML5 File API`. This is relied on for the ability to chunk files into smaller pieces. Currently, this means that support is widely available in to Firefox 4+, Chrome 11+, Safari 6+ and Internet Explorer 10+. Samples and examples are available in the `samples/` folder. Please push your own as Markdown to help document the project. @@ -13,39 +13,47 @@ Samples and examples are available in the `samples/` folder. Please push your ow A new `Resumable` object is created with information of what and where to post: - var r = new Resumable({ - target:'/api/photo/redeem-upload-token', - query:{upload_token:'my_token'} - }); - // Resumable.js isn't supported, fall back on a different method - if(!r.support) location.href = '/some-old-crappy-uploader'; - -To allow files to be either selected and drag-dropped, you'll assign drop target and a DOM item to be clicked for browsing: +```js +var r = new Resumable({ + target:'/api/photo/redeem-upload-token', + query:{upload_token:'my_token'} +}); +// Resumable.js isn't supported, fall back on a different method +if(!r.support) location.href = '/some-old-crappy-uploader'; +``` - r.assignBrowse(document.getElementById('browseButton')); - r.assignDrop(document.getElementById('dropTarget')); +To allow files to be selected and drag-dropped, you need to assign a drop target and a DOM item to be clicked for browsing: + +```js +r.assignBrowse(document.getElementById('browseButton')); +r.assignDrop(document.getElementById('dropTarget')); +``` + +It is recommended to use an HTML span for the browse button. Using an actual button does not work reliably across all browsers, because Resumable.js creates the file input as a child of this control, and this may be invalid in the case of an HTML button. After this, interaction with Resumable.js is done by listening to events: - r.on('fileAdded', function(file, event){ - ... - }); - r.on('fileSuccess', function(file,message){ - ... - }); - r.on('fileError', function(file, message){ - ... - }); +```js +r.on('fileAdded', function(file, event){ + ... + }); +r.on('fileSuccess', function(file, message){ + ... + }); +r.on('fileError', function(file, message){ + ... + }); +``` ## How do I set it up with my server? -Most of the magic for Resumable.js happens in the user's browser, but files still need to be reassembled from chunks on the server side. This should be a fairly simple task and can be achieved in any web framework or language, which is able to receive file uploads. +Most of the magic for Resumable.js happens in the user's browser, but files still need to be reassembled from chunks on the server side. This should be a fairly simple task, which and can be achieved using any web framework or language that is capable of handling file uploads. To handle the state of upload chunks, a number of extra parameters are sent along with all requests: * `resumableChunkNumber`: The index of the chunk in the current upload. First chunk is `1` (no base-0 counting here). -* `resumableTotalChunks`: The total number of chunks. -* `resumableChunkSize`: The general chunk size. Using this value and `resumableTotalSize` you can calculate the total number of chunks. Please note that the size of the data received in the HTTP might be lower than `resumableChunkSize` of this for the last chunk for a file. +* `resumableTotalChunks`: The total number of chunks. +* `resumableChunkSize`: The general chunk size. Using this value and `resumableTotalSize` you can calculate the total number of chunks. Please note that the size of the data received in the HTTP might be higher than `resumableChunkSize` for the last chunk for a file. * `resumableTotalSize`: The total file size. * `resumableIdentifier`: A unique identifier for the file contained in the request. * `resumableFilename`: The original file name (since a bug in Firefox results in the file name not being transmitted in chunk multipart posts). @@ -53,10 +61,10 @@ To handle the state of upload chunks, a number of extra parameters are sent alon You should allow for the same chunk to be uploaded more than once; this isn't standard behaviour, but on an unstable network environment it could happen, and this case is exactly what Resumable.js is designed for. -For every request, you can confirm reception in HTTP status codes (can be change through the `permanentErrors` option): +For every request, you can confirm reception in HTTP status codes (can be changed through the `permanentErrors` option): -* `200`: The chunk was accepted and correct. No need to re-upload. -* `415`. `500`, `501`: The file for which the chunk was uploaded is not supported, cancel the entire upload. +* `200`, `201`: The chunk was accepted and correct. No need to re-upload. +* `400`, `404`, `409`, `415`, `500`, `501`: The file for which the chunk was uploaded is not supported, cancel the entire upload. * _Anything else_: Something went wrong, but try reuploading the file. ## Handling GET (or `test()` requests) @@ -64,7 +72,7 @@ For every request, you can confirm reception in HTTP status codes (can be change Enabling the `testChunks` option will allow uploads to be resumed after browser restarts and even across browsers (in theory you could even run the same file upload across multiple tabs or different browsers). The `POST` data requests listed are required to use Resumable.js to receive data, but you can extend support by implementing a corresponding `GET` request with the same parameters: * If this request returns a `200` HTTP code, the chunks is assumed to have been completed. -* If the request returns anything else, the chunk will be uploaded in the standard fashion. +* If the request returns anything else, the chunk will be uploaded in the standard fashion. (It is recommended to return *204 No Content* in these cases if possible to [avoid unwarranted notices in browser consoles](https://github.com/23/resumable.js/issues/160).) After this is done and `testChunks` enabled, an upload can quickly catch up even after a browser restart by simply verifying already uploaded chunks that do not need to be uploaded again. @@ -73,24 +81,43 @@ After this is done and `testChunks` enabled, an upload can quickly catch up even ### Resumable #### Configuration -The object is loaded with a configuation hash: +The object is loaded with a configuration hash: - var r = new Resumable({opt1:'val', ...}); - +```js +var r = new Resumable({opt1:'val', ...}); +``` + +All POST parameters can be omitted by setting them to a falsy value +(e.g. `null`, `false` or empty string). Available configuration options are: -* `target` The target URL for the multipart POST request (Default: `/`) +* `target` The target URL for the multipart POST request. This can be a `string` or a `function` that allows you you to construct and return a value, based on supplied `params`. (Default: `/`) +* `testTarget` The target URL for the GET request to the server for each chunk to see if it already exists. This can be a `string` or a `function` that allows you you to construct and return a value, based on supplied `params`. (Default: `null`) * `chunkSize` The size in bytes of each uploaded chunk of data. The last uploaded chunk will be at least this size and up to two the size, see [Issue #51](https://github.com/23/resumable.js/issues/51) for details and reasons. (Default: `1*1024*1024`) * `forceChunkSize` Force all chunks to be less or equal than chunkSize. Otherwise, the last chunk will be greater than or equal to `chunkSize`. (Default: `false`) * `simultaneousUploads` Number of simultaneous uploads (Default: `3`) -* `fileParameterName` The name of the multipart POST parameter to use for the file chunk (Default: `file`) -* `query` Extra parameters to include in the multipart POST with data. This can be an object or a function. If a function, it will be passed a ResumableFile and a ResumableChunk object (Default: `{}`) -* `headers` Extra headers to include in the multipart POST with data (Default: `{}`) -* `method` Method to use when POSTing chunks to the server (`multipart` or `octet`) (Default: `multipart`) +* `fileParameterName` The name of the multipart request parameter to use for the file chunk (Default: `file`) +* `chunkNumberParameterName` The name of the chunk index (base-1) in the current upload POST parameter to use for the file chunk (Default: `resumableChunkNumber`) +* `totalChunksParameterName` The name of the total number of chunks POST parameter to use for the file chunk (Default: `resumableTotalChunks`) +* `chunkSizeParameterName` The name of the general chunk size POST parameter to use for the file chunk (Default: `resumableChunkSize`) +* `totalSizeParameterName` The name of the total file size number POST parameter to use for the file chunk (Default: `resumableTotalSize`) +* `identifierParameterName` The name of the unique identifier POST parameter to use for the file chunk (Default: `resumableIdentifier`) +* `fileNameParameterName` The name of the original file name POST parameter to use for the file chunk (Default: `resumableFilename`) +* `relativePathParameterName` The name of the file's relative path POST parameter to use for the file chunk (Default: `resumableRelativePath`) +* `currentChunkSizeParameterName` The name of the current chunk size POST parameter to use for the file chunk (Default: `resumableCurrentChunkSize`) +* `typeParameterName` The name of the file type POST parameter to use for the file chunk (Default: `resumableType`) +* `query` Extra parameters to include in the multipart request with data. This can be an object or a function. If a function, it will be passed a ResumableFile and a ResumableChunk object (Default: `{}`) +* `testMethod` Method for chunk test request. (Default: `'GET'`) +* `uploadMethod` HTTP method to use when sending chunks to the server (`POST`, `PUT`, `PATCH`) (Default: `POST`) +* `parameterNamespace` Extra prefix added before the name of each parameter included in the multipart POST or in the test GET. (Default: `''`) +* `headers` Extra headers to include in the multipart POST with data. This can be an `object` or a `function` that allows you to construct and return a value, based on supplied `file` (Default: `{}`) +* `method` Method to use when sending chunks to the server (`multipart` or `octet`) (Default: `multipart`) * `prioritizeFirstAndLastChunk` Prioritize first and last chunks of all files. This can be handy if you can determine if a file is valid for your service from only the first or last chunk. For example, photo or video meta data is usually located in the first part of a file, making it easy to test support from only the first chunk. (Default: `false`) * `testChunks` Make a GET request to the server for each chunks to see if it already exists. If implemented on the server-side, this will allow for upload resumes even after a browser crash or even a computer restart. (Default: `true`) * `preprocess` Optional function to process each chunk before testing & sending. Function is passed the chunk as parameter, and should call the `preprocessFinished` method on the chunk when finished. (Default: `null`) -* `generateUniqueIdentifier` Override the function that generates unique identifiers for each file. (Default: `null`) +* `preprocessFile` Optional function to process each file before testing & sending the corresponding chunks. Function is passed the file as parameter, and should call the `preprocessFinished` method on the file when finished. (Default: `null`) +* `generateUniqueIdentifier(file, event)` Override the function that generates unique identifiers for each file. May return [Promise](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Promise)-like object with `then()` method for asynchronous id generation. Parameters are the ES `File` object and the event that led to +adding the file. (Default: `null`) * `maxFiles` Indicates how many files can be uploaded in a single session. Valid values are any positive integer and `undefined` for no limit. (Default: `undefined`) * `maxFilesErrorCallback(files, errorCount)` A function which displays the *please upload n file(s) at a time* message. (Default: displays an alert box with the message *Please n one file(s) at a time.*) * `minFileSize` The minimum allowed file size. (Default: `undefined`) @@ -99,6 +126,13 @@ Available configuration options are: * `maxFileSizeErrorCallback(file, errorCount)` A function which displays an error a selected file is larger than allowed. (Default: displays an alert for every bad file.) * `fileType` The file types allowed to upload. An empty array allow any file type. (Default: `[]`) * `fileTypeErrorCallback(file, errorCount)` A function which displays an error a selected file has type not allowed. (Default: displays an alert for every bad file.) +* `maxChunkRetries` The maximum number of retries for a chunk before the upload is failed. Valid values are any positive integer and `undefined` for no limit. (Default: `undefined`) +* `permanentErrors` List of HTTP status codes that define if the chunk upload was a permanent error and should not retry the upload. (Default: `[400, 404, 409, 415, 500, 501]`) +* `chunkRetryInterval` The number of milliseconds to wait before retrying a chunk on a non-permanent error. Valid values are any positive integer and `undefined` for immediate retry. (Default: `undefined`) +* `withCredentials` Standard CORS requests do not send or set any cookies by default. In order to include cookies as part of the request, you need to set the `withCredentials` property to true. (Default: `false`) +* `xhrTimeout` The timeout in milliseconds for each request (Default: `0`) +* `setChunkTypeFromFile` Set chunk content-type from original file.type. (Default: `false`, if `false` default Content-Type: `application/octet-stream`) +* `dragOverClass` The class name to add on drag over an assigned drop zone. (Default: `dragover`) #### Properties @@ -108,7 +142,7 @@ Available configuration options are: #### Methods -* `.assignBrowse(domNodes, isDirectory)` Assign a browse action to one or more DOM nodes. Pass in `true` to allow directories to be selected (Chrome only). +* `.assignBrowse(domNodes, isDirectory)` Assign a browse action to one or more DOM nodes. Pass in `true` to allow directories to be selected (Chrome only). See the note above about using an HTML span instead of an actual button. * `.assignDrop(domNodes)` Assign one or more DOM nodes as a drop target. * `.on(event, callback)` Listen for event from Resumable.js (see below) * `.upload()` Start or resume uploading. @@ -117,24 +151,29 @@ Available configuration options are: * `.progress()` Returns a float between 0 and 1 indicating the current upload progress of all files. * `.isUploading()` Returns a boolean indicating whether or not the instance is currently uploading anything. * `.addFile(file)` Add a HTML5 File object to the list of files. +* `.addFiles(files)` Add an Array of HTML5 File objects to the list of files. * `.removeFile(file)` Cancel upload of a specific `ResumableFile` object on the list from the list. * `.getFromUniqueIdentifier(uniqueIdentifier)` Look up a `ResumableFile` object by its unique identifier. * `.getSize()` Returns the total size of the upload in bytes. #### Events -* `.fileSuccess(file)` A specific file was completed. -* `.fileProgress(file)` Uploading progressed for a specific file. +* `.fileSuccess(file, message)` A specific file was completed. `message` is the response body from the server. +* `.fileProgress(file, message)` Uploading progressed for a specific file. * `.fileAdded(file, event)` A new file was added. Optionally, you can use the browser `event` object from when the file was added. -* `.filesAdded(array)` New files were added. +* `.filesAdded(arrayAdded, arraySkipped)` New files were added (and maybe some have been skipped). * `.fileRetry(file)` Something went wrong during upload of a specific file, uploading is being retried. -* `.fileError(file, message)` An error occured during upload of a specific file. +* `.fileError(file, message)` An error occurred during upload of a specific file. * `.uploadStart()` Upload has been started on the Resumable object. * `.complete()` Uploading completed. * `.progress()` Uploading progress. -* `.error(message, file)` An error, including fileError, occured. +* `.error(message, file)` An error, including fileError, occurred. * `.pause()` Uploading was paused. +* `.beforeCancel()` Triggers before the items are cancelled allowing to do any processing on uploading files. * `.cancel()` Uploading was canceled. +* `.chunkingStart(file)` Started preparing file for upload +* `.chunkingProgress(file,ratio)` Show progress in file preparation +* `.chunkingComplete(file)` File is ready for upload * `.catchAll(event, ...)` Listen to all the events listed above with the same callback function. ### ResumableFile @@ -156,10 +195,11 @@ Available configuration options are: * `.retry()` Retry uploading the file. * `.bootstrap()` Rebuild the state of a `ResumableFile` object, including reassigning chunks and XMLHttpRequest instances. * `.isUploading()` Returns a boolean indicating whether file chunks is uploading. +* `.isComplete()` Returns a boolean indicating whether the file has completed uploading and received a server response. +* `.markChunksCompleted()` starts upload from the next chunk number while marking all previous chunks complete. Must be called before upload() method. ## Alternatives -This library is explicitly designed for modern browsers supporting advanced HTML5 file features, and the motivation has been to provide stable and resumable support for large files (allowing uploads of several GB files through HTTP in a predictable fashion). - -If your aim is just to support progress indications during upload/uploading multiple files at once, Resumable.js isn't for you. In those cases, [SWFUpload](http://swfupload.org/) and [Plupload](http://plupload.com/) provides the same features with wider browser support. +This library is explicitly designed for modern browsers supporting advanced HTML5 file features, and the motivation has been to provide stable and resumable support for large files (allowing uploads of several GB files through HTTP in a predictable fashion). +If your aim is just to support progress indications during upload/uploading multiple files at once, Resumable.js isn't for you. In those cases, something like [Plupload](http://plupload.com/) provides the same features with wider browser support. diff --git a/bower.json b/bower.json new file mode 100644 index 00000000..ea9f0879 --- /dev/null +++ b/bower.json @@ -0,0 +1,13 @@ +{ + "name": "resumable.js", + "main": "resumable.js", + "ignore": [ + ".gitignore", + "*.md" + ], + "keywords": [ + "HTML5 File API", + "Upload", + "Large files" + ] +} diff --git a/component.json b/component.json index 4e739855..a4d80bec 100644 --- a/component.json +++ b/component.json @@ -1,7 +1,7 @@ { "name": "resumable.js", "repo": "23/resumable.js", - "version": "1.0.0", + "version": "1.1.2", "main": "resumable.js", "scripts": ["resumable.js"] } diff --git a/package.json b/package.json new file mode 100644 index 00000000..051c9475 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "resumablejs", + "version": "1.1.2", + "description": "A JavaScript library for providing multiple simultaneous, stable, fault-tolerant and resumable/restartable uploads via the HTML5 File API.", + "main": "resumable.js", + "types": "./resumable.d.ts", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/23/resumable.js.git" + }, + "keywords": [ + "html5", + "file", + "upload" + ], + "author": "https://github.com/23/resumable.js/graphs/contributors", + "license": "MIT", + "bugs": { + "url": "https://github.com/23/resumable.js/issues" + }, + "homepage": "https://github.com/23/resumable.js#readme" +} diff --git a/resumable-tests.ts b/resumable-tests.ts new file mode 100644 index 00000000..6791fcf3 --- /dev/null +++ b/resumable-tests.ts @@ -0,0 +1,32 @@ +import Resumable = require('./index'); + +let resumable: Resumable = new Resumable({chunkSize: 123}); +let resumableNoOpts: Resumable = new Resumable(); + +resumable.addFile(new File([], 'test.tmp'), {}); +resumable.addFiles([new File([], 'test.tmp')], {}); +resumable.assignBrowse(document, true); +resumable.assignBrowse([document], true); +resumable.assignDrop(document); +resumable.assignDrop([document]); +resumable.cancel(); +let defaults: Object = resumable.defaults; +let events: any[] = resumable.events; +let files: any[] = resumable.files; +resumable.fire(); +let {} = resumable.getFromUniqueIdentifier('test'); +let {} = resumable.getOpt('test'); +let size:number = resumable.getSize(); +resumable.handleChangeEvent({}); +resumable.handleDropEvent({}); +let isUploading: boolean = resumable.isUploading(); +resumable.on('test', function() {}); +let opts: Object = resumable.opts; +resumable.pause(); +let progress:number = resumable.progress(); +resumable.removeFile('TODO'); +let support: boolean = resumable.support; +resumable.unAssignDrop({}); +resumable.upload(); +resumable.uploadNextChunk(); +let version:number = resumable.version; \ No newline at end of file diff --git a/resumable.d.ts b/resumable.d.ts new file mode 100644 index 00000000..8ada5eeb --- /dev/null +++ b/resumable.d.ts @@ -0,0 +1,374 @@ +// Type definitions for Resumable.js v1.0.2 +// Project: https://github.com/23/resumable.js +// Definitions by: Daniel McAssey +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +declare namespace Resumable { + interface ConfigurationHash { + /** + * The target URL for the multipart POST request. This can be a string or a function that allows you you to construct and return a value, based on supplied params. (Default: /) + **/ + target?: string; + /** + * The size in bytes of each uploaded chunk of data. The last uploaded chunk will be at least this size and up to two the size, see Issue #51 for details and reasons. (Default: 1*1024*1024) + **/ + chunkSize?: number; + /** + * Force all chunks to be less or equal than chunkSize. Otherwise, the last chunk will be greater than or equal to chunkSize. (Default: false) + **/ + forceChunkSize?: boolean; + /** + * Number of simultaneous uploads (Default: 3) + **/ + simultaneousUploads?: number; + /** + * The name of the multipart POST parameter to use for the file chunk (Default: file) + **/ + fileParameterName?: string; + /** + * The name of the chunk index (base-1) in the current upload POST parameter to use for the file chunk (Default: resumableChunkNumber) + */ + chunkNumberParameterName?: string; + /** + * The name of the total number of chunks POST parameter to use for the file chunk (Default: resumableTotalChunks) + */ + totalChunksParameterName?: string; + /** + * The name of the general chunk size POST parameter to use for the file chunk (Default: resumableChunkSize) + */ + chunkSizeParameterName?: string; + /** + * The name of the total file size number POST parameter to use for the file chunk (Default: resumableTotalSize) + */ + totalSizeParameterName?: string; + /** + * The name of the unique identifier POST parameter to use for the file chunk (Default: resumableIdentifier) + */ + identifierParameterName?: string; + /** + * The name of the original file name POST parameter to use for the file chunk (Default: resumableFilename) + */ + fileNameParameterName?: string; + /** + * The name of the file's relative path POST parameter to use for the file chunk (Default: resumableRelativePath) + */ + relativePathParameterName?: string; + /** + * The name of the current chunk size POST parameter to use for the file chunk (Default: resumableCurrentChunkSize) + */ + currentChunkSizeParameterName?: string; + /** + * The name of the file type POST parameter to use for the file chunk (Default: resumableType) + */ + typeParameterName?: string; + /** + * Extra parameters to include in the multipart POST with data. This can be an object or a function. If a function, it will be passed a ResumableFile and a ResumableChunk object (Default: {}) + **/ + query?: Object; + /** + * Method for chunk test request. (Default: 'GET') + **/ + testMethod?: string; + /** + * Method for chunk upload request. (Default: 'POST') + **/ + uploadMethod?: string; + /** + * Extra prefix added before the name of each parameter included in the multipart POST or in the test GET. (Default: '') + **/ + parameterNamespace?: string; + /** + * Extra headers to include in the multipart POST with data. This can be an object or a function that allows you to construct and return a value, based on supplied file (Default: {}) + **/ + headers?: Object | ((file: ResumableFile) => Object); + /** + * Method to use when POSTing chunks to the server (multipart or octet) (Default: multipart) + **/ + method?: string; + /** + * Prioritize first and last chunks of all files. This can be handy if you can determine if a file is valid for your service from only the first or last chunk. For example, photo or video meta data is usually located in the first part of a file, making it easy to test support from only the first chunk. (Default: false) + **/ + prioritizeFirstAndLastChunk?: boolean; + /** + * Make a GET request to the server for each chunks to see if it already exists. If implemented on the server-side, this will allow for upload resumes even after a browser crash or even a computer restart. (Default: true) + **/ + testChunks?: boolean; + /** + * Optional function to process each chunk before testing & sending. Function is passed the chunk as parameter, and should call the preprocessFinished method on the chunk when finished. (Default: null) + **/ + preprocess?: (chunk: ResumableChunk) => ResumableChunk; + /** + * Override the function that generates unique identifiers for each file. (Default: null) + **/ + generateUniqueIdentifier?: () => string; + /** + * Indicates how many files can be uploaded in a single session. Valid values are any positive integer and undefined for no limit. (Default: undefined) + **/ + maxFiles?: number; + /** + * A function which displays the please upload n file(s) at a time message. (Default: displays an alert box with the message Please n one file(s) at a time.) + **/ + maxFilesErrorCallback?: (files: ResumableFile, errorCount: number) => void; + /** + * The minimum allowed file size. (Default: undefined) + **/ + minFileSize?: boolean; + /** + * A function which displays an error a selected file is smaller than allowed. (Default: displays an alert for every bad file.) + **/ + minFileSizeErrorCallback?: (file: ResumableFile, errorCount: number) => void; + /** + * The maximum allowed file size. (Default: undefined) + **/ + maxFileSize?: boolean; + /** + * A function which displays an error a selected file is larger than allowed. (Default: displays an alert for every bad file.) + **/ + maxFileSizeErrorCallback?: (file: ResumableFile, errorCount: number) => void; + /** + * The file types allowed to upload. An empty array allow any file type. (Default: []) + **/ + fileType?: string[]; + /** + * A function which displays an error a selected file has type not allowed. (Default: displays an alert for every bad file.) + **/ + fileTypeErrorCallback?: (file: ResumableFile, errorCount: number) => void; + /** + * The maximum number of retries for a chunk before the upload is failed. Valid values are any positive integer and undefined for no limit. (Default: undefined) + **/ + maxChunkRetries?: number; + /** + * The number of milliseconds to wait before retrying a chunk on a non-permanent error. Valid values are any positive integer and undefined for immediate retry. (Default: undefined) + **/ + chunkRetryInterval?: number; + /** + * Standard CORS requests do not send or set any cookies by default. In order to include cookies as part of the request, you need to set the withCredentials property to true. (Default: false) + **/ + withCredentials?: boolean; + } + + class Resumable { + constructor(options: ConfigurationHash); + + /** + * A boolean value indicator whether or not Resumable.js is supported by the current browser. + **/ + support: boolean; + /** + * A hash object of the configuration of the Resumable.js instance. + **/ + opts: ConfigurationHash; + /** + * An array of ResumableFile file objects added by the user (see full docs for this object type below). + **/ + files: ResumableFile[]; + + defaults: ConfigurationHash; + + events: Event[]; + version: number; + + /** + * Assign a browse action to one or more DOM nodes. Pass in true to allow directories to be selected (Chrome only). + **/ + assignBrowse(domNode: Element, isDirectory: boolean): void; + assignBrowse(domNodes: Element[], isDirectory: boolean): void; + /** + * Assign one or more DOM nodes as a drop target. + **/ + assignDrop(domNode: Element): void; + assignDrop(domNodes: Element[]): void; + unAssignDrop(domNode: Element): void; + unAssignDrop(domNodes: Element[]): void; + /** + * Start or resume uploading. + **/ + upload(): void; + uploadNextChunk(): void; + /** + * Pause uploading. + **/ + pause(): void; + /** + * Cancel upload of all ResumableFile objects and remove them from the list. + **/ + cancel(): void; + fire(): void; + /** + * Returns a float between 0 and 1 indicating the current upload progress of all files. + **/ + progress(): number; + /** + * Returns a boolean indicating whether or not the instance is currently uploading anything. + **/ + isUploading(): boolean; + /** + * Add a HTML5 File object to the list of files. + **/ + addFile(file: File, event: Event): void; + /** + * Cancel upload of a specific ResumableFile object on the list from the list. + **/ + removeFile(file: ResumableFile): void; + /** + * Look up a ResumableFile object by its unique identifier. + **/ + getFromUniqueIdentifier(uniqueIdentifier: string): ResumableFile; + /** + * Returns the total size of the upload in bytes. + **/ + getSize(): number; + getOpt(o: string): any; + + // Events + /** + * Change event handler + **/ + handleChangeEvent(e: Event): void; + + /** + * Drop event handler + **/ + handleDropEvent(e: Event): void; + + /** + * A specific file was completed. + **/ + on(event: 'fileSuccess', callback: (file: ResumableFile) => void): void; + /** + * Uploading progressed for a specific file. + **/ + on(event: 'fileProgress', callback: (file: ResumableFile) => void): void; + /** + * A new file was added. Optionally, you can use the browser event object from when the file was added. + **/ + on(event: 'fileAdded', callback: (file: ResumableFile, event: DragEvent) => void): void; + /** + * New files were added. + **/ + on(event: 'filesAdded', callback: (files: ResumableFile[]) => void): void; + /** + * Something went wrong during upload of a specific file, uploading is being retried. + **/ + on(event: 'fileRetry', callback: (file: ResumableFile) => void): void; + /** + * An error occurred during upload of a specific file. + **/ + on(event: 'fileError', callback: (file: ResumableFile, message: string) => void): void; + /** + * Upload has been started on the Resumable object. + **/ + on(event: 'uploadStart', callback: () => void): void; + /** + * Uploading completed. + **/ + on(event: 'complete', callback: () => void): void; + /** + * Uploading progress. + **/ + on(event: 'progress', callback: () => void): void; + /** + * An error, including fileError, occurred. + **/ + on(event: 'error', callback: (message: string, file: ResumableFile) => void): void; + /** + * Uploading was paused. + **/ + on(event: 'pause', callback: () => void): void; + /** + * Triggers before the items are cancelled allowing to do any processing on uploading files. + **/ + on(event: 'beforeCancel', callback: () => void): void; + /** + * Uploading was canceled. + **/ + on(event: 'cancel', callback: () => void): void; + /** + * Started preparing file for upload + **/ + on(event: 'chunkingStart', callback: (file: ResumableFile) => void): void; + /** + * Show progress in file preparation + **/ + on(event: 'chunkingProgress', callback: (file: ResumableFile, ratio: number) => void): void; + /** + * File is ready for upload + **/ + on(event: 'chunkingComplete', callback: (file: ResumableFile) => void): void; + /** + * Listen to all the events listed above with the same callback function. + **/ + on(event: 'catchAll', callback: () => void): void; + /** + * Listen for event from Resumable.js (see below) + **/ + on(event: string, callback: Function): void; + } + + interface ResumableFile { + /** + * A back-reference to the parent Resumable object. + **/ + resumableObj: Resumable; + /** + * The correlating HTML5 File object. + **/ + file: File; + /** + * The name of the file. + **/ + fileName: string; + /** + * The relative path to the file (defaults to file name if relative path doesn't exist) + **/ + relativePath: string; + /** + * Size in bytes of the file. + **/ + size: number; + /** + * A unique identifier assigned to this file object. This value is included in uploads to the server for reference, but can also be used in CSS classes etc when building your upload UI. + **/ + uniqueIdentifier: string; + /** + * An array of ResumableChunk items. You shouldn't need to dig into these. + **/ + chunks: ResumableChunk[]; + + + /** + * Returns a float between 0 and 1 indicating the current upload progress of the file. If relative is true, the value is returned relative to all files in the Resumable.js instance. + **/ + progress: (relative: boolean) => number; + /** + * Abort uploading the file. + **/ + abort: () => void; + /** + * Abort uploading the file and delete it from the list of files to upload. + **/ + cancel: () => void; + /** + * Retry uploading the file. + **/ + retry: () => void; + /** + * Rebuild the state of a ResumableFile object, including reassigning chunks and XMLHttpRequest instances. + **/ + bootstrap: () => void; + /** + * Returns a boolean indicating whether file chunks is uploading. + **/ + isUploading: () => boolean; + /** + * Returns a boolean indicating whether the file has completed uploading and received a server response. + **/ + isComplete: () => boolean; + } + + interface ResumableChunk { } +} + +declare module 'resumablejs' { + export = Resumable; +} diff --git a/resumable.js b/resumable.js index b9f6119e..2e828a3e 100644 --- a/resumable.js +++ b/resumable.js @@ -1,636 +1,1017 @@ -"use strict"; - /* * MIT Licensed -* http://www.23developer.com/opensource -* http://github.com/23/resumable.js -* Steffen Tiedemann Christensen, steffen@23company.com +* https://www.twentythree.com +* https://github.com/23/resumable.js +* Steffen Fagerström Christensen, steffen@twentythree.com */ -var Resumable = function(opts){ - if ( !(this instanceof Resumable ) ) { - return new Resumable( opts ); - } - this.version = 1.0; - // SUPPORTED BY BROWSER? - // Check if these features are support by the browser: - // - File object type - // - Blob object type - // - FileList object type - // - slicing files - this.support = ( - (typeof(File)!=='undefined') - && - (typeof(Blob)!=='undefined') - && - (typeof(FileList)!=='undefined') - && - (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false) - ); - if(!this.support) return(false); - - - // PROPERTIES - var $ = this; - $.files = []; - $.defaults = { - chunkSize:1*1024*1024, - forceChunkSize:false, - simultaneousUploads:3, - fileParameterName:'file', - throttleProgressCallbacks:0.5, - query:{}, - headers:{}, - preprocess:null, - method:'multipart', - prioritizeFirstAndLastChunk:false, - target:'/', - testChunks:true, - generateUniqueIdentifier:null, - maxChunkRetries:undefined, - chunkRetryInterval:undefined, - permanentErrors:[415, 500, 501], - maxFiles:undefined, - maxFilesErrorCallback:function (files, errorCount) { - var maxFiles = $.getOpt('maxFiles'); - alert('Please upload ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.'); - }, - minFileSize:1, - minFileSizeErrorCallback:function(file, errorCount) { - alert(file.fileName +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.'); - }, - maxFileSize:undefined, - maxFileSizeErrorCallback:function(file, errorCount) { - alert(file.fileName +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.'); - }, - fileType: [], - fileTypeErrorCallback: function(file, errorCount) { - alert(file.fileName +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.'); - } - }; - $.opts = opts||{}; - $.getOpt = function(o) { - var $this = this; - // Get multiple option if passed an array - if(o instanceof Array) { - var options = {}; - $h.each(o, function(option){ - options[option] = $this.getOpt(option); - }); - return options; - } - // Otherwise, just return a simple option - if ($this instanceof ResumableChunk) { - if (typeof $this.opts[o] !== 'undefined') { return $this.opts[o]; } - else { $this = $this.fileObj; } - } - if ($this instanceof ResumableFile) { - if (typeof $this.opts[o] !== 'undefined') { return $this.opts[o]; } - else { $this = $this.resumableObj; } - } - if ($this instanceof Resumable) { - if (typeof $this.opts[o] !== 'undefined') { return $this.opts[o]; } - else { return $this.defaults[o]; } - } - }; +(function(){ +"use strict"; - // EVENTS - // catchAll(event, ...) - // fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message), - // complete(), progress(), error(message, file), pause() - $.events = []; - $.on = function(event,callback){ - $.events.push(event.toLowerCase(), callback); - }; - $.fire = function(){ - // `arguments` is an object, not array, in FF, so: - var args = []; - for (var i=0; i= 0) { // only for file drop + e.stopPropagation(); + dt.dropEffect = "copy"; + dt.effectAllowed = "copy"; + e.currentTarget.classList.add($.getOpt('dragOverClass')); + } else { // not work on IE/Edge.... + dt.dropEffect = "none"; + dt.effectAllowed = "none"; + } + }; + + /** + * processes a single upload item (file or directory) + * @param {Object} item item to upload, may be file or directory entry + * @param {string} path current file path + * @param {File[]} items list of files to append new items to + * @param {Function} cb callback invoked when item is processed + */ + function processItem(item, path, items, cb) { + var entry; + if(item.isFile){ + // file provided + return item.file(function(file){ + file.relativePath = path + file.name; + items.push(file); + cb(); + }); + }else if(item.isDirectory){ + // item is already a directory entry, just assign + entry = item; + }else if(item instanceof File) { + items.push(item); + } + if('function' === typeof item.webkitGetAsEntry){ + // get entry from file object + entry = item.webkitGetAsEntry(); + } + if(entry && entry.isDirectory){ + // directory provided, process it + return processDirectory(entry, path + entry.name + '/', items, cb); + } + if('function' === typeof item.getAsFile){ + // item represents a File object, convert it + item = item.getAsFile(); + if(item instanceof File) { + item.relativePath = path + item.name; + items.push(item); + } + } + cb(); // indicate processing is done + } - return result; - }, - formatSize:function(size){ - if(size<1024) { - return size + ' bytes'; - } else if(size<1024*1024) { - return (size/1024.0).toFixed(0) + ' KB'; - } else if(size<1024*1024*1024) { - return (size/1024.0/1024.0).toFixed(1) + ' MB'; - } else { - return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB'; + + /** + * cps-style list iteration. + * invokes all functions in list and waits for their callback to be + * triggered. + * @param {Function[]} items list of functions expecting callback parameter + * @param {Function} cb callback to trigger after the last callback has been invoked + */ + function processCallbacks(items, cb){ + if(!items || items.length === 0){ + // empty or no list, invoke callback + return cb(); } + // invoke current function, pass the next part as continuation + items[0](function(){ + processCallbacks(items.slice(1), cb); + }); } - }; - var onDrop = function(event){ - $h.stopEvent(event); - appendFilesFromFileList(event.dataTransfer.files, event); - }; - var onDragOver = function(e) { - e.preventDefault(); - }; + /** + * recursively traverse directory and collect files to upload + * @param {Object} directory directory to process + * @param {string} path current path + * @param {File[]} items target list of items + * @param {Function} cb callback invoked after traversing directory + */ + function processDirectory (directory, path, items, cb) { + var dirReader = directory.createReader(); + var allEntries = []; - // INTERNAL METHODS (both handy and responsible for the heavy load) - var appendFilesFromFileList = function(fileList, event){ - // check for uploading too many files - var errorCount = 0; - var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']); - if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) { - // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file - if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) { - $.removeFile($.files[0]); - } else { - o.maxFilesErrorCallback(fileList, errorCount++); - return false; + function readEntries () { + dirReader.readEntries(function(entries){ + if (entries.length) { + allEntries = allEntries.concat(entries); + return readEntries(); + } + + // process all conversion callbacks, finally invoke own one + processCallbacks( + allEntries.map(function(entry){ + // bind all properties except for callback + return processItem.bind(null, entry, path, items); + }), + cb + ); + }); } + + readEntries(); } - var files = []; - $h.each(fileList, function(file){ - if (o.fileType.length > 0 && !$h.contains(o.fileType, file.type.split('/')[1])) { + + /** + * process items to extract files to be uploaded + * @param {File[]} items items to process + * @param {Event} event event that led to upload + */ + function loadFiles(items, event) { + if(!items.length){ + return; // nothing to do + } + $.fire('beforeAdd'); + var files = []; + processCallbacks( + Array.prototype.map.call(items, function(item){ + // bind all properties except for callback + var entry = item; + if('function' === typeof item.webkitGetAsEntry){ + entry = item.webkitGetAsEntry(); + } + return processItem.bind(null, entry, "", files); + }), + function(){ + if(files.length){ + // at least one file found + appendFilesFromFileList(files, event); + } + } + ); + }; + + var appendFilesFromFileList = function(fileList, event){ + // check for uploading too many files + var errorCount = 0; + var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']); + if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) { + // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file + if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) { + $.removeFile($.files[0]); + } else { + o.maxFilesErrorCallback(fileList, errorCount++); + return false; + } + } + var files = [], filesSkipped = [], remaining = fileList.length; + var decreaseReamining = function(){ + if(!--remaining){ + // all files processed, trigger event + if(!files.length && !filesSkipped.length){ + // no succeeded files, just skip + return; + } + window.setTimeout(function(){ + $.fire('filesAdded', files, filesSkipped); + },0); + } + }; + $h.each(fileList, function(file){ + var fileName = file.name; + var fileType = file.type; // e.g video/mp4 + if(o.fileType.length > 0){ + var fileTypeFound = false; + for(var index in o.fileType){ + // For good behaviour we do some inital sanitizing. Remove spaces and lowercase all + o.fileType[index] = o.fileType[index].replace(/\s/g, '').toLowerCase(); + + // Allowing for both [extension, .extension, mime/type, mime/*] + var extension = ((o.fileType[index].match(/^[^.][^/]+$/)) ? '.' : '') + o.fileType[index]; + + if ((fileName.substr(-1 * extension.length).toLowerCase() === extension) || + //If MIME type, check for wildcard or if extension matches the files tiletype + (extension.indexOf('/') !== -1 && ( + (extension.indexOf('*') !== -1 && fileType.substr(0, extension.indexOf('*')) === extension.substr(0, extension.indexOf('*'))) || + fileType === extension + )) + ){ + fileTypeFound = true; + break; + } + } + if (!fileTypeFound) { o.fileTypeErrorCallback(file, errorCount++); - return false; + return true; + } } if (typeof(o.minFileSize)!=='undefined' && file.sizeo.maxFileSize) { - o.maxFileSizeErrorCallback(file, errorCount++); - return false; + o.maxFileSizeErrorCallback(file, errorCount++); + return true; } + function addFile(uniqueIdentifier){ + if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {(function(){ + file.uniqueIdentifier = uniqueIdentifier; + var f = new ResumableFile($, file, uniqueIdentifier); + $.files.push(f); + files.push(f); + f.container = (typeof event != 'undefined' ? event.srcElement : null); + window.setTimeout(function(){ + $.fire('fileAdded', f, event) + },0); + })()} else { + filesSkipped.push(file); + }; + decreaseReamining(); + } // directories have size == 0 - if (!$.getFromUniqueIdentifier($h.generateUniqueIdentifier(file))) { - var f = new ResumableFile($, file); - $.files.push(f); - files.push(f); - $.fire('fileAdded', f, event); + var uniqueIdentifier = $h.generateUniqueIdentifier(file, event); + if(uniqueIdentifier && typeof uniqueIdentifier.then === 'function'){ + // Promise or Promise-like object provided as unique identifier + uniqueIdentifier + .then( + function(uniqueIdentifier){ + // unique identifier generation succeeded + addFile(uniqueIdentifier); + }, + function(){ + // unique identifier generation failed + // skip further processing, only decrease file count + decreaseReamining(); + } + ); + }else{ + // non-Promise provided as unique identifier, process synchronously + addFile(uniqueIdentifier); } }); - $.fire('filesAdded', files); - }; - - // INTERNAL OBJECT TYPES - function ResumableFile(resumableObj, file){ - var $ = this; - $.opts = {}; - $.getOpt = resumableObj.getOpt; - $._prevProgress = 0; - $.resumableObj = resumableObj; - $.file = file; - $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox - $.size = file.size; - $.relativePath = file.webkitRelativePath || $.fileName; - $.uniqueIdentifier = $h.generateUniqueIdentifier(file); - var _error = false; - - // Callback when something happens within the chunk - var chunkEvent = function(event, message){ - // event can be 'progress', 'success', 'error' or 'retry' - switch(event){ - case 'progress': - $.resumableObj.fire('fileProgress', $); - break; - case 'error': - $.abort(); - _error = true; - $.chunks = []; - $.resumableObj.fire('fileError', $, message); - break; - case 'success': - if(_error) return; - $.resumableObj.fire('fileProgress', $); // it's at least progress - if($.progress()==1) { - $.resumableObj.fire('fileSuccess', $, message); - } - break; - case 'retry': - $.resumableObj.fire('fileRetry', $); - break; - } }; - // Main code to set up a file object with chunks, - // packaged to be able to handle retries if needed. - $.chunks = []; - $.abort = function(){ - // Stop current uploads - $h.each($.chunks, function(c){ - if(c.status()=='uploading') c.abort(); - }); - $.resumableObj.fire('fileProgress', $); - }; - $.cancel = function(){ - // Reset this file to be void - var _chunks = $.chunks; + // INTERNAL OBJECT TYPES + function ResumableFile(resumableObj, file, uniqueIdentifier){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $._prevProgress = 0; + $.resumableObj = resumableObj; + $.file = file; + $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox + $.size = file.size; + $.relativePath = file.relativePath || file.webkitRelativePath || $.fileName; + $.uniqueIdentifier = uniqueIdentifier; + $._pause = false; + $.container = ''; + $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished + var _error = uniqueIdentifier !== undefined; + + // Callback when something happens within the chunk + var chunkEvent = function(event, message){ + // event can be 'progress', 'success', 'error' or 'retry' + switch(event){ + case 'progress': + $.resumableObj.fire('fileProgress', $, message); + break; + case 'error': + $.abort(); + _error = true; + $.chunks = []; + $.resumableObj.fire('fileError', $, message); + break; + case 'success': + if(_error) return; + $.resumableObj.fire('fileProgress', $, message); // it's at least progress + if($.isComplete()) { + $.resumableObj.fire('fileSuccess', $, message); + } + break; + case 'retry': + $.resumableObj.fire('fileRetry', $); + break; + } + }; + + // Main code to set up a file object with chunks, + // packaged to be able to handle retries if needed. $.chunks = []; - // Stop current uploads - $h.each(_chunks, function(c){ + $.abort = function(){ + // Stop current uploads + var abortCount = 0; + $h.each($.chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + abortCount++; + } + }); + if(abortCount>0) $.resumableObj.fire('fileProgress', $); + }; + $.cancel = function(){ + // Reset this file to be void + var _chunks = $.chunks; + $.chunks = []; + // Stop current uploads + $h.each(_chunks, function(c){ if(c.status()=='uploading') { c.abort(); $.resumableObj.uploadNextChunk(); } }); - $.resumableObj.removeFile($); - $.resumableObj.fire('fileProgress', $); - }; - $.retry = function(){ - $.bootstrap(); - $.resumableObj.upload(); - }; - $.bootstrap = function(){ - $.abort(); + $.resumableObj.removeFile($); + $.resumableObj.fire('fileProgress', $); + }; + $.retry = function(){ + $.bootstrap(); + var firedRetry = false; + $.resumableObj.on('chunkingComplete', function(){ + if(!firedRetry) $.resumableObj.upload(); + firedRetry = true; + }); + }; + $.bootstrap = function(){ + $.abort(); _error = false; - // Rebuild stack of chunks from file - $.chunks = []; - $._prevProgress = 0; - var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor; - for (var offset=0; offset0.999 ? 1 : ret)); - ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused - $._prevProgress = ret; - return(ret); - }; - $.isUploading = function(){ - var uploading = false; - $h.each($.chunks, function(chunk){ - if(chunk.status()=='uploading') { - uploading = true; + ret = (error ? 1 : (ret>0.99999 ? 1 : ret)); + ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused + $._prevProgress = ret; + return(ret); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.chunks, function(chunk){ + if(chunk.status()=='uploading') { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.isComplete = function(){ + var outstanding = false; + if ($.preprocessState === 1) { return(false); } - }); - return(uploading); - }; - - // Bootstrap and return - $.bootstrap(); - return(this); - } + $h.each($.chunks, function(chunk){ + var status = chunk.status(); + if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { + outstanding = true; + return(false); + } + }); + return(!outstanding); + }; + $.pause = function(pause){ + if(typeof(pause)==='undefined'){ + $._pause = ($._pause ? false : true); + }else{ + $._pause = pause; + } + }; + $.isPaused = function() { + return $._pause; + }; + $.preprocessFinished = function(){ + $.preprocessState = 2; + $.upload(); + }; + $.upload = function () { + var found = false; + if ($.isPaused() === false) { + var preprocess = $.getOpt('preprocessFile'); + if(typeof preprocess === 'function') { + switch($.preprocessState) { + case 0: $.preprocessState = 1; preprocess($); return(true); + case 1: return(true); + case 2: break; + } + } + $h.each($.chunks, function (chunk) { + if (chunk.status() == 'pending' && chunk.preprocessState !== 1) { + chunk.send(); + found = true; + return(false); + } + }); + } + return(found); + } + $.markChunksCompleted = function (chunkNumber) { + if (!$.chunks || $.chunks.length <= chunkNumber) { + return; + } + for (var num = 0; num < chunkNumber; num++) { + $.chunks[num].markComplete = true; + } + }; - function ResumableChunk(resumableObj, fileObj, offset, callback){ - var $ = this; - $.opts = {}; - $.getOpt = resumableObj.getOpt; - $.resumableObj = resumableObj; - $.fileObj = fileObj; - $.fileObjSize = fileObj.size; - $.offset = offset; - $.callback = callback; - $.lastProgressCallback = (new Date); - $.tested = false; - $.retries = 0; - $.pendingRetry = false; - $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished - - // Computed properties - var chunkSize = $.getOpt('chunkSize'); - $.loaded = 0; - $.startByte = $.offset*chunkSize; - $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); - if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { - // The last chunk will be bigger than the chunk size, but less than 2*chunkSize - $.endByte = $.fileObjSize; + // Bootstrap and return + $.resumableObj.fire('chunkingStart', $); + $.bootstrap(); + return(this); } - $.xhr = null; - - // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session - $.test = function(){ - // Set up request and listen for event - $.xhr = new XMLHttpRequest(); - - var testHandler = function(e){ - $.tested = true; - var status = $.status(); - if(status=='success') { - $.callback(status, $.message()); - $.resumableObj.uploadNextChunk(); - } else { - $.send(); + + + function ResumableChunk(resumableObj, fileObj, offset, callback){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $.resumableObj = resumableObj; + $.fileObj = fileObj; + $.fileObjSize = fileObj.size; + $.fileObjType = fileObj.file.type; + $.offset = offset; + $.callback = callback; + $.lastProgressCallback = (new Date); + $.tested = false; + $.retries = 0; + $.pendingRetry = false; + $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished + $.markComplete = false; + + // Computed properties + var chunkSize = $.getOpt('chunkSize'); + $.loaded = 0; + $.startByte = $.offset*chunkSize; + $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); + if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { + // The last chunk will be bigger than the chunk size, but less than 2*chunkSize + $.endByte = $.fileObjSize; + } + $.xhr = null; + + // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session + $.test = function(){ + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); + + var testHandler = function(e){ + $.tested = true; + var status = $.status(); + if(status=='success') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.send(); + } + }; + $.xhr.addEventListener('load', testHandler, false); + $.xhr.addEventListener('error', testHandler, false); + $.xhr.addEventListener('timeout', testHandler, false); + + // Add data from the query options + var params = []; + var parameterNamespace = $.getOpt('parameterNamespace'); + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('=')); + }); + // Add extra data to identify chunk + params = params.concat( + [ + // define key/value pairs for additional parameters + ['chunkNumberParameterName', $.offset + 1], + ['chunkSizeParameterName', $.getOpt('chunkSize')], + ['currentChunkSizeParameterName', $.endByte - $.startByte], + ['totalSizeParameterName', $.fileObjSize], + ['typeParameterName', $.fileObjType], + ['identifierParameterName', $.fileObj.uniqueIdentifier], + ['fileNameParameterName', $.fileObj.fileName], + ['relativePathParameterName', $.fileObj.relativePath], + ['totalChunksParameterName', $.fileObj.chunks.length] + ].filter(function(pair){ + // include items that resolve to truthy values + // i.e. exclude false, null, undefined and empty strings + return $.getOpt(pair[0]); + }) + .map(function(pair){ + // map each key/value pair to its final form + return [ + parameterNamespace + $.getOpt(pair[0]), + encodeURIComponent(pair[1]) + ].join('='); + }) + ); + // Append the relevant chunk and send it + $.xhr.open($.getOpt('testMethod'), $h.getTarget('test', params)); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + var customHeaders = $.getOpt('headers'); + if(typeof customHeaders === 'function') { + customHeaders = customHeaders($.fileObj, $); } - }; - $.xhr.addEventListener("load", testHandler, false); - $.xhr.addEventListener("error", testHandler, false); - - // Add data from the query options - var params = []; - var customQuery = $.getOpt('query'); - if(typeof customQuery == "function") customQuery = customQuery($.fileObj, $); - $h.each(customQuery, function(k,v){ - params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); + $h.each(customHeaders, function(k,v) { + $.xhr.setRequestHeader(k, v); }); - // Add extra data to identify chunk - params.push(['resumableChunkNumber', encodeURIComponent($.offset+1)].join('=')); - params.push(['resumableChunkSize', encodeURIComponent($.getOpt('chunkSize'))].join('=')); - params.push(['resumableCurrentChunkSize', encodeURIComponent($.endByte - $.startByte)].join('=')); - params.push(['resumableTotalSize', encodeURIComponent($.fileObjSize)].join('=')); - params.push(['resumableIdentifier', encodeURIComponent($.fileObj.uniqueIdentifier)].join('=')); - params.push(['resumableFilename', encodeURIComponent($.fileObj.fileName)].join('=')); - params.push(['resumableRelativePath', encodeURIComponent($.fileObj.relativePath)].join('=')); - // Append the relevant chunk and send it - $.xhr.open("GET", $.getOpt('target') + '?' + params.join('&')); - // Add data from header options - $h.each($.getOpt('headers'), function(k,v) { - $.xhr.setRequestHeader(k, v); - }); - $.xhr.send(null); - }; + $.xhr.send(null); + }; - $.preprocessFinished = function(){ - $.preprocessState = 2; - $.send(); - }; + $.preprocessFinished = function(){ + $.preprocessState = 2; + $.send(); + }; - // send() uploads the actual data in a POST call - $.send = function(){ - var preprocess = $.getOpt('preprocess'); - if(typeof preprocess === 'function') { - switch($.preprocessState) { - case 0: preprocess($); $.preprocessState = 1; return; + // send() uploads the actual data in a POST call + $.send = function(){ + var preprocess = $.getOpt('preprocess'); + if(typeof preprocess === 'function') { + switch($.preprocessState) { + case 0: $.preprocessState = 1; preprocess($); return; case 1: return; case 2: break; + } + } + if($.getOpt('testChunks') && !$.tested) { + $.test(); + return; } - } - if($.getOpt('testChunks') && !$.tested) { - $.test(); - return; - } - // Set up request and listen for event - $.xhr = new XMLHttpRequest(); + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); - // Progress - $.xhr.upload.addEventListener("progress", function(e){ + // Progress + $.xhr.upload.addEventListener('progress', function(e){ if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) { $.callback('progress'); $.lastProgressCallback = (new Date); } $.loaded=e.loaded||0; }, false); - $.loaded = 0; - $.pendingRetry = false; - $.callback('progress'); - - // Done (either done, failed or retry) - var doneHandler = function(e){ - var status = $.status(); - if(status=='success'||status=='error') { - $.callback(status, $.message()); - $.resumableObj.uploadNextChunk(); - } else { - $.callback('retry', $.message()); - $.abort(); - $.retries++; - var retryInterval = $.getOpt('chunkRetryInterval'); - if(retryInterval !== undefined) { - $.pendingRetry = true; - setTimeout($.send, retryInterval); + $.loaded = 0; + $.pendingRetry = false; + $.callback('progress'); + + // Done (either done, failed or retry) + var doneHandler = function(e){ + var status = $.status(); + if(status=='success'||status=='error') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); } else { - $.send(); + $.callback('retry', $.message()); + $.abort(); + $.retries++; + var retryInterval = $.getOpt('chunkRetryInterval'); + if(retryInterval !== undefined) { + $.pendingRetry = true; + setTimeout($.send, retryInterval); + } else { + $.send(); + } } - } - }; - $.xhr.addEventListener("load", doneHandler, false); - $.xhr.addEventListener("error", doneHandler, false); - - // Set up the basic query data from Resumable - var query = { - resumableChunkNumber: $.offset+1, - resumableChunkSize: $.getOpt('chunkSize'), - resumableCurrentChunkSize: $.endByte - $.startByte, - resumableTotalSize: $.fileObjSize, - resumableIdentifier: $.fileObj.uniqueIdentifier, - resumableFilename: $.fileObj.fileName, - resumableRelativePath: $.fileObj.relativePath, - resumableTotalChunks: $.fileObj.chunks.length - }; - // Mix in custom data - var customQuery = $.getOpt('query'); - if(typeof customQuery == "function") customQuery = customQuery($.fileObj, $); - $h.each(customQuery, function(k,v){ - query[k] = v; - }); + }; + $.xhr.addEventListener('load', doneHandler, false); + $.xhr.addEventListener('error', doneHandler, false); + $.xhr.addEventListener('timeout', doneHandler, false); - var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))), - bytes = $.fileObj.file[func]($.startByte,$.endByte), - data = null, - target = $.getOpt('target'); - - if ($.getOpt('method') === 'octet') { - // Add data from the query options - data = bytes; - var params = []; - $h.each(query, function(k,v){ - params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); + // Set up the basic query data from Resumable + var query = [ + ['chunkNumberParameterName', $.offset + 1], + ['chunkSizeParameterName', $.getOpt('chunkSize')], + ['currentChunkSizeParameterName', $.endByte - $.startByte], + ['totalSizeParameterName', $.fileObjSize], + ['typeParameterName', $.fileObjType], + ['identifierParameterName', $.fileObj.uniqueIdentifier], + ['fileNameParameterName', $.fileObj.fileName], + ['relativePathParameterName', $.fileObj.relativePath], + ['totalChunksParameterName', $.fileObj.chunks.length], + ].filter(function(pair){ + // include items that resolve to truthy values + // i.e. exclude false, null, undefined and empty strings + return $.getOpt(pair[0]); + }) + .reduce(function(query, pair){ + // assign query key/value + query[$.getOpt(pair[0])] = pair[1]; + return query; + }, {}); + // Mix in custom data + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + query[k] = v; }); - target += '?' + params.join('&'); - } else { - // Add data from the query options - data = new FormData(); - $h.each(query, function(k,v){ - data.append(k,v); + + var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))); + var bytes = $.fileObj.file[func]($.startByte, $.endByte, $.getOpt('setChunkTypeFromFile') ? $.fileObj.file.type : ""); + var data = null; + var params = []; + + var parameterNamespace = $.getOpt('parameterNamespace'); + if ($.getOpt('method') === 'octet') { + // Add data from the query options + data = bytes; + $h.each(query, function (k, v) { + params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('=')); + }); + } else { + // Add data from the query options + data = new FormData(); + $h.each(query, function (k, v) { + data.append(parameterNamespace + k, v); + params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('=')); + }); + if ($.getOpt('chunkFormat') == 'blob') { + data.append(parameterNamespace + $.getOpt('fileParameterName'), bytes, $.fileObj.fileName); + } + else if ($.getOpt('chunkFormat') == 'base64') { + var fr = new FileReader(); + fr.onload = function (e) { + data.append(parameterNamespace + $.getOpt('fileParameterName'), fr.result); + $.xhr.send(data); + } + fr.readAsDataURL(bytes); + } + } + + var target = $h.getTarget('upload', params); + var method = $.getOpt('uploadMethod'); + + $.xhr.open(method, target); + if ($.getOpt('method') === 'octet') { + $.xhr.setRequestHeader('Content-Type', 'application/octet-stream'); + } + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + var customHeaders = $.getOpt('headers'); + if(typeof customHeaders === 'function') { + customHeaders = customHeaders($.fileObj, $); + } + + $h.each(customHeaders, function(k,v) { + $.xhr.setRequestHeader(k, v); }); - data.append($.getOpt('fileParameterName'), bytes); - } - - $.xhr.open('POST', target); - // Add data from header options - $h.each($.getOpt('headers'), function(k,v) { - $.xhr.setRequestHeader(k, v); - }); - $.xhr.send(data); - }; - $.abort = function(){ - // Abort and reset - if($.xhr) $.xhr.abort(); - $.xhr = null; - }; - $.status = function(){ - // Returns: 'pending', 'uploading', 'success', 'error' - if($.pendingRetry) { - // if pending retry then that's effectively the same as actively uploading, - // there might just be a slight delay before the retry starts - return('uploading') - } else if(!$.xhr) { - return('pending'); - } else if($.xhr.readyState<4) { - // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening - return('uploading'); - } else { - if($.xhr.status==200) { - // HTTP 200, perfect - return('success'); - } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { - // HTTP 415/500/501, permanent error - return('error'); - } else { - // this should never happen, but we'll reset and queue a retry - // a likely case for this would be 503 service unavailable - $.abort(); + + if ($.getOpt('chunkFormat') == 'blob') { + $.xhr.send(data); + } + }; + $.abort = function(){ + // Abort and reset + if($.xhr) $.xhr.abort(); + $.xhr = null; + }; + $.status = function(){ + // Returns: 'pending', 'uploading', 'success', 'error' + if($.pendingRetry) { + // if pending retry then that's effectively the same as actively uploading, + // there might just be a slight delay before the retry starts + return('uploading'); + } else if($.markComplete) { + return 'success'; + } else if(!$.xhr) { return('pending'); + } else if($.xhr.readyState<4) { + // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening + return('uploading'); + } else { + if($.xhr.status == 200 || $.xhr.status == 201) { + // HTTP 200, 201 (created) + return('success'); + } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { + // HTTP 400, 404, 409, 415, 500, 501 (permanent error) + return('error'); + } else { + // this should never happen, but we'll reset and queue a retry + // a likely case for this would be 503 service unavailable + $.abort(); + return('pending'); + } } - } - }; - $.message = function(){ - return($.xhr ? $.xhr.responseText : ''); - }; - $.progress = function(relative){ - if(typeof(relative)==='undefined') relative = false; - var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); - if($.pendingRetry) return(0); - var s = $.status(); - switch(s){ - case 'success': - case 'error': - return(1*factor); - case 'pending': - return(0*factor); - default: - return($.loaded/($.endByte-$.startByte)*factor); - } - }; - return(this); - } + }; + $.message = function(){ + return($.xhr ? $.xhr.responseText : ''); + }; + $.progress = function(relative){ + if(typeof(relative)==='undefined') relative = false; + var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); + if($.pendingRetry) return(0); + if((!$.xhr || !$.xhr.status) && !$.markComplete) factor*=.95; + var s = $.status(); + switch(s){ + case 'success': + case 'error': + return(1*factor); + case 'pending': + return(0*factor); + default: + return($.loaded/($.endByte-$.startByte)*factor); + } + }; + return(this); + } - // QUEUE - $.uploadNextChunk = function(){ - var found = false; + // QUEUE + $.uploadNextChunk = function(){ + var found = false; - // In some cases (such as videos) it's really handy to upload the first - // and last chunk of a file quickly; this let's the server check the file's - // metadata and determine if there's even a point in continuing. - if ($.getOpt('prioritizeFirstAndLastChunk')) { - $h.each($.files, function(file){ + // In some cases (such as videos) it's really handy to upload the first + // and last chunk of a file quickly; this let's the server check the file's + // metadata and determine if there's even a point in continuing. + if ($.getOpt('prioritizeFirstAndLastChunk')) { + $h.each($.files, function(file){ if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) { file.chunks[0].send(); found = true; return(false); } - if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[0].preprocessState === 0) { + if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) { file.chunks[file.chunks.length-1].send(); found = true; return(false); } }); - if(found) return(true); - } + if(found) return(true); + } - // Now, simply look for the next, best thing to upload - $h.each($.files, function(file){ - $h.each(file.chunks, function(chunk){ - if(chunk.status()=='pending' && chunk.preprocessState === 0) { - chunk.send(); - found = true; - return(false); - } - }); + // Now, simply look for the next, best thing to upload + $h.each($.files, function(file){ + found = file.upload(); if(found) return(false); }); - if(found) return(true); - - // The are no more outstanding chunks to upload, check is everything is done - var outstanding = false; - $h.each($.files, function(file){ - $h.each(file.chunks, function(chunk){ - var status = chunk.status(); - if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { - outstanding = true; - return(false); - } - }); - if(outstanding) return(false); - }); - if(!outstanding) { - // All chunks have been uploaded, complete - $.fire('complete'); - } - return(false); - }; + if(found) return(true); + // The are no more outstanding chunks to upload, check is everything is done + var outstanding = false; + $h.each($.files, function(file){ + if(!file.isComplete()) { + outstanding = true; + return(false); + } + }); + if(!outstanding) { + // All chunks have been uploaded, complete + $.fire('complete'); + } + return(false); + }; - // PUBLIC METHODS FOR RESUMABLE.JS - $.assignBrowse = function(domNodes, isDirectory){ - if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; - // We will create an and overlay it on the domNode - // (crappy, but since HTML5 doesn't have a cross-browser.browse() method we haven't a choice. - // FF4+ allows click() for this though: https://developer.mozilla.org/en/using_files_from_web_applications) - $h.each(domNodes, function(domNode) { + // PUBLIC METHODS FOR RESUMABLE.JS + $.assignBrowse = function(domNodes, isDirectory){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + $h.each(domNodes, function(domNode) { var input; if(domNode.tagName==='INPUT' && domNode.type==='file'){ - input = domNode; + input = domNode; } else { - input = document.createElement('input'); - input.setAttribute('type', 'file'); - // Place with the dom node an position the input to fill the entire space - domNode.style.display = 'inline-block'; - domNode.style.position = 'relative'; - input.style.position = 'absolute'; - input.style.top = input.style.left = input.style.bottom = input.style.right = 0; + input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.style.display = 'none'; + domNode.addEventListener('click', function(){ input.style.opacity = 0; - input.style.cursor = 'pointer'; - domNode.appendChild(input); + input.style.display='block'; + input.focus(); + input.click(); + input.style.display='none'; + }, false); + domNode.appendChild(input); } var maxFiles = $.getOpt('maxFiles'); if (typeof(maxFiles)==='undefined'||maxFiles!=1){ @@ -643,101 +1024,147 @@ var Resumable = function(opts){ } else { input.removeAttribute('webkitdirectory'); } + var fileTypes = $.getOpt('fileType'); + if (typeof (fileTypes) !== 'undefined' && fileTypes.length >= 1) { + input.setAttribute('accept', fileTypes.map(function (e) { + e = e.replace(/\s/g, '').toLowerCase(); + if(e.match(/^[^.][^/]+$/)){ + e = '.' + e; + } + return e; + }).join(',')); + } + else { + input.removeAttribute('accept'); + } // When new files are added, simply append them to the overall list input.addEventListener('change', function(e){ - appendFilesFromFileList(e.target.files); + appendFilesFromFileList(e.target.files,e); + var clearInput = $.getOpt('clearInput'); + if (clearInput) { e.target.value = ''; + } }, false); - }); - }; - $.assignDrop = function(domNodes){ - if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + }); + }; + $.assignDrop = function(domNodes){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; - $h.each(domNodes, function(domNode) { - domNode.addEventListener('dragover', onDragOver, false); + $h.each(domNodes, function(domNode) { + domNode.addEventListener('dragover', onDragOverEnter, false); + domNode.addEventListener('dragenter', onDragOverEnter, false); + domNode.addEventListener('dragleave', onDragLeave, false); domNode.addEventListener('drop', onDrop, false); }); - }; - $.unAssignDrop = function(domNodes) { - if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; + }; + $.unAssignDrop = function(domNodes) { + if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; - $h.each(domNodes, function(domNode) { - domNode.removeEventListener('dragover', onDragOver); + $h.each(domNodes, function(domNode) { + domNode.removeEventListener('dragover', onDragOverEnter); + domNode.removeEventListener('dragenter', onDragOverEnter); + domNode.removeEventListener('dragleave', onDragLeave); domNode.removeEventListener('drop', onDrop); }); - }; - $.isUploading = function(){ - var uploading = false; - $h.each($.files, function(file){ - if (file.isUploading()) { - uploading = true; - return(false); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.files, function(file){ + if (file.isUploading()) { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.upload = function(){ + // Make sure we don't start too many uploads at once + if($.isUploading()) return; + // Kick off the queue + $.fire('uploadStart'); + for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { + $.uploadNextChunk(); } - }); - return(uploading); - }; - $.upload = function(){ - // Make sure we don't start too many uploads at once - if($.isUploading()) return; - // Kick off the queue - $.fire('uploadStart'); - for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { - $.uploadNextChunk(); - } - }; - $.pause = function(){ - // Resume all chunks currently being uploaded - $h.each($.files, function(file){ + }; + $.pause = function(){ + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ file.abort(); }); - $.fire('pause'); - }; - $.cancel = function(){ - for(var i = $.files.length - 1; i >= 0; i--) { - $.files[i].cancel(); - } - $.fire('cancel'); - }; - $.progress = function(){ - var totalDone = 0; - var totalSize = 0; - // Resume all chunks currently being uploaded - $h.each($.files, function(file){ + $.fire('pause'); + }; + $.cancel = function(){ + $.fire('beforeCancel'); + for(var i = $.files.length - 1; i >= 0; i--) { + $.files[i].cancel(); + } + $.fire('cancel'); + }; + $.progress = function(){ + var totalDone = 0; + var totalSize = 0; + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ totalDone += file.progress()*file.size; totalSize += file.size; }); - return(totalSize>0 ? totalDone/totalSize : 0); - }; - $.addFile = function(file){ - appendFilesFromFileList([file]); - }; - $.removeFile = function(file){ - for(var i = $.files.length - 1; i >= 0; i--) { - if($.files[i] === file) { - $.files.splice(i, 1); + return(totalSize>0 ? totalDone/totalSize : 0); + }; + $.addFile = function(file, event){ + appendFilesFromFileList([file], event); + }; + $.addFiles = function(files, event){ + appendFilesFromFileList(files, event); + }; + $.removeFile = function(file){ + for(var i = $.files.length - 1; i >= 0; i--) { + if($.files[i] === file) { + $.files.splice(i, 1); + } } - } - }; - $.getFromUniqueIdentifier = function(uniqueIdentifier){ - var ret = false; - $h.each($.files, function(f){ + }; + $.getFromUniqueIdentifier = function(uniqueIdentifier){ + var ret = false; + $h.each($.files, function(f){ if(f.uniqueIdentifier==uniqueIdentifier) ret = f; }); - return(ret); - }; - $.getSize = function(){ - var totalSize = 0; - $h.each($.files, function(file){ + return(ret); + }; + $.getSize = function(){ + var totalSize = 0; + $h.each($.files, function(file){ totalSize += file.size; }); - return(totalSize); + return(totalSize); + }; + $.handleDropEvent = function (e) { + onDrop(e); + }; + $.handleChangeEvent = function (e) { + appendFilesFromFileList(e.target.files, e); + e.target.value = ''; + }; + $.updateQuery = function(query){ + $.opts.query = query; + }; + + return(this); }; - return(this); -}; + // Node.js-style export for Node and Component + if (typeof module != 'undefined') { + // left here for backwards compatibility + module.exports = Resumable; + module.exports.Resumable = Resumable; + } else if (typeof define === "function" && define.amd) { + // AMD/requirejs: Define the module + define(function(){ + return Resumable; + }); + } else { + // Browser: Expose to window + window.Resumable = Resumable; + } -// Node.js-style export for Node and Component -if(typeof module != 'undefined') { - module.exports = Resumable; -} +})(); diff --git a/samples/Backend on PHP.md b/samples/Backend on PHP.md index 66a9e9dc..a45b6946 100644 --- a/samples/Backend on PHP.md +++ b/samples/Backend on PHP.md @@ -21,6 +21,9 @@ It's a sample implementation to illustrate chunking. It should probably not be u * * @author Gregory Chris (http://online-php.com) * @email www.online.php@gmail.com + * + * @editor Bivek Joshi (http://www.bivekjoshi.com.np) + * @email meetbivek@gmail.com */ @@ -73,27 +76,26 @@ function rrmdir($dir) { * * Check if all the parts exist, and * gather all the parts of the file together - * @param string $dir - the temporary directory holding all the parts of the file + * @param string $temp_dir - the temporary directory holding all the parts of the file * @param string $fileName - the original file name * @param string $chunkSize - each chunk size (in bytes) * @param string $totalSize - original file size (in bytes) */ -function createFileFromChunks($temp_dir, $fileName, $chunkSize, $totalSize) { +function createFileFromChunks($temp_dir, $fileName, $chunkSize, $totalSize,$total_files) { // count all the parts of this file - $total_files = 0; + $total_files_on_server_size = 0; + $temp_total = 0; foreach(scandir($temp_dir) as $file) { - if (stripos($file, $fileName) !== false) { - $total_files++; - } + $temp_total = $total_files_on_server_size; + $tempfilesize = filesize($temp_dir.'/'.$file); + $total_files_on_server_size = $temp_total + $tempfilesize; } - // check that all the parts are present - // the size of the last part is between chunkSize and 2*$chunkSize - if ($total_files * $chunkSize >= ($totalSize - $chunkSize + 1)) { - - // create the final destination file - if (($fp = fopen('temp/'.$fileName, 'w')) !== false) { + // If the Size of all the chunks on the server is equal to the size of the file uploaded. + if ($total_files_on_server_size >= $totalSize) { + // create the final destination file + if (($fp = fopen($temp_dir.'/'.$fileName, 'w')) !== false) { for ($i=1; $i<=$total_files; $i++) { fwrite($fp, file_get_contents($temp_dir.'/'.$fileName.'.part'.$i)); _log('writing chunk '.$i); @@ -123,17 +125,23 @@ function createFileFromChunks($temp_dir, $fileName, $chunkSize, $totalSize) { //check if request is GET and the requested chunk exists or not. this makes testChunks work if ($_SERVER['REQUEST_METHOD'] === 'GET') { + if(!(isset($_GET['resumableIdentifier']) && trim($_GET['resumableIdentifier'])!='')){ + $_GET['resumableIdentifier']=''; + } $temp_dir = 'temp/'.$_GET['resumableIdentifier']; + if(!(isset($_GET['resumableFilename']) && trim($_GET['resumableFilename'])!='')){ + $_GET['resumableFilename']=''; + } + if(!(isset($_GET['resumableChunkNumber']) && trim($_GET['resumableChunkNumber'])!='')){ + $_GET['resumableChunkNumber']=''; + } $chunk_file = $temp_dir.'/'.$_GET['resumableFilename'].'.part'.$_GET['resumableChunkNumber']; if (file_exists($chunk_file)) { header("HTTP/1.0 200 Ok"); - } else - { + } else { header("HTTP/1.0 404 Not Found"); } - } - - +} // loop through files and move the chunks to a temporarily created directory if (!empty($_FILES)) foreach ($_FILES as $file) { @@ -146,7 +154,9 @@ if (!empty($_FILES)) foreach ($_FILES as $file) { // init the destination file (format .part<#chunk> // the file is stored in a temporary directory - $temp_dir = 'temp/'.$_POST['resumableIdentifier']; + if(isset($_POST['resumableIdentifier']) && trim($_POST['resumableIdentifier'])!=''){ + $temp_dir = 'temp/'.$_POST['resumableIdentifier']; + } $dest_file = $temp_dir.'/'.$_POST['resumableFilename'].'.part'.$_POST['resumableChunkNumber']; // create the temporary directory @@ -158,10 +168,8 @@ if (!empty($_FILES)) foreach ($_FILES as $file) { if (!move_uploaded_file($file['tmp_name'], $dest_file)) { _log('Error saving (move_uploaded_file) chunk '.$_POST['resumableChunkNumber'].' for file '.$_POST['resumableFilename']); } else { - // check if all the parts present, and create the final destination file - createFileFromChunks($temp_dir, $_POST['resumableFilename'], - $_POST['resumableChunkSize'], $_POST['resumableTotalSize']); + createFileFromChunks($temp_dir, $_POST['resumableFilename'],$_POST['resumableChunkSize'], $_POST['resumableTotalSize'],$_POST['resumableTotalChunks']); } } ``` diff --git a/samples/Backend on Resumable.PHP.md b/samples/Backend on Resumable.PHP.md new file mode 100644 index 00000000..63b2adf5 --- /dev/null +++ b/samples/Backend on Resumable.PHP.md @@ -0,0 +1,36 @@ +# Resumable.php is a PHP package for resumable.js + +[Resumable.php](https://github.com/dilab/resumable.php) provides abstraction for handling file upload in PHP. + +## Installation + +To install, use composer: + +``` composer require dilab/resumable.php ``` + +## How to use +**upload.php** + +``` +tempFolder = 'tmps'; +$resumable->uploadFolder = 'uploads'; +$resumable->process(); + +``` + + +## Testing +``` +$ ./vendor/bin/phpunit +``` diff --git a/samples/Backend on Ruby On Rails.md b/samples/Backend on Ruby On Rails.md new file mode 100644 index 00000000..2c286dc6 --- /dev/null +++ b/samples/Backend on Ruby On Rails.md @@ -0,0 +1,105 @@ +# Sample server implementation in Ruby on Rails + +[Bert Sinnema](https://github.com/bertsinnema) has provided this sample implementation for Ruby on Rails. + +This is a sample backend controller for Ruby on Rails (3.2) + +Tested on ruby 2.0.0 + +######config/routes.rb +Add a chunk resource to the routes file + +```ruby +resource :chunk, :only => [:create, :show] +``` +######app/controllers/chunks_controller.rb +Add a chunks controller + +```ruby + + class ChunksController < ApplicationController + layout nil + + #GET /chunk + def show + #chunk folder path based on the parameters + dir = "/tmp/#{params[:resumableIdentifier]}" + #chunk path based on the parameters + chunk = "#{dir}/#{params[:resumableFilename]}.part#{params[:resumableChunkNumber]}" + + if File.exists?(chunk) + #Let resumable.js know this chunk already exists + render :nothing => true, :status => 200 + else + #Let resumable.js know this chunk doesnt exists and needs to be uploaded + render :nothing => true, :status => 404 + end + + end + + #POST /chunk + def create + + #chunk folder path based on the parameters + dir = "/tmp/#{params[:resumableIdentifier]}" + #chunk path based on the parameters + chunk = "#{dir}/#{params[:resumableFilename]}.part#{params[:resumableChunkNumber]}" + + #Create chunks directory when not present on system + if !File.directory?(dir) + FileUtils.mkdir(dir, :mode => 0700) + elsif params[:resumableChunkNumber].to_i == 1 + FileUtils.rm_rf Dir.glob("#{dir}/*") + end + + #Move the uploaded chunk to the directory + FileUtils.mv params[:file].tempfile, chunk + + #Concatenate all the partial files into the original file + + currentSize = params[:resumableChunkNumber].to_i * params[:resumableChunkSize].to_i + filesize = params[:resumableTotalSize].to_i + + #When all chunks are uploaded + if (currentSize + params[:resumableCurrentChunkSize].to_i) >= filesize + + #Create a target file + File.open("#{dir}/#{params[:resumableFilename]}","a") do |target| + #Loop trough the chunks + for i in 1..params[:resumableChunkNumber].to_i + #Select the chunk + chunk = File.open("#{dir}/#{params[:resumableFilename]}.part#{i}", 'r').read + + #Write chunk into target file + chunk.each_line do |line| + target << line + end + + #Deleting chunk + FileUtils.rm "#{dir}/#{params[:resumableFilename]}.part#{i}", :force => true + end + end + #You can use the file now + puts "File saved to #{dir}/#{params[:resumableFilename]}" + end + + render :nothing => true, :status => 200 + end + + end +``` + +###### Resumable.js configuration +Ruby on Rails needs X-CSRF-Token headers. You can pass this is in the headers option. The token should be in a meta tag of the application layout file. In this example is used coffeescript. + +```coffeescript + + jQuery -> + r = new Resumable + target: "/chunk" + headers: + 'X-CSRF-Token' : $('meta[name="csrf-token"]').attr('content') + + if !r.support + alert('No Support!!!!!') +``` \ No newline at end of file diff --git a/samples/Backend with Python (Flask).md b/samples/Backend with Python (Flask).md new file mode 100644 index 00000000..422f528b --- /dev/null +++ b/samples/Backend with Python (Flask).md @@ -0,0 +1,90 @@ +# Sample Server in Python using the Flask framework + +Based on [szelcsanyi's](https://github.com/szelcsanyi/) [example gist](https://github.com/szelcsanyi/resumable.js/blob/8872d84c57b8c8cc756f7b74ec51b78233f82021/samples/Backend%20on%20Flask.md): + +```python +from flask import Flask, render_template, request, abort, jsonify +import os +app = Flask(__name__) +app.debug = True + +temp_base = os.path.expanduser("~/tmp/flask_uploads/") + +# landing page +@app.route("/resumable") +def resumable_example(): + return render_template("resumable_upload.html") + +# resumable.js uses a GET request to check if it uploaded the file already. +# NOTE: your validation here needs to match whatever you do in the POST (otherwise it will NEVER find the files) +@app.route("/resumable_upload", methods=['GET']) +def resumable(): + resumableIdentfier = request.args.get('resumableIdentifier', type=str) + resumableFilename = request.args.get('resumableFilename', type=str) + resumableChunkNumber = request.args.get('resumableChunkNumber', type=int) + + if not resumableIdentfier or not resumableFilename or not resumableChunkNumber: + # Parameters are missing or invalid + abort(500, 'Parameter error') + + # chunk folder path based on the parameters + temp_dir = os.path.join(temp_base, resumableIdentfier) + + # chunk path based on the parameters + chunk_file = os.path.join(temp_dir, get_chunk_name(resumableFilename, resumableChunkNumber)) + app.logger.debug('Getting chunk: %s', chunk_file) + + if os.path.isfile(chunk_file): + # Let resumable.js know this chunk already exists + return 'OK' + else: + # Let resumable.js know this chunk does not exists and needs to be uploaded + abort(404, 'Not found') + + +# if it didn't already upload, resumable.js sends the file here +@app.route("/resumable_upload", methods=['POST']) +def resumable_post(): + resumableTotalChunks = request.form.get('resumableTotalChunks', type=int) + resumableChunkNumber = request.form.get('resumableChunkNumber', default=1, type=int) + resumableFilename = request.form.get('resumableFilename', default='error', type=str) + resumableIdentfier = request.form.get('resumableIdentifier', default='error', type=str) + + # get the chunk data + chunk_data = request.files['file'] + + # make our temp directory + temp_dir = os.path.join(temp_base, resumableIdentfier) + if not os.path.isdir(temp_dir): + os.makedirs(temp_dir, 0777) + + # save the chunk data + chunk_name = get_chunk_name(resumableFilename, resumableChunkNumber) + chunk_file = os.path.join(temp_dir, chunk_name) + chunk_data.save(chunk_file) + app.logger.debug('Saved chunk: %s', chunk_file) + + # check if the upload is complete + chunk_paths = [os.path.join(temp_dir, get_chunk_name(resumableFilename, x)) for x in range(1, resumableTotalChunks+1)] + upload_complete = all([os.path.exists(p) for p in chunk_paths]) + + # combine all the chunks to create the final file + if upload_complete: + target_file_name = os.path.join(temp_base, resumableFilename) + with open(target_file_name, "ab") as target_file: + for p in chunk_paths: + stored_chunk_file_name = p + stored_chunk_file = open(stored_chunk_file_name, 'rb') + target_file.write(stored_chunk_file.read()) + stored_chunk_file.close() + os.unlink(stored_chunk_file_name) + target_file.close() + os.rmdir(temp_dir) + app.logger.debug('File saved to: %s', target_file_name) + + return 'OK' + + +def get_chunk_name(uploaded_filename, chunk_number): + return uploaded_filename + "_part_%03d" % chunk_number +``` diff --git a/samples/DotNET/Controllers/ResumableController.cs b/samples/DotNET/Controllers/ResumableController.cs new file mode 100644 index 00000000..d590ab3f --- /dev/null +++ b/samples/DotNET/Controllers/ResumableController.cs @@ -0,0 +1,224 @@ +using Resumable.Models; +using System; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using System.Web.Http; + +namespace Resumable.Controllers +{ + [RoutePrefix("api/File")] + public class FileUploadController : ApiController + { + private string root = System.Web.Hosting.HostingEnvironment.MapPath("~/upload"); + + [Route("Upload"), HttpOptions] + public object UploadFileOptions() + { + return Request.CreateResponse(HttpStatusCode.OK); + } + + [Route("Upload"), HttpGet] + public object Upload(int resumableChunkNumber, string resumableIdentifier) + { + return ChunkIsHere(resumableChunkNumber, resumableIdentifier) ? Request.CreateResponse(HttpStatusCode.OK) : Request.CreateResponse(HttpStatusCode.NoContent); + } + + [Route("Upload"), HttpPost] + public async Task Upload() + { + // Check if the request contains multipart/form-data. + if (!Request.Content.IsMimeMultipartContent()) + { + throw new HttpResponseException(HttpStatusCode.UnsupportedMediaType); + } + if (!Directory.Exists(root)) Directory.CreateDirectory(root); + var provider = new MultipartFormDataStreamProvider(root); + + if (await readPart(provider)) + { + // Success + return Request.CreateResponse(HttpStatusCode.OK); + } + else + { + // Fail + var message = DeleteInvalidChunkData(provider) ? "Cannot read multi part file data." : "Cannot delete temporary file chunk data."; + return Request.CreateErrorResponse(HttpStatusCode.InternalServerError, new System.Exception(message)); + } + } + + private static bool DeleteInvalidChunkData(MultipartFormDataStreamProvider provider) + { + try + { + var localFileName = provider.FileData[0].LocalFileName; + if (File.Exists(localFileName)) + { + File.Delete(localFileName); + } + return true; + } + catch { + return false; + } + } + + private async Task readPart(MultipartFormDataStreamProvider provider) + { + try + { + await Request.Content.ReadAsMultipartAsync(provider); + ResumableConfiguration configuration = GetUploadConfiguration(provider); + int chunkNumber = GetChunkNumber(provider); + + // Rename generated file + MultipartFileData chunk = provider.FileData[0]; // Only one file in multipart message + RenameChunk(chunk, chunkNumber, configuration.Identifier); + + // Assemble chunks into single file if they're all here + TryAssembleFile(configuration); + return true; + } + catch { + return false; + } + } + + #region Get configuration + + [NonAction] + private ResumableConfiguration GetUploadConfiguration(MultipartFormDataStreamProvider provider) + { + return ResumableConfiguration.Create(identifier: GetId(provider), filename: GetFileName(provider), chunks: GetTotalChunks(provider)); + } + + [NonAction] + private string GetFileName(MultipartFormDataStreamProvider provider) + { + var filename = provider.FormData["resumableFilename"]; + return !String.IsNullOrEmpty(filename) ? filename : provider.FileData[0].Headers.ContentDisposition.FileName.Trim('\"'); + } + + [NonAction] + private string GetId(MultipartFormDataStreamProvider provider) + { + var id = provider.FormData["resumableIdentifier"]; + return !String.IsNullOrEmpty(id) ? id : Guid.NewGuid().ToString(); + } + + [NonAction] + private int GetTotalChunks(MultipartFormDataStreamProvider provider) + { + var total = provider.FormData["resumableTotalChunks"]; + return !String.IsNullOrEmpty(total) ? Convert.ToInt32(total) : 1; + } + + [NonAction] + private int GetChunkNumber(MultipartFormDataStreamProvider provider) + { + var chunk = provider.FormData["resumableChunkNumber"]; + return !String.IsNullOrEmpty(chunk) ? Convert.ToInt32(chunk) : 1; + } + + #endregion + + #region Chunk methods + + [NonAction] + private string GetChunkFileName(int chunkNumber, string identifier) + { + return Path.Combine(root, string.Format("{0}_{1}", identifier, chunkNumber.ToString())); + } + + [NonAction] + private void RenameChunk(MultipartFileData chunk, int chunkNumber, string identifier) + { + string generatedFileName = chunk.LocalFileName; + string chunkFileName = GetChunkFileName(chunkNumber, identifier); + if (File.Exists(chunkFileName)) File.Delete(chunkFileName); + File.Move(generatedFileName, chunkFileName); + + } + + [NonAction] + private string GetFilePath(ResumableConfiguration configuration) + { + return Path.Combine(root, configuration.Identifier); + } + + [NonAction] + private bool ChunkIsHere(int chunkNumber, string identifier) + { + string fileName = GetChunkFileName(chunkNumber, identifier); + return File.Exists(fileName); + } + + [NonAction] + private bool AllChunksAreHere(ResumableConfiguration configuration) + { + for (int chunkNumber = 1; chunkNumber <= configuration.Chunks; chunkNumber++) + if (!ChunkIsHere(chunkNumber, configuration.Identifier)) return false; + return true; + } + + [NonAction] + private void TryAssembleFile(ResumableConfiguration configuration) + { + if (AllChunksAreHere(configuration)) + { + // Create a single file + var path = ConsolidateFile(configuration); + + // Rename consolidated with original name of upload + RenameFile(path, Path.Combine(root, configuration.FileName)); + + // Delete chunk files + DeleteChunks(configuration); + } + } + + [NonAction] + private void DeleteChunks(ResumableConfiguration configuration) + { + for (int chunkNumber = 1; chunkNumber <= configuration.Chunks; chunkNumber++) + { + var chunkFileName = GetChunkFileName(chunkNumber, configuration.Identifier); + File.Delete(chunkFileName); + } + } + + [NonAction] + private string ConsolidateFile(ResumableConfiguration configuration) + { + var path = GetFilePath(configuration); + using (var destStream = File.Create(path, 15000)) + { + for (int chunkNumber = 1; chunkNumber <= configuration.Chunks; chunkNumber++) + { + var chunkFileName = GetChunkFileName(chunkNumber, configuration.Identifier); + using (var sourceStream = File.OpenRead(chunkFileName)) + { + sourceStream.CopyTo(destStream); + } + } + destStream.Close(); + } + + return path; + } + + #endregion + + [NonAction] + private string RenameFile(string sourceName, string targetName) + { + targetName = Path.GetFileName(targetName); // Strip to filename if directory is specified (avoid cross-directory attack) + string realFileName = Path.Combine(root, targetName); + if (File.Exists(realFileName)) File.Delete(realFileName); + File.Move(sourceName, realFileName); + return targetName; + } + } +} \ No newline at end of file diff --git a/samples/DotNET/Models/ResumableConfiguration.cs b/samples/DotNET/Models/ResumableConfiguration.cs new file mode 100644 index 00000000..034dfbb8 --- /dev/null +++ b/samples/DotNET/Models/ResumableConfiguration.cs @@ -0,0 +1,37 @@ +namespace Resumable.Models +{ + public class ResumableConfiguration + { + /// + /// Gets or sets number of expected chunks in this upload. + /// + public int Chunks { get; set; } + + /// + /// Gets or sets unique identifier for current upload. + /// + public string Identifier { get; set; } + + /// + /// Gets or sets file name. + /// + public string FileName { get; set; } + + public ResumableConfiguration() + { + + } + + /// + /// Creates an object with file upload configuration. + /// + /// Upload unique identifier. + /// File name. + /// Number of file chunks. + /// File upload configuration. + public static ResumableConfiguration Create(string identifier, string filename, int chunks) + { + return new ResumableConfiguration { Identifier = identifier, FileName = filename, Chunks = chunks }; + } + } +} \ No newline at end of file diff --git a/samples/Frontend in jQuery.md b/samples/Frontend in jQuery.md index b61184e3..b8486871 100644 --- a/samples/Frontend in jQuery.md +++ b/samples/Frontend in jQuery.md @@ -1,7 +1,7 @@ # Resumable.js front-end in jQuery [@steffentchr](http://twitter.com/steffentchr) -This library is originally built to work with [23 Video](http://www.23video.com), and the 23 uploader is a good example of: +This library is originally built to work with [TwentyThree](https://www.twentythree.com), and the 23 uploader is a good example of: * Selecing files or drag-dropping them in * Using events to build UI and progress bar @@ -11,16 +11,16 @@ This library is originally built to work with [23 Video](http://www.23video.com) * Building thumbnails from chunks to give better feedback during upload * Falling back to alternative upload options when Resumable.js is not supported. -There's [a free trial for 23 Video](http://www.23video.com/signup) if +There's [a free trial for TwentyThree](https://www.twentythree.com) if you want to see this in action, but the pieces are: * Resumable.js itself. -* [A piece of jQuery](http://reinvent.23video.com/resources/um/script/resumable-uploader.js), which sets up Resumable.js and glues it to the UI. -* [An API methods](http://www.23developer.com/api/photo-redeem-upload-token) with support for Resumable.js chunks and feedback. +* [A piece of jQuery](https://videos.twentythree.com/resources/um/script/resumable-uploader.js), which sets up Resumable.js and glues it to the UI. +* [An API methods](https://www.twentythree.com/api/photo-redeem-upload-token) with support for Resumable.js chunks and feedback. * Finally, some HTML elements for the glue script to use. ```html -
+
Drop video files here to upload or select from your computer
diff --git a/samples/Node.js/README.md b/samples/Node.js/README.md index 54f3d62a..63df4561 100644 --- a/samples/Node.js/README.md +++ b/samples/Node.js/README.md @@ -1,18 +1,23 @@ # Sample code for Node.js -This sample is written for [Node.js 0.6+](http://nodejs.org/) and requires [Express](http://expressjs.com/) to make the sample code cleaner. +This sample is written for [Node.js 0.10+](http://nodejs.org/) and requires +[Express 4+](http://expressjs.com/) to make the sample code cleaner. To install and run: cd samples/Node.js - npm install express + npm install node app.js Then browse to [localhost:3000](http://localhost:3000). - ## Enabling Cross-domain Uploads -If you would like to load the resumable.js library from one domain and have your Node.js reside on another, you must allow 'Access-Control-Allow-Origin' from '*'. Please remember, there are some potential security risks with enabling this functionality. If you would still like to implement cross-domain uploads, open app.js and uncomment lines 24-31 and uncomment line 17. +If you would like to load the resumable.js library from one domain and have your +Node.js reside on another, you must set the header +`Access-Control-Allow-Origin: *`. Please remember, there are some potential +security risks with enabling this functionality. If you would still like to +implement cross-domain uploads, open app.js and uncomment lines 12-15. -Then in public/index.html, on line 49, update the target with your server's address. For example: target:'http://www.example.com/upload' +Then in public/index.html, on line 49, update the target with your servers +address. For example: target:'http://www.example.com/upload' diff --git a/samples/Node.js/app.js b/samples/Node.js/app.js index 860b7786..6ad4b264 100644 --- a/samples/Node.js/app.js +++ b/samples/Node.js/app.js @@ -1,45 +1,49 @@ var express = require('express'); var resumable = require('./resumable-node.js')('/tmp/resumable.js/'); var app = express(); +var multipart = require('connect-multiparty'); +var crypto = require('crypto'); // Host most stuff in the public folder app.use(express.static(__dirname + '/public')); -app.use(express.bodyParser()); +app.use(multipart()); + +// Uncomment to allow CORS +// app.use(function (req, res, next) { +// res.header('Access-Control-Allow-Origin', '*'); +// next(); +// }); + +// retrieve file id. invoke with /fileid?filename=my-file.jpg +app.get('/fileid', function(req, res){ + if(!req.query.filename){ + return res.status(500).end('query parameter missing'); + } + // create md5 hash from filename + res.end( + crypto.createHash('md5') + .update(req.query.filename) + .digest('hex') + ); +}); // Handle uploads through Resumable.js app.post('/upload', function(req, res){ - - // console.log(req); - resumable.post(req, function(status, filename, original_filename, identifier){ console.log('POST', status, original_filename, identifier); - res.send(status, { - // NOTE: Uncomment this funciton to enable cross-domain request. - //'Access-Control-Allow-Origin': '*' - }); + res.send(status); }); }); -// Handle cross-domain requests -// NOTE: Uncomment this funciton to enable cross-domain request. -/* - app.options('/upload', function(req, res){ - console.log('OPTIONS'); - res.send(true, { - 'Access-Control-Allow-Origin': '*' - }, 200); - }); -*/ - // Handle status checks on chunks through Resumable.js app.get('/upload', function(req, res){ resumable.get(req, function(status, filename, original_filename, identifier){ console.log('GET', status); - res.send(status, (status == 'found' ? 200 : 404)); - }); - }); + res.send((status == 'found' ? 200 : 404), status); + }); +}); app.get('/download/:identifier', function(req, res){ resumable.write(req.params.identifier, res); diff --git a/samples/Node.js/package.json b/samples/Node.js/package.json new file mode 100644 index 00000000..cdc11dff --- /dev/null +++ b/samples/Node.js/package.json @@ -0,0 +1,11 @@ +{ + "name": "resumable.js", + "version": "0.0.1", + "scripts": { + "start": "node app.js" + }, + "dependencies": { + "express": "~4.6.1", + "connect-multiparty": "~1.1.0" + } +} diff --git a/samples/Node.js/public/index-async.html b/samples/Node.js/public/index-async.html new file mode 100644 index 00000000..55f6645e --- /dev/null +++ b/samples/Node.js/public/index-async.html @@ -0,0 +1,119 @@ + + + + Resumable.js - Multiple simultaneous, stable and resumable uploads via the HTML5 File API + + + + +
+ +

Resumable.js

+

It's a JavaScript library providing multiple simultaneous, stable and resumable uploads via the HTML5 File API.

+ +

The library is designed to introduce fault-tolerance into the upload of large files through HTTP. This is done by splitting each files into small chunks; whenever the upload of a chunk fails, uploading is retried until the procedure completes. This allows uploads to automatically resume uploading after a network connection is lost either locally or to the server. Additionally, it allows for users to pause and resume uploads without loosing state.

+ +

Resumable.js relies on the HTML5 File API and the ability to chunks files into smaller pieces. Currently, this means that support is limited to Firefox 4+ and Chrome 11+.

+ +
+ +

Demo with async id generation

+ + + +
+ Your browser, unfortunately, is not supported by Resumable.js. The library requires support for the HTML5 File API along with file slicing. +
+ +
+ Drop video files here to upload or select from your computer +
+ +
+ + + + + + +
+ + + +
+
+ +
    + + + +
    + + diff --git a/samples/Node.js/public/index.html b/samples/Node.js/public/index.html index b90bbb3d..0efd086f 100644 --- a/samples/Node.js/public/index.html +++ b/samples/Node.js/public/index.html @@ -25,7 +25,7 @@

    Demo

    Your browser, unfortunately, is not supported by Resumable.js. The library requires support for the HTML5 File API along with file slicing.
    -
    +
    Drop video files here to upload or select from your computer
    diff --git a/samples/Node.js/public/style.css b/samples/Node.js/public/style.css index 026656fc..6b447b13 100644 --- a/samples/Node.js/public/style.css +++ b/samples/Node.js/public/style.css @@ -19,7 +19,7 @@ body {text-align:center; margin:40px;} /* Uploader: Drag & Drop */ .resumable-error {display:none; font-size:14px; font-style:italic;} .resumable-drop {padding:15px; font-size:13px; text-align:center; color:#666; font-weight:bold;background-color:#eee; border:2px dashed #aaa; border-radius:10px; margin-top:40px; z-index:9999; display:none;} -.resumable-dragover {padding:30px; color:#555; background-color:#ddd; border:1px solid #999;} +.dragover {padding:30px; color:#555; background-color:#ddd; border:1px solid #999;} /* Uploader: Progress bar */ .resumable-progress {margin:30px 0 30px 0; width:100%; display:none;} @@ -48,4 +48,4 @@ body {text-align:center; margin:40px;} /* Uploader: Error status */ .is-error .uploader-item:hover, .is-active.is-error .uploader-item {border-color:#900;} .is-error .uploader-item:hover .uploader-item-title, .is-active.is-error .uploader-item .uploader-item-title {background-color:rgba(153,0,0,0.6);} -.is-error .uploader-item-creating-thumbnail {display:none;} \ No newline at end of file +.is-error .uploader-item-creating-thumbnail {display:none;} diff --git a/samples/Node.js/resumable-node.js b/samples/Node.js/resumable-node.js index 0c07ddf9..4a1fd567 100644 --- a/samples/Node.js/resumable-node.js +++ b/samples/Node.js/resumable-node.js @@ -71,7 +71,7 @@ module.exports = resumable = function(temporaryFolder){ if(validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename)=='valid') { var chunkFilename = getChunkFilename(chunkNumber, identifier); - path.exists(chunkFilename, function(exists){ + fs.exists(chunkFilename, function(exists){ if(exists){ callback('found', chunkFilename, filename, identifier); } else { @@ -115,7 +115,7 @@ module.exports = resumable = function(temporaryFolder){ var currentTestChunk = 1; var numberOfChunks = Math.max(Math.floor(totalSize/(chunkSize*1.0)), 1); var testChunkExists = function(){ - path.exists(getChunkFilename(currentTestChunk, identifier), function(exists){ + fs.exists(getChunkFilename(currentTestChunk, identifier), function(exists){ if(exists){ currentTestChunk++; if(currentTestChunk>numberOfChunks) { @@ -153,7 +153,7 @@ module.exports = resumable = function(temporaryFolder){ var pipeChunk = function(number) { var chunkFilename = getChunkFilename(number, identifier); - path.exists(chunkFilename, function(exists) { + fs.exists(chunkFilename, function(exists) { if (exists) { // If the chunk with the current number exists, @@ -188,12 +188,12 @@ module.exports = resumable = function(temporaryFolder){ var chunkFilename = getChunkFilename(number, identifier); //console.log('removing pipeChunkRm ', number, 'chunkFilename', chunkFilename); - path.exists(chunkFilename, function(exists) { + fs.exists(chunkFilename, function(exists) { if (exists) { console.log('exist removing ', chunkFilename); fs.unlink(chunkFilename, function(err) { - if (options.onError) opentions.onError(err); + if (err && options.onError) options.onError(err); }); pipeChunkRm(number + 1); @@ -209,4 +209,4 @@ module.exports = resumable = function(temporaryFolder){ } return $; -} \ No newline at end of file +} diff --git a/samples/coffeescript/resumable.coffee b/samples/coffeescript/resumable.coffee index d1713cef..8f28f21f 100644 --- a/samples/coffeescript/resumable.coffee +++ b/samples/coffeescript/resumable.coffee @@ -11,6 +11,15 @@ window.Resumable = class Resumable forceChunkSize: false simultaneousUploads: 3 fileParameterName: 'file' + chunkNumberParameterName: 'resumableChunkNumber' + chunkSizeParameterName: 'resumableChunkSize' + currentChunkSizeParameterName: 'resumableCurrentChunkSize' + totalSizeParameterName: 'resumableTotalSize' + typeParameterName: 'resumableType' + identifierParameterName: 'resumableIdentifier' + fileNameParameterName: 'resumableFilename' + relativePathParameterName: 'resumableRelativePath' + totalChunksParameterName: 'resumableTotalChunks' throttleProgressCallbacks: 0.5 query: {} headers: {} @@ -36,6 +45,7 @@ window.Resumable = class Resumable maxFileSizeErrorCallback: (file, errorCount) -> #TODO @getOpt alert(file.fileName +' is too large, please upload files less than ' + @formatSize(@getOpt('maxFileSize')) + '.') + dragOverClass: 'dragover' @opt = {} if not @opt? @events = [] @@ -352,13 +362,15 @@ window.ResumableChunk = class ResumableChunk pushParams key, value #Add extra data to identify chunk - @pushParams params, 'resumableChunkNumber', (@offset + 1) - @pushParams params, 'resumableChunkSize', @chunkSize - @pushParams params, 'resumableCurrentChunkSize', (@endByte - @startByte) - @pushParams params, 'resumableTotalSize', @fileObjSize - @pushParams params, 'resumableIdentifier', @fileObj.uniqueIdentifier - @pushParams params, 'resumableFilename', @fileObj.fileName - @pushParams params, 'resumableRelativePath', @fileObj.relativePath + @pushParams params, (@getOpt 'chunkNumberParameterName'), (@offset + 1) + @pushParams params, (@getOpt 'chunkSizeParameterName'), @chunkSize + @pushParams params, (@getOpt 'currentChunkSizeParameterName'), (@endByte - @startByte) + @pushParams params, (@getOpt 'totalSizeParameterName'), @fileObjSize + #TODO: @pushParams params, (@getOpt 'typeParameterName'), + @pushParams params, (@getOpt 'identifierParameterName'), @fileObj.uniqueIdentifier + @pushParams params, (@getOpt 'fileNameParameterName'), @fileObj.fileName + @pushParams params, (@getOpt 'relativePathParameterName'), @fileObj.relativePath + #TODO: @pushParams params, (@getOpt 'totalChunksParameterName'), #Append the relevant chunk and send it @xhr.open 'GET', @getOpt('target') + '?' + params.join('&') @@ -415,7 +427,7 @@ window.ResumableChunk = class ResumableChunk @callback 'retry', @message() @abort() @retries++ - retryInterval = getOpt('chunkRetryInterval') + retryInterval = @getOpt('chunkRetryInterval') if retryInterval? setTimeout @send, retryInterval @@ -442,15 +454,17 @@ window.ResumableChunk = class ResumableChunk target = @getOpt 'target' #Set up the basic query data from Resumable - query = - resumableChunkNumber: @offset+1 - resumableChunkSize: @getOpt('chunkSize') - resumableCurrentChunkSize: @endByte - @startByte - resumableTotalSize: @fileObjSize - resumableIdentifier: @fileObj.uniqueIdentifier - resumableFilename: @fileObj.fileName - resumableRelativePath: @fileObj.relativePath - + query = {} + + query[(@getOpt 'chunkNumber')] = @offset+1 + query[(@getOpt 'chunkSize')] = @getOpt('chunkSize') + query[(@getOpt 'currentChunkSize')] = @endByte - @startByte + query[(@getOpt 'totalSize')] = @fileObjSize + #TODO: query[(@getOpt 'typeParameterName')] = + query[(@getOpt 'identifier')] = @fileObj.uniqueIdentifier + query[(@getOpt 'filename')] = @fileObj.fileName + query[(@getOpt 'relativePath')] = @fileObj.relativePath + #TODO: query[(@getOpt 'totalChunksParameterName')] = customQuery = @getOpt 'query' customQuery = customQuery(@fileObj, @) if typeof customQuery is 'function' diff --git a/samples/coffeescript/resumable.js b/samples/coffeescript/resumable.js index cc1056e9..c4830f05 100644 --- a/samples/coffeescript/resumable.js +++ b/samples/coffeescript/resumable.js @@ -1,800 +1,1164 @@ -//@ sourceMappingURL=resumable.map -// Generated by CoffeeScript 1.6.1 -(function() { - var Resumable, ResumableChunk, ResumableFile, - __slice = [].slice, - __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; - - window.Resumable = Resumable = (function() { - - function Resumable(opt) { - this.opt = opt; - console.log('constructor'); - this.support = (typeof File !== "undefined" && File !== null) && (typeof Blob !== "undefined" && Blob !== null) && (typeof FileList !== "undefined" && FileList !== null) && ((Blob.prototype.webkitSlice != null) || (Blob.prototype.mozSlice != null) || (Blob.prototype.slice != null)); - this.files = []; - this.defaults = { - chunkSize: 1 * 1024 * 1024, - forceChunkSize: false, - simultaneousUploads: 3, - fileParameterName: 'file', - throttleProgressCallbacks: 0.5, - query: {}, - headers: {}, - preprocess: null, - method: 'multipart', - prioritizeFirstAndLastChunk: false, - target: '/', - testChunks: true, - generateUniqueIdentifier: null, - maxChunkRetries: void 0, - chunkRetryInterval: void 0, - permanentErrors: [415, 500, 501], - maxFiles: void 0, - maxFilesErrorCallback: function(files, errorCount) { - var maxFiles, _ref; - maxFiles = this.getOpt('maxFiles'); - return alert('Please upload ' + maxFiles + ' file' + ((_ref = maxFiles === 1) != null ? _ref : { - '': 's' - }) + ' at a time.'); - }, - minFileSize: void 0, - minFileSizeErrorCallback: function(file, errorCount) { - return alert(file.fileName(+' is too small, please upload files larger than ' + this.formatSize(this.getOpt('minFileSize')) + '.')); - }, - maxFileSize: void 0, - maxFileSizeErrorCallback: function(file, errorCount) { - return alert(file.fileName(+' is too large, please upload files less than ' + this.formatSize(this.getOpt('maxFileSize')) + '.')); - } - }; - if (this.opt == null) { - this.opt = {}; - } - this.events = []; - } +/* +* MIT Licensed +* https://www.twentythree.com +* https://github.com/23/resumable.js +* Steffen Fagerström Christensen, steffen@twentythree.com +*/ - Resumable.prototype.getOpt = function(o) { - var item, opts, _i, _len; - if (o instanceof Array) { - opts = {}; - for (_i = 0, _len = o.length; _i < _len; _i++) { - item = o[_i]; - opts[item] = this.getOpt(item); - } - return opts; - } else { - if (this.opt[o] != null) { - return this.opt[o]; - } else { - return this.defaults[o]; - } - } - }; +(function(){ +"use strict"; - Resumable.prototype.formatSize = function(size) { - if (size < 1024) { - return size + ' bytes'; - } else if (size < 1024 * 1024) { - return (size / 1024.0).toFixed(0) + ' KB'; - } else if (size < 1024 * 1024 * 1024) { - return (size / 1024.0 / 1024.0).toFixed(1) + ' MB'; - } else { - return (size / 1024.0 / 1024.0 / 1024.0).toFixed(1) + ' GB'; - } - }; + var Resumable = function(opts){ + if ( !(this instanceof Resumable) ) { + return new Resumable(opts); + } + this.version = 1.0; + // SUPPORTED BY BROWSER? + // Check if these features are support by the browser: + // - File object type + // - Blob object type + // - FileList object type + // - slicing files + this.support = ( + (typeof(File)!=='undefined') + && + (typeof(Blob)!=='undefined') + && + (typeof(FileList)!=='undefined') + && + (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false) + ); + if(!this.support) return(false); - Resumable.prototype.stopEvent = function(e) { - console.log('stopEvent'); - e.stopPropagation(); - return e.preventDefault(); - }; - Resumable.prototype.generateUniqueIdentifier = function(file) { - var custom, relativePath, size; - console.log('generateUniqueIdentifier'); - custom = this.getOpt('generateUniqueIdentifier'); - if (typeof custom === 'function') { - return custom(file); - } else { - relativePath = file.webkitRelativePath || file.fileName || file.name; - size = file.size; - return size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''); + // PROPERTIES + var $ = this; + $.files = []; + $.defaults = { + chunkSize:1*1024*1024, + forceChunkSize:false, + simultaneousUploads:3, + fileParameterName:'file', + chunkNumberParameterName: 'resumableChunkNumber', + chunkSizeParameterName: 'resumableChunkSize', + currentChunkSizeParameterName: 'resumableCurrentChunkSize', + totalSizeParameterName: 'resumableTotalSize', + typeParameterName: 'resumableType', + identifierParameterName: 'resumableIdentifier', + fileNameParameterName: 'resumableFilename', + relativePathParameterName: 'resumableRelativePath', + totalChunksParameterName: 'resumableTotalChunks', + dragOverClass: 'dragover', + throttleProgressCallbacks: 0.5, + query:{}, + headers:{}, + preprocess:null, + preprocessFile:null, + method:'multipart', + uploadMethod: 'POST', + testMethod: 'GET', + prioritizeFirstAndLastChunk:false, + target:'/', + testTarget: null, + parameterNamespace:'', + testChunks:true, + generateUniqueIdentifier:null, + getTarget:null, + maxChunkRetries:100, + chunkRetryInterval:undefined, + permanentErrors:[400, 404, 409, 415, 500, 501], + maxFiles:undefined, + withCredentials:false, + xhrTimeout:0, + clearInput:true, + chunkFormat:'blob', + setChunkTypeFromFile:false, + maxFilesErrorCallback:function (files, errorCount) { + var maxFiles = $.getOpt('maxFiles'); + alert('Please upload no more than ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.'); + }, + minFileSize:1, + minFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.'); + }, + maxFileSize:undefined, + maxFileSizeErrorCallback:function(file, errorCount) { + alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.'); + }, + fileType: [], + fileTypeErrorCallback: function(file, errorCount) { + alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.'); } }; - - Resumable.prototype.on = function(event, callback) { - console.log("on: " + event); - return this.events.push({ - event: event, - callback: callback - }); - }; - - Resumable.prototype.fire = function() { - var args, e, event, _i, _len, _ref; - args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; - console.log("fire: " + args[0]); - event = args[0].toLowerCase(); - _ref = this.events; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - e = _ref[_i]; - if (e.event.toLowerCase() === event) { - e.callback.apply(this, args.slice(1)); - } - if (e.event.toLowerCase() === 'catchall') { - e.callback.apply(null, args); - } + $.opts = opts||{}; + $.getOpt = function(o) { + var $opt = this; + // Get multiple option if passed an array + if(o instanceof Array) { + var options = {}; + $h.each(o, function(option){ + options[option] = $opt.getOpt(option); + }); + return options; } - if (event === 'fireerror') { - this.fire('error', args[2], args[1]); + // Otherwise, just return a simple option + if ($opt instanceof ResumableChunk) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.fileObj; } } - if (event === 'fileprogress') { - return this.fire('progress'); + if ($opt instanceof ResumableFile) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { $opt = $opt.resumableObj; } + } + if ($opt instanceof Resumable) { + if (typeof $opt.opts[o] !== 'undefined') { return $opt.opts[o]; } + else { return $opt.defaults[o]; } } }; + $.indexOf = function(array, obj) { + if (array.indexOf) { return array.indexOf(obj); } + for (var i = 0; i < array.length; i++) { + if (array[i] === obj) { return i; } + } + return -1; + } - Resumable.prototype.onDrop = function(event) { - console.log("onDrop"); - this.stopEvent(event); - return this.appendFilesFromFileList(event.dataTransfer.files, event); - }; - - Resumable.prototype.onDragOver = function(event) { - console.log("onDragOver"); - return event.preventDefault(); + // EVENTS + // catchAll(event, ...) + // fileSuccess(file), fileProgress(file), fileAdded(file, event), filesAdded(files, filesSkipped), fileRetry(file), + // fileError(file, message), complete(), progress(), error(message, file), pause() + $.events = []; + $.on = function(event,callback){ + $.events.push(event.toLowerCase(), callback); }; - - Resumable.prototype.appendFilesFromFileList = function(fileList, event) { - var errorCount, file, files, maxFileSize, maxFileSizeErrorCallback, maxFiles, maxFilesErrorCallback, minFileSize, minFileSizeErrorCallback, resumableFile, _i, _len, _ref; - console.log("appendFilesFromFileList"); - errorCount = 0; - _ref = this.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback']), maxFiles = _ref[0], minFileSize = _ref[1], maxFileSize = _ref[2], maxFilesErrorCallback = _ref[3], minFileSizeErrorCallback = _ref[4], maxFileSizeErrorCallback = _ref[5]; - if ((maxFiles != null) && maxFiles < (fileList.length + this.files.length)) { - maxFilesErrorCallback(fileList, errorCount++); - return false; - } - files = []; - for (_i = 0, _len = fileList.length; _i < _len; _i++) { - file = fileList[_i]; - file.name = file.fileName = file.name || file.fileName; - if ((minFileSize != null) && file.size < minFileSize) { - minFileSizeErrorCallback(file, errorCount++); - return false; - } - if ((maxFileSize != null) && file.size > maxFileSize) { - maxFilesErrorCallback(file, errorCount++); - return false; - } - if (file.size > 0 && !this.getFromUniqueIdentifier(this.generateUniqueIdentifier(file))) { - resumableFile = new ResumableFile(this, file); - this.files.push(resumableFile); - files.push(resumableFile); - this.fire('fileAdded', resumableFile, event); - } + $.fire = function(){ + // `arguments` is an object, not array, in FF, so: + var args = []; + for (var i=0; i 1 && file.chunks[file.chunks.length - 1].status() === 'pending' && file.chunks[file.chunks.length - 1].preprocessState === 0) { - file.chunks[file.chunks.length - 1].send(); - found = true; - break; + } else { + for (i in o) { + // Object + if(callback(i,o[i])===false) return; } } - if (found) { - return true; + }, + generateUniqueIdentifier:function(file, event){ + var custom = $.getOpt('generateUniqueIdentifier'); + if(typeof custom === 'function') { + return custom(file, event); } - } - _ref1 = this.files; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - file = _ref1[_j]; - _ref2 = file.chunks; - for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { - chunk = _ref2[_k]; - if (chunk.status() === 'pending' && chunk.preprocessState === 0) { - chunk.send(); - found = true; - break; + var relativePath = file.webkitRelativePath||file.relativePath||file.fileName||file.name; // Some confusion in different versions of Firefox + var size = file.size; + return(size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')); + }, + contains:function(array,test) { + var result = false; + + $h.each(array, function(value) { + if (value == test) { + result = true; + return false; } + return true; + }); + + return result; + }, + formatSize:function(size){ + if(size<1024) { + return size + ' bytes'; + } else if(size<1024*1024) { + return (size/1024.0).toFixed(0) + ' KB'; + } else if(size<1024*1024*1024) { + return (size/1024.0/1024.0).toFixed(1) + ' MB'; + } else { + return (size/1024.0/1024.0/1024.0).toFixed(1) + ' GB'; } - if (found) { - break; + }, + getTarget:function(request, params){ + var target = $.getOpt('target'); + + if (request === 'test' && $.getOpt('testTarget')) { + target = $.getOpt('testTarget') === '/' ? $.getOpt('target') : $.getOpt('testTarget'); + } + + if (typeof target === 'function') { + return target(params); } + + var separator = target.indexOf('?') < 0 ? '?' : '&'; + var joinedParams = params.join('&'); + + return target + separator + joinedParams; } - if (found) { - return true; + }; + + var onDrop = function(e){ + e.currentTarget.classList.remove($.getOpt('dragOverClass')); + $h.stopEvent(e); + + //handle dropped things as items if we can (this lets us deal with folders nicer in some cases) + if (e.dataTransfer && e.dataTransfer.items) { + loadFiles(e.dataTransfer.items, event); } - _ref3 = this.files; - for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { - file = _ref3[_l]; - outstanding = false; - _ref4 = file.chunks; - for (_m = 0, _len4 = _ref4.length; _m < _len4; _m++) { - chunk = _ref4[_m]; - status = chunk.status(); - if (status === 'pending' || status === 'uploading' || chunk.preprocessState === 1) { - outstanding = true; - break; - } - } - if (outstanding) { - break; - } + //else handle them as files + else if (e.dataTransfer && e.dataTransfer.files) { + loadFiles(e.dataTransfer.files, event); } - if (!outstanding) { - this.fire('complete'); + }; + var onDragLeave = function(e){ + e.currentTarget.classList.remove($.getOpt('dragOverClass')); + }; + var onDragOverEnter = function(e) { + e.preventDefault(); + var dt = e.dataTransfer; + if ($.indexOf(dt.types, "Files") >= 0) { // only for file drop + e.stopPropagation(); + dt.dropEffect = "copy"; + dt.effectAllowed = "copy"; + e.currentTarget.classList.add($.getOpt('dragOverClass')); + } else { // not work on IE/Edge.... + dt.dropEffect = "none"; + dt.effectAllowed = "none"; } - return false; }; - Resumable.prototype.assignBrowse = function(domNodes, isDirectory) { - var changeHandler, dn, input, maxFiles, _i, _len, - _this = this; - console.log("assignBrowse"); - if (domNodes.length == null) { - domNodes = [domNodes]; + /** + * processes a single upload item (file or directory) + * @param {Object} item item to upload, may be file or directory entry + * @param {string} path current file path + * @param {File[]} items list of files to append new items to + * @param {Function} cb callback invoked when item is processed + */ + function processItem(item, path, items, cb) { + var entry; + if(item.isFile){ + // file provided + return item.file(function(file){ + file.relativePath = path + file.name; + items.push(file); + cb(); + }); + }else if(item.isDirectory){ + // item is already a directory entry, just assign + entry = item; + }else if(item instanceof File) { + items.push(item); } - for (_i = 0, _len = domNodes.length; _i < _len; _i++) { - dn = domNodes[_i]; - if (dn.tagName === 'INPUT' && dn.type === 'file') { - input = dn; - } else { - input = document.createElement('input'); - input.setAttribute('type', 'file'); - dn.style.display = 'inline-block'; - dn.style.position = 'relative'; - input.style.position = 'absolute'; - input.style.top = input.style.left = input.style.bottom = input.style.right = 0; - input.style.opacity = 0; - input.style.cursor = 'pointer'; - dn.appendChild(input); - } + if('function' === typeof item.webkitGetAsEntry){ + // get entry from file object + entry = item.webkitGetAsEntry(); } - maxFiles = this.getOpt('maxFiles'); - if ((maxFiles != null) || maxFiles !== 1) { - input.setAttribute('multiple', 'multiple'); - } else { - input.removeAttribute('multiple'); + if(entry && entry.isDirectory){ + // directory provided, process it + return processDirectory(entry, path + entry.name + '/', items, cb); } - if (isDirectory) { - input.setAttribute('webkitdirectory', 'webkitdirectory'); - } else { - input.removeAttribute('webkitdirectory'); + if('function' === typeof item.getAsFile){ + // item represents a File object, convert it + item = item.getAsFile(); + if(item instanceof File) { + item.relativePath = path + item.name; + items.push(item); + } } - changeHandler = function(e) { - _this.appendFilesFromFileList(e.target.files); - return e.target.value = ''; - }; - return input.addEventListener('change', changeHandler, false); - }; + cb(); // indicate processing is done + } - Resumable.prototype.assignDrop = function(domNodes) { - var dn, _i, _len, _results; - console.log("assignDrop"); - if (domNodes.length == null) { - domNodes = [domNodes]; + + /** + * cps-style list iteration. + * invokes all functions in list and waits for their callback to be + * triggered. + * @param {Function[]} items list of functions expecting callback parameter + * @param {Function} cb callback to trigger after the last callback has been invoked + */ + function processCallbacks(items, cb){ + if(!items || items.length === 0){ + // empty or no list, invoke callback + return cb(); } - _results = []; - for (_i = 0, _len = domNodes.length; _i < _len; _i++) { - dn = domNodes[_i]; - dn.addEventListener('dragover', this.onDragOver, false); - _results.push(dn.addEventListener('drop', this.onDrop, false)); + // invoke current function, pass the next part as continuation + items[0](function(){ + processCallbacks(items.slice(1), cb); + }); + } + + /** + * recursively traverse directory and collect files to upload + * @param {Object} directory directory to process + * @param {string} path current path + * @param {File[]} items target list of items + * @param {Function} cb callback invoked after traversing directory + */ + function processDirectory (directory, path, items, cb) { + var dirReader = directory.createReader(); + var allEntries = []; + + function readEntries () { + dirReader.readEntries(function(entries){ + if (entries.length) { + allEntries = allEntries.concat(entries); + return readEntries(); + } + + // process all conversion callbacks, finally invoke own one + processCallbacks( + allEntries.map(function(entry){ + // bind all properties except for callback + return processItem.bind(null, entry, path, items); + }), + cb + ); + }); } - return _results; - }; - Resumable.prototype.unAssignDrop = function(domNodes) { - var dn, _i, _len, _results; - console.log("unAssignDrop"); - if (domNodes.length == null) { - domNodes = [domNodes]; + readEntries(); + } + + /** + * process items to extract files to be uploaded + * @param {File[]} items items to process + * @param {Event} event event that led to upload + */ + function loadFiles(items, event) { + if(!items.length){ + return; // nothing to do } - _results = []; - for (_i = 0, _len = domNodes.length; _i < _len; _i++) { - dn = domNodes[_i]; - dn.removeEventListener('dragover', this.onDragOver); - _results.push(dn.removeEventListener('drop', this.onDrop)); + $.fire('beforeAdd'); + var files = []; + processCallbacks( + Array.prototype.map.call(items, function(item){ + // bind all properties except for callback + return processItem.bind(null, item, "", files); + }), + function(){ + if(files.length){ + // at least one file found + appendFilesFromFileList(files, event); + } + } + ); + }; + + var appendFilesFromFileList = function(fileList, event){ + // check for uploading too many files + var errorCount = 0; + var o = $.getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback', 'fileType', 'fileTypeErrorCallback']); + if (typeof(o.maxFiles)!=='undefined' && o.maxFiles<(fileList.length+$.files.length)) { + // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file + if (o.maxFiles===1 && $.files.length===1 && fileList.length===1) { + $.removeFile($.files[0]); + } else { + o.maxFilesErrorCallback(fileList, errorCount++); + return false; + } } - return _results; + var files = [], filesSkipped = [], remaining = fileList.length; + var decreaseReamining = function(){ + if(!--remaining){ + // all files processed, trigger event + if(!files.length && !filesSkipped.length){ + // no succeeded files, just skip + return; + } + window.setTimeout(function(){ + $.fire('filesAdded', files, filesSkipped); + },0); + } + }; + $h.each(fileList, function(file){ + var fileName = file.name; + var fileType = file.type; // e.g video/mp4 + if(o.fileType.length > 0){ + var fileTypeFound = false; + for(var index in o.fileType){ + // For good behaviour we do some inital sanitizing. Remove spaces and lowercase all + o.fileType[index] = o.fileType[index].replace(/\s/g, '').toLowerCase(); + + // Allowing for both [extension, .extension, mime/type, mime/*] + var extension = ((o.fileType[index].match(/^[^.][^/]+$/)) ? '.' : '') + o.fileType[index]; + + if ((fileName.substr(-1 * extension.length).toLowerCase() === extension) || + //If MIME type, check for wildcard or if extension matches the files tiletype + (extension.indexOf('/') !== -1 && ( + (extension.indexOf('*') !== -1 && fileType.substr(0, extension.indexOf('*')) === extension.substr(0, extension.indexOf('*'))) || + fileType === extension + )) + ){ + fileTypeFound = true; + break; + } + } + if (!fileTypeFound) { + o.fileTypeErrorCallback(file, errorCount++); + return true; + } + } + + if (typeof(o.minFileSize)!=='undefined' && file.sizeo.maxFileSize) { + o.maxFileSizeErrorCallback(file, errorCount++); + return true; + } + + function addFile(uniqueIdentifier){ + if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {(function(){ + file.uniqueIdentifier = uniqueIdentifier; + var f = new ResumableFile($, file, uniqueIdentifier); + $.files.push(f); + files.push(f); + f.container = (typeof event != 'undefined' ? event.srcElement : null); + window.setTimeout(function(){ + $.fire('fileAdded', f, event) + },0); + })()} else { + filesSkipped.push(file); + }; + decreaseReamining(); + } + // directories have size == 0 + var uniqueIdentifier = $h.generateUniqueIdentifier(file, event); + if(uniqueIdentifier && typeof uniqueIdentifier.then === 'function'){ + // Promise or Promise-like object provided as unique identifier + uniqueIdentifier + .then( + function(uniqueIdentifier){ + // unique identifier generation succeeded + addFile(uniqueIdentifier); + }, + function(){ + // unique identifier generation failed + // skip further processing, only decrease file count + decreaseReamining(); + } + ); + }else{ + // non-Promise provided as unique identifier, process synchronously + addFile(uniqueIdentifier); + } + }); }; - Resumable.prototype.isUploading = function() { - var chunk, file, uploading, _i, _j, _len, _len1, _ref, _ref1; - uploading = false; - _ref = this.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - file = _ref[_i]; - _ref1 = file.chunks; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - chunk = _ref1[_j]; - if (chunk.status() === 'uploading') { + // INTERNAL OBJECT TYPES + function ResumableFile(resumableObj, file, uniqueIdentifier){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $._prevProgress = 0; + $.resumableObj = resumableObj; + $.file = file; + $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox + $.size = file.size; + $.relativePath = file.relativePath || file.webkitRelativePath || $.fileName; + $.uniqueIdentifier = uniqueIdentifier; + $._pause = false; + $.container = ''; + $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished + var _error = uniqueIdentifier !== undefined; + + // Callback when something happens within the chunk + var chunkEvent = function(event, message){ + // event can be 'progress', 'success', 'error' or 'retry' + switch(event){ + case 'progress': + $.resumableObj.fire('fileProgress', $, message); + break; + case 'error': + $.abort(); + _error = true; + $.chunks = []; + $.resumableObj.fire('fileError', $, message); + break; + case 'success': + if(_error) return; + $.resumableObj.fire('fileProgress', $, message); // it's at least progress + if($.isComplete()) { + $.resumableObj.fire('fileSuccess', $, message); + } + break; + case 'retry': + $.resumableObj.fire('fileRetry', $); + break; + } + }; + + // Main code to set up a file object with chunks, + // packaged to be able to handle retries if needed. + $.chunks = []; + $.abort = function(){ + // Stop current uploads + var abortCount = 0; + $h.each($.chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + abortCount++; + } + }); + if(abortCount>0) $.resumableObj.fire('fileProgress', $); + }; + $.cancel = function(){ + // Reset this file to be void + var _chunks = $.chunks; + $.chunks = []; + // Stop current uploads + $h.each(_chunks, function(c){ + if(c.status()=='uploading') { + c.abort(); + $.resumableObj.uploadNextChunk(); + } + }); + $.resumableObj.removeFile($); + $.resumableObj.fire('fileProgress', $); + }; + $.retry = function(){ + $.bootstrap(); + var firedRetry = false; + $.resumableObj.on('chunkingComplete', function(){ + if(!firedRetry) $.resumableObj.upload(); + firedRetry = true; + }); + }; + $.bootstrap = function(){ + $.abort(); + _error = false; + // Rebuild stack of chunks from file + $.chunks = []; + $._prevProgress = 0; + var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor; + var maxOffset = Math.max(round($.file.size/$.getOpt('chunkSize')),1); + for (var offset=0; offset0.99999 ? 1 : ret)); + ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused + $._prevProgress = ret; + return(ret); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.chunks, function(chunk){ + if(chunk.status()=='uploading') { uploading = true; - break; + return(false); } + }); + return(uploading); + }; + $.isComplete = function(){ + var outstanding = false; + if ($.preprocessState === 1) { + return(false); } - if (uploading) { - break; + $h.each($.chunks, function(chunk){ + var status = chunk.status(); + if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { + outstanding = true; + return(false); + } + }); + return(!outstanding); + }; + $.pause = function(pause){ + if(typeof(pause)==='undefined'){ + $._pause = ($._pause ? false : true); + }else{ + $._pause = pause; + } + }; + $.isPaused = function() { + return $._pause; + }; + $.preprocessFinished = function(){ + $.preprocessState = 2; + $.upload(); + }; + $.upload = function () { + var found = false; + if ($.isPaused() === false) { + var preprocess = $.getOpt('preprocessFile'); + if(typeof preprocess === 'function') { + switch($.preprocessState) { + case 0: $.preprocessState = 1; preprocess($); return(true); + case 1: return(true); + case 2: break; + } + } + $h.each($.chunks, function (chunk) { + if (chunk.status() == 'pending' && chunk.preprocessState !== 1) { + chunk.send(); + found = true; + return(false); + } + }); } + return(found); } - return uploading; - }; + $.markChunksCompleted = function (chunkNumber) { + if (!$.chunks || $.chunks.length <= chunkNumber) { + return; + } + for (var num = 0; num < chunkNumber; num++) { + $.chunks[num].markComplete = true; + } + }; - Resumable.prototype.upload = function() { - var num, _i, _ref, _results; - console.log("upload"); - if (this.isUploading()) { - return; - } - this.fire('uploadStart'); - _results = []; - for (num = _i = 0, _ref = this.getOpt('simultaneousUploads'); 0 <= _ref ? _i <= _ref : _i >= _ref; num = 0 <= _ref ? ++_i : --_i) { - _results.push(this.uploadNextChunk()); - } - return _results; - }; + // Bootstrap and return + $.resumableObj.fire('chunkingStart', $); + $.bootstrap(); + return(this); + } - Resumable.prototype.pause = function() { - var file, _i, _len, _ref; - console.log("pause"); - _ref = this.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - file = _ref[_i]; - file.abort(); - } - return this.fire('pause'); - }; - Resumable.prototype.cancel = function() { - var file, _i, _len, _ref; - console.log("cancel"); - _ref = this.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - file = _ref[_i]; - file.cancel(); - } - return this.fire('cancel'); - }; + function ResumableChunk(resumableObj, fileObj, offset, callback){ + var $ = this; + $.opts = {}; + $.getOpt = resumableObj.getOpt; + $.resumableObj = resumableObj; + $.fileObj = fileObj; + $.fileObjSize = fileObj.size; + $.fileObjType = fileObj.file.type; + $.offset = offset; + $.callback = callback; + $.lastProgressCallback = (new Date); + $.tested = false; + $.retries = 0; + $.pendingRetry = false; + $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished + $.markComplete = false; - Resumable.prototype.progress = function() { - var file, totalDone, totalSize, _i, _len, _ref; - console.log("progress"); - totalDone = 0; - totalSize = 0; - _ref = this.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - file = _ref[_i]; - totalDone += file.progress() * file.size; - totalSize += file.size; + // Computed properties + var chunkSize = $.getOpt('chunkSize'); + $.loaded = 0; + $.startByte = $.offset*chunkSize; + $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); + if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { + // The last chunk will be bigger than the chunk size, but less than 2*chunkSize + $.endByte = $.fileObjSize; } - return (totalSize > 0 ? totalDone / totalSize : 0); - }; + $.xhr = null; - Resumable.prototype.addFile = function(file) { - console.log("addFile"); - return this.appendFilesFromFileList([file]); - }; + // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session + $.test = function(){ + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); - Resumable.prototype.removeFile = function(file) { - var f, files, _i, _len, _ref; - console.log("removeFile"); - files = []; - _ref = this.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - f = _ref[_i]; - if (f !== file) { - files.push(f); + var testHandler = function(e){ + $.tested = true; + var status = $.status(); + if(status=='success') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.send(); + } + }; + $.xhr.addEventListener('load', testHandler, false); + $.xhr.addEventListener('error', testHandler, false); + $.xhr.addEventListener('timeout', testHandler, false); + + // Add data from the query options + var params = []; + var parameterNamespace = $.getOpt('parameterNamespace'); + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + params.push([encodeURIComponent(parameterNamespace+k), encodeURIComponent(v)].join('=')); + }); + // Add extra data to identify chunk + params = params.concat( + [ + // define key/value pairs for additional parameters + ['chunkNumberParameterName', $.offset + 1], + ['chunkSizeParameterName', $.getOpt('chunkSize')], + ['currentChunkSizeParameterName', $.endByte - $.startByte], + ['totalSizeParameterName', $.fileObjSize], + ['typeParameterName', $.fileObjType], + ['identifierParameterName', $.fileObj.uniqueIdentifier], + ['fileNameParameterName', $.fileObj.fileName], + ['relativePathParameterName', $.fileObj.relativePath], + ['totalChunksParameterName', $.fileObj.chunks.length] + ].filter(function(pair){ + // include items that resolve to truthy values + // i.e. exclude false, null, undefined and empty strings + return $.getOpt(pair[0]); + }) + .map(function(pair){ + // map each key/value pair to its final form + return [ + parameterNamespace + $.getOpt(pair[0]), + encodeURIComponent(pair[1]) + ].join('='); + }) + ); + // Append the relevant chunk and send it + $.xhr.open($.getOpt('testMethod'), $h.getTarget('test', params)); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + var customHeaders = $.getOpt('headers'); + if(typeof customHeaders === 'function') { + customHeaders = customHeaders($.fileObj, $); } - } - return this.files = files; - }; + $h.each(customHeaders, function(k,v) { + $.xhr.setRequestHeader(k, v); + }); + $.xhr.send(null); + }; - Resumable.prototype.getFromUniqueIdentifier = function(uniqueIdentifier) { - var f, _i, _len, _ref; - console.log("getFromUniqueIdentifier"); - _ref = this.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - f = _ref[_i]; - if (f.uniqueIdentifier === uniqueIdentifier) { - return f; + $.preprocessFinished = function(){ + $.preprocessState = 2; + $.send(); + }; + + // send() uploads the actual data in a POST call + $.send = function(){ + var preprocess = $.getOpt('preprocess'); + if(typeof preprocess === 'function') { + switch($.preprocessState) { + case 0: $.preprocessState = 1; preprocess($); return; + case 1: return; + case 2: break; + } + } + if($.getOpt('testChunks') && !$.tested) { + $.test(); + return; } - } - return false; - }; - Resumable.prototype.getSize = function() { - var file, totalSize, _i, _len, _ref; - console.log("getSize"); - totalSize = 0; - _ref = this.files; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - file = _ref[_i]; - totalSize += file.size; - } - return totalSize; - }; + // Set up request and listen for event + $.xhr = new XMLHttpRequest(); - return Resumable; - - })(); - - window.ResumableChunk = ResumableChunk = (function() { - - function ResumableChunk(resumableObj, fileObj, offset, callback) { - this.resumableObj = resumableObj; - this.fileObj = fileObj; - this.offset = offset; - this.callback = callback; - this.opt = {}; - this.fileObjSize = this.fileObj.size; - this.lastProgressCallback = new Date; - this.tested = false; - this.retries = 0; - this.preprocessState = 0; - this.chunkSize = this.getOpt('chunkSize'); - this.loaded = 0; - this.startByte = this.offset * this.chunkSize; - this.endByte = Math.min(this.fileObjSize, (this.offset + 1) * this.chunkSize); - if ((this.fileObjSize - this.endByte < this.chunkSize) && (!this.getOpt('forceChunkSize'))) { - this.endByte = this.fileObjSize; - } - this.xhr = null; - } + // Progress + $.xhr.upload.addEventListener('progress', function(e){ + if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) { + $.callback('progress'); + $.lastProgressCallback = (new Date); + } + $.loaded=e.loaded||0; + }, false); + $.loaded = 0; + $.pendingRetry = false; + $.callback('progress'); - ResumableChunk.prototype.getOpt = function(o) { - return this.resumableObj.getOpt(o); - }; + // Done (either done, failed or retry) + var doneHandler = function(e){ + var status = $.status(); + if(status=='success'||status=='error') { + $.callback(status, $.message()); + $.resumableObj.uploadNextChunk(); + } else { + $.callback('retry', $.message()); + $.abort(); + $.retries++; + var retryInterval = $.getOpt('chunkRetryInterval'); + if(retryInterval !== undefined) { + $.pendingRetry = true; + setTimeout($.send, retryInterval); + } else { + $.send(); + } + } + }; + $.xhr.addEventListener('load', doneHandler, false); + $.xhr.addEventListener('error', doneHandler, false); + $.xhr.addEventListener('timeout', doneHandler, false); - ResumableChunk.prototype.pushParams = function(params, key, value) { - return params.push([encodeURIComponent(key), encodeURIComponent(value)].join('=')); - }; + // Set up the basic query data from Resumable + var query = [ + ['chunkNumberParameterName', $.offset + 1], + ['chunkSizeParameterName', $.getOpt('chunkSize')], + ['currentChunkSizeParameterName', $.endByte - $.startByte], + ['totalSizeParameterName', $.fileObjSize], + ['typeParameterName', $.fileObjType], + ['identifierParameterName', $.fileObj.uniqueIdentifier], + ['fileNameParameterName', $.fileObj.fileName], + ['relativePathParameterName', $.fileObj.relativePath], + ['totalChunksParameterName', $.fileObj.chunks.length], + ].filter(function(pair){ + // include items that resolve to truthy values + // i.e. exclude false, null, undefined and empty strings + return $.getOpt(pair[0]); + }) + .reduce(function(query, pair){ + // assign query key/value + query[$.getOpt(pair[0])] = pair[1]; + return query; + }, {}); + // Mix in custom data + var customQuery = $.getOpt('query'); + if(typeof customQuery == 'function') customQuery = customQuery($.fileObj, $); + $h.each(customQuery, function(k,v){ + query[k] = v; + }); - ResumableChunk.prototype.test = function() { - var customQuery, headers, key, params, testHandler, value, - _this = this; - this.xhr = new XMLHttpRequest(); - testHandler = function(e) { - var status; - _this.tested = true; - status = _this.status(); - if (status === 'success') { - _this.callback(status, _this.message()); - return _this.resumableObj.uploadNextChunk(); - } else { - return _this.send(); + var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))); + var bytes = $.fileObj.file[func]($.startByte, $.endByte, $.getOpt('setChunkTypeFromFile') ? $.fileObj.file.type : ""); + var data = null; + var params = []; + + var parameterNamespace = $.getOpt('parameterNamespace'); + if ($.getOpt('method') === 'octet') { + // Add data from the query options + data = bytes; + $h.each(query, function (k, v) { + params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('=')); + }); + } else { + // Add data from the query options + data = new FormData(); + $h.each(query, function (k, v) { + data.append(parameterNamespace + k, v); + params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('=')); + }); + if ($.getOpt('chunkFormat') == 'blob') { + data.append(parameterNamespace + $.getOpt('fileParameterName'), bytes, $.fileObj.fileName); + } + else if ($.getOpt('chunkFormat') == 'base64') { + var fr = new FileReader(); + fr.onload = function (e) { + data.append(parameterNamespace + $.getOpt('fileParameterName'), fr.result); + $.xhr.send(data); + } + fr.readAsDataURL(bytes); + } + } + + var target = $h.getTarget('upload', params); + var method = $.getOpt('uploadMethod'); + + $.xhr.open(method, target); + if ($.getOpt('method') === 'octet') { + $.xhr.setRequestHeader('Content-Type', 'application/octet-stream'); } - }; - this.xhr.addEventListener('load', testHandler, false); - this.xhr.addEventListener('error', testHandler, false); - params = []; - customQuery = this.getOpt('query'); - if (typeof customQuery === 'function') { - customQuery = customQuery(this.fileObj, this); - } - if (customQuery != null) { - for (key in customQuery) { - value = customQuery[key]; - pushParams(key, value); + $.xhr.timeout = $.getOpt('xhrTimeout'); + $.xhr.withCredentials = $.getOpt('withCredentials'); + // Add data from header options + var customHeaders = $.getOpt('headers'); + if(typeof customHeaders === 'function') { + customHeaders = customHeaders($.fileObj, $); } - } - this.pushParams(params, 'resumableChunkNumber', this.offset + 1); - this.pushParams(params, 'resumableChunkSize', this.chunkSize); - this.pushParams(params, 'resumableCurrentChunkSize', this.endByte - this.startByte); - this.pushParams(params, 'resumableTotalSize', this.fileObjSize); - this.pushParams(params, 'resumableIdentifier', this.fileObj.uniqueIdentifier); - this.pushParams(params, 'resumableFilename', this.fileObj.fileName); - this.pushParams(params, 'resumableRelativePath', this.fileObj.relativePath); - this.xhr.open('GET', this.getOpt('target') + '?' + params.join('&')); - headers = this.getOpt('headers'); - if (headers == null) { - headers = {}; - } - for (key in headers) { - value = headers[key]; - this.xhr.setRequestHeader(key, value); - } - return this.xhr.send(null); - }; - ResumableChunk.prototype.preprocessFinished = function() { - this.preprocessState = 2; - return this.send(); - }; + $h.each(customHeaders, function(k,v) { + $.xhr.setRequestHeader(k, v); + }); - ResumableChunk.prototype.send = function() { - var bytes, customQuery, data, doneHandler, func, headers, key, params, preprocess, progressHandler, query, ret, target, value, - _this = this; - preprocess = this.getOpt('preprocess'); - if (typeof preprocess === 'function') { - ret = false; - switch (this.preprocessState) { - case 0: - preprocess(this); - this.preprocessState = 1; - ret = true; - break; - case 1: - ret = true; - break; - case 2: - ret = false; - } - if (ret) { - return; + if ($.getOpt('chunkFormat') == 'blob') { + $.xhr.send(data); } - } - if (this.getOpt('testChunks') && !this.tested) { - this.test(); - return; - } - this.xhr = new XMLHttpRequest(); - this.loaded = 0; - progressHandler = function(e) { - if ((new Date) - _this.lastProgressCallback > _this.getOpt('throttleProgressCallbacks') * 1000) { - _this.callback('progress'); - _this.lastProgressCallback = new Date; - } - return _this.loaded = e.loaded || 0; }; - this.xhr.upload.addEventListener('progress', progressHandler, false); - this.callback('progress'); - doneHandler = function(e) { - var retryInterval, status; - status = _this.status(); - if (status === 'success' || status === 'error') { - _this.callback(status, _this.message()); - return _this.resumableObj.uploadNextChunk(); + $.abort = function(){ + // Abort and reset + if($.xhr) $.xhr.abort(); + $.xhr = null; + }; + $.status = function(){ + // Returns: 'pending', 'uploading', 'success', 'error' + if($.pendingRetry) { + // if pending retry then that's effectively the same as actively uploading, + // there might just be a slight delay before the retry starts + return('uploading'); + } else if($.markComplete) { + return 'success'; + } else if(!$.xhr) { + return('pending'); + } else if($.xhr.readyState<4) { + // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening + return('uploading'); } else { - _this.callback('retry', _this.message()); - _this.abort(); - _this.retries++; - retryInterval = getOpt('chunkRetryInterval'); - if (retryInterval != null) { - return setTimeout(_this.send, retryInterval); + if($.xhr.status == 200 || $.xhr.status == 201) { + // HTTP 200, 201 (created) + return('success'); + } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { + // HTTP 400, 404, 409, 415, 500, 501 (permanent error) + return('error'); + } else { + // this should never happen, but we'll reset and queue a retry + // a likely case for this would be 503 service unavailable + $.abort(); + return('pending'); } } }; - this.xhr.addEventListener('load', doneHandler, false); - this.xhr.addEventListener('error', doneHandler, false); - headers = this.getOpt('headers'); - if (headers == null) { - headers = {}; - } - for (key in headers) { - value = headers[key]; - this.xhr.setRequestHeader(key, value); - } - if (this.fileObj.file.slice != null) { - func = 'slice'; - } else if (this.fileObj.file.mozSlice != null) { - func = 'mozSlice'; - } else if (this.fileObj.file.webkitSlice != null) { - func = 'webkitSlice'; - } else { - func = 'slice'; - } - bytes = this.fileObj.file[func](this.startByte, this.endByte); - data = null; - target = this.getOpt('target'); - query = { - resumableChunkNumber: this.offset + 1, - resumableChunkSize: this.getOpt('chunkSize'), - resumableCurrentChunkSize: this.endByte - this.startByte, - resumableTotalSize: this.fileObjSize, - resumableIdentifier: this.fileObj.uniqueIdentifier, - resumableFilename: this.fileObj.fileName, - resumableRelativePath: this.fileObj.relativePath + $.message = function(){ + return($.xhr ? $.xhr.responseText : ''); }; - customQuery = this.getOpt('query'); - if (typeof customQuery === 'function') { - customQuery = customQuery(this.fileObj, this); - } - if (customQuery == null) { - customQuery = {}; - } - for (key in customQuery) { - value = customQuery[key]; - pushParams(query, key, value); - } - if (this.getOpt('method') === 'octet') { - data = bytes; - params = []; - for (key in query) { - value = query[key]; - this.pushParams(params, key, value); - } - target += '?' + params.join('&'); - } else { - data = new FormData(); - for (key in query) { - value = query[key]; - data.append(key, value); - } - data.append(this.getOpt('fileParameterName'), bytes); - } - this.xhr.open('POST', target); - return this.xhr.send(data); - }; - - ResumableChunk.prototype.abort = function() { - if (this.xhr != null) { - this.xhr.abort(); - } - return this.xhr = null; - }; - - ResumableChunk.prototype.status = function() { - var maxChunkRetries, permanentErrors, _ref; - permanentErrors = this.getOpt('permanentErrors'); - maxChunkRetries = this.getOpt('maxChunkRetries'); - if (permanentErrors == null) { - permanentErrors = {}; - } - if (maxChunkRetries == null) { - maxChunkRetries = 0; - } - if (this.xhr == null) { - return 'pending'; - } else if (this.xhr.readyState < 4) { - return 'uploading'; - } else if (this.xhr.status === 200) { - return 'success'; - } else if ((_ref = this.xhr.status, __indexOf.call(permanentErrors, _ref) >= 0) || (this.retries >= maxChunkRetries)) { - return 'error'; - } else { - this.abort(); - return 'pending'; - } - }; - - ResumableChunk.prototype.message = function() { - return (this.xhr != null ? this.xhr.responseText : ''); - }; - - ResumableChunk.prototype.progress = function(relative) { - var factor; - factor = (relative != null ? (this.endByte - this.startByte) / this.fileObjSize : 1); - switch (this.status()) { + $.progress = function(relative){ + if(typeof(relative)==='undefined') relative = false; + var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); + if($.pendingRetry) return(0); + if((!$.xhr || !$.xhr.status) && !$.markComplete) factor*=.95; + var s = $.status(); + switch(s){ case 'success': case 'error': - return 1 * factor; + return(1*factor); case 'pending': - return 0 * factor; + return(0*factor); default: - return this.loaded / (this.endByte - this.startByte) * factor; - } - }; - - return ResumableChunk; - - })(); - - window.ResumableFile = ResumableFile = (function() { - - function ResumableFile(resumableObj, file) { - this.resumableObj = resumableObj; - this.file = file; - this.opt = {}; - this._prevProgress = 0; - this.fileName = this.file.fileName || this.file.name; - this.size = this.file.size; - this.relativePath = this.file.webkitRelativePath || this.fileName; - this.uniqueIdentifier = this.resumableObj.generateUniqueIdentifier(this.file); - this._error = false; - this.chunks = []; - this.bootstrap(); + return($.loaded/($.endByte-$.startByte)*factor); + } + }; + return(this); } - ResumableFile.prototype.getOpt = function(o) { - return this.resumableObj.getOpt(o); - }; + // QUEUE + $.uploadNextChunk = function(){ + var found = false; - ResumableFile.prototype.chunkEvent = function(event, message) { - switch (event) { - case "progress": - return this.resumableObj.fire('fileProgress', this); - case "error": - this.abort(); - this._error = true; - this.chunks = []; - return this.resumableObj.fire('fileError', this, message); - case "success": - if (!this._error) { - this.resumableObj.fire('fileProgress', this); - if (this.progress() === 1) { - return this.resumableObj.fire('fileSuccess', this, message); - } + // In some cases (such as videos) it's really handy to upload the first + // and last chunk of a file quickly; this let's the server check the file's + // metadata and determine if there's even a point in continuing. + if ($.getOpt('prioritizeFirstAndLastChunk')) { + $h.each($.files, function(file){ + if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) { + file.chunks[0].send(); + found = true; + return(false); } - break; - case "retry": - return this.resumableObj.fire('fileRetry', this); + if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[file.chunks.length-1].preprocessState === 0) { + file.chunks[file.chunks.length-1].send(); + found = true; + return(false); + } + }); + if(found) return(true); } - }; - ResumableFile.prototype.abort = function() { - var c, _i, _len, _ref; - _ref = this.chunks; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - c = _ref[_i]; - if (c.status() === 'uploading') { - c.abort(); + // Now, simply look for the next, best thing to upload + $h.each($.files, function(file){ + found = file.upload(); + if(found) return(false); + }); + if(found) return(true); + + // The are no more outstanding chunks to upload, check is everything is done + var outstanding = false; + $h.each($.files, function(file){ + if(!file.isComplete()) { + outstanding = true; + return(false); } + }); + if(!outstanding) { + // All chunks have been uploaded, complete + $.fire('complete'); } - return this.resumableObj.fire('fileProgress', this); + return(false); }; - ResumableFile.prototype.cancel = function() { - var c, _chunks, _i, _len; - _chunks = this.chunks; - this.chunks = []; - for (_i = 0, _len = _chunks.length; _i < _len; _i++) { - c = _chunks[_i]; - if (c.status() === 'uploading') { - c.abort(); - this.resumableObj.uploadNextChunk(); + + // PUBLIC METHODS FOR RESUMABLE.JS + $.assignBrowse = function(domNodes, isDirectory){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; + $h.each(domNodes, function(domNode) { + var input; + if(domNode.tagName==='INPUT' && domNode.type==='file'){ + input = domNode; + } else { + input = document.createElement('input'); + input.setAttribute('type', 'file'); + input.style.display = 'none'; + domNode.addEventListener('click', function(){ + input.style.opacity = 0; + input.style.display='block'; + input.focus(); + input.click(); + input.style.display='none'; + }, false); + domNode.appendChild(input); } - } - this.resumableObj.removeFile(this); - return this.resumableObj.fire('fileProgress', this); + var maxFiles = $.getOpt('maxFiles'); + if (typeof(maxFiles)==='undefined'||maxFiles!=1){ + input.setAttribute('multiple', 'multiple'); + } else { + input.removeAttribute('multiple'); + } + if(isDirectory){ + input.setAttribute('webkitdirectory', 'webkitdirectory'); + } else { + input.removeAttribute('webkitdirectory'); + } + var fileTypes = $.getOpt('fileType'); + if (typeof (fileTypes) !== 'undefined' && fileTypes.length >= 1) { + input.setAttribute('accept', fileTypes.map(function (e) { + e = e.replace(/\s/g, '').toLowerCase(); + if(e.match(/^[^.][^/]+$/)){ + e = '.' + e; + } + return e; + }).join(',')); + } + else { + input.removeAttribute('accept'); + } + // When new files are added, simply append them to the overall list + input.addEventListener('change', function(e){ + appendFilesFromFileList(e.target.files,e); + var clearInput = $.getOpt('clearInput'); + if (clearInput) { + e.target.value = ''; + } + }, false); + }); }; + $.assignDrop = function(domNodes){ + if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; - ResumableFile.prototype.retry = function() { - this.bootstrap(); - return this.resumableObj.upload(); + $h.each(domNodes, function(domNode) { + domNode.addEventListener('dragover', onDragOverEnter, false); + domNode.addEventListener('dragenter', onDragOverEnter, false); + domNode.addEventListener('dragleave', onDragLeave, false); + domNode.addEventListener('drop', onDrop, false); + }); }; + $.unAssignDrop = function(domNodes) { + if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; - ResumableFile.prototype.bootstrap = function() { - var max, offset, round, _i, _ref, _results; - this.abort(); - this._error = false; - this.chunks = []; - this._prevProgress = 0; - if (this.getOpt('forceChunkSize') != null) { - round = Math.ceil; - } else { - round = Math.floor; - } - offset = 0; - max = Math.max(round(this.file.size / this.getOpt('chunkSize')), 1); - _results = []; - for (offset = _i = 0, _ref = max - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; offset = 0 <= _ref ? ++_i : --_i) { - _results.push(this.chunks.push(new ResumableChunk(this.resumableObj, this, offset, this.chunkEvent))); + $h.each(domNodes, function(domNode) { + domNode.removeEventListener('dragover', onDragOverEnter); + domNode.removeEventListener('dragenter', onDragOverEnter); + domNode.removeEventListener('dragleave', onDragLeave); + domNode.removeEventListener('drop', onDrop); + }); + }; + $.isUploading = function(){ + var uploading = false; + $h.each($.files, function(file){ + if (file.isUploading()) { + uploading = true; + return(false); + } + }); + return(uploading); + }; + $.upload = function(){ + // Make sure we don't start too many uploads at once + if($.isUploading()) return; + // Kick off the queue + $.fire('uploadStart'); + for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { + $.uploadNextChunk(); } - return _results; }; - - ResumableFile.prototype.progress = function() { - var c, error, ret, _i, _len, _ref; - if (this._error) { - return 1.; + $.pause = function(){ + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + file.abort(); + }); + $.fire('pause'); + }; + $.cancel = function(){ + $.fire('beforeCancel'); + for(var i = $.files.length - 1; i >= 0; i--) { + $.files[i].cancel(); } - ret = 0; - error = false; - _ref = this.chunks; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - c = _ref[_i]; - error = c.status() === 'error'; - ret += c.progress(true); + $.fire('cancel'); + }; + $.progress = function(){ + var totalDone = 0; + var totalSize = 0; + // Resume all chunks currently being uploaded + $h.each($.files, function(file){ + totalDone += file.progress()*file.size; + totalSize += file.size; + }); + return(totalSize>0 ? totalDone/totalSize : 0); + }; + $.addFile = function(file, event){ + appendFilesFromFileList([file], event); + }; + $.addFiles = function(files, event){ + appendFilesFromFileList(files, event); + }; + $.removeFile = function(file){ + for(var i = $.files.length - 1; i >= 0; i--) { + if($.files[i] === file) { + $.files.splice(i, 1); + } } - ret = (error || error > 0.99 ? 1 : ret); - ret = Math.max(this._prevProgress, ret); - this._prevProgress = ret; - return ret; }; + $.getFromUniqueIdentifier = function(uniqueIdentifier){ + var ret = false; + $h.each($.files, function(f){ + if(f.uniqueIdentifier==uniqueIdentifier) ret = f; + }); + return(ret); + }; + $.getSize = function(){ + var totalSize = 0; + $h.each($.files, function(file){ + totalSize += file.size; + }); + return(totalSize); + }; + $.handleDropEvent = function (e) { + onDrop(e); + }; + $.handleChangeEvent = function (e) { + appendFilesFromFileList(e.target.files, e); + e.target.value = ''; + }; + $.updateQuery = function(query){ + $.opts.query = query; + }; + + return(this); + }; - return ResumableFile; - })(); + // Node.js-style export for Node and Component + if (typeof module != 'undefined') { + module.exports = Resumable; + } else if (typeof define === "function" && define.amd) { + // AMD/requirejs: Define the module + define(function(){ + return Resumable; + }); + } else { + // Browser: Expose to window + window.Resumable = Resumable; + } -}).call(this); +})(); diff --git a/samples/java/README.md b/samples/java/README.md index f909ca4e..cc2f8f1b 100644 --- a/samples/java/README.md +++ b/samples/java/README.md @@ -1,10 +1,18 @@ -##Java Demo for Resumable.js +## Java Demo for Resumable.js This is a resumable.js demo for people who use java-servlet in server side. `resumable.js.upload.UploadServlet` is the servlet. -###Upload chunks +### Run + +mvn jetty:run + +http://localhost:8080/java-example/ + +Uploaded files will appear in "upload_dir". + +### Upload chunks UploadServlet accepts Resumable.js Upload with 'octet' type, gets parameters from url like @@ -17,12 +25,12 @@ and gets chunk-data from http-body. Besides, UploadServlet uses RandomAccessFile to speed up File-Upload progress, which avoids merging chunk-files at last. -###testChunks +### testChunks UploadServlet supports Resumable.js's `testChunks` feature, which makes file upload resumable. -###Resumable.js options +### Resumable.js options UploadServlet only supports 'octet' upload, so make sure method in your resumable options is 'octet'. diff --git a/samples/java/pom.xml b/samples/java/pom.xml new file mode 100755 index 00000000..a357f7b7 --- /dev/null +++ b/samples/java/pom.xml @@ -0,0 +1,49 @@ + + 4.0.0 + resumable.js + java-example + war + 0.0.1-SNAPSHOT + Java Example + http://maven.apache.org + + + UTF-8 + + + + javax.servlet + servlet-api + 2.5 + + + + + junit + junit + 3.8.1 + test + + + + + resumable.js + + + org.mortbay.jetty + maven-jetty-plugin + 6.1.10 + + 10 + + + 8080 + 60000 + + + + + + + diff --git a/samples/java/src/resumable/js/upload/HttpUtils.java b/samples/java/src/main/java/resumable/js/upload/HttpUtils.java similarity index 100% rename from samples/java/src/resumable/js/upload/HttpUtils.java rename to samples/java/src/main/java/resumable/js/upload/HttpUtils.java diff --git a/samples/java/src/resumable/js/upload/ResumableInfo.java b/samples/java/src/main/java/resumable/js/upload/ResumableInfo.java similarity index 97% rename from samples/java/src/resumable/js/upload/ResumableInfo.java rename to samples/java/src/main/java/resumable/js/upload/ResumableInfo.java index c04a0d70..342daab9 100644 --- a/samples/java/src/resumable/js/upload/ResumableInfo.java +++ b/samples/java/src/main/java/resumable/js/upload/ResumableInfo.java @@ -54,7 +54,7 @@ public boolean vaild(){ public boolean checkIfUploadFinished() { //check if upload finished int count = (int) Math.ceil(((double) resumableTotalSize) / ((double) resumableChunkSize)); - for(int i = 1; i < count + 1; i ++) { + for(int i = 1; i < count; i ++) { if (!uploadedChunks.contains(new ResumableChunkNumber(i))) { return false; } diff --git a/samples/java/src/resumable/js/upload/ResumableInfoStorage.java b/samples/java/src/main/java/resumable/js/upload/ResumableInfoStorage.java similarity index 100% rename from samples/java/src/resumable/js/upload/ResumableInfoStorage.java rename to samples/java/src/main/java/resumable/js/upload/ResumableInfoStorage.java diff --git a/samples/java/src/resumable/js/upload/UploadServlet.java b/samples/java/src/main/java/resumable/js/upload/UploadServlet.java similarity index 95% rename from samples/java/src/resumable/js/upload/UploadServlet.java rename to samples/java/src/main/java/resumable/js/upload/UploadServlet.java index b10a3f57..ff953cae 100644 --- a/samples/java/src/resumable/js/upload/UploadServlet.java +++ b/samples/java/src/main/java/resumable/js/upload/UploadServlet.java @@ -17,7 +17,7 @@ */ public class UploadServlet extends HttpServlet { - public static final String UPLOAD_DIR = "e:\\"; + public static final String UPLOAD_DIR = "upload_dir"; protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { int resumableChunkNumber = getResumableChunkNumber(request); @@ -27,7 +27,7 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) RandomAccessFile raf = new RandomAccessFile(info.resumableFilePath, "rw"); //Seek to position - raf.seek((resumableChunkNumber - 1) * info.resumableChunkSize); + raf.seek((resumableChunkNumber - 1) * (long)info.resumableChunkSize); //Save to file InputStream is = request.getInputStream(); @@ -80,6 +80,7 @@ private ResumableInfo getResumableInfo(HttpServletRequest request) throws Servle String resumableFilename = request.getParameter("resumableFilename"); String resumableRelativePath = request.getParameter("resumableRelativePath"); //Here we add a ".temp" to every upload file to indicate NON-FINISHED + new File(base_dir).mkdir(); String resumableFilePath = new File(base_dir, resumableFilename).getAbsolutePath() + ".temp"; ResumableInfoStorage storage = ResumableInfoStorage.getInstance(); diff --git a/samples/java/web/WEB-INF/web.xml b/samples/java/src/main/webapp/WEB-INF/web.xml similarity index 100% rename from samples/java/web/WEB-INF/web.xml rename to samples/java/src/main/webapp/WEB-INF/web.xml diff --git a/samples/java/web/index.html b/samples/java/src/main/webapp/index.html similarity index 95% rename from samples/java/web/index.html rename to samples/java/src/main/webapp/index.html index 5724b5a7..80244edc 100644 --- a/samples/java/web/index.html +++ b/samples/java/src/main/webapp/index.html @@ -25,7 +25,7 @@

    Demo

    Your browser, unfortunately, is not supported by Resumable.js. The library requires support for the HTML5 File API along with file slicing.
    -
    +
    Drop video files here to upload or select from your computer
    @@ -46,7 +46,7 @@

    Demo