├── .gitignore ├── samples ├── java │ ├── web │ │ ├── pause.png │ │ ├── resume.png │ │ ├── WEB-INF │ │ │ └── web.xml │ │ ├── style.css │ │ ├── index.html │ │ └── resumable.js │ ├── src │ │ └── resumable │ │ │ └── js │ │ │ └── upload │ │ │ ├── ResumableInfoStorage.java │ │ │ ├── HttpUtils.java │ │ │ ├── ResumableInfo.java │ │ │ └── UploadServlet.java │ └── README.md ├── Node.js │ ├── public │ │ ├── cancel.png │ │ ├── pause.png │ │ ├── resume.png │ │ ├── style.css │ │ └── index.html │ ├── README.md │ ├── app.js │ └── resumable-node.js ├── coffeescript │ ├── README.md │ ├── resumable.map │ ├── resumable.coffee │ └── resumable.js ├── Frontend in jQuery.md ├── Backend on AOLserver and OpenACS.md └── Backend on PHP.md ├── component.json ├── MIT-LICENSE ├── test.html ├── README.md └── resumable.js /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | samples/Node.js/node_modules/ 3 | -------------------------------------------------------------------------------- /samples/java/web/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/resumable.js/master/samples/java/web/pause.png -------------------------------------------------------------------------------- /samples/java/web/resume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/resumable.js/master/samples/java/web/resume.png -------------------------------------------------------------------------------- /samples/Node.js/public/cancel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/resumable.js/master/samples/Node.js/public/cancel.png -------------------------------------------------------------------------------- /samples/Node.js/public/pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/resumable.js/master/samples/Node.js/public/pause.png -------------------------------------------------------------------------------- /samples/Node.js/public/resume.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/resumable.js/master/samples/Node.js/public/resume.png -------------------------------------------------------------------------------- /samples/java/src/resumable/js/upload/ResumableInfoStorage.java: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/davidchase/resumable.js/master/samples/java/src/resumable/js/upload/ResumableInfoStorage.java -------------------------------------------------------------------------------- /component.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "resumable.js", 3 | "repo": "23/resumable.js", 4 | "version": "1.0.0", 5 | "main": "resumable.js", 6 | "scripts": ["resumable.js"] 7 | } 8 | -------------------------------------------------------------------------------- /samples/java/web/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | UploadServlet 10 | resumable.js.upload.UploadServlet 11 | 12 | 13 | UploadServlet 14 | /upload 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/coffeescript/README.md: -------------------------------------------------------------------------------- 1 | ##Resumable.coffee 2 | 3 | Resumable.coffee is a `coffeescript` version for resumable.js. 4 | All api and structure of resumable.js are not changed in resumable.coffee. 5 | 6 | There are also three classes: 7 | 8 | * Resumable 9 | * ResumableChunk 10 | * ResumableFile 11 | 12 | I wrote it just for fun. But it is more identifiable than js version. 13 | 14 | 15 | ## NOTICE 16 | 17 | Resumable.coffee is not maintained alongsize the Resumable.js. 18 | 19 | 20 | ##How to build 21 | 22 | First, install coffee script. 23 | 24 | npm install coffee -g 25 | 26 | Build resumable.js 27 | 28 | coffee -c resumable.coffee 29 | 30 | Add -m if you would like to generate source map. 31 | -------------------------------------------------------------------------------- /samples/Node.js/README.md: -------------------------------------------------------------------------------- 1 | # Sample code for Node.js 2 | 3 | This sample is written for [Node.js 0.6+](http://nodejs.org/) and requires [Express](http://expressjs.com/) to make the sample code cleaner. 4 | 5 | To install and run: 6 | 7 | cd samples/Node.js 8 | npm install express 9 | node app.js 10 | 11 | Then browse to [localhost:3000](http://localhost:3000). 12 | 13 | 14 | ## Enabling Cross-domain Uploads 15 | 16 | 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. 17 | 18 | Then in public/index.html, on line 49, update the target with your server's address. For example: target:'http://www.example.com/upload' 19 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, 23, http://www.23developer.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /samples/java/src/resumable/js/upload/HttpUtils.java: -------------------------------------------------------------------------------- 1 | package resumable.js.upload; 2 | 3 | /** 4 | * by fanxu123 5 | */ 6 | public class HttpUtils { 7 | 8 | public static boolean isEmpty(String value) { 9 | return value == null || "".equals(value); 10 | } 11 | /** 12 | * Convert String to long 13 | * @param value 14 | * @param def default value 15 | * @return 16 | */ 17 | public static long toLong(String value, long def) { 18 | if (isEmpty(value)) { 19 | return def; 20 | } 21 | 22 | try { 23 | return Long.valueOf(value); 24 | } catch (NumberFormatException e) { 25 | e.printStackTrace(); 26 | return def; 27 | } 28 | } 29 | 30 | /** 31 | * Convert String to int 32 | * @param value 33 | * @param def default value 34 | * @return 35 | */ 36 | public static int toInt(String value, int def) { 37 | if (isEmpty(value)) { 38 | return def; 39 | } 40 | try { 41 | return Integer.valueOf(value); 42 | } catch (NumberFormatException e) { 43 | e.printStackTrace(); 44 | return def; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test.html: -------------------------------------------------------------------------------- 1 | Select files 2 | 3 | 4 | 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /samples/java/README.md: -------------------------------------------------------------------------------- 1 | ##Java Demo for Resumable.js 2 | 3 | This is a resumable.js demo for people who use java-servlet in server side. 4 | 5 | `resumable.js.upload.UploadServlet` is the servlet. 6 | 7 | ###Upload chunks 8 | 9 | UploadServlet accepts Resumable.js Upload with 'octet' type, gets parameters from url like 10 | 11 | ``` 12 | http://localhost:8080/test/upload?resumableChunkNumber=21&resumableChunkSize=1048576&resumableCurrentChunkSize=1048576&resumableTotalSize=28052543&resumableIdentifier=28052543-wampserver22e-php5313-httpd2222-mysql5524-32bexe&resumableFilename=wampserver2.2e-php5.3.13-httpd2.2.22-mysql5.5.24-32b.exe&resumableRelativePath=wampserver2.2e-php5.3.13-httpd2.2.22-mysql5.5.24-32b.exe 13 | ``` 14 | 15 | and gets chunk-data from http-body. 16 | 17 | Besides, UploadServlet uses RandomAccessFile to speed up File-Upload progress, which avoids merging chunk-files at last. 18 | 19 | 20 | ###testChunks 21 | 22 | UploadServlet supports Resumable.js's `testChunks` feature, which makes file upload resumable. 23 | 24 | 25 | ###Resumable.js options 26 | 27 | UploadServlet only supports 'octet' upload, so make sure method in your resumable options is 'octet'. 28 | 29 | var r = new Resumable({ 30 | target:'/test/upload', 31 | chunkSize:1*1024*1024, 32 | simultaneousUploads:4, 33 | testChunks: true, 34 | throttleProgressCallbacks:1, 35 | method: "octet" 36 | }); 37 | 38 | 39 | -------------------------------------------------------------------------------- /samples/Node.js/app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var resumable = require('./resumable-node.js')('/tmp/resumable.js/'); 3 | var app = express(); 4 | 5 | // Host most stuff in the public folder 6 | app.use(express.static(__dirname + '/public')); 7 | 8 | app.use(express.bodyParser()); 9 | 10 | // Handle uploads through Resumable.js 11 | app.post('/upload', function(req, res){ 12 | 13 | // console.log(req); 14 | 15 | resumable.post(req, function(status, filename, original_filename, identifier){ 16 | console.log('POST', status, original_filename, identifier); 17 | 18 | res.send(status, { 19 | // NOTE: Uncomment this funciton to enable cross-domain request. 20 | //'Access-Control-Allow-Origin': '*' 21 | }); 22 | }); 23 | }); 24 | 25 | // Handle cross-domain requests 26 | // NOTE: Uncomment this funciton to enable cross-domain request. 27 | /* 28 | app.options('/upload', function(req, res){ 29 | console.log('OPTIONS'); 30 | res.send(true, { 31 | 'Access-Control-Allow-Origin': '*' 32 | }, 200); 33 | }); 34 | */ 35 | 36 | // Handle status checks on chunks through Resumable.js 37 | app.get('/upload', function(req, res){ 38 | resumable.get(req, function(status, filename, original_filename, identifier){ 39 | console.log('GET', status); 40 | res.send(status, (status == 'found' ? 200 : 404)); 41 | }); 42 | }); 43 | 44 | app.get('/download/:identifier', function(req, res){ 45 | resumable.write(req.params.identifier, res); 46 | }); 47 | app.get('/resumable.js', function (req, res) { 48 | var fs = require('fs'); 49 | res.setHeader("content-type", "application/javascript"); 50 | fs.createReadStream("../../resumable.js").pipe(res); 51 | }); 52 | 53 | app.listen(3000); 54 | -------------------------------------------------------------------------------- /samples/java/src/resumable/js/upload/ResumableInfo.java: -------------------------------------------------------------------------------- 1 | package resumable.js.upload; 2 | 3 | import java.io.File; 4 | import java.util.HashSet; 5 | import java.util.logging.ConsoleHandler; 6 | import java.util.logging.Level; 7 | import java.util.logging.Logger; 8 | 9 | /** 10 | * by fanxu 11 | */ 12 | public class ResumableInfo { 13 | 14 | public int resumableChunkSize; 15 | public long resumableTotalSize; 16 | public String resumableIdentifier; 17 | public String resumableFilename; 18 | public String resumableRelativePath; 19 | 20 | public static class ResumableChunkNumber { 21 | public ResumableChunkNumber(int number) { 22 | this.number = number; 23 | } 24 | 25 | public int number; 26 | 27 | @Override 28 | public boolean equals(Object obj) { 29 | return obj instanceof ResumableChunkNumber 30 | ? ((ResumableChunkNumber)obj).number == this.number : false; 31 | } 32 | 33 | @Override 34 | public int hashCode() { 35 | return number; 36 | } 37 | } 38 | 39 | //Chunks uploaded 40 | public HashSet uploadedChunks = new HashSet(); 41 | 42 | public String resumableFilePath; 43 | 44 | public boolean vaild(){ 45 | if (resumableChunkSize < 0 || resumableTotalSize < 0 46 | || HttpUtils.isEmpty(resumableIdentifier) 47 | || HttpUtils.isEmpty(resumableFilename) 48 | || HttpUtils.isEmpty(resumableRelativePath)) { 49 | return false; 50 | } else { 51 | return true; 52 | } 53 | } 54 | public boolean checkIfUploadFinished() { 55 | //check if upload finished 56 | int count = (int) Math.ceil(((double) resumableTotalSize) / ((double) resumableChunkSize)); 57 | for(int i = 1; i < count + 1; i ++) { 58 | if (!uploadedChunks.contains(new ResumableChunkNumber(i))) { 59 | return false; 60 | } 61 | } 62 | 63 | //Upload finished, change filename. 64 | File file = new File(resumableFilePath); 65 | String new_path = file.getAbsolutePath().substring(0, file.getAbsolutePath().length() - ".temp".length()); 66 | file.renameTo(new File(new_path)); 67 | return true; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /samples/Frontend in jQuery.md: -------------------------------------------------------------------------------- 1 | # Resumable.js front-end in jQuery 2 | [@steffentchr](http://twitter.com/steffentchr) 3 | 4 | This library is originally built to work with [23 Video](http://www.23video.com), and the 23 uploader is a good example of: 5 | 6 | * Selecing files or drag-dropping them in 7 | * Using events to build UI and progress bar 8 | * Pausing and Resuming uploads 9 | * Recovering uploads after browser crashes and even across different computers 10 | * Detecting file support from chunks, for example whether an upload is actually a video file 11 | * Building thumbnails from chunks to give better feedback during upload 12 | * Falling back to alternative upload options when Resumable.js is not supported. 13 | 14 | There's [a free trial for 23 Video](http://www.23video.com/signup) if 15 | you want to see this in action, but the pieces are: 16 | 17 | * Resumable.js itself. 18 | * [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. 19 | * [An API methods](http://www.23developer.com/api/photo-redeem-upload-token) with support for Resumable.js chunks and feedback. 20 | * Finally, some HTML elements for the glue script to use. 21 | 22 | ```html 23 |
24 | Drop video files here to upload or select from your computer 25 |
26 | 27 | 39 | 40 | 45 | 46 | 49 | 50 | 51 | 52 | 59 | ``` 60 | -------------------------------------------------------------------------------- /samples/java/web/style.css: -------------------------------------------------------------------------------- 1 | /* Reset */ 2 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,th,var{font-style:normal;font-weight:normal;}ol,ul {list-style:none;}caption,th {text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;} 3 | 4 | /* Baseline */ 5 | body, p, h1, h2, h3, h4, h5, h6 {font:normal 12px/1.3em Helvetica, Arial, sans-serif; color:#333; } 6 | h1 {font-size:22px; font-weight:bold;} 7 | h2 {font-size:19px; font-weight:bold;} 8 | h3 {font-size:16px; font-weight:bold;} 9 | h4 {font-size:14px; font-weight:bold;} 10 | h5 {font-size:12px; font-weight:bold;} 11 | p {margin:10px 0;} 12 | 13 | 14 | body {text-align:center; margin:40px;} 15 | #frame {margin:0 auto; width:800px; text-align:left;} 16 | 17 | 18 | 19 | /* Uploader: Drag & Drop */ 20 | .resumable-error {display:none; font-size:14px; font-style:italic;} 21 | .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;} 22 | .resumable-dragover {padding:30px; color:#555; background-color:#ddd; border:1px solid #999;} 23 | 24 | /* Uploader: Progress bar */ 25 | .resumable-progress {margin:30px 0 30px 0; width:100%; display:none;} 26 | .progress-container {height:7px; background:#9CBD94; position:relative; } 27 | .progress-bar {position:absolute; top:0; left:0; bottom:0; background:#45913A; width:0;} 28 | .progress-text {font-size:11px; line-height:9px; padding-left:10px;} 29 | .progress-pause {padding:0 0 0 7px;} 30 | .progress-resume-link {display:none;} 31 | .is-paused .progress-resume-link {display:inline;} 32 | .is-paused .progress-pause-link {display:none;} 33 | .is-complete .progress-pause {display:none;} 34 | 35 | /* Uploader: List of items being uploaded */ 36 | .resumable-list {overflow:auto; margin-right:-20px; display:none;} 37 | .uploader-item {width:148px; height:90px; background-color:#666; position:relative; border:2px solid black; float:left; margin:0 6px 6px 0;} 38 | .uploader-item-thumbnail {width:100%; height:100%; position:absolute; top:0; left:0;} 39 | .uploader-item img.uploader-item-thumbnail {opacity:0;} 40 | .uploader-item-creating-thumbnail {padding:0 5px; font-size:9px; color:white;} 41 | .uploader-item-title {position:absolute; font-size:9px; line-height:11px; padding:3px 50px 3px 5px; bottom:0; left:0; right:0; color:white; background-color:rgba(0,0,0,0.6); min-height:27px;} 42 | .uploader-item-status {position:absolute; bottom:3px; right:3px;} 43 | 44 | /* Uploader: Hover & Active status */ 45 | .uploader-item:hover, .is-active .uploader-item {border-color:#4a873c; cursor:pointer; } 46 | .uploader-item:hover .uploader-item-title, .is-active .uploader-item .uploader-item-title {background-color:rgba(74,135,60,0.8);} 47 | 48 | /* Uploader: Error status */ 49 | .is-error .uploader-item:hover, .is-active.is-error .uploader-item {border-color:#900;} 50 | .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);} 51 | .is-error .uploader-item-creating-thumbnail {display:none;} -------------------------------------------------------------------------------- /samples/Node.js/public/style.css: -------------------------------------------------------------------------------- 1 | /* Reset */ 2 | body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td{margin:0;padding:0;}table{border-collapse:collapse;border-spacing:0;}fieldset,img{border:0;}address,caption,cite,code,dfn,th,var{font-style:normal;font-weight:normal;}ol,ul {list-style:none;}caption,th {text-align:left;}h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal;}q:before,q:after{content:'';}abbr,acronym {border:0;} 3 | 4 | /* Baseline */ 5 | body, p, h1, h2, h3, h4, h5, h6 {font:normal 12px/1.3em Helvetica, Arial, sans-serif; color:#333; } 6 | h1 {font-size:22px; font-weight:bold;} 7 | h2 {font-size:19px; font-weight:bold;} 8 | h3 {font-size:16px; font-weight:bold;} 9 | h4 {font-size:14px; font-weight:bold;} 10 | h5 {font-size:12px; font-weight:bold;} 11 | p {margin:10px 0;} 12 | 13 | 14 | body {text-align:center; margin:40px;} 15 | #frame {margin:0 auto; width:800px; text-align:left;} 16 | 17 | 18 | 19 | /* Uploader: Drag & Drop */ 20 | .resumable-error {display:none; font-size:14px; font-style:italic;} 21 | .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;} 22 | .resumable-dragover {padding:30px; color:#555; background-color:#ddd; border:1px solid #999;} 23 | 24 | /* Uploader: Progress bar */ 25 | .resumable-progress {margin:30px 0 30px 0; width:100%; display:none;} 26 | .progress-container {height:7px; background:#9CBD94; position:relative; } 27 | .progress-bar {position:absolute; top:0; left:0; bottom:0; background:#45913A; width:0;} 28 | .progress-text {font-size:11px; line-height:9px; padding-left:10px;} 29 | .progress-pause {padding:0 0 0 7px;} 30 | .progress-resume-link {display:none;} 31 | .is-paused .progress-resume-link {display:inline;} 32 | .is-paused .progress-pause-link {display:none;} 33 | .is-complete .progress-pause {display:none;} 34 | 35 | /* Uploader: List of items being uploaded */ 36 | .resumable-list {overflow:auto; margin-right:-20px; display:none;} 37 | .uploader-item {width:148px; height:90px; background-color:#666; position:relative; border:2px solid black; float:left; margin:0 6px 6px 0;} 38 | .uploader-item-thumbnail {width:100%; height:100%; position:absolute; top:0; left:0;} 39 | .uploader-item img.uploader-item-thumbnail {opacity:0;} 40 | .uploader-item-creating-thumbnail {padding:0 5px; font-size:9px; color:white;} 41 | .uploader-item-title {position:absolute; font-size:9px; line-height:11px; padding:3px 50px 3px 5px; bottom:0; left:0; right:0; color:white; background-color:rgba(0,0,0,0.6); min-height:27px;} 42 | .uploader-item-status {position:absolute; bottom:3px; right:3px;} 43 | 44 | /* Uploader: Hover & Active status */ 45 | .uploader-item:hover, .is-active .uploader-item {border-color:#4a873c; cursor:pointer; } 46 | .uploader-item:hover .uploader-item-title, .is-active .uploader-item .uploader-item-title {background-color:rgba(74,135,60,0.8);} 47 | 48 | /* Uploader: Error status */ 49 | .is-error .uploader-item:hover, .is-active.is-error .uploader-item {border-color:#900;} 50 | .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);} 51 | .is-error .uploader-item-creating-thumbnail {display:none;} -------------------------------------------------------------------------------- /samples/java/src/resumable/js/upload/UploadServlet.java: -------------------------------------------------------------------------------- 1 | package resumable.js.upload; 2 | 3 | import javax.servlet.ServletException; 4 | import javax.servlet.http.HttpServlet; 5 | import javax.servlet.http.HttpServletRequest; 6 | import javax.servlet.http.HttpServletResponse; 7 | import java.io.File; 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | import java.io.RandomAccessFile; 11 | 12 | /** 13 | * 14 | * This is a servlet demo, for using Resumable.js to upload files. 15 | * 16 | * by fanxu123 17 | */ 18 | public class UploadServlet extends HttpServlet { 19 | 20 | public static final String UPLOAD_DIR = "e:\\"; 21 | 22 | protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 23 | int resumableChunkNumber = getResumableChunkNumber(request); 24 | 25 | ResumableInfo info = getResumableInfo(request); 26 | 27 | RandomAccessFile raf = new RandomAccessFile(info.resumableFilePath, "rw"); 28 | 29 | //Seek to position 30 | raf.seek((resumableChunkNumber - 1) * info.resumableChunkSize); 31 | 32 | //Save to file 33 | InputStream is = request.getInputStream(); 34 | long readed = 0; 35 | long content_length = request.getContentLength(); 36 | byte[] bytes = new byte[1024 * 100]; 37 | while(readed < content_length) { 38 | int r = is.read(bytes); 39 | if (r < 0) { 40 | break; 41 | } 42 | raf.write(bytes, 0, r); 43 | readed += r; 44 | } 45 | raf.close(); 46 | 47 | 48 | //Mark as uploaded. 49 | info.uploadedChunks.add(new ResumableInfo.ResumableChunkNumber(resumableChunkNumber)); 50 | if (info.checkIfUploadFinished()) { //Check if all chunks uploaded, and change filename 51 | ResumableInfoStorage.getInstance().remove(info); 52 | response.getWriter().print("All finished."); 53 | } else { 54 | response.getWriter().print("Upload"); 55 | } 56 | } 57 | 58 | protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { 59 | int resumableChunkNumber = getResumableChunkNumber(request); 60 | 61 | ResumableInfo info = getResumableInfo(request); 62 | 63 | if (info.uploadedChunks.contains(new ResumableInfo.ResumableChunkNumber(resumableChunkNumber))) { 64 | response.getWriter().print("Uploaded."); //This Chunk has been Uploaded. 65 | } else { 66 | response.setStatus(HttpServletResponse.SC_NOT_FOUND); 67 | } 68 | } 69 | 70 | private int getResumableChunkNumber(HttpServletRequest request) { 71 | return HttpUtils.toInt(request.getParameter("resumableChunkNumber"), -1); 72 | } 73 | 74 | private ResumableInfo getResumableInfo(HttpServletRequest request) throws ServletException { 75 | String base_dir = UPLOAD_DIR; 76 | 77 | int resumableChunkSize = HttpUtils.toInt(request.getParameter("resumableChunkSize"), -1); 78 | long resumableTotalSize = HttpUtils.toLong(request.getParameter("resumableTotalSize"), -1); 79 | String resumableIdentifier = request.getParameter("resumableIdentifier"); 80 | String resumableFilename = request.getParameter("resumableFilename"); 81 | String resumableRelativePath = request.getParameter("resumableRelativePath"); 82 | //Here we add a ".temp" to every upload file to indicate NON-FINISHED 83 | String resumableFilePath = new File(base_dir, resumableFilename).getAbsolutePath() + ".temp"; 84 | 85 | ResumableInfoStorage storage = ResumableInfoStorage.getInstance(); 86 | 87 | ResumableInfo info = storage.get(resumableChunkSize, resumableTotalSize, 88 | resumableIdentifier, resumableFilename, resumableRelativePath, resumableFilePath); 89 | if (!info.vaild()) { 90 | storage.remove(info); 91 | throw new ServletException("Invalid request params."); 92 | } 93 | return info; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /samples/java/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Resumable.js - Multiple simultaneous, stable and resumable uploads via the HTML5 File API 5 | 6 | 7 | 8 | 9 |
10 | 11 |

Resumable.js

12 |

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

13 | 14 |

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.

15 | 16 |

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+.

17 | 18 |
19 | 20 |

Demo

21 | 22 | 23 | 24 |
25 | Your browser, unfortunately, is not supported by Resumable.js. The library requires support for the HTML5 File API along with file slicing. 26 |
27 | 28 |
29 | Drop video files here to upload or select from your computer 30 |
31 | 32 |
33 | 34 | 35 | 36 | 37 | 41 | 42 |
38 | 39 | 40 |
43 |
44 | 45 |
    46 | 47 | 102 | 103 |
    104 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /samples/Backend on AOLserver and OpenACS.md: -------------------------------------------------------------------------------- 1 | # AOLserver and OpenACS 2 | [@steffentchr](http://twitter.com/steffentchr) 3 | 4 | Our upload managers are using [AOLserver](http://www.aolserver.com/) and [OpenACS](http://www.openacs.com/). 5 | Generally, all Resumable.js request are handled through a single method: 6 | 7 | ad_proc handle_resumable_file { 8 | {-file_parameter_name "file"} 9 | {-folder "/tmp"} 10 | -check_video:boolean 11 | {-max_file_size ""} 12 | } {} { 13 | # Check parameter to see if this is indeed a resumable 14 | ad_page_contract {} { 15 | {resumableChunkNumber:integer "0"} 16 | {resumableChunkSize:integer "0"} 17 | {resumableTotalSize:integer "0"} 18 | {resumableIdentifier ""} 19 | {resumableFilename ""} 20 | } 21 | 22 | # Clean up the identifier 23 | regsub -all {[^0-9A-Za-z_-]} $resumableIdentifier "" resumableIdentifier 24 | 25 | # Check if the request is sane 26 | if { $resumableChunkNumber==0 || $resumableChunkSize==0 || $resumableTotalSize==0 || $resumableIdentifier eq "" } { 27 | return "non_resumable_request" 28 | } 29 | set number_of_chunks [expr int(floor($resumableTotalSize/($resumableChunkSize*1.0)))] 30 | if { $number_of_chunks==0 } {set number_of_chunks 1} 31 | if { $resumableChunkNumber>$number_of_chunks } { 32 | return "invalid_resumable_request" 33 | } 34 | 35 | # What would the file name be? 36 | set filename [file join $folder "resumable-${resumableIdentifier}.${resumableChunkNumber}"] 37 | 38 | # If this is a GET request, we should tell the uploader if the file is already in place or not 39 | if { [ns_conn method] eq "GET" } { 40 | if { [file exists $filename] && [file size $filename]==$resumableChunkSize } { 41 | doc_return 200 text/plain "ok" 42 | } else { 43 | doc_return 404 text/plain "not found" 44 | } 45 | ad_script_abort 46 | } 47 | 48 | # Assign a tmp file 49 | ad_page_contract {} [list "${file_parameter_name}:trim" "${file_parameter_name}.tmpfile:tmpfile"] 50 | set tmp_filename [set "${file_parameter_name}.tmpfile"] 51 | if { $resumableFilename ne "" } { 52 | set original_filename $resumableFilename 53 | } else { 54 | set original_filename [set $file_parameter_name] 55 | } 56 | 57 | # Check data size 58 | if { $max_file_size ne "" && $resumableTotalSize>$max_file_size } { 59 | return [list "invalid_resumable_request" "The file is too large" $original_filename] 60 | } elseif { $resumableChunkNumber<$number_of_chunks && [file size $tmp_filename]!=$resumableChunkSize } { 61 | return [list "invalid_resumable_request" "Wrong data size" $original_filename] 62 | } elseif { $number_of_chunks>1 && $resumableChunkNumber==$number_of_chunks && [file size $tmp_filename] != [expr ($resumableTotalSize % $resumableChunkSize) + $resumableChunkSize] } { 63 | return [list "invalid_resumable_request" "Wrong data size" $original_filename] 64 | } elseif { $number_of_chunks==1 && [file size $tmp_filename] != $resumableTotalSize } { 65 | return [list "invalid_resumable_request" "Wrong data size" $original_filename] 66 | } 67 | 68 | # Save the chunk 69 | file mkdir $folder 70 | file copy -force $tmp_filename $filename 71 | 72 | # Try collating the first and last chunk -- and identify 73 | if { $check_video_p && ($resumableChunkNumber==1 || $resumableChunkNumber==$number_of_chunks) } { 74 | ## (Here you can do check on first and last chunk if needed 75 | ## For example, we will check if this is a support video file.) 76 | } 77 | 78 | # Check if all chunks have come in 79 | set chunk_num 1 80 | set chunk_files [list] 81 | while { $chunk_num<=$number_of_chunks } { 82 | set chunk_filename [file join $folder "resumable-${resumableIdentifier}.${chunk_num}"] 83 | if { ![file exists $chunk_filename] } { 84 | return [list "partly_done" $filename $original_filename] 85 | } 86 | lappend chunk_files $chunk_filename 87 | incr chunk_num 88 | } 89 | 90 | # We've come this far, meaning that all the pieces are in place 91 | set output_filename [file join $folder "resumable-${resumableIdentifier}.final"] 92 | foreach file $chunk_files { 93 | exec cat $file >> $output_filename 94 | catch { 95 | file delete $file 96 | } 97 | } 98 | 99 | return [list "done" $output_filename $original_filename $resumableIdentifier] 100 | } 101 | 102 | 103 | After this, all Resumable.js cases can be handled easily, including 104 | fallback to non-resumable requests. 105 | 106 | # Is this a file from Resumable.js? 107 | lassign [handle_resumable_file] resumable_status resumable_context resumable_original_filename resumable_identifier 108 | if { $resumable_status ne "non_resumable_request" } { 109 | # Yes, it is 110 | switch -exact $resumable_status { 111 | partly_done { 112 | doc_return 200 text/plain ok 113 | ad_script_abort 114 | } 115 | done { 116 | # All the pieces are in place 117 | ad_page_contract {} { 118 | {file:trim "unknown"} 119 | } 120 | set "file.tmpfile" $resumable_context 121 | set file $resumable_identifier 122 | } 123 | invalid_resumable_request - 124 | default { 125 | doc_return 500 text/plain $resumable_context 126 | ad_script_abort 127 | } 128 | } 129 | } else { 130 | # Nope, it's just a straight-forward HTTP request 131 | ad_page_contract {} { 132 | file:trim 133 | file.tmpfile:tmpfile 134 | } 135 | } 136 | 137 | -------------------------------------------------------------------------------- /samples/Node.js/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Resumable.js - Multiple simultaneous, stable and resumable uploads via the HTML5 File API 5 | 6 | 7 | 8 | 9 |
    10 | 11 |

    Resumable.js

    12 |

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

    13 | 14 |

    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.

    15 | 16 |

    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+.

    17 | 18 |
    19 | 20 |

    Demo

    21 | 22 | 23 | 24 |
    25 | Your browser, unfortunately, is not supported by Resumable.js. The library requires support for the HTML5 File API along with file slicing. 26 |
    27 | 28 |
    29 | Drop video files here to upload or select from your computer 30 |
    31 | 32 |
    33 | 34 | 35 | 36 | 37 | 42 | 43 |
    38 | 39 | 40 | 41 |
    44 |
    45 | 46 |
      47 | 48 | 110 | 111 |
      112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /samples/Backend on PHP.md: -------------------------------------------------------------------------------- 1 | # Sample server implementation in PHP 2 | 3 | [Chris Gregory](http://online-php.com) has provided this sample implementation for PHP. 4 | 5 | It's a sample implementation to illustrate chunking. It should probably not be used as-is (for example, be sure to clean file names for dot and dashes to make sure you don't allow files to escape the tempory upload directory). The script is unsupported. 6 | 7 | ```php 8 | `. Once all 19 | * the parts have been uploaded, a final destination file is 20 | * being created from all the stored parts (appending one by one). 21 | * 22 | * @author Gregory Chris (http://online-php.com) 23 | * @email www.online.php@gmail.com 24 | */ 25 | 26 | 27 | //////////////////////////////////////////////////////////////////// 28 | // THE FUNCTIONS 29 | //////////////////////////////////////////////////////////////////// 30 | 31 | /** 32 | * 33 | * Logging operation - to a file (upload_log.txt) and to the stdout 34 | * @param string $str - the logging string 35 | */ 36 | function _log($str) { 37 | 38 | // log to the output 39 | $log_str = date('d.m.Y').": {$str}\r\n"; 40 | echo $log_str; 41 | 42 | // log to file 43 | if (($fp = fopen('upload_log.txt', 'a+')) !== false) { 44 | fputs($fp, $log_str); 45 | fclose($fp); 46 | } 47 | } 48 | 49 | /** 50 | * 51 | * Delete a directory RECURSIVELY 52 | * @param string $dir - directory path 53 | * @link http://php.net/manual/en/function.rmdir.php 54 | */ 55 | function rrmdir($dir) { 56 | if (is_dir($dir)) { 57 | $objects = scandir($dir); 58 | foreach ($objects as $object) { 59 | if ($object != "." && $object != "..") { 60 | if (filetype($dir . "/" . $object) == "dir") { 61 | rrmdir($dir . "/" . $object); 62 | } else { 63 | unlink($dir . "/" . $object); 64 | } 65 | } 66 | } 67 | reset($objects); 68 | rmdir($dir); 69 | } 70 | } 71 | 72 | /** 73 | * 74 | * Check if all the parts exist, and 75 | * gather all the parts of the file together 76 | * @param string $dir - the temporary directory holding all the parts of the file 77 | * @param string $fileName - the original file name 78 | * @param string $chunkSize - each chunk size (in bytes) 79 | * @param string $totalSize - original file size (in bytes) 80 | */ 81 | function createFileFromChunks($temp_dir, $fileName, $chunkSize, $totalSize) { 82 | 83 | // count all the parts of this file 84 | $total_files = 0; 85 | foreach(scandir($temp_dir) as $file) { 86 | if (stripos($file, $fileName) !== false) { 87 | $total_files++; 88 | } 89 | } 90 | 91 | // check that all the parts are present 92 | // the size of the last part is between chunkSize and 2*$chunkSize 93 | if ($total_files * $chunkSize >= ($totalSize - $chunkSize + 1)) { 94 | 95 | // create the final destination file 96 | if (($fp = fopen('temp/'.$fileName, 'w')) !== false) { 97 | for ($i=1; $i<=$total_files; $i++) { 98 | fwrite($fp, file_get_contents($temp_dir.'/'.$fileName.'.part'.$i)); 99 | _log('writing chunk '.$i); 100 | } 101 | fclose($fp); 102 | } else { 103 | _log('cannot create the destination file'); 104 | return false; 105 | } 106 | 107 | // rename the temporary directory (to avoid access from other 108 | // concurrent chunks uploads) and than delete it 109 | if (rename($temp_dir, $temp_dir.'_UNUSED')) { 110 | rrmdir($temp_dir.'_UNUSED'); 111 | } else { 112 | rrmdir($temp_dir); 113 | } 114 | } 115 | 116 | } 117 | 118 | 119 | //////////////////////////////////////////////////////////////////// 120 | // THE SCRIPT 121 | //////////////////////////////////////////////////////////////////// 122 | 123 | //check if request is GET and the requested chunk exists or not. this makes testChunks work 124 | if ($_SERVER['REQUEST_METHOD'] === 'GET') { 125 | 126 | $temp_dir = 'temp/'.$_GET['resumableIdentifier']; 127 | $chunk_file = $temp_dir.'/'.$_GET['resumableFilename'].'.part'.$_GET['resumableChunkNumber']; 128 | if (file_exists($chunk_file)) { 129 | header("HTTP/1.0 200 Ok"); 130 | } else 131 | { 132 | header("HTTP/1.0 404 Not Found"); 133 | } 134 | } 135 | 136 | 137 | 138 | // loop through files and move the chunks to a temporarily created directory 139 | if (!empty($_FILES)) foreach ($_FILES as $file) { 140 | 141 | // check the error status 142 | if ($file['error'] != 0) { 143 | _log('error '.$file['error'].' in file '.$_POST['resumableFilename']); 144 | continue; 145 | } 146 | 147 | // init the destination file (format .part<#chunk> 148 | // the file is stored in a temporary directory 149 | $temp_dir = 'temp/'.$_POST['resumableIdentifier']; 150 | $dest_file = $temp_dir.'/'.$_POST['resumableFilename'].'.part'.$_POST['resumableChunkNumber']; 151 | 152 | // create the temporary directory 153 | if (!is_dir($temp_dir)) { 154 | mkdir($temp_dir, 0777, true); 155 | } 156 | 157 | // move the temporary file 158 | if (!move_uploaded_file($file['tmp_name'], $dest_file)) { 159 | _log('Error saving (move_uploaded_file) chunk '.$_POST['resumableChunkNumber'].' for file '.$_POST['resumableFilename']); 160 | } else { 161 | 162 | // check if all the parts present, and create the final destination file 163 | createFileFromChunks($temp_dir, $_POST['resumableFilename'], 164 | $_POST['resumableChunkSize'], $_POST['resumableTotalSize']); 165 | } 166 | } 167 | ``` 168 | 169 | 170 | -------------------------------------------------------------------------------- /samples/Node.js/resumable-node.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), path = require('path'), util = require('util'), Stream = require('stream').Stream; 2 | 3 | 4 | 5 | module.exports = resumable = function(temporaryFolder){ 6 | var $ = this; 7 | $.temporaryFolder = temporaryFolder; 8 | $.maxFileSize = null; 9 | $.fileParameterName = 'file'; 10 | 11 | try { 12 | fs.mkdirSync($.temporaryFolder); 13 | }catch(e){} 14 | 15 | 16 | var cleanIdentifier = function(identifier){ 17 | return identifier.replace(/^0-9A-Za-z_-/img, ''); 18 | } 19 | 20 | var getChunkFilename = function(chunkNumber, identifier){ 21 | // Clean up the identifier 22 | identifier = cleanIdentifier(identifier); 23 | // What would the file name be? 24 | return path.join($.temporaryFolder, './resumable-'+identifier+'.'+chunkNumber); 25 | } 26 | 27 | var validateRequest = function(chunkNumber, chunkSize, totalSize, identifier, filename, fileSize){ 28 | // Clean up the identifier 29 | identifier = cleanIdentifier(identifier); 30 | 31 | // Check if the request is sane 32 | if (chunkNumber==0 || chunkSize==0 || totalSize==0 || identifier.length==0 || filename.length==0) { 33 | return 'non_resumable_request'; 34 | } 35 | var numberOfChunks = Math.max(Math.floor(totalSize/(chunkSize*1.0)), 1); 36 | if (chunkNumber>numberOfChunks) { 37 | return 'invalid_resumable_request1'; 38 | } 39 | 40 | // Is the file too big? 41 | if($.maxFileSize && totalSize>$.maxFileSize) { 42 | return 'invalid_resumable_request2'; 43 | } 44 | 45 | if(typeof(fileSize)!='undefined') { 46 | if(chunkNumber1 && chunkNumber==numberOfChunks && fileSize!=((totalSize%chunkSize)+chunkSize)) { 51 | // The chunks in the POST is the last one, and the fil is not the correct size 52 | return 'invalid_resumable_request4'; 53 | } 54 | if(numberOfChunks==1 && fileSize!=totalSize) { 55 | // The file is only a single chunk, and the data size does not fit 56 | return 'invalid_resumable_request5'; 57 | } 58 | } 59 | 60 | return 'valid'; 61 | } 62 | 63 | //'found', filename, original_filename, identifier 64 | //'not_found', null, null, null 65 | $.get = function(req, callback){ 66 | var chunkNumber = req.param('resumableChunkNumber', 0); 67 | var chunkSize = req.param('resumableChunkSize', 0); 68 | var totalSize = req.param('resumableTotalSize', 0); 69 | var identifier = req.param('resumableIdentifier', ""); 70 | var filename = req.param('resumableFilename', ""); 71 | 72 | if(validateRequest(chunkNumber, chunkSize, totalSize, identifier, filename)=='valid') { 73 | var chunkFilename = getChunkFilename(chunkNumber, identifier); 74 | path.exists(chunkFilename, function(exists){ 75 | if(exists){ 76 | callback('found', chunkFilename, filename, identifier); 77 | } else { 78 | callback('not_found', null, null, null); 79 | } 80 | }); 81 | } else { 82 | callback('not_found', null, null, null); 83 | } 84 | } 85 | 86 | //'partly_done', filename, original_filename, identifier 87 | //'done', filename, original_filename, identifier 88 | //'invalid_resumable_request', null, null, null 89 | //'non_resumable_request', null, null, null 90 | $.post = function(req, callback){ 91 | 92 | var fields = req.body; 93 | var files = req.files; 94 | 95 | var chunkNumber = fields['resumableChunkNumber']; 96 | var chunkSize = fields['resumableChunkSize']; 97 | var totalSize = fields['resumableTotalSize']; 98 | var identifier = cleanIdentifier(fields['resumableIdentifier']); 99 | var filename = fields['resumableFilename']; 100 | 101 | var original_filename = fields['resumableIdentifier']; 102 | 103 | if(!files[$.fileParameterName] || !files[$.fileParameterName].size) { 104 | callback('invalid_resumable_request', null, null, null); 105 | return; 106 | } 107 | var validation = validateRequest(chunkNumber, chunkSize, totalSize, identifier, files[$.fileParameterName].size); 108 | if(validation=='valid') { 109 | var chunkFilename = getChunkFilename(chunkNumber, identifier); 110 | 111 | // Save the chunk (TODO: OVERWRITE) 112 | fs.rename(files[$.fileParameterName].path, chunkFilename, function(){ 113 | 114 | // Do we have all the chunks? 115 | var currentTestChunk = 1; 116 | var numberOfChunks = Math.max(Math.floor(totalSize/(chunkSize*1.0)), 1); 117 | var testChunkExists = function(){ 118 | path.exists(getChunkFilename(currentTestChunk, identifier), function(exists){ 119 | if(exists){ 120 | currentTestChunk++; 121 | if(currentTestChunk>numberOfChunks) { 122 | callback('done', filename, original_filename, identifier); 123 | } else { 124 | // Recursion 125 | testChunkExists(); 126 | } 127 | } else { 128 | callback('partly_done', filename, original_filename, identifier); 129 | } 130 | }); 131 | } 132 | testChunkExists(); 133 | }); 134 | } else { 135 | callback(validation, filename, original_filename, identifier); 136 | } 137 | } 138 | 139 | 140 | // Pipe chunks directly in to an existsing WritableStream 141 | // r.write(identifier, response); 142 | // r.write(identifier, response, {end:false}); 143 | // 144 | // var stream = fs.createWriteStream(filename); 145 | // r.write(identifier, stream); 146 | // stream.on('data', function(data){...}); 147 | // stream.on('end', function(){...}); 148 | $.write = function(identifier, writableStream, options) { 149 | options = options || {}; 150 | options.end = (typeof options['end'] == 'undefined' ? true : options['end']); 151 | 152 | // Iterate over each chunk 153 | var pipeChunk = function(number) { 154 | 155 | var chunkFilename = getChunkFilename(number, identifier); 156 | path.exists(chunkFilename, function(exists) { 157 | 158 | if (exists) { 159 | // If the chunk with the current number exists, 160 | // then create a ReadStream from the file 161 | // and pipe it to the specified writableStream. 162 | var sourceStream = fs.createReadStream(chunkFilename); 163 | sourceStream.pipe(writableStream, { 164 | end: false 165 | }); 166 | sourceStream.on('end', function() { 167 | // When the chunk is fully streamed, 168 | // jump to the next one 169 | pipeChunk(number + 1); 170 | }); 171 | } else { 172 | // When all the chunks have been piped, end the stream 173 | if (options.end) writableStream.end(); 174 | if (options.onDone) options.onDone(); 175 | } 176 | }); 177 | } 178 | pipeChunk(1); 179 | } 180 | 181 | 182 | $.clean = function(identifier, options) { 183 | options = options || {}; 184 | 185 | // Iterate over each chunk 186 | var pipeChunkRm = function(number) { 187 | 188 | var chunkFilename = getChunkFilename(number, identifier); 189 | 190 | //console.log('removing pipeChunkRm ', number, 'chunkFilename', chunkFilename); 191 | path.exists(chunkFilename, function(exists) { 192 | if (exists) { 193 | 194 | console.log('exist removing ', chunkFilename); 195 | fs.unlink(chunkFilename, function(err) { 196 | if (options.onError) opentions.onError(err); 197 | }); 198 | 199 | pipeChunkRm(number + 1); 200 | 201 | } else { 202 | 203 | if (options.onDone) options.onDone(); 204 | 205 | } 206 | }); 207 | } 208 | pipeChunkRm(1); 209 | } 210 | 211 | return $; 212 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## What is Resumable.js 2 | 3 | Resumable.js is a JavaScript library providing multiple simultaneous, stable and resumable uploads via the HTML5 File API. 4 | 5 | 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. 6 | 7 | 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+. 8 | 9 | Samples and examples are available in the `samples/` folder. Please push your own as Markdown to help document the project. 10 | 11 | 12 | ## How can I use it? 13 | 14 | A new `Resumable` object is created with information of what and where to post: 15 | 16 | var r = new Resumable({ 17 | target:'/api/photo/redeem-upload-token', 18 | query:{upload_token:'my_token'} 19 | }); 20 | // Resumable.js isn't supported, fall back on a different method 21 | if(!r.support) location.href = '/some-old-crappy-uploader'; 22 | 23 | To allow files to be either selected and drag-dropped, you'll assign drop target and a DOM item to be clicked for browsing: 24 | 25 | r.assignBrowse(document.getElementById('browseButton')); 26 | r.assignDrop(document.getElementById('dropTarget')); 27 | 28 | After this, interaction with Resumable.js is done by listening to events: 29 | 30 | r.on('fileAdded', function(file, event){ 31 | ... 32 | }); 33 | r.on('fileSuccess', function(file,message){ 34 | ... 35 | }); 36 | r.on('fileError', function(file, message){ 37 | ... 38 | }); 39 | 40 | ## How do I set it up with my server? 41 | 42 | 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. 43 | 44 | To handle the state of upload chunks, a number of extra parameters are sent along with all requests: 45 | 46 | * `resumableChunkNumber`: The index of the chunk in the current upload. First chunk is `1` (no base-0 counting here). 47 | * `resumableTotalChunks`: The total number of chunks. 48 | * `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. 49 | * `resumableTotalSize`: The total file size. 50 | * `resumableIdentifier`: A unique identifier for the file contained in the request. 51 | * `resumableFilename`: The original file name (since a bug in Firefox results in the file name not being transmitted in chunk multipart posts). 52 | * `resumableRelativePath`: The file's relative path when selecting a directory (defaults to file name in all browsers except Chrome). 53 | 54 | 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. 55 | 56 | For every request, you can confirm reception in HTTP status codes (can be change through the `permanentErrors` option): 57 | 58 | * `200`: The chunk was accepted and correct. No need to re-upload. 59 | * `404`, `415`. `500`, `501`: The file for which the chunk was uploaded is not supported, cancel the entire upload. 60 | * _Anything else_: Something went wrong, but try reuploading the file. 61 | 62 | ## Handling GET (or `test()` requests) 63 | 64 | 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: 65 | 66 | * If this request returns a `200` HTTP code, the chunks is assumed to have been completed. 67 | * If the request returns anything else, the chunk will be uploaded in the standard fashion. 68 | 69 | 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. 70 | 71 | ## Full documentation 72 | 73 | ### Resumable 74 | #### Configuration 75 | 76 | The object is loaded with a configuation hash: 77 | 78 | var r = new Resumable({opt1:'val', ...}); 79 | 80 | Available configuration options are: 81 | 82 | * `target` The target URL for the multipart POST request (Default: `/`) 83 | * `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`) 84 | * `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`) 85 | * `simultaneousUploads` Number of simultaneous uploads (Default: `3`) 86 | * `fileParameterName` The name of the multipart POST parameter to use for the file chunk (Default: `file`) 87 | * `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: `{}`) 88 | * `headers` Extra headers to include in the multipart POST with data (Default: `{}`) 89 | * `method` Method to use when POSTing chunks to the server (`multipart` or `octet`) (Default: `multipart`) 90 | * `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`) 91 | * `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`) 92 | * `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`) 93 | * `generateUniqueIdentifier` Override the function that generates unique identifiers for each file. (Default: `null`) 94 | * `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`) 95 | * `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.*) 96 | * `minFileSize` The minimum allowed file size. (Default: `undefined`) 97 | * `minFileSizeErrorCallback(file, errorCount)` A function which displays an error a selected file is smaller than allowed. (Default: displays an alert for every bad file.) 98 | * `maxFileSize` The maximum allowed file size. (Default: `undefined`) 99 | * `maxFileSizeErrorCallback(file, errorCount)` A function which displays an error a selected file is larger than allowed. (Default: displays an alert for every bad file.) 100 | * `fileType` The file types allowed to upload. An empty array allow any file type. (Default: `[]`) 101 | * `fileTypeErrorCallback(file, errorCount)` A function which displays an error a selected file has type not allowed. (Default: displays an alert for every bad file.) 102 | * `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`) 103 | * `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`) 104 | * `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`) 105 | 106 | #### Properties 107 | 108 | * `.support` A boolean value indicator whether or not Resumable.js is supported by the current browser. 109 | * `.opts` A hash object of the configuration of the Resumable.js instance. 110 | * `.files` An array of `ResumableFile` file objects added by the user (see full docs for this object type below). 111 | 112 | #### Methods 113 | 114 | * `.assignBrowse(domNodes, isDirectory)` Assign a browse action to one or more DOM nodes. Pass in `true` to allow directories to be selected (Chrome only). 115 | * `.assignDrop(domNodes)` Assign one or more DOM nodes as a drop target. 116 | * `.on(event, callback)` Listen for event from Resumable.js (see below) 117 | * `.upload()` Start or resume uploading. 118 | * `.pause()` Pause uploading. 119 | * `.cancel()` Cancel upload of all `ResumableFile` objects and remove them from the list. 120 | * `.progress()` Returns a float between 0 and 1 indicating the current upload progress of all files. 121 | * `.isUploading()` Returns a boolean indicating whether or not the instance is currently uploading anything. 122 | * `.addFile(file)` Add a HTML5 File object to the list of files. 123 | * `.removeFile(file)` Cancel upload of a specific `ResumableFile` object on the list from the list. 124 | * `.getFromUniqueIdentifier(uniqueIdentifier)` Look up a `ResumableFile` object by its unique identifier. 125 | * `.getSize()` Returns the total size of the upload in bytes. 126 | 127 | #### Events 128 | 129 | * `.fileSuccess(file)` A specific file was completed. 130 | * `.fileProgress(file)` Uploading progressed for a specific file. 131 | * `.fileAdded(file, event)` A new file was added. Optionally, you can use the browser `event` object from when the file was added. 132 | * `.filesAdded(array)` New files were added. 133 | * `.fileRetry(file)` Something went wrong during upload of a specific file, uploading is being retried. 134 | * `.fileError(file, message)` An error occured during upload of a specific file. 135 | * `.uploadStart()` Upload has been started on the Resumable object. 136 | * `.complete()` Uploading completed. 137 | * `.progress()` Uploading progress. 138 | * `.error(message, file)` An error, including fileError, occured. 139 | * `.pause()` Uploading was paused. 140 | * `.cancel()` Uploading was canceled. 141 | * `.catchAll(event, ...)` Listen to all the events listed above with the same callback function. 142 | 143 | ### ResumableFile 144 | #### Properties 145 | 146 | * `.resumableObj` A back-reference to the parent `Resumable` object. 147 | * `.file` The correlating HTML5 `File` object. 148 | * `.fileName` The name of the file. 149 | * `.relativePath` The relative path to the file (defaults to file name if relative path doesn't exist) 150 | * `.size` Size in bytes of the file. 151 | * `.uniqueIdentifier` 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. 152 | * `.chunks` An array of `ResumableChunk` items. You shouldn't need to dig into these. 153 | 154 | #### Methods 155 | 156 | * `.progress(relative)` 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. 157 | * `.abort()` Abort uploading the file. 158 | * `.cancel()` Abort uploading the file and delete it from the list of files to upload. 159 | * `.retry()` Retry uploading the file. 160 | * `.bootstrap()` Rebuild the state of a `ResumableFile` object, including reassigning chunks and XMLHttpRequest instances. 161 | * `.isUploading()` Returns a boolean indicating whether file chunks is uploading. 162 | * `.isComplete()` Returns a boolean indicating whether the file has completed uploading and received a server response. 163 | 164 | ## Alternatives 165 | 166 | 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). 167 | 168 | 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. 169 | 170 | -------------------------------------------------------------------------------- /samples/coffeescript/resumable.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "resumable.js", 4 | "sourceRoot": "", 5 | "sources": [ 6 | "resumable.coffee" 7 | ], 8 | "names": [], 9 | "mappings": "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" 10 | } -------------------------------------------------------------------------------- /samples/coffeescript/resumable.coffee: -------------------------------------------------------------------------------- 1 | window.Resumable = class Resumable 2 | 3 | constructor: (@opt)-> 4 | console.log 'constructor' 5 | 6 | #Properties 7 | @support = File? and Blob? and FileList? and (Blob.prototype.webkitSlice? or Blob.prototype.mozSlice? or Blob.prototype.slice?) 8 | @files = [] 9 | @defaults = 10 | chunkSize: 1 * 1024 * 1024 11 | forceChunkSize: false 12 | simultaneousUploads: 3 13 | fileParameterName: 'file' 14 | throttleProgressCallbacks: 0.5 15 | query: {} 16 | headers: {} 17 | preprocess: null 18 | method: 'multipart' 19 | prioritizeFirstAndLastChunk: false 20 | target: '/' 21 | testChunks: true 22 | generateUniqueIdentifier: null 23 | maxChunkRetries: undefined 24 | chunkRetryInterval: undefined 25 | permanentErrors: [415, 500, 501] 26 | maxFiles: undefined 27 | maxFilesErrorCallback: (files, errorCount)-> 28 | #TODO @getOpt 29 | maxFiles = @getOpt('maxFiles') 30 | alert('Please upload ' + maxFiles + ' file' + (maxFiles == 1 ? '': 's') + ' at a time.'); 31 | minFileSize: undefined 32 | minFileSizeErrorCallback: (file, errorCount) -> 33 | #TODO @getOpt 34 | alert(file.fileName +' is too small, please upload files larger than ' + @formatSize(@getOpt('minFileSize')) + '.') 35 | maxFileSize: undefined 36 | maxFileSizeErrorCallback: (file, errorCount) -> 37 | #TODO @getOpt 38 | alert(file.fileName +' is too large, please upload files less than ' + @formatSize(@getOpt('maxFileSize')) + '.') 39 | @opt = {} if not @opt? 40 | @events = [] 41 | 42 | getOpt: (o)-> 43 | 44 | if o instanceof Array 45 | opts = {} 46 | for item in o 47 | opts[item] = @getOpt(item) 48 | return opts 49 | else 50 | return if @opt[o]? then @opt[o] else @defaults[o] 51 | 52 | formatSize: (size)-> 53 | if size < 1024 54 | size + ' bytes' 55 | else if size < 1024 * 1024 56 | (size / 1024.0).toFixed(0) + ' KB' 57 | else if size < 1024 * 1024 * 1024 58 | (size / 1024.0 / 1024.0).toFixed(1) + ' MB' 59 | else 60 | (size / 1024.0 / 1024.0 / 1024.0).toFixed(1) + ' GB' 61 | 62 | stopEvent: (e) -> 63 | console.log 'stopEvent' 64 | e.stopPropagation() 65 | e.preventDefault() 66 | 67 | generateUniqueIdentifier: (file) -> 68 | console.log 'generateUniqueIdentifier' 69 | custom = @getOpt('generateUniqueIdentifier') 70 | if typeof custom is 'function' 71 | return custom file 72 | else 73 | # Some confusion in different versions of Firefox 74 | relativePath = file.webkitRelativePath || file.fileName || file.name 75 | size = file.size 76 | return (size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')) 77 | 78 | # EVENTS 79 | # catchAll(event, ...) 80 | # fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message), 81 | # complete(), progress(), error(message, file), pause() 82 | on:(event, callback) -> 83 | console.log "on: #{event}" 84 | @events.push({event: event, callback: callback}) 85 | 86 | fire: (args...) -> 87 | console.log "fire: #{args[0]}" 88 | event = args[0].toLowerCase() 89 | for e in @events 90 | if e.event.toLowerCase() is event then e.callback.apply(this, args[1..]) 91 | if e.event.toLowerCase() is 'catchall' then e.callback.apply(null, args) 92 | 93 | if event is 'fireerror' then @fire('error', args[2], args[1]) 94 | if event is 'fileprogress' then @fire('progress') 95 | 96 | #Drop 97 | onDrop: (event) -> 98 | console.log "onDrop" 99 | @stopEvent(event) 100 | @appendFilesFromFileList(event.dataTransfer.files, event) 101 | onDragOver: (event) -> 102 | console.log "onDragOver" 103 | event.preventDefault() 104 | 105 | # // INTERNAL METHODS (both handy and responsible for the heavy load) 106 | appendFilesFromFileList: (fileList, event) -> 107 | console.log "appendFilesFromFileList" 108 | # check for uploading too many files 109 | errorCount = 0; 110 | [maxFiles, minFileSize, maxFileSize, maxFilesErrorCallback, minFileSizeErrorCallback, maxFileSizeErrorCallback] = @getOpt(['maxFiles', 'minFileSize', 'maxFileSize', 'maxFilesErrorCallback', 'minFileSizeErrorCallback', 'maxFileSizeErrorCallback']); 111 | 112 | if maxFiles? and maxFiles < (fileList.length + @files.length) 113 | maxFilesErrorCallback fileList, errorCount++ 114 | return false 115 | 116 | files = [] 117 | for file in fileList 118 | #consistency across browsers for the error message 119 | file.name = file.fileName = file.name || file.fileName 120 | 121 | #Check file size 122 | if minFileSize? and file.size < minFileSize 123 | minFileSizeErrorCallback file, errorCount++ 124 | return false 125 | if maxFileSize? and file.size > maxFileSize 126 | maxFilesErrorCallback file, errorCount++ 127 | return false 128 | 129 | #Directories have size == 0 130 | if file.size > 0 and not @getFromUniqueIdentifier(@generateUniqueIdentifier(file)) 131 | resumableFile = new ResumableFile(this, file) 132 | @files.push resumableFile 133 | files.push resumableFile 134 | @fire 'fileAdded', resumableFile, event 135 | 136 | @fire 'fileAdded', files 137 | 138 | uploadNextChunk: ()-> 139 | console.log "uploadNextChunk" 140 | 141 | found = false 142 | # In some cases (such as videos) it's really handy to upload the first 143 | # and last chunk of a file quickly; this let's the server check the file's 144 | # metadata and determine if there's even a point in continuing. 145 | if @getOpt 'prioritizeFirstAndLastChunk' 146 | for file in @files 147 | if file.chunks.length and file.chunks[0].status() is 'pending' and file.chunks[0].preprocessState is 0 148 | file.chunks[0].send() 149 | found = true 150 | break 151 | 152 | if file.chunks.length > 1 and file.chunks[file.chunks.length - 1].status() is 'pending' and file.chunks[file.chunks.length - 1].preprocessState is 0 153 | file.chunks[file.chunks.length - 1].send() 154 | found = true 155 | break 156 | return true if found 157 | 158 | # Now, simply look for the next, best thing to upload 159 | for file in @files 160 | for chunk in file.chunks 161 | if chunk.status() is 'pending' and chunk.preprocessState is 0 162 | chunk.send() 163 | found = true 164 | break 165 | break if found 166 | 167 | return true if found 168 | 169 | #The are no more outstanding chunks to upload, check is everything is done 170 | for file in @files 171 | outstanding = false 172 | for chunk in file.chunks 173 | status = chunk.status() 174 | if status is 'pending' or status is 'uploading' or chunk.preprocessState is 1 175 | outstanding = true 176 | break 177 | break if outstanding 178 | 179 | @fire('complete') if not outstanding #All chunks have been uploaded, complete 180 | 181 | return false 182 | 183 | #PUBLIC METHODS FOR RESUMABLE.JS 184 | assignBrowse: (domNodes, isDirectory) -> 185 | console.log "assignBrowse" 186 | domNodes = [domNodes] if not domNodes.length? 187 | # We will create an and overlay it on the domNode 188 | # (crappy, but since HTML5 doesn't have a cross-browser.browse() method we haven't a choice. 189 | # FF4+ allows click() for this though: https://developer.mozilla.org/en/using_files_from_web_applications) 190 | for dn in domNodes 191 | if dn.tagName is 'INPUT' and dn.type is 'file' 192 | input = dn 193 | else 194 | input = document.createElement('input') 195 | input.setAttribute('type', 'file') 196 | #Place with the dom node an position the input to fill the entire space 197 | dn.style.display = 'inline-block' 198 | dn.style.position = 'relative' 199 | input.style.position = 'absolute' 200 | input.style.top = input.style.left = input.style.bottom = input.style.right = 0 201 | input.style.opacity = 0 202 | input.style.cursor = 'pointer' 203 | dn.appendChild(input) 204 | 205 | maxFiles = @getOpt('maxFiles') 206 | if maxFiles? or maxFiles isnt 1 207 | input.setAttribute('multiple', 'multiple') 208 | else 209 | input.removeAttribute('multiple') 210 | 211 | if isDirectory 212 | input.setAttribute 'webkitdirectory', 'webkitdirectory' 213 | else 214 | input.removeAttribute 'webkitdirectory' 215 | 216 | # When new files are added, simply append them to the overall list 217 | changeHandler = (e)=> 218 | @appendFilesFromFileList(e.target.files) 219 | e.target.value = '' 220 | 221 | input.addEventListener 'change', changeHandler, false 222 | 223 | 224 | 225 | assignDrop: (domNodes) -> 226 | console.log "assignDrop" 227 | domNodes = [domNodes] if not domNodes.length? 228 | for dn in domNodes 229 | dn.addEventListener 'dragover', @onDragOver, false 230 | dn.addEventListener 'drop', @onDrop, false 231 | 232 | unAssignDrop: (domNodes) -> 233 | console.log "unAssignDrop" 234 | domNodes = [domNodes] if not domNodes.length? 235 | for dn in domNodes 236 | dn.removeEventListener 'dragover', @onDragOver 237 | dn.removeEventListener 'drop', @onDrop 238 | 239 | isUploading: () -> 240 | uploading = false 241 | for file in @files 242 | for chunk in file.chunks 243 | if chunk.status() is 'uploading' 244 | uploading = true 245 | break 246 | break if uploading 247 | 248 | return uploading 249 | 250 | upload: () -> 251 | console.log "upload" 252 | #Make sure we don't start too many uploads at once 253 | return if @isUploading() 254 | #Kick off the queue 255 | @fire('uploadStart') 256 | for num in [0..@getOpt('simultaneousUploads')] 257 | @uploadNextChunk() 258 | 259 | pause: ()-> 260 | console.log "pause" 261 | #Resume all chunks currently being uploaded 262 | for file in @files 263 | file.abort() 264 | @fire 'pause' 265 | 266 | cancel:() -> 267 | console.log "cancel" 268 | for file in @files 269 | file.cancel() 270 | @fire 'cancel' 271 | 272 | progress: ()-> 273 | console.log "progress" 274 | totalDone = 0 275 | totalSize = 0 276 | for file in @files 277 | totalDone += file.progress() * file.size 278 | totalSize += file.size 279 | return(if totalSize>0 then totalDone/totalSize else 0) 280 | 281 | addFile: (file)-> 282 | console.log "addFile" 283 | @appendFilesFromFileList([file]) 284 | 285 | removeFile: (file) -> 286 | console.log "removeFile" 287 | files = []; 288 | for f in @files 289 | files.push(f) if f isnt file 290 | @files = files; 291 | getFromUniqueIdentifier: (uniqueIdentifier) -> 292 | console.log "getFromUniqueIdentifier" 293 | for f in @files 294 | return f if f.uniqueIdentifier is uniqueIdentifier 295 | return false 296 | getSize: ()-> 297 | console.log "getSize" 298 | totalSize = 0 299 | for file in @files 300 | totalSize += file.size 301 | return totalSize 302 | 303 | window.ResumableChunk = class ResumableChunk 304 | constructor: (@resumableObj, @fileObj, @offset, @callback) -> 305 | @opt = {} 306 | @fileObjSize = @fileObj.size 307 | @lastProgressCallback = (new Date) 308 | @tested = false 309 | @retries = 0 310 | @preprocessState = 0 # 0 = unprocessed, 1 = processing, 2 = finished 311 | 312 | #Computed properties 313 | @chunkSize = @getOpt('chunkSize') 314 | @loaded = 0 315 | @startByte = @offset * @chunkSize 316 | @endByte = Math.min @fileObjSize, (@offset + 1) * @chunkSize 317 | if (@fileObjSize - @endByte < @chunkSize) and (not @getOpt('forceChunkSize')) 318 | @endByte = @fileObjSize 319 | 320 | @xhr = null 321 | 322 | getOpt: (o)-> 323 | return @resumableObj.getOpt o 324 | 325 | pushParams: (params, key, value) -> 326 | params.push [encodeURIComponent(key), encodeURIComponent(value)].join('=') 327 | 328 | # makes a GET request without any data to see if the chunk has already been uploaded in a previous session 329 | test: () -> 330 | @xhr = new XMLHttpRequest() 331 | 332 | testHandler = (e) => 333 | @tested = true 334 | status = @status() 335 | 336 | if status is 'success' 337 | @callback status, @message() 338 | @resumableObj.uploadNextChunk() 339 | else 340 | @send() 341 | 342 | @xhr.addEventListener 'load' , testHandler, false 343 | @xhr.addEventListener 'error', testHandler, false 344 | 345 | #Add data from the query options 346 | params = [] 347 | 348 | customQuery = @getOpt 'query' 349 | customQuery = customQuery(@fileObj, @) if typeof customQuery is 'function' 350 | if customQuery? 351 | for key, value of customQuery 352 | pushParams key, value 353 | 354 | #Add extra data to identify chunk 355 | @pushParams params, 'resumableChunkNumber', (@offset + 1) 356 | @pushParams params, 'resumableChunkSize', @chunkSize 357 | @pushParams params, 'resumableCurrentChunkSize', (@endByte - @startByte) 358 | @pushParams params, 'resumableTotalSize', @fileObjSize 359 | @pushParams params, 'resumableIdentifier', @fileObj.uniqueIdentifier 360 | @pushParams params, 'resumableFilename', @fileObj.fileName 361 | @pushParams params, 'resumableRelativePath', @fileObj.relativePath 362 | 363 | #Append the relevant chunk and send it 364 | @xhr.open 'GET', @getOpt('target') + '?' + params.join('&') 365 | #Add data from header options 366 | headers = @getOpt('headers') 367 | headers = {} if not headers? 368 | @xhr.setRequestHeader(key, value) for key, value of headers 369 | @xhr.send null 370 | 371 | preprocessFinished: () -> 372 | @preprocessState = 2 373 | @send() 374 | 375 | #send() uploads the actual data in a POST call 376 | send: () -> 377 | preprocess = @getOpt('preprocess') 378 | if typeof preprocess is 'function' 379 | ret = false 380 | switch @preprocessState 381 | when 0 #Go to preprocess 382 | preprocess @ 383 | @preprocessState = 1 384 | ret = true 385 | when 1 then ret = true #Processing 386 | when 2 then ret = false #Go on 387 | return if ret 388 | 389 | if @getOpt('testChunks') and not @tested 390 | @test() 391 | return 392 | 393 | #Set up request and listen for event 394 | @xhr = new XMLHttpRequest() 395 | 396 | #Progress 397 | @loaded = 0 398 | progressHandler = (e) => 399 | if (new Date) - @lastProgressCallback > @getOpt('throttleProgressCallbacks') * 1000 400 | @callback 'progress' 401 | @lastProgressCallback = (new Date) 402 | 403 | @loaded = e.loaded || 0 404 | 405 | @xhr.upload.addEventListener 'progress', progressHandler, false 406 | @callback 'progress' 407 | 408 | # Done (either done, failed or retry) 409 | doneHandler = (e) => 410 | status = @status() 411 | if status is 'success' or status is 'error' 412 | @callback status, @message() 413 | @resumableObj.uploadNextChunk() 414 | else 415 | @callback 'retry', @message() 416 | @abort() 417 | @retries++ 418 | retryInterval = getOpt('chunkRetryInterval') 419 | if retryInterval? 420 | setTimeout @send, retryInterval 421 | 422 | @xhr.addEventListener 'load', doneHandler, false 423 | @xhr.addEventListener 'error', doneHandler, false 424 | 425 | 426 | #Add data from header options 427 | headers = @getOpt('headers') 428 | headers = {} if not headers? 429 | @xhr.setRequestHeader(key, value) for key, value of headers 430 | 431 | if @fileObj.file.slice? 432 | func = 'slice' 433 | else if @fileObj.file.mozSlice? 434 | func = 'mozSlice' 435 | else if @fileObj.file.webkitSlice? 436 | func = 'webkitSlice' 437 | else 438 | func = 'slice' 439 | 440 | bytes = @fileObj.file[func](@startByte, @endByte) 441 | data = null 442 | target = @getOpt 'target' 443 | 444 | #Set up the basic query data from Resumable 445 | query = 446 | resumableChunkNumber: @offset+1 447 | resumableChunkSize: @getOpt('chunkSize') 448 | resumableCurrentChunkSize: @endByte - @startByte 449 | resumableTotalSize: @fileObjSize 450 | resumableIdentifier: @fileObj.uniqueIdentifier 451 | resumableFilename: @fileObj.fileName 452 | resumableRelativePath: @fileObj.relativePath 453 | 454 | 455 | customQuery = @getOpt 'query' 456 | customQuery = customQuery(@fileObj, @) if typeof customQuery is 'function' 457 | customQuery = {} if not customQuery? 458 | 459 | pushParams query, key, value for key, value of customQuery 460 | 461 | if @getOpt('method') is 'octet' 462 | # Add data from the query options 463 | data = bytes 464 | params = [] 465 | for key, value of query 466 | @pushParams params, key, value 467 | 468 | target += '?' + params.join('&') 469 | else 470 | #Add data from the query options 471 | data = new FormData() 472 | for key, value of query 473 | data.append(key, value) 474 | 475 | data.append(@getOpt('fileParameterName'), bytes) 476 | 477 | @xhr.open 'POST', target 478 | @xhr.send data 479 | 480 | abort: ()-> 481 | @xhr.abort() if @xhr? 482 | @xhr = null 483 | 484 | status: ()-> 485 | #Returns: 'pending', 'uploading', 'success', 'error' 486 | 487 | permanentErrors = @getOpt('permanentErrors') 488 | maxChunkRetries = @getOpt('maxChunkRetries') 489 | permanentErrors = {} if not permanentErrors? 490 | maxChunkRetries = 0 if not maxChunkRetries? 491 | 492 | if not @xhr? 493 | return 'pending' 494 | else if @xhr.readyState < 4 495 | # Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening 496 | return 'uploading' 497 | else if @xhr.status is 200 498 | return 'success' 499 | else if (@xhr.status in permanentErrors) or (@retries >= maxChunkRetries) 500 | #HTTP 415/500/501, permanent error 501 | return 'error' 502 | else 503 | #this should never happen, but we'll reset and queue a retry 504 | #a likely case for this would be 503 service unavailable 505 | @abort() 506 | return 'pending' 507 | 508 | message: ()-> 509 | return (if @xhr? then @xhr.responseText else '') 510 | 511 | progress: (relative)-> 512 | factor = (if relative? then (@endByte - @startByte)/@fileObjSize else 1) 513 | switch @status() 514 | when 'success', 'error' 515 | return 1 * factor 516 | when 'pending' 517 | return 0 * factor 518 | else 519 | return @loaded / (@endByte - @startByte) * factor 520 | 521 | window.ResumableFile = class ResumableFile 522 | constructor: (@resumableObj, @file)-> 523 | @opt = {} 524 | @_prevProgress = 0; 525 | @fileName = @file.fileName || @file.name 526 | @size = @file.size 527 | @relativePath = @file.webkitRelativePath || @fileName 528 | @uniqueIdentifier = @resumableObj.generateUniqueIdentifier @file 529 | @_error = false 530 | @chunks = [] 531 | #Bootstrap and return 532 | @bootstrap() 533 | 534 | getOpt: (o)-> 535 | return @resumableObj.getOpt o 536 | 537 | #Callback when something happens within the chunk 538 | chunkEvent: (event, message)-> 539 | switch event 540 | when "progress" then @resumableObj.fire('fileProgress', this) 541 | when "error" 542 | @abort() 543 | @_error = true 544 | @chunks = [] 545 | @resumableObj.fire('fileError', this, message) 546 | when "success" 547 | if not @_error 548 | @resumableObj.fire('fileProgress', this) 549 | if @progress() is 1 550 | @resumableObj.fire('fileSuccess', this, message) 551 | when "retry" then @resumableObj.fire('fileRetry', this) 552 | 553 | # Main code to set up a file object with chunks, 554 | # packaged to be able to handle retries if needed. 555 | abort: () -> 556 | for c in @chunks 557 | c.abort() if c.status() is 'uploading' 558 | @resumableObj.fire 'fileProgress', this 559 | 560 | cancel: ()-> 561 | #Reset this file to be void 562 | _chunks = @chunks 563 | @chunks = [] 564 | # Stop current uploads 565 | for c in _chunks 566 | if c.status() is 'uploading' 567 | c.abort() 568 | @resumableObj.uploadNextChunk() 569 | @resumableObj.removeFile this 570 | @resumableObj.fire('fileProgress', this) 571 | 572 | retry: () -> 573 | @bootstrap(); 574 | @resumableObj.upload() 575 | 576 | bootstrap: () -> 577 | @abort() 578 | @_error = false 579 | # Rebuild stack of chunks from file 580 | @chunks = [] 581 | @_prevProgress = 0 582 | 583 | if @getOpt('forceChunkSize')? 584 | round = Math.ceil 585 | else 586 | round = Math.floor 587 | offset = 0 588 | max = Math.max(round(@file.size / @getOpt('chunkSize')), 1) 589 | for offset in [0..(max-1)] 590 | @chunks.push new ResumableChunk(@resumableObj, this, offset, @chunkEvent) 591 | 592 | progress: ()-> 593 | return (1) if @_error 594 | #Sum up progress across everything 595 | ret = 0 596 | error = false 597 | for c in @chunks 598 | error = c.status() is 'error' 599 | #get chunk progress relative to entire file 600 | ret += c.progress(true) 601 | ret = (if error or error > 0.99 then 1 else ret) 602 | #We don't want to lose percentages when an upload is paused 603 | ret = Math.max(@_prevProgress, ret) 604 | @_prevProgress = ret 605 | return ret 606 | -------------------------------------------------------------------------------- /resumable.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | * MIT Licensed 5 | * http://www.23developer.com/opensource 6 | * http://github.com/23/resumable.js 7 | * Steffen Tiedemann Christensen, steffen@23company.com 8 | */ 9 | 10 | var Resumable = function(opts){ 11 | if ( !(this instanceof Resumable ) ) { 12 | return new Resumable( opts ); 13 | } 14 | this.version = 1.0; 15 | // SUPPORTED BY BROWSER? 16 | // Check if these features are support by the browser: 17 | // - File object type 18 | // - Blob object type 19 | // - FileList object type 20 | // - slicing files 21 | this.support = ( 22 | (typeof(File)!=='undefined') 23 | && 24 | (typeof(Blob)!=='undefined') 25 | && 26 | (typeof(FileList)!=='undefined') 27 | && 28 | (!!Blob.prototype.webkitSlice||!!Blob.prototype.mozSlice||!!Blob.prototype.slice||false) 29 | ); 30 | if(!this.support) return(false); 31 | 32 | 33 | // PROPERTIES 34 | var $ = this; 35 | $.files = []; 36 | $.defaults = { 37 | chunkSize:1*1024*1024, 38 | forceChunkSize:false, 39 | simultaneousUploads:3, 40 | fileParameterName:'file', 41 | throttleProgressCallbacks:0.5, 42 | query:{}, 43 | headers:{}, 44 | preprocess:null, 45 | method:'multipart', 46 | prioritizeFirstAndLastChunk:false, 47 | target:'/', 48 | testChunks:true, 49 | generateUniqueIdentifier:null, 50 | maxChunkRetries:undefined, 51 | chunkRetryInterval:undefined, 52 | permanentErrors:[404, 415, 500, 501], 53 | maxFiles:undefined, 54 | withCredentials:false, 55 | maxFilesErrorCallback:function (files, errorCount) { 56 | var maxFiles = $.getOpt('maxFiles'); 57 | alert('Please upload ' + maxFiles + ' file' + (maxFiles === 1 ? '' : 's') + ' at a time.'); 58 | }, 59 | minFileSize:1, 60 | minFileSizeErrorCallback:function(file, errorCount) { 61 | alert(file.fileName||file.name +' is too small, please upload files larger than ' + $h.formatSize($.getOpt('minFileSize')) + '.'); 62 | }, 63 | maxFileSize:undefined, 64 | maxFileSizeErrorCallback:function(file, errorCount) { 65 | alert(file.fileName||file.name +' is too large, please upload files less than ' + $h.formatSize($.getOpt('maxFileSize')) + '.'); 66 | }, 67 | fileType: [], 68 | fileTypeErrorCallback: function(file, errorCount) { 69 | alert(file.fileName||file.name +' has type not allowed, please upload files of type ' + $.getOpt('fileType') + '.'); 70 | } 71 | }; 72 | $.opts = opts||{}; 73 | $.getOpt = function(o) { 74 | var $this = this; 75 | // Get multiple option if passed an array 76 | if(o instanceof Array) { 77 | var options = {}; 78 | $h.each(o, function(option){ 79 | options[option] = $this.getOpt(option); 80 | }); 81 | return options; 82 | } 83 | // Otherwise, just return a simple option 84 | if ($this instanceof ResumableChunk) { 85 | if (typeof $this.opts[o] !== 'undefined') { return $this.opts[o]; } 86 | else { $this = $this.fileObj; } 87 | } 88 | if ($this instanceof ResumableFile) { 89 | if (typeof $this.opts[o] !== 'undefined') { return $this.opts[o]; } 90 | else { $this = $this.resumableObj; } 91 | } 92 | if ($this instanceof Resumable) { 93 | if (typeof $this.opts[o] !== 'undefined') { return $this.opts[o]; } 94 | else { return $this.defaults[o]; } 95 | } 96 | }; 97 | 98 | // EVENTS 99 | // catchAll(event, ...) 100 | // fileSuccess(file), fileProgress(file), fileAdded(file, event), fileRetry(file), fileError(file, message), 101 | // complete(), progress(), error(message, file), pause() 102 | $.events = []; 103 | $.on = function(event,callback){ 104 | $.events.push(event.toLowerCase(), callback); 105 | }; 106 | $.fire = function(){ 107 | // `arguments` is an object, not array, in FF, so: 108 | var args = []; 109 | for (var i=0; i 0 && !$h.contains(o.fileType, fileType)) { 212 | o.fileTypeErrorCallback(file, errorCount++); 213 | return false; 214 | } 215 | 216 | if (typeof(o.minFileSize)!=='undefined' && file.sizeo.maxFileSize) { 221 | o.maxFileSizeErrorCallback(file, errorCount++); 222 | return false; 223 | } 224 | 225 | // directories have size == 0 226 | if (!$.getFromUniqueIdentifier($h.generateUniqueIdentifier(file))) { 227 | var f = new ResumableFile($, file); 228 | $.files.push(f); 229 | files.push(f); 230 | $.fire('fileAdded', f, event); 231 | } 232 | }); 233 | $.fire('filesAdded', files); 234 | }; 235 | 236 | // INTERNAL OBJECT TYPES 237 | function ResumableFile(resumableObj, file){ 238 | var $ = this; 239 | $.opts = {}; 240 | $.getOpt = resumableObj.getOpt; 241 | $._prevProgress = 0; 242 | $.resumableObj = resumableObj; 243 | $.file = file; 244 | $.fileName = file.fileName||file.name; // Some confusion in different versions of Firefox 245 | $.size = file.size; 246 | $.relativePath = file.webkitRelativePath || $.fileName; 247 | $.uniqueIdentifier = $h.generateUniqueIdentifier(file); 248 | var _error = false; 249 | 250 | // Callback when something happens within the chunk 251 | var chunkEvent = function(event, message){ 252 | // event can be 'progress', 'success', 'error' or 'retry' 253 | switch(event){ 254 | case 'progress': 255 | $.resumableObj.fire('fileProgress', $); 256 | break; 257 | case 'error': 258 | $.abort(); 259 | _error = true; 260 | $.chunks = []; 261 | $.resumableObj.fire('fileError', $, message); 262 | break; 263 | case 'success': 264 | if(_error) return; 265 | $.resumableObj.fire('fileProgress', $); // it's at least progress 266 | if($.isComplete()) { 267 | $.resumableObj.fire('fileSuccess', $, message); 268 | } 269 | break; 270 | case 'retry': 271 | $.resumableObj.fire('fileRetry', $); 272 | break; 273 | } 274 | }; 275 | 276 | // Main code to set up a file object with chunks, 277 | // packaged to be able to handle retries if needed. 278 | $.chunks = []; 279 | $.abort = function(){ 280 | // Stop current uploads 281 | $h.each($.chunks, function(c){ 282 | if(c.status()=='uploading') c.abort(); 283 | }); 284 | $.resumableObj.fire('fileProgress', $); 285 | }; 286 | $.cancel = function(){ 287 | // Reset this file to be void 288 | var _chunks = $.chunks; 289 | $.chunks = []; 290 | // Stop current uploads 291 | $h.each(_chunks, function(c){ 292 | if(c.status()=='uploading') { 293 | c.abort(); 294 | $.resumableObj.uploadNextChunk(); 295 | } 296 | }); 297 | $.resumableObj.removeFile($); 298 | $.resumableObj.fire('fileProgress', $); 299 | }; 300 | $.retry = function(){ 301 | $.bootstrap(); 302 | $.resumableObj.upload(); 303 | }; 304 | $.bootstrap = function(){ 305 | $.abort(); 306 | _error = false; 307 | // Rebuild stack of chunks from file 308 | $.chunks = []; 309 | $._prevProgress = 0; 310 | var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor; 311 | for (var offset=0; offset0.999 ? 1 : ret)); 325 | ret = Math.max($._prevProgress, ret); // We don't want to lose percentages when an upload is paused 326 | $._prevProgress = ret; 327 | return(ret); 328 | }; 329 | $.isUploading = function(){ 330 | var uploading = false; 331 | $h.each($.chunks, function(chunk){ 332 | if(chunk.status()=='uploading') { 333 | uploading = true; 334 | return(false); 335 | } 336 | }); 337 | return(uploading); 338 | }; 339 | $.isComplete = function(){ 340 | var outstanding = false; 341 | $h.each($.chunks, function(chunk){ 342 | var status = chunk.status(); 343 | if(status=='pending' || status=='uploading' || chunk.preprocessState === 1) { 344 | outstanding = true; 345 | return(false); 346 | } 347 | }); 348 | return(!outstanding); 349 | }; 350 | 351 | 352 | // Bootstrap and return 353 | $.bootstrap(); 354 | return(this); 355 | } 356 | 357 | function ResumableChunk(resumableObj, fileObj, offset, callback){ 358 | var $ = this; 359 | $.opts = {}; 360 | $.getOpt = resumableObj.getOpt; 361 | $.resumableObj = resumableObj; 362 | $.fileObj = fileObj; 363 | $.fileObjSize = fileObj.size; 364 | $.fileObjType = fileObj.file.type; 365 | $.offset = offset; 366 | $.callback = callback; 367 | $.lastProgressCallback = (new Date); 368 | $.tested = false; 369 | $.retries = 0; 370 | $.pendingRetry = false; 371 | $.preprocessState = 0; // 0 = unprocessed, 1 = processing, 2 = finished 372 | 373 | // Computed properties 374 | var chunkSize = $.getOpt('chunkSize'); 375 | $.loaded = 0; 376 | $.startByte = $.offset*chunkSize; 377 | $.endByte = Math.min($.fileObjSize, ($.offset+1)*chunkSize); 378 | if ($.fileObjSize-$.endByte < chunkSize && !$.getOpt('forceChunkSize')) { 379 | // The last chunk will be bigger than the chunk size, but less than 2*chunkSize 380 | $.endByte = $.fileObjSize; 381 | } 382 | $.xhr = null; 383 | 384 | // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session 385 | $.test = function(){ 386 | // Set up request and listen for event 387 | $.xhr = new XMLHttpRequest(); 388 | 389 | var testHandler = function(e){ 390 | $.tested = true; 391 | var status = $.status(); 392 | if(status=='success') { 393 | $.callback(status, $.message()); 394 | $.resumableObj.uploadNextChunk(); 395 | } else { 396 | $.send(); 397 | } 398 | }; 399 | $.xhr.addEventListener("load", testHandler, false); 400 | $.xhr.addEventListener("error", testHandler, false); 401 | 402 | // Add data from the query options 403 | var params = []; 404 | var customQuery = $.getOpt('query'); 405 | if(typeof customQuery == "function") customQuery = customQuery($.fileObj, $); 406 | $h.each(customQuery, function(k,v){ 407 | params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); 408 | }); 409 | // Add extra data to identify chunk 410 | params.push(['resumableChunkNumber', encodeURIComponent($.offset+1)].join('=')); 411 | params.push(['resumableChunkSize', encodeURIComponent($.getOpt('chunkSize'))].join('=')); 412 | params.push(['resumableCurrentChunkSize', encodeURIComponent($.endByte - $.startByte)].join('=')); 413 | params.push(['resumableTotalSize', encodeURIComponent($.fileObjSize)].join('=')); 414 | params.push(['resumableType', encodeURIComponent($.fileObjType)].join('=')); 415 | params.push(['resumableIdentifier', encodeURIComponent($.fileObj.uniqueIdentifier)].join('=')); 416 | params.push(['resumableFilename', encodeURIComponent($.fileObj.fileName)].join('=')); 417 | params.push(['resumableRelativePath', encodeURIComponent($.fileObj.relativePath)].join('=')); 418 | // Append the relevant chunk and send it 419 | $.xhr.open("GET", $h.getTarget(params)); 420 | $.xhr.withCredentials = $.getOpt('withCredentials'); 421 | // Add data from header options 422 | $h.each($.getOpt('headers'), function(k,v) { 423 | $.xhr.setRequestHeader(k, v); 424 | }); 425 | $.xhr.send(null); 426 | }; 427 | 428 | $.preprocessFinished = function(){ 429 | $.preprocessState = 2; 430 | $.send(); 431 | }; 432 | 433 | // send() uploads the actual data in a POST call 434 | $.send = function(){ 435 | var preprocess = $.getOpt('preprocess'); 436 | if(typeof preprocess === 'function') { 437 | switch($.preprocessState) { 438 | case 0: preprocess($); $.preprocessState = 1; return; 439 | case 1: return; 440 | case 2: break; 441 | } 442 | } 443 | if($.getOpt('testChunks') && !$.tested) { 444 | $.test(); 445 | return; 446 | } 447 | 448 | // Set up request and listen for event 449 | $.xhr = new XMLHttpRequest(); 450 | 451 | // Progress 452 | $.xhr.upload.addEventListener("progress", function(e){ 453 | if( (new Date) - $.lastProgressCallback > $.getOpt('throttleProgressCallbacks') * 1000 ) { 454 | $.callback('progress'); 455 | $.lastProgressCallback = (new Date); 456 | } 457 | $.loaded=e.loaded||0; 458 | }, false); 459 | $.loaded = 0; 460 | $.pendingRetry = false; 461 | $.callback('progress'); 462 | 463 | // Done (either done, failed or retry) 464 | var doneHandler = function(e){ 465 | var status = $.status(); 466 | if(status=='success'||status=='error') { 467 | $.callback(status, $.message()); 468 | $.resumableObj.uploadNextChunk(); 469 | } else { 470 | $.callback('retry', $.message()); 471 | $.abort(); 472 | $.retries++; 473 | var retryInterval = $.getOpt('chunkRetryInterval'); 474 | if(retryInterval !== undefined) { 475 | $.pendingRetry = true; 476 | setTimeout($.send, retryInterval); 477 | } else { 478 | $.send(); 479 | } 480 | } 481 | }; 482 | $.xhr.addEventListener("load", doneHandler, false); 483 | $.xhr.addEventListener("error", doneHandler, false); 484 | 485 | // Set up the basic query data from Resumable 486 | var query = { 487 | resumableChunkNumber: $.offset+1, 488 | resumableChunkSize: $.getOpt('chunkSize'), 489 | resumableCurrentChunkSize: $.endByte - $.startByte, 490 | resumableTotalSize: $.fileObjSize, 491 | resumableType: $.fileObjType, 492 | resumableIdentifier: $.fileObj.uniqueIdentifier, 493 | resumableFilename: $.fileObj.fileName, 494 | resumableRelativePath: $.fileObj.relativePath, 495 | resumableTotalChunks: $.fileObj.chunks.length 496 | }; 497 | // Mix in custom data 498 | var customQuery = $.getOpt('query'); 499 | if(typeof customQuery == "function") customQuery = customQuery($.fileObj, $); 500 | $h.each(customQuery, function(k,v){ 501 | query[k] = v; 502 | }); 503 | 504 | var func = ($.fileObj.file.slice ? 'slice' : ($.fileObj.file.mozSlice ? 'mozSlice' : ($.fileObj.file.webkitSlice ? 'webkitSlice' : 'slice'))), 505 | bytes = $.fileObj.file[func]($.startByte,$.endByte), 506 | data = null, 507 | target = $.getOpt('target'); 508 | 509 | if ($.getOpt('method') === 'octet') { 510 | // Add data from the query options 511 | data = bytes; 512 | var params = []; 513 | $h.each(query, function(k,v){ 514 | params.push([encodeURIComponent(k), encodeURIComponent(v)].join('=')); 515 | }); 516 | target = $h.getTarget(params); 517 | } else { 518 | // Add data from the query options 519 | data = new FormData(); 520 | $h.each(query, function(k,v){ 521 | data.append(k,v); 522 | }); 523 | data.append($.getOpt('fileParameterName'), bytes); 524 | } 525 | 526 | $.xhr.open('POST', target); 527 | $.xhr.withCredentials = $.getOpt('withCredentials'); 528 | // Add data from header options 529 | $h.each($.getOpt('headers'), function(k,v) { 530 | $.xhr.setRequestHeader(k, v); 531 | }); 532 | $.xhr.send(data); 533 | }; 534 | $.abort = function(){ 535 | // Abort and reset 536 | if($.xhr) $.xhr.abort(); 537 | $.xhr = null; 538 | }; 539 | $.status = function(){ 540 | // Returns: 'pending', 'uploading', 'success', 'error' 541 | if($.pendingRetry) { 542 | // if pending retry then that's effectively the same as actively uploading, 543 | // there might just be a slight delay before the retry starts 544 | return('uploading') 545 | } else if(!$.xhr) { 546 | return('pending'); 547 | } else if($.xhr.readyState<4) { 548 | // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening 549 | return('uploading'); 550 | } else { 551 | if($.xhr.status==200) { 552 | // HTTP 200, perfect 553 | return('success'); 554 | } else if($h.contains($.getOpt('permanentErrors'), $.xhr.status) || $.retries >= $.getOpt('maxChunkRetries')) { 555 | // HTTP 415/500/501, permanent error 556 | return('error'); 557 | } else { 558 | // this should never happen, but we'll reset and queue a retry 559 | // a likely case for this would be 503 service unavailable 560 | $.abort(); 561 | return('pending'); 562 | } 563 | } 564 | }; 565 | $.message = function(){ 566 | return($.xhr ? $.xhr.responseText : ''); 567 | }; 568 | $.progress = function(relative){ 569 | if(typeof(relative)==='undefined') relative = false; 570 | var factor = (relative ? ($.endByte-$.startByte)/$.fileObjSize : 1); 571 | if($.pendingRetry) return(0); 572 | var s = $.status(); 573 | switch(s){ 574 | case 'success': 575 | case 'error': 576 | return(1*factor); 577 | case 'pending': 578 | return(0*factor); 579 | default: 580 | return($.loaded/($.endByte-$.startByte)*factor); 581 | } 582 | }; 583 | return(this); 584 | } 585 | 586 | // QUEUE 587 | $.uploadNextChunk = function(){ 588 | var found = false; 589 | 590 | // In some cases (such as videos) it's really handy to upload the first 591 | // and last chunk of a file quickly; this let's the server check the file's 592 | // metadata and determine if there's even a point in continuing. 593 | if ($.getOpt('prioritizeFirstAndLastChunk')) { 594 | $h.each($.files, function(file){ 595 | if(file.chunks.length && file.chunks[0].status()=='pending' && file.chunks[0].preprocessState === 0) { 596 | file.chunks[0].send(); 597 | found = true; 598 | return(false); 599 | } 600 | if(file.chunks.length>1 && file.chunks[file.chunks.length-1].status()=='pending' && file.chunks[0].preprocessState === 0) { 601 | file.chunks[file.chunks.length-1].send(); 602 | found = true; 603 | return(false); 604 | } 605 | }); 606 | if(found) return(true); 607 | } 608 | 609 | // Now, simply look for the next, best thing to upload 610 | $h.each($.files, function(file){ 611 | $h.each(file.chunks, function(chunk){ 612 | if(chunk.status()=='pending' && chunk.preprocessState === 0) { 613 | chunk.send(); 614 | found = true; 615 | return(false); 616 | } 617 | }); 618 | if(found) return(false); 619 | }); 620 | if(found) return(true); 621 | 622 | // The are no more outstanding chunks to upload, check is everything is done 623 | var outstanding = false; 624 | $h.each($.files, function(file){ 625 | if(!file.isComplete()) { 626 | outstanding = true; 627 | return(false); 628 | } 629 | }); 630 | if(!outstanding) { 631 | // All chunks have been uploaded, complete 632 | $.fire('complete'); 633 | } 634 | return(false); 635 | }; 636 | 637 | 638 | // PUBLIC METHODS FOR RESUMABLE.JS 639 | $.assignBrowse = function(domNodes, isDirectory){ 640 | if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; 641 | 642 | // We will create an and overlay it on the domNode 643 | // (crappy, but since HTML5 doesn't have a cross-browser.browse() method we haven't a choice. 644 | // FF4+ allows click() for this though: https://developer.mozilla.org/en/using_files_from_web_applications) 645 | $h.each(domNodes, function(domNode) { 646 | var input; 647 | if(domNode.tagName==='INPUT' && domNode.type==='file'){ 648 | input = domNode; 649 | } else { 650 | input = document.createElement('input'); 651 | input.setAttribute('type', 'file'); 652 | // Place with the dom node an position the input to fill the entire space 653 | domNode.style.display = 'inline-block'; 654 | domNode.style.position = 'relative'; 655 | input.style.position = 'absolute'; 656 | input.style.top = input.style.left = input.style.bottom = input.style.right = 0; 657 | input.style.opacity = 0; 658 | input.style.cursor = 'pointer'; 659 | domNode.appendChild(input); 660 | } 661 | var maxFiles = $.getOpt('maxFiles'); 662 | if (typeof(maxFiles)==='undefined'||maxFiles!=1){ 663 | input.setAttribute('multiple', 'multiple'); 664 | } else { 665 | input.removeAttribute('multiple'); 666 | } 667 | if(isDirectory){ 668 | input.setAttribute('webkitdirectory', 'webkitdirectory'); 669 | } else { 670 | input.removeAttribute('webkitdirectory'); 671 | } 672 | // When new files are added, simply append them to the overall list 673 | input.addEventListener('change', function(e){ 674 | appendFilesFromFileList(e.target.files); 675 | e.target.value = ''; 676 | }, false); 677 | }); 678 | }; 679 | $.assignDrop = function(domNodes){ 680 | if(typeof(domNodes.length)=='undefined') domNodes = [domNodes]; 681 | 682 | $h.each(domNodes, function(domNode) { 683 | domNode.addEventListener('dragover', onDragOver, false); 684 | domNode.addEventListener('drop', onDrop, false); 685 | }); 686 | }; 687 | $.unAssignDrop = function(domNodes) { 688 | if (typeof(domNodes.length) == 'undefined') domNodes = [domNodes]; 689 | 690 | $h.each(domNodes, function(domNode) { 691 | domNode.removeEventListener('dragover', onDragOver); 692 | domNode.removeEventListener('drop', onDrop); 693 | }); 694 | }; 695 | $.isUploading = function(){ 696 | var uploading = false; 697 | $h.each($.files, function(file){ 698 | if (file.isUploading()) { 699 | uploading = true; 700 | return(false); 701 | } 702 | }); 703 | return(uploading); 704 | }; 705 | $.upload = function(){ 706 | // Make sure we don't start too many uploads at once 707 | if($.isUploading()) return; 708 | // Kick off the queue 709 | $.fire('uploadStart'); 710 | for (var num=1; num<=$.getOpt('simultaneousUploads'); num++) { 711 | $.uploadNextChunk(); 712 | } 713 | }; 714 | $.pause = function(){ 715 | // Resume all chunks currently being uploaded 716 | $h.each($.files, function(file){ 717 | file.abort(); 718 | }); 719 | $.fire('pause'); 720 | }; 721 | $.cancel = function(){ 722 | for(var i = $.files.length - 1; i >= 0; i--) { 723 | $.files[i].cancel(); 724 | } 725 | $.fire('cancel'); 726 | }; 727 | $.progress = function(){ 728 | var totalDone = 0; 729 | var totalSize = 0; 730 | // Resume all chunks currently being uploaded 731 | $h.each($.files, function(file){ 732 | totalDone += file.progress()*file.size; 733 | totalSize += file.size; 734 | }); 735 | return(totalSize>0 ? totalDone/totalSize : 0); 736 | }; 737 | $.addFile = function(file){ 738 | appendFilesFromFileList([file]); 739 | }; 740 | $.removeFile = function(file){ 741 | for(var i = $.files.length - 1; i >= 0; i--) { 742 | if($.files[i] === file) { 743 | $.files.splice(i, 1); 744 | } 745 | } 746 | }; 747 | $.getFromUniqueIdentifier = function(uniqueIdentifier){ 748 | var ret = false; 749 | $h.each($.files, function(f){ 750 | if(f.uniqueIdentifier==uniqueIdentifier) ret = f; 751 | }); 752 | return(ret); 753 | }; 754 | $.getSize = function(){ 755 | var totalSize = 0; 756 | $h.each($.files, function(file){ 757 | totalSize += file.size; 758 | }); 759 | return(totalSize); 760 | }; 761 | 762 | return(this); 763 | }; 764 | 765 | 766 | // Node.js-style export for Node and Component 767 | if(typeof module != 'undefined') { 768 | module.exports = Resumable; 769 | } 770 | -------------------------------------------------------------------------------- /samples/java/web/resumable.js: -------------------------------------------------------------------------------- 1 | // Generated by CoffeeScript 1.6.1 2 | (function() { 3 | var Resumable, ResumableChunk, ResumableFile, 4 | __slice = [].slice, 5 | __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; }; 6 | 7 | window.Resumable = Resumable = (function() { 8 | 9 | function Resumable(opt) { 10 | this.opt = opt; 11 | console.log('constructor'); 12 | 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)); 13 | this.files = []; 14 | this.defaults = { 15 | chunkSize: 1 * 1024 * 1024, 16 | forceChunkSize: false, 17 | simultaneousUploads: 3, 18 | fileParameterName: 'file', 19 | throttleProgressCallbacks: 0.5, 20 | query: {}, 21 | headers: {}, 22 | preprocess: null, 23 | method: 'multipart', 24 | prioritizeFirstAndLastChunk: false, 25 | target: '/', 26 | testChunks: true, 27 | generateUniqueIdentifier: null, 28 | maxChunkRetries: void 0, 29 | chunkRetryInterval: void 0, 30 | permanentErrors: [415, 500, 501], 31 | maxFiles: void 0, 32 | maxFilesErrorCallback: function(files, errorCount) { 33 | var maxFiles, _ref; 34 | maxFiles = this.getOpt('maxFiles'); 35 | return alert('Please upload ' + maxFiles + ' file' + ((_ref = maxFiles === 1) != null ? _ref : { 36 | '': 's' 37 | }) + ' at a time.'); 38 | }, 39 | minFileSize: void 0, 40 | minFileSizeErrorCallback: function(file, errorCount) { 41 | return alert(file.fileName(+' is too small, please upload files larger than ' + this.formatSize(this.getOpt('minFileSize')) + '.')); 42 | }, 43 | maxFileSize: void 0, 44 | maxFileSizeErrorCallback: function(file, errorCount) { 45 | return alert(file.fileName(+' is too large, please upload files less than ' + this.formatSize(this.getOpt('maxFileSize')) + '.')); 46 | } 47 | }; 48 | if (this.opt == null) { 49 | this.opt = {}; 50 | } 51 | this.events = []; 52 | } 53 | 54 | Resumable.prototype.getOpt = function(o) { 55 | var item, opts, _i, _len; 56 | if (o instanceof Array) { 57 | opts = {}; 58 | for (_i = 0, _len = o.length; _i < _len; _i++) { 59 | item = o[_i]; 60 | opts[item] = this.getOpt(item); 61 | } 62 | return opts; 63 | } else { 64 | if (this.opt[o] != null) { 65 | return this.opt[o]; 66 | } else { 67 | return this.defaults[o]; 68 | } 69 | } 70 | }; 71 | 72 | Resumable.prototype.formatSize = function(size) { 73 | if (size < 1024) { 74 | return size + ' bytes'; 75 | } else if (size < 1024 * 1024) { 76 | return (size / 1024.0).toFixed(0) + ' KB'; 77 | } else if (size < 1024 * 1024 * 1024) { 78 | return (size / 1024.0 / 1024.0).toFixed(1) + ' MB'; 79 | } else { 80 | return (size / 1024.0 / 1024.0 / 1024.0).toFixed(1) + ' GB'; 81 | } 82 | }; 83 | 84 | Resumable.prototype.stopEvent = function(e) { 85 | console.log('stopEvent'); 86 | e.stopPropagation(); 87 | return e.preventDefault(); 88 | }; 89 | 90 | Resumable.prototype.generateUniqueIdentifier = function(file) { 91 | var custom, relativePath, size; 92 | console.log('generateUniqueIdentifier'); 93 | custom = this.getOpt('generateUniqueIdentifier'); 94 | if (typeof custom === 'function') { 95 | return custom(file); 96 | } else { 97 | relativePath = file.webkitRelativePath || file.fileName || file.name; 98 | size = file.size; 99 | return size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''); 100 | } 101 | }; 102 | 103 | Resumable.prototype.on = function(event, callback) { 104 | console.log("on: " + event); 105 | return this.events.push({ 106 | event: event, 107 | callback: callback 108 | }); 109 | }; 110 | 111 | Resumable.prototype.fire = function() { 112 | var args, e, event, _i, _len, _ref; 113 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 114 | console.log("fire: " + args[0]); 115 | event = args[0].toLowerCase(); 116 | _ref = this.events; 117 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 118 | e = _ref[_i]; 119 | if (e.event.toLowerCase() === event) { 120 | e.callback.apply(this, args.slice(1)); 121 | } 122 | if (e.event.toLowerCase() === 'catchall') { 123 | e.callback.apply(null, args); 124 | } 125 | } 126 | if (event === 'fireerror') { 127 | this.fire('error', args[2], args[1]); 128 | } 129 | if (event === 'fileprogress') { 130 | return this.fire('progress'); 131 | } 132 | }; 133 | 134 | Resumable.prototype.onDrop = function(event) { 135 | console.log("onDrop"); 136 | this.stopEvent(event); 137 | return this.appendFilesFromFileList(event.dataTransfer.files, event); 138 | }; 139 | 140 | Resumable.prototype.onDragOver = function(event) { 141 | console.log("onDragOver"); 142 | return event.preventDefault(); 143 | }; 144 | 145 | Resumable.prototype.appendFilesFromFileList = function(fileList, event) { 146 | var errorCount, file, files, maxFileSize, maxFileSizeErrorCallback, maxFiles, maxFilesErrorCallback, minFileSize, minFileSizeErrorCallback, resumableFile, _i, _len, _ref; 147 | console.log("appendFilesFromFileList"); 148 | errorCount = 0; 149 | _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]; 150 | if ((maxFiles != null) && maxFiles < (fileList.length + this.files.length)) { 151 | maxFilesErrorCallback(fileList, errorCount++); 152 | return false; 153 | } 154 | files = []; 155 | for (_i = 0, _len = fileList.length; _i < _len; _i++) { 156 | file = fileList[_i]; 157 | file.name = file.fileName = file.name || file.fileName; 158 | if ((minFileSize != null) && file.size < minFileSize) { 159 | minFileSizeErrorCallback(file, errorCount++); 160 | return false; 161 | } 162 | if ((maxFileSize != null) && file.size > maxFileSize) { 163 | maxFilesErrorCallback(file, errorCount++); 164 | return false; 165 | } 166 | if (file.size > 0 && !this.getFromUniqueIdentifier(this.generateUniqueIdentifier(file))) { 167 | resumableFile = new ResumableFile(this, file); 168 | this.files.push(resumableFile); 169 | files.push(resumableFile); 170 | this.fire('fileAdded', resumableFile, event); 171 | } 172 | } 173 | return this.fire('fileAdded', files); 174 | }; 175 | 176 | Resumable.prototype.uploadNextChunk = function() { 177 | var chunk, file, found, outstanding, status, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _m, _ref, _ref1, _ref2, _ref3, _ref4; 178 | console.log("uploadNextChunk"); 179 | found = false; 180 | if (this.getOpt('prioritizeFirstAndLastChunk')) { 181 | _ref = this.files; 182 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 183 | file = _ref[_i]; 184 | if (file.chunks.length && file.chunks[0].status() === 'pending' && file.chunks[0].preprocessState === 0) { 185 | file.chunks[0].send(); 186 | found = true; 187 | break; 188 | } 189 | if (file.chunks.length > 1 && file.chunks[file.chunks.length - 1].status() === 'pending' && file.chunks[file.chunks.length - 1].preprocessState === 0) { 190 | file.chunks[file.chunks.length - 1].send(); 191 | found = true; 192 | break; 193 | } 194 | } 195 | if (found) { 196 | return true; 197 | } 198 | } 199 | _ref1 = this.files; 200 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 201 | file = _ref1[_j]; 202 | _ref2 = file.chunks; 203 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 204 | chunk = _ref2[_k]; 205 | if (chunk.status() === 'pending' && chunk.preprocessState === 0) { 206 | chunk.send(); 207 | found = true; 208 | break; 209 | } 210 | } 211 | if (found) { 212 | break; 213 | } 214 | } 215 | if (found) { 216 | return true; 217 | } 218 | _ref3 = this.files; 219 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 220 | file = _ref3[_l]; 221 | outstanding = false; 222 | _ref4 = file.chunks; 223 | for (_m = 0, _len4 = _ref4.length; _m < _len4; _m++) { 224 | chunk = _ref4[_m]; 225 | status = chunk.status(); 226 | if (status === 'pending' || status === 'uploading' || chunk.preprocessState === 1) { 227 | outstanding = true; 228 | break; 229 | } 230 | } 231 | if (outstanding) { 232 | break; 233 | } 234 | } 235 | if (!outstanding) { 236 | this.fire('complete'); 237 | } 238 | return false; 239 | }; 240 | 241 | Resumable.prototype.assignBrowse = function(domNodes, isDirectory) { 242 | var changeHandler, dn, input, maxFiles, _i, _len, 243 | _this = this; 244 | console.log("assignBrowse"); 245 | if (domNodes.length == null) { 246 | domNodes = [domNodes]; 247 | } 248 | for (_i = 0, _len = domNodes.length; _i < _len; _i++) { 249 | dn = domNodes[_i]; 250 | if (dn.tagName === 'INPUT' && dn.type === 'file') { 251 | input = dn; 252 | } else { 253 | input = document.createElement('input'); 254 | input.setAttribute('type', 'file'); 255 | dn.style.display = 'inline-block'; 256 | dn.style.position = 'relative'; 257 | input.style.position = 'absolute'; 258 | input.style.top = input.style.left = input.style.bottom = input.style.right = 0; 259 | input.style.opacity = 0; 260 | input.style.cursor = 'pointer'; 261 | dn.appendChild(input); 262 | } 263 | } 264 | maxFiles = this.getOpt('maxFiles'); 265 | if ((maxFiles != null) || maxFiles !== 1) { 266 | input.setAttribute('multiple', 'multiple'); 267 | } else { 268 | input.removeAttribute('multiple'); 269 | } 270 | if (isDirectory) { 271 | input.setAttribute('webkitdirectory', 'webkitdirectory'); 272 | } else { 273 | input.removeAttribute('webkitdirectory'); 274 | } 275 | changeHandler = function(e) { 276 | _this.appendFilesFromFileList(e.target.files); 277 | return e.target.value = ''; 278 | }; 279 | return input.addEventListener('change', changeHandler, false); 280 | }; 281 | 282 | Resumable.prototype.assignDrop = function(domNodes) { 283 | var dn, _i, _len, _results; 284 | console.log("assignDrop"); 285 | if (domNodes.length == null) { 286 | domNodes = [domNodes]; 287 | } 288 | _results = []; 289 | for (_i = 0, _len = domNodes.length; _i < _len; _i++) { 290 | dn = domNodes[_i]; 291 | dn.addEventListener('dragover', this.onDragOver, false); 292 | _results.push(dn.addEventListener('drop', this.onDrop, false)); 293 | } 294 | return _results; 295 | }; 296 | 297 | Resumable.prototype.unAssignDrop = function(domNodes) { 298 | var dn, _i, _len, _results; 299 | console.log("unAssignDrop"); 300 | if (domNodes.length == null) { 301 | domNodes = [domNodes]; 302 | } 303 | _results = []; 304 | for (_i = 0, _len = domNodes.length; _i < _len; _i++) { 305 | dn = domNodes[_i]; 306 | dn.removeEventListener('dragover', this.onDragOver); 307 | _results.push(dn.removeEventListener('drop', this.onDrop)); 308 | } 309 | return _results; 310 | }; 311 | 312 | Resumable.prototype.isUploading = function() { 313 | var chunk, file, uploading, _i, _j, _len, _len1, _ref, _ref1; 314 | uploading = false; 315 | _ref = this.files; 316 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 317 | file = _ref[_i]; 318 | _ref1 = file.chunks; 319 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 320 | chunk = _ref1[_j]; 321 | if (chunk.status() === 'uploading') { 322 | uploading = true; 323 | break; 324 | } 325 | } 326 | if (uploading) { 327 | break; 328 | } 329 | } 330 | return uploading; 331 | }; 332 | 333 | Resumable.prototype.upload = function() { 334 | var num, _i, _ref, _results; 335 | console.log("upload"); 336 | if (this.isUploading()) { 337 | return; 338 | } 339 | this.fire('uploadStart'); 340 | _results = []; 341 | for (num = _i = 0, _ref = this.getOpt('simultaneousUploads'); 0 <= _ref ? _i <= _ref : _i >= _ref; num = 0 <= _ref ? ++_i : --_i) { 342 | _results.push(this.uploadNextChunk()); 343 | } 344 | return _results; 345 | }; 346 | 347 | Resumable.prototype.pause = function() { 348 | var file, _i, _len, _ref; 349 | console.log("pause"); 350 | _ref = this.files; 351 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 352 | file = _ref[_i]; 353 | file.abort(); 354 | } 355 | return this.fire('pause'); 356 | }; 357 | 358 | Resumable.prototype.cancel = function() { 359 | var file, _i, _len, _ref; 360 | console.log("cancel"); 361 | _ref = this.files; 362 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 363 | file = _ref[_i]; 364 | file.cancel(); 365 | } 366 | return this.fire('cancel'); 367 | }; 368 | 369 | Resumable.prototype.progress = function() { 370 | var file, totalDone, totalSize, _i, _len, _ref; 371 | console.log("progress"); 372 | totalDone = 0; 373 | totalSize = 0; 374 | _ref = this.files; 375 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 376 | file = _ref[_i]; 377 | totalDone += file.progress() * file.size; 378 | totalSize += file.size; 379 | } 380 | return (totalSize > 0 ? totalDone / totalSize : 0); 381 | }; 382 | 383 | Resumable.prototype.addFile = function(file) { 384 | console.log("addFile"); 385 | return this.appendFilesFromFileList([file]); 386 | }; 387 | 388 | Resumable.prototype.removeFile = function(file) { 389 | var f, files, _i, _len, _ref; 390 | console.log("removeFile"); 391 | files = []; 392 | _ref = this.files; 393 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 394 | f = _ref[_i]; 395 | if (f !== file) { 396 | files.push(f); 397 | } 398 | } 399 | return this.files = files; 400 | }; 401 | 402 | Resumable.prototype.getFromUniqueIdentifier = function(uniqueIdentifier) { 403 | var f, _i, _len, _ref; 404 | console.log("getFromUniqueIdentifier"); 405 | _ref = this.files; 406 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 407 | f = _ref[_i]; 408 | if (f.uniqueIdentifier === uniqueIdentifier) { 409 | return f; 410 | } 411 | } 412 | return false; 413 | }; 414 | 415 | Resumable.prototype.getSize = function() { 416 | var file, totalSize, _i, _len, _ref; 417 | console.log("getSize"); 418 | totalSize = 0; 419 | _ref = this.files; 420 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 421 | file = _ref[_i]; 422 | totalSize += file.size; 423 | } 424 | return totalSize; 425 | }; 426 | 427 | return Resumable; 428 | 429 | })(); 430 | 431 | window.ResumableChunk = ResumableChunk = (function() { 432 | 433 | function ResumableChunk(resumableObj, fileObj, offset, callback) { 434 | this.resumableObj = resumableObj; 435 | this.fileObj = fileObj; 436 | this.offset = offset; 437 | this.callback = callback; 438 | this.opt = {}; 439 | this.fileObjSize = this.fileObj.size; 440 | this.lastProgressCallback = new Date; 441 | this.tested = false; 442 | this.retries = 0; 443 | this.preprocessState = 0; 444 | this.chunkSize = this.getOpt('chunkSize'); 445 | this.loaded = 0; 446 | this.startByte = this.offset * this.chunkSize; 447 | this.endByte = Math.min(this.fileObjSize, (this.offset + 1) * this.chunkSize); 448 | if ((this.fileObjSize - this.endByte < this.chunkSize) && (!this.getOpt('forceChunkSize'))) { 449 | this.endByte = this.fileObjSize; 450 | } 451 | this.xhr = null; 452 | } 453 | 454 | ResumableChunk.prototype.getOpt = function(o) { 455 | return this.resumableObj.getOpt(o); 456 | }; 457 | 458 | ResumableChunk.prototype.pushParams = function(params, key, value) { 459 | return params.push([encodeURIComponent(key), encodeURIComponent(value)].join('=')); 460 | }; 461 | 462 | ResumableChunk.prototype.test = function() { 463 | var customQuery, headers, key, params, testHandler, value, 464 | _this = this; 465 | this.xhr = new XMLHttpRequest(); 466 | testHandler = function(e) { 467 | var status; 468 | _this.tested = true; 469 | status = _this.status(); 470 | if (status === 'success') { 471 | _this.callback(status, _this.message()); 472 | return _this.resumableObj.uploadNextChunk(); 473 | } else { 474 | return _this.send(); 475 | } 476 | }; 477 | this.xhr.addEventListener('load', testHandler, false); 478 | this.xhr.addEventListener('error', testHandler, false); 479 | params = []; 480 | customQuery = this.getOpt('query'); 481 | if (typeof customQuery === 'function') { 482 | customQuery = customQuery(this.fileObj, this); 483 | } 484 | if (customQuery != null) { 485 | for (key in customQuery) { 486 | value = customQuery[key]; 487 | pushParams(key, value); 488 | } 489 | } 490 | this.pushParams(params, 'resumableChunkNumber', this.offset + 1); 491 | this.pushParams(params, 'resumableChunkSize', this.chunkSize); 492 | this.pushParams(params, 'resumableCurrentChunkSize', this.endByte - this.startByte); 493 | this.pushParams(params, 'resumableTotalSize', this.fileObjSize); 494 | this.pushParams(params, 'resumableIdentifier', this.fileObj.uniqueIdentifier); 495 | this.pushParams(params, 'resumableFilename', this.fileObj.fileName); 496 | this.pushParams(params, 'resumableRelativePath', this.fileObj.relativePath); 497 | this.xhr.open('GET', this.getOpt('target') + '?' + params.join('&')); 498 | headers = this.getOpt('headers'); 499 | if (headers == null) { 500 | headers = {}; 501 | } 502 | for (key in headers) { 503 | value = headers[key]; 504 | this.xhr.setRequestHeader(key, value); 505 | } 506 | return this.xhr.send(null); 507 | }; 508 | 509 | ResumableChunk.prototype.preprocessFinished = function() { 510 | this.preprocessState = 2; 511 | return this.send(); 512 | }; 513 | 514 | ResumableChunk.prototype.send = function() { 515 | var bytes, customQuery, data, doneHandler, func, headers, key, params, preprocess, progressHandler, query, ret, target, value, 516 | _this = this; 517 | preprocess = this.getOpt('preprocess'); 518 | if (typeof preprocess === 'function') { 519 | ret = false; 520 | switch (this.preprocessState) { 521 | case 0: 522 | preprocess(this); 523 | this.preprocessState = 1; 524 | ret = true; 525 | break; 526 | case 1: 527 | ret = true; 528 | break; 529 | case 2: 530 | ret = false; 531 | } 532 | if (ret) { 533 | return; 534 | } 535 | } 536 | if (this.getOpt('testChunks') && !this.tested) { 537 | this.test(); 538 | return; 539 | } 540 | this.xhr = new XMLHttpRequest(); 541 | this.loaded = 0; 542 | progressHandler = function(e) { 543 | if ((new Date) - _this.lastProgressCallback > _this.getOpt('throttleProgressCallbacks') * 1000) { 544 | _this.callback('progress'); 545 | _this.lastProgressCallback = new Date; 546 | } 547 | return _this.loaded = e.loaded || 0; 548 | }; 549 | this.xhr.upload.addEventListener('progress', progressHandler, false); 550 | this.callback('progress'); 551 | doneHandler = function(e) { 552 | var retryInterval, status; 553 | status = _this.status(); 554 | if (status === 'success' || status === 'error') { 555 | _this.callback(status, _this.message()); 556 | return _this.resumableObj.uploadNextChunk(); 557 | } else { 558 | _this.callback('retry', _this.message()); 559 | _this.abort(); 560 | _this.retries++; 561 | retryInterval = getOpt('chunkRetryInterval'); 562 | if (retryInterval != null) { 563 | return setTimeout(_this.send, retryInterval); 564 | } 565 | } 566 | }; 567 | this.xhr.addEventListener('load', doneHandler, false); 568 | this.xhr.addEventListener('error', doneHandler, false); 569 | headers = this.getOpt('headers'); 570 | if (headers == null) { 571 | headers = {}; 572 | } 573 | for (key in headers) { 574 | value = headers[key]; 575 | this.xhr.setRequestHeader(key, value); 576 | } 577 | if (this.fileObj.file.slice != null) { 578 | func = 'slice'; 579 | } else if (this.fileObj.file.mozSlice != null) { 580 | func = 'mozSlice'; 581 | } else if (this.fileObj.file.webkitSlice != null) { 582 | func = 'webkitSlice'; 583 | } else { 584 | func = 'slice'; 585 | } 586 | bytes = this.fileObj.file[func](this.startByte, this.endByte); 587 | data = null; 588 | target = this.getOpt('target'); 589 | query = { 590 | resumableChunkNumber: this.offset + 1, 591 | resumableChunkSize: this.getOpt('chunkSize'), 592 | resumableCurrentChunkSize: this.endByte - this.startByte, 593 | resumableTotalSize: this.fileObjSize, 594 | resumableIdentifier: this.fileObj.uniqueIdentifier, 595 | resumableFilename: this.fileObj.fileName, 596 | resumableRelativePath: this.fileObj.relativePath 597 | }; 598 | customQuery = this.getOpt('query'); 599 | if (typeof customQuery === 'function') { 600 | customQuery = customQuery(this.fileObj, this); 601 | } 602 | if (customQuery == null) { 603 | customQuery = {}; 604 | } 605 | for (key in customQuery) { 606 | value = customQuery[key]; 607 | pushParams(query, key, value); 608 | } 609 | if (this.getOpt('method') === 'octet') { 610 | data = bytes; 611 | params = []; 612 | for (key in query) { 613 | value = query[key]; 614 | this.pushParams(params, key, value); 615 | } 616 | target += '?' + params.join('&'); 617 | } else { 618 | data = new FormData(); 619 | for (key in query) { 620 | value = query[key]; 621 | data.append(key, value); 622 | } 623 | data.append(this.getOpt('fileParameterName'), bytes); 624 | } 625 | this.xhr.open('POST', target); 626 | return this.xhr.send(data); 627 | }; 628 | 629 | ResumableChunk.prototype.abort = function() { 630 | if (this.xhr != null) { 631 | this.xhr.abort(); 632 | } 633 | return this.xhr = null; 634 | }; 635 | 636 | ResumableChunk.prototype.status = function() { 637 | var maxChunkRetries, permanentErrors, _ref; 638 | permanentErrors = this.getOpt('permanentErrors'); 639 | maxChunkRetries = this.getOpt('maxChunkRetries'); 640 | if (permanentErrors == null) { 641 | permanentErrors = {}; 642 | } 643 | if (maxChunkRetries == null) { 644 | maxChunkRetries = 0; 645 | } 646 | if (this.xhr == null) { 647 | return 'pending'; 648 | } else if (this.xhr.readyState < 4) { 649 | return 'uploading'; 650 | } else if (this.xhr.status === 200) { 651 | return 'success'; 652 | } else if ((_ref = this.xhr.status, __indexOf.call(permanentErrors, _ref) >= 0) || (this.retries >= maxChunkRetries)) { 653 | return 'error'; 654 | } else { 655 | this.abort(); 656 | return 'pending'; 657 | } 658 | }; 659 | 660 | ResumableChunk.prototype.message = function() { 661 | return (this.xhr != null ? this.xhr.responseText : ''); 662 | }; 663 | 664 | ResumableChunk.prototype.progress = function(relative) { 665 | var factor; 666 | factor = (relative != null ? (this.endByte - this.startByte) / this.fileObjSize : 1); 667 | switch (this.status()) { 668 | case 'success': 669 | case 'error': 670 | return 1 * factor; 671 | case 'pending': 672 | return 0 * factor; 673 | default: 674 | return this.loaded / (this.endByte - this.startByte) * factor; 675 | } 676 | }; 677 | 678 | return ResumableChunk; 679 | 680 | })(); 681 | 682 | window.ResumableFile = ResumableFile = (function() { 683 | 684 | function ResumableFile(resumableObj, file) { 685 | this.resumableObj = resumableObj; 686 | this.file = file; 687 | this.opt = {}; 688 | this._prevProgress = 0; 689 | this.fileName = this.file.fileName || this.file.name; 690 | this.size = this.file.size; 691 | this.relativePath = this.file.webkitRelativePath || this.fileName; 692 | this.uniqueIdentifier = this.resumableObj.generateUniqueIdentifier(this.file); 693 | this._error = false; 694 | this.chunks = []; 695 | this.bootstrap(); 696 | } 697 | 698 | ResumableFile.prototype.getOpt = function(o) { 699 | return this.resumableObj.getOpt(o); 700 | }; 701 | 702 | ResumableFile.prototype.chunkEvent = function(event, message) { 703 | switch (event) { 704 | case "progress": 705 | return this.resumableObj.fire('fileProgress', this); 706 | case "error": 707 | this.abort(); 708 | this._error = true; 709 | this.chunks = []; 710 | return this.resumableObj.fire('fileError', this, message); 711 | case "success": 712 | if (!this._error) { 713 | this.resumableObj.fire('fileProgress', this); 714 | if (this.progress() === 1) { 715 | return this.resumableObj.fire('fileSuccess', this, message); 716 | } 717 | } 718 | break; 719 | case "retry": 720 | return this.resumableObj.fire('fileRetry', this); 721 | } 722 | }; 723 | 724 | ResumableFile.prototype.abort = function() { 725 | var c, _i, _len, _ref; 726 | _ref = this.chunks; 727 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 728 | c = _ref[_i]; 729 | if (c.status() === 'uploading') { 730 | c.abort(); 731 | } 732 | } 733 | return this.resumableObj.fire('fileProgress', this); 734 | }; 735 | 736 | ResumableFile.prototype.cancel = function() { 737 | var c, _chunks, _i, _len; 738 | _chunks = this.chunks; 739 | this.chunks = []; 740 | for (_i = 0, _len = _chunks.length; _i < _len; _i++) { 741 | c = _chunks[_i]; 742 | if (c.status() === 'uploading') { 743 | c.abort(); 744 | this.resumableObj.uploadNextChunk(); 745 | } 746 | } 747 | this.resumableObj.removeFile(this); 748 | return this.resumableObj.fire('fileProgress', this); 749 | }; 750 | 751 | ResumableFile.prototype.retry = function() { 752 | this.bootstrap(); 753 | return this.resumableObj.upload(); 754 | }; 755 | 756 | ResumableFile.prototype.bootstrap = function() { 757 | var max, offset, round, _i, _ref, _results; 758 | this.abort(); 759 | this._error = false; 760 | this.chunks = []; 761 | this._prevProgress = 0; 762 | if (this.getOpt('forceChunkSize') != null) { 763 | round = Math.ceil; 764 | } else { 765 | round = Math.floor; 766 | } 767 | offset = 0; 768 | max = Math.max(round(this.file.size / this.getOpt('chunkSize')), 1); 769 | _results = []; 770 | for (offset = _i = 0, _ref = max - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; offset = 0 <= _ref ? ++_i : --_i) { 771 | _results.push(this.chunks.push(new ResumableChunk(this.resumableObj, this, offset, this.chunkEvent))); 772 | } 773 | return _results; 774 | }; 775 | 776 | ResumableFile.prototype.progress = function() { 777 | var c, error, ret, _i, _len, _ref; 778 | if (this._error) { 779 | return 1.; 780 | } 781 | ret = 0; 782 | error = false; 783 | _ref = this.chunks; 784 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 785 | c = _ref[_i]; 786 | error = c.status() === 'error'; 787 | ret += c.progress(true); 788 | } 789 | ret = (error || error > 0.99 ? 1 : ret); 790 | ret = Math.max(this._prevProgress, ret); 791 | this._prevProgress = ret; 792 | return ret; 793 | }; 794 | 795 | return ResumableFile; 796 | 797 | })(); 798 | 799 | }).call(this); 800 | -------------------------------------------------------------------------------- /samples/coffeescript/resumable.js: -------------------------------------------------------------------------------- 1 | //@ sourceMappingURL=resumable.map 2 | // Generated by CoffeeScript 1.6.1 3 | (function() { 4 | var Resumable, ResumableChunk, ResumableFile, 5 | __slice = [].slice, 6 | __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; }; 7 | 8 | window.Resumable = Resumable = (function() { 9 | 10 | function Resumable(opt) { 11 | this.opt = opt; 12 | console.log('constructor'); 13 | 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)); 14 | this.files = []; 15 | this.defaults = { 16 | chunkSize: 1 * 1024 * 1024, 17 | forceChunkSize: false, 18 | simultaneousUploads: 3, 19 | fileParameterName: 'file', 20 | throttleProgressCallbacks: 0.5, 21 | query: {}, 22 | headers: {}, 23 | preprocess: null, 24 | method: 'multipart', 25 | prioritizeFirstAndLastChunk: false, 26 | target: '/', 27 | testChunks: true, 28 | generateUniqueIdentifier: null, 29 | maxChunkRetries: void 0, 30 | chunkRetryInterval: void 0, 31 | permanentErrors: [415, 500, 501], 32 | maxFiles: void 0, 33 | maxFilesErrorCallback: function(files, errorCount) { 34 | var maxFiles, _ref; 35 | maxFiles = this.getOpt('maxFiles'); 36 | return alert('Please upload ' + maxFiles + ' file' + ((_ref = maxFiles === 1) != null ? _ref : { 37 | '': 's' 38 | }) + ' at a time.'); 39 | }, 40 | minFileSize: void 0, 41 | minFileSizeErrorCallback: function(file, errorCount) { 42 | return alert(file.fileName(+' is too small, please upload files larger than ' + this.formatSize(this.getOpt('minFileSize')) + '.')); 43 | }, 44 | maxFileSize: void 0, 45 | maxFileSizeErrorCallback: function(file, errorCount) { 46 | return alert(file.fileName(+' is too large, please upload files less than ' + this.formatSize(this.getOpt('maxFileSize')) + '.')); 47 | } 48 | }; 49 | if (this.opt == null) { 50 | this.opt = {}; 51 | } 52 | this.events = []; 53 | } 54 | 55 | Resumable.prototype.getOpt = function(o) { 56 | var item, opts, _i, _len; 57 | if (o instanceof Array) { 58 | opts = {}; 59 | for (_i = 0, _len = o.length; _i < _len; _i++) { 60 | item = o[_i]; 61 | opts[item] = this.getOpt(item); 62 | } 63 | return opts; 64 | } else { 65 | if (this.opt[o] != null) { 66 | return this.opt[o]; 67 | } else { 68 | return this.defaults[o]; 69 | } 70 | } 71 | }; 72 | 73 | Resumable.prototype.formatSize = function(size) { 74 | if (size < 1024) { 75 | return size + ' bytes'; 76 | } else if (size < 1024 * 1024) { 77 | return (size / 1024.0).toFixed(0) + ' KB'; 78 | } else if (size < 1024 * 1024 * 1024) { 79 | return (size / 1024.0 / 1024.0).toFixed(1) + ' MB'; 80 | } else { 81 | return (size / 1024.0 / 1024.0 / 1024.0).toFixed(1) + ' GB'; 82 | } 83 | }; 84 | 85 | Resumable.prototype.stopEvent = function(e) { 86 | console.log('stopEvent'); 87 | e.stopPropagation(); 88 | return e.preventDefault(); 89 | }; 90 | 91 | Resumable.prototype.generateUniqueIdentifier = function(file) { 92 | var custom, relativePath, size; 93 | console.log('generateUniqueIdentifier'); 94 | custom = this.getOpt('generateUniqueIdentifier'); 95 | if (typeof custom === 'function') { 96 | return custom(file); 97 | } else { 98 | relativePath = file.webkitRelativePath || file.fileName || file.name; 99 | size = file.size; 100 | return size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, ''); 101 | } 102 | }; 103 | 104 | Resumable.prototype.on = function(event, callback) { 105 | console.log("on: " + event); 106 | return this.events.push({ 107 | event: event, 108 | callback: callback 109 | }); 110 | }; 111 | 112 | Resumable.prototype.fire = function() { 113 | var args, e, event, _i, _len, _ref; 114 | args = 1 <= arguments.length ? __slice.call(arguments, 0) : []; 115 | console.log("fire: " + args[0]); 116 | event = args[0].toLowerCase(); 117 | _ref = this.events; 118 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 119 | e = _ref[_i]; 120 | if (e.event.toLowerCase() === event) { 121 | e.callback.apply(this, args.slice(1)); 122 | } 123 | if (e.event.toLowerCase() === 'catchall') { 124 | e.callback.apply(null, args); 125 | } 126 | } 127 | if (event === 'fireerror') { 128 | this.fire('error', args[2], args[1]); 129 | } 130 | if (event === 'fileprogress') { 131 | return this.fire('progress'); 132 | } 133 | }; 134 | 135 | Resumable.prototype.onDrop = function(event) { 136 | console.log("onDrop"); 137 | this.stopEvent(event); 138 | return this.appendFilesFromFileList(event.dataTransfer.files, event); 139 | }; 140 | 141 | Resumable.prototype.onDragOver = function(event) { 142 | console.log("onDragOver"); 143 | return event.preventDefault(); 144 | }; 145 | 146 | Resumable.prototype.appendFilesFromFileList = function(fileList, event) { 147 | var errorCount, file, files, maxFileSize, maxFileSizeErrorCallback, maxFiles, maxFilesErrorCallback, minFileSize, minFileSizeErrorCallback, resumableFile, _i, _len, _ref; 148 | console.log("appendFilesFromFileList"); 149 | errorCount = 0; 150 | _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]; 151 | if ((maxFiles != null) && maxFiles < (fileList.length + this.files.length)) { 152 | maxFilesErrorCallback(fileList, errorCount++); 153 | return false; 154 | } 155 | files = []; 156 | for (_i = 0, _len = fileList.length; _i < _len; _i++) { 157 | file = fileList[_i]; 158 | file.name = file.fileName = file.name || file.fileName; 159 | if ((minFileSize != null) && file.size < minFileSize) { 160 | minFileSizeErrorCallback(file, errorCount++); 161 | return false; 162 | } 163 | if ((maxFileSize != null) && file.size > maxFileSize) { 164 | maxFilesErrorCallback(file, errorCount++); 165 | return false; 166 | } 167 | if (file.size > 0 && !this.getFromUniqueIdentifier(this.generateUniqueIdentifier(file))) { 168 | resumableFile = new ResumableFile(this, file); 169 | this.files.push(resumableFile); 170 | files.push(resumableFile); 171 | this.fire('fileAdded', resumableFile, event); 172 | } 173 | } 174 | return this.fire('fileAdded', files); 175 | }; 176 | 177 | Resumable.prototype.uploadNextChunk = function() { 178 | var chunk, file, found, outstanding, status, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _m, _ref, _ref1, _ref2, _ref3, _ref4; 179 | console.log("uploadNextChunk"); 180 | found = false; 181 | if (this.getOpt('prioritizeFirstAndLastChunk')) { 182 | _ref = this.files; 183 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 184 | file = _ref[_i]; 185 | if (file.chunks.length && file.chunks[0].status() === 'pending' && file.chunks[0].preprocessState === 0) { 186 | file.chunks[0].send(); 187 | found = true; 188 | break; 189 | } 190 | if (file.chunks.length > 1 && file.chunks[file.chunks.length - 1].status() === 'pending' && file.chunks[file.chunks.length - 1].preprocessState === 0) { 191 | file.chunks[file.chunks.length - 1].send(); 192 | found = true; 193 | break; 194 | } 195 | } 196 | if (found) { 197 | return true; 198 | } 199 | } 200 | _ref1 = this.files; 201 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 202 | file = _ref1[_j]; 203 | _ref2 = file.chunks; 204 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 205 | chunk = _ref2[_k]; 206 | if (chunk.status() === 'pending' && chunk.preprocessState === 0) { 207 | chunk.send(); 208 | found = true; 209 | break; 210 | } 211 | } 212 | if (found) { 213 | break; 214 | } 215 | } 216 | if (found) { 217 | return true; 218 | } 219 | _ref3 = this.files; 220 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 221 | file = _ref3[_l]; 222 | outstanding = false; 223 | _ref4 = file.chunks; 224 | for (_m = 0, _len4 = _ref4.length; _m < _len4; _m++) { 225 | chunk = _ref4[_m]; 226 | status = chunk.status(); 227 | if (status === 'pending' || status === 'uploading' || chunk.preprocessState === 1) { 228 | outstanding = true; 229 | break; 230 | } 231 | } 232 | if (outstanding) { 233 | break; 234 | } 235 | } 236 | if (!outstanding) { 237 | this.fire('complete'); 238 | } 239 | return false; 240 | }; 241 | 242 | Resumable.prototype.assignBrowse = function(domNodes, isDirectory) { 243 | var changeHandler, dn, input, maxFiles, _i, _len, 244 | _this = this; 245 | console.log("assignBrowse"); 246 | if (domNodes.length == null) { 247 | domNodes = [domNodes]; 248 | } 249 | for (_i = 0, _len = domNodes.length; _i < _len; _i++) { 250 | dn = domNodes[_i]; 251 | if (dn.tagName === 'INPUT' && dn.type === 'file') { 252 | input = dn; 253 | } else { 254 | input = document.createElement('input'); 255 | input.setAttribute('type', 'file'); 256 | dn.style.display = 'inline-block'; 257 | dn.style.position = 'relative'; 258 | input.style.position = 'absolute'; 259 | input.style.top = input.style.left = input.style.bottom = input.style.right = 0; 260 | input.style.opacity = 0; 261 | input.style.cursor = 'pointer'; 262 | dn.appendChild(input); 263 | } 264 | } 265 | maxFiles = this.getOpt('maxFiles'); 266 | if ((maxFiles != null) || maxFiles !== 1) { 267 | input.setAttribute('multiple', 'multiple'); 268 | } else { 269 | input.removeAttribute('multiple'); 270 | } 271 | if (isDirectory) { 272 | input.setAttribute('webkitdirectory', 'webkitdirectory'); 273 | } else { 274 | input.removeAttribute('webkitdirectory'); 275 | } 276 | changeHandler = function(e) { 277 | _this.appendFilesFromFileList(e.target.files); 278 | return e.target.value = ''; 279 | }; 280 | return input.addEventListener('change', changeHandler, false); 281 | }; 282 | 283 | Resumable.prototype.assignDrop = function(domNodes) { 284 | var dn, _i, _len, _results; 285 | console.log("assignDrop"); 286 | if (domNodes.length == null) { 287 | domNodes = [domNodes]; 288 | } 289 | _results = []; 290 | for (_i = 0, _len = domNodes.length; _i < _len; _i++) { 291 | dn = domNodes[_i]; 292 | dn.addEventListener('dragover', this.onDragOver, false); 293 | _results.push(dn.addEventListener('drop', this.onDrop, false)); 294 | } 295 | return _results; 296 | }; 297 | 298 | Resumable.prototype.unAssignDrop = function(domNodes) { 299 | var dn, _i, _len, _results; 300 | console.log("unAssignDrop"); 301 | if (domNodes.length == null) { 302 | domNodes = [domNodes]; 303 | } 304 | _results = []; 305 | for (_i = 0, _len = domNodes.length; _i < _len; _i++) { 306 | dn = domNodes[_i]; 307 | dn.removeEventListener('dragover', this.onDragOver); 308 | _results.push(dn.removeEventListener('drop', this.onDrop)); 309 | } 310 | return _results; 311 | }; 312 | 313 | Resumable.prototype.isUploading = function() { 314 | var chunk, file, uploading, _i, _j, _len, _len1, _ref, _ref1; 315 | uploading = false; 316 | _ref = this.files; 317 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 318 | file = _ref[_i]; 319 | _ref1 = file.chunks; 320 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 321 | chunk = _ref1[_j]; 322 | if (chunk.status() === 'uploading') { 323 | uploading = true; 324 | break; 325 | } 326 | } 327 | if (uploading) { 328 | break; 329 | } 330 | } 331 | return uploading; 332 | }; 333 | 334 | Resumable.prototype.upload = function() { 335 | var num, _i, _ref, _results; 336 | console.log("upload"); 337 | if (this.isUploading()) { 338 | return; 339 | } 340 | this.fire('uploadStart'); 341 | _results = []; 342 | for (num = _i = 0, _ref = this.getOpt('simultaneousUploads'); 0 <= _ref ? _i <= _ref : _i >= _ref; num = 0 <= _ref ? ++_i : --_i) { 343 | _results.push(this.uploadNextChunk()); 344 | } 345 | return _results; 346 | }; 347 | 348 | Resumable.prototype.pause = function() { 349 | var file, _i, _len, _ref; 350 | console.log("pause"); 351 | _ref = this.files; 352 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 353 | file = _ref[_i]; 354 | file.abort(); 355 | } 356 | return this.fire('pause'); 357 | }; 358 | 359 | Resumable.prototype.cancel = function() { 360 | var file, _i, _len, _ref; 361 | console.log("cancel"); 362 | _ref = this.files; 363 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 364 | file = _ref[_i]; 365 | file.cancel(); 366 | } 367 | return this.fire('cancel'); 368 | }; 369 | 370 | Resumable.prototype.progress = function() { 371 | var file, totalDone, totalSize, _i, _len, _ref; 372 | console.log("progress"); 373 | totalDone = 0; 374 | totalSize = 0; 375 | _ref = this.files; 376 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 377 | file = _ref[_i]; 378 | totalDone += file.progress() * file.size; 379 | totalSize += file.size; 380 | } 381 | return (totalSize > 0 ? totalDone / totalSize : 0); 382 | }; 383 | 384 | Resumable.prototype.addFile = function(file) { 385 | console.log("addFile"); 386 | return this.appendFilesFromFileList([file]); 387 | }; 388 | 389 | Resumable.prototype.removeFile = function(file) { 390 | var f, files, _i, _len, _ref; 391 | console.log("removeFile"); 392 | files = []; 393 | _ref = this.files; 394 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 395 | f = _ref[_i]; 396 | if (f !== file) { 397 | files.push(f); 398 | } 399 | } 400 | return this.files = files; 401 | }; 402 | 403 | Resumable.prototype.getFromUniqueIdentifier = function(uniqueIdentifier) { 404 | var f, _i, _len, _ref; 405 | console.log("getFromUniqueIdentifier"); 406 | _ref = this.files; 407 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 408 | f = _ref[_i]; 409 | if (f.uniqueIdentifier === uniqueIdentifier) { 410 | return f; 411 | } 412 | } 413 | return false; 414 | }; 415 | 416 | Resumable.prototype.getSize = function() { 417 | var file, totalSize, _i, _len, _ref; 418 | console.log("getSize"); 419 | totalSize = 0; 420 | _ref = this.files; 421 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 422 | file = _ref[_i]; 423 | totalSize += file.size; 424 | } 425 | return totalSize; 426 | }; 427 | 428 | return Resumable; 429 | 430 | })(); 431 | 432 | window.ResumableChunk = ResumableChunk = (function() { 433 | 434 | function ResumableChunk(resumableObj, fileObj, offset, callback) { 435 | this.resumableObj = resumableObj; 436 | this.fileObj = fileObj; 437 | this.offset = offset; 438 | this.callback = callback; 439 | this.opt = {}; 440 | this.fileObjSize = this.fileObj.size; 441 | this.lastProgressCallback = new Date; 442 | this.tested = false; 443 | this.retries = 0; 444 | this.preprocessState = 0; 445 | this.chunkSize = this.getOpt('chunkSize'); 446 | this.loaded = 0; 447 | this.startByte = this.offset * this.chunkSize; 448 | this.endByte = Math.min(this.fileObjSize, (this.offset + 1) * this.chunkSize); 449 | if ((this.fileObjSize - this.endByte < this.chunkSize) && (!this.getOpt('forceChunkSize'))) { 450 | this.endByte = this.fileObjSize; 451 | } 452 | this.xhr = null; 453 | } 454 | 455 | ResumableChunk.prototype.getOpt = function(o) { 456 | return this.resumableObj.getOpt(o); 457 | }; 458 | 459 | ResumableChunk.prototype.pushParams = function(params, key, value) { 460 | return params.push([encodeURIComponent(key), encodeURIComponent(value)].join('=')); 461 | }; 462 | 463 | ResumableChunk.prototype.test = function() { 464 | var customQuery, headers, key, params, testHandler, value, 465 | _this = this; 466 | this.xhr = new XMLHttpRequest(); 467 | testHandler = function(e) { 468 | var status; 469 | _this.tested = true; 470 | status = _this.status(); 471 | if (status === 'success') { 472 | _this.callback(status, _this.message()); 473 | return _this.resumableObj.uploadNextChunk(); 474 | } else { 475 | return _this.send(); 476 | } 477 | }; 478 | this.xhr.addEventListener('load', testHandler, false); 479 | this.xhr.addEventListener('error', testHandler, false); 480 | params = []; 481 | customQuery = this.getOpt('query'); 482 | if (typeof customQuery === 'function') { 483 | customQuery = customQuery(this.fileObj, this); 484 | } 485 | if (customQuery != null) { 486 | for (key in customQuery) { 487 | value = customQuery[key]; 488 | pushParams(key, value); 489 | } 490 | } 491 | this.pushParams(params, 'resumableChunkNumber', this.offset + 1); 492 | this.pushParams(params, 'resumableChunkSize', this.chunkSize); 493 | this.pushParams(params, 'resumableCurrentChunkSize', this.endByte - this.startByte); 494 | this.pushParams(params, 'resumableTotalSize', this.fileObjSize); 495 | this.pushParams(params, 'resumableIdentifier', this.fileObj.uniqueIdentifier); 496 | this.pushParams(params, 'resumableFilename', this.fileObj.fileName); 497 | this.pushParams(params, 'resumableRelativePath', this.fileObj.relativePath); 498 | this.xhr.open('GET', this.getOpt('target') + '?' + params.join('&')); 499 | headers = this.getOpt('headers'); 500 | if (headers == null) { 501 | headers = {}; 502 | } 503 | for (key in headers) { 504 | value = headers[key]; 505 | this.xhr.setRequestHeader(key, value); 506 | } 507 | return this.xhr.send(null); 508 | }; 509 | 510 | ResumableChunk.prototype.preprocessFinished = function() { 511 | this.preprocessState = 2; 512 | return this.send(); 513 | }; 514 | 515 | ResumableChunk.prototype.send = function() { 516 | var bytes, customQuery, data, doneHandler, func, headers, key, params, preprocess, progressHandler, query, ret, target, value, 517 | _this = this; 518 | preprocess = this.getOpt('preprocess'); 519 | if (typeof preprocess === 'function') { 520 | ret = false; 521 | switch (this.preprocessState) { 522 | case 0: 523 | preprocess(this); 524 | this.preprocessState = 1; 525 | ret = true; 526 | break; 527 | case 1: 528 | ret = true; 529 | break; 530 | case 2: 531 | ret = false; 532 | } 533 | if (ret) { 534 | return; 535 | } 536 | } 537 | if (this.getOpt('testChunks') && !this.tested) { 538 | this.test(); 539 | return; 540 | } 541 | this.xhr = new XMLHttpRequest(); 542 | this.loaded = 0; 543 | progressHandler = function(e) { 544 | if ((new Date) - _this.lastProgressCallback > _this.getOpt('throttleProgressCallbacks') * 1000) { 545 | _this.callback('progress'); 546 | _this.lastProgressCallback = new Date; 547 | } 548 | return _this.loaded = e.loaded || 0; 549 | }; 550 | this.xhr.upload.addEventListener('progress', progressHandler, false); 551 | this.callback('progress'); 552 | doneHandler = function(e) { 553 | var retryInterval, status; 554 | status = _this.status(); 555 | if (status === 'success' || status === 'error') { 556 | _this.callback(status, _this.message()); 557 | return _this.resumableObj.uploadNextChunk(); 558 | } else { 559 | _this.callback('retry', _this.message()); 560 | _this.abort(); 561 | _this.retries++; 562 | retryInterval = getOpt('chunkRetryInterval'); 563 | if (retryInterval != null) { 564 | return setTimeout(_this.send, retryInterval); 565 | } 566 | } 567 | }; 568 | this.xhr.addEventListener('load', doneHandler, false); 569 | this.xhr.addEventListener('error', doneHandler, false); 570 | headers = this.getOpt('headers'); 571 | if (headers == null) { 572 | headers = {}; 573 | } 574 | for (key in headers) { 575 | value = headers[key]; 576 | this.xhr.setRequestHeader(key, value); 577 | } 578 | if (this.fileObj.file.slice != null) { 579 | func = 'slice'; 580 | } else if (this.fileObj.file.mozSlice != null) { 581 | func = 'mozSlice'; 582 | } else if (this.fileObj.file.webkitSlice != null) { 583 | func = 'webkitSlice'; 584 | } else { 585 | func = 'slice'; 586 | } 587 | bytes = this.fileObj.file[func](this.startByte, this.endByte); 588 | data = null; 589 | target = this.getOpt('target'); 590 | query = { 591 | resumableChunkNumber: this.offset + 1, 592 | resumableChunkSize: this.getOpt('chunkSize'), 593 | resumableCurrentChunkSize: this.endByte - this.startByte, 594 | resumableTotalSize: this.fileObjSize, 595 | resumableIdentifier: this.fileObj.uniqueIdentifier, 596 | resumableFilename: this.fileObj.fileName, 597 | resumableRelativePath: this.fileObj.relativePath 598 | }; 599 | customQuery = this.getOpt('query'); 600 | if (typeof customQuery === 'function') { 601 | customQuery = customQuery(this.fileObj, this); 602 | } 603 | if (customQuery == null) { 604 | customQuery = {}; 605 | } 606 | for (key in customQuery) { 607 | value = customQuery[key]; 608 | pushParams(query, key, value); 609 | } 610 | if (this.getOpt('method') === 'octet') { 611 | data = bytes; 612 | params = []; 613 | for (key in query) { 614 | value = query[key]; 615 | this.pushParams(params, key, value); 616 | } 617 | target += '?' + params.join('&'); 618 | } else { 619 | data = new FormData(); 620 | for (key in query) { 621 | value = query[key]; 622 | data.append(key, value); 623 | } 624 | data.append(this.getOpt('fileParameterName'), bytes); 625 | } 626 | this.xhr.open('POST', target); 627 | return this.xhr.send(data); 628 | }; 629 | 630 | ResumableChunk.prototype.abort = function() { 631 | if (this.xhr != null) { 632 | this.xhr.abort(); 633 | } 634 | return this.xhr = null; 635 | }; 636 | 637 | ResumableChunk.prototype.status = function() { 638 | var maxChunkRetries, permanentErrors, _ref; 639 | permanentErrors = this.getOpt('permanentErrors'); 640 | maxChunkRetries = this.getOpt('maxChunkRetries'); 641 | if (permanentErrors == null) { 642 | permanentErrors = {}; 643 | } 644 | if (maxChunkRetries == null) { 645 | maxChunkRetries = 0; 646 | } 647 | if (this.xhr == null) { 648 | return 'pending'; 649 | } else if (this.xhr.readyState < 4) { 650 | return 'uploading'; 651 | } else if (this.xhr.status === 200) { 652 | return 'success'; 653 | } else if ((_ref = this.xhr.status, __indexOf.call(permanentErrors, _ref) >= 0) || (this.retries >= maxChunkRetries)) { 654 | return 'error'; 655 | } else { 656 | this.abort(); 657 | return 'pending'; 658 | } 659 | }; 660 | 661 | ResumableChunk.prototype.message = function() { 662 | return (this.xhr != null ? this.xhr.responseText : ''); 663 | }; 664 | 665 | ResumableChunk.prototype.progress = function(relative) { 666 | var factor; 667 | factor = (relative != null ? (this.endByte - this.startByte) / this.fileObjSize : 1); 668 | switch (this.status()) { 669 | case 'success': 670 | case 'error': 671 | return 1 * factor; 672 | case 'pending': 673 | return 0 * factor; 674 | default: 675 | return this.loaded / (this.endByte - this.startByte) * factor; 676 | } 677 | }; 678 | 679 | return ResumableChunk; 680 | 681 | })(); 682 | 683 | window.ResumableFile = ResumableFile = (function() { 684 | 685 | function ResumableFile(resumableObj, file) { 686 | this.resumableObj = resumableObj; 687 | this.file = file; 688 | this.opt = {}; 689 | this._prevProgress = 0; 690 | this.fileName = this.file.fileName || this.file.name; 691 | this.size = this.file.size; 692 | this.relativePath = this.file.webkitRelativePath || this.fileName; 693 | this.uniqueIdentifier = this.resumableObj.generateUniqueIdentifier(this.file); 694 | this._error = false; 695 | this.chunks = []; 696 | this.bootstrap(); 697 | } 698 | 699 | ResumableFile.prototype.getOpt = function(o) { 700 | return this.resumableObj.getOpt(o); 701 | }; 702 | 703 | ResumableFile.prototype.chunkEvent = function(event, message) { 704 | switch (event) { 705 | case "progress": 706 | return this.resumableObj.fire('fileProgress', this); 707 | case "error": 708 | this.abort(); 709 | this._error = true; 710 | this.chunks = []; 711 | return this.resumableObj.fire('fileError', this, message); 712 | case "success": 713 | if (!this._error) { 714 | this.resumableObj.fire('fileProgress', this); 715 | if (this.progress() === 1) { 716 | return this.resumableObj.fire('fileSuccess', this, message); 717 | } 718 | } 719 | break; 720 | case "retry": 721 | return this.resumableObj.fire('fileRetry', this); 722 | } 723 | }; 724 | 725 | ResumableFile.prototype.abort = function() { 726 | var c, _i, _len, _ref; 727 | _ref = this.chunks; 728 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 729 | c = _ref[_i]; 730 | if (c.status() === 'uploading') { 731 | c.abort(); 732 | } 733 | } 734 | return this.resumableObj.fire('fileProgress', this); 735 | }; 736 | 737 | ResumableFile.prototype.cancel = function() { 738 | var c, _chunks, _i, _len; 739 | _chunks = this.chunks; 740 | this.chunks = []; 741 | for (_i = 0, _len = _chunks.length; _i < _len; _i++) { 742 | c = _chunks[_i]; 743 | if (c.status() === 'uploading') { 744 | c.abort(); 745 | this.resumableObj.uploadNextChunk(); 746 | } 747 | } 748 | this.resumableObj.removeFile(this); 749 | return this.resumableObj.fire('fileProgress', this); 750 | }; 751 | 752 | ResumableFile.prototype.retry = function() { 753 | this.bootstrap(); 754 | return this.resumableObj.upload(); 755 | }; 756 | 757 | ResumableFile.prototype.bootstrap = function() { 758 | var max, offset, round, _i, _ref, _results; 759 | this.abort(); 760 | this._error = false; 761 | this.chunks = []; 762 | this._prevProgress = 0; 763 | if (this.getOpt('forceChunkSize') != null) { 764 | round = Math.ceil; 765 | } else { 766 | round = Math.floor; 767 | } 768 | offset = 0; 769 | max = Math.max(round(this.file.size / this.getOpt('chunkSize')), 1); 770 | _results = []; 771 | for (offset = _i = 0, _ref = max - 1; 0 <= _ref ? _i <= _ref : _i >= _ref; offset = 0 <= _ref ? ++_i : --_i) { 772 | _results.push(this.chunks.push(new ResumableChunk(this.resumableObj, this, offset, this.chunkEvent))); 773 | } 774 | return _results; 775 | }; 776 | 777 | ResumableFile.prototype.progress = function() { 778 | var c, error, ret, _i, _len, _ref; 779 | if (this._error) { 780 | return 1.; 781 | } 782 | ret = 0; 783 | error = false; 784 | _ref = this.chunks; 785 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 786 | c = _ref[_i]; 787 | error = c.status() === 'error'; 788 | ret += c.progress(true); 789 | } 790 | ret = (error || error > 0.99 ? 1 : ret); 791 | ret = Math.max(this._prevProgress, ret); 792 | this._prevProgress = ret; 793 | return ret; 794 | }; 795 | 796 | return ResumableFile; 797 | 798 | })(); 799 | 800 | }).call(this); 801 | --------------------------------------------------------------------------------