uploads 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (97) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +3 -0
  3. data/Rakefile +38 -0
  4. data/app/assets/javascripts/anjlab/uploads.js +7 -0
  5. data/app/assets/javascripts/anjlab/uploads/button.js.coffee +80 -0
  6. data/app/assets/javascripts/anjlab/uploads/dnd.js.coffee +215 -0
  7. data/app/assets/javascripts/anjlab/uploads/handler.base.js.coffee +101 -0
  8. data/app/assets/javascripts/anjlab/uploads/handler.form.js.coffee +173 -0
  9. data/app/assets/javascripts/anjlab/uploads/handler.xhr.js.coffee +393 -0
  10. data/app/assets/javascripts/anjlab/uploads/uploader.js.coffee +487 -0
  11. data/app/assets/javascripts/anjlab/uploads/utils.js.coffee +116 -0
  12. data/lib/tasks/uploads_tasks.rake +4 -0
  13. data/lib/uploads.rb +10 -0
  14. data/lib/uploads/engine.rb +6 -0
  15. data/lib/uploads/version.rb +3 -0
  16. data/test/dummy/README.rdoc +261 -0
  17. data/test/dummy/Rakefile +7 -0
  18. data/test/dummy/app/assets/javascripts/application.js +16 -0
  19. data/test/dummy/app/assets/stylesheets/application.css +13 -0
  20. data/test/dummy/app/controllers/application_controller.rb +3 -0
  21. data/test/dummy/app/controllers/uploads_controller.rb +15 -0
  22. data/test/dummy/app/controllers/welcome_controller.rb +4 -0
  23. data/test/dummy/app/helpers/application_helper.rb +2 -0
  24. data/test/dummy/app/views/layouts/application.html.erb +14 -0
  25. data/test/dummy/app/views/welcome/index.html.slim +53 -0
  26. data/test/dummy/config.ru +4 -0
  27. data/test/dummy/config/application.rb +59 -0
  28. data/test/dummy/config/boot.rb +10 -0
  29. data/test/dummy/config/database.yml +25 -0
  30. data/test/dummy/config/environment.rb +5 -0
  31. data/test/dummy/config/environments/development.rb +37 -0
  32. data/test/dummy/config/environments/production.rb +67 -0
  33. data/test/dummy/config/environments/test.rb +37 -0
  34. data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
  35. data/test/dummy/config/initializers/inflections.rb +15 -0
  36. data/test/dummy/config/initializers/mime_types.rb +5 -0
  37. data/test/dummy/config/initializers/secret_token.rb +7 -0
  38. data/test/dummy/config/initializers/session_store.rb +8 -0
  39. data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
  40. data/test/dummy/config/locales/en.yml +5 -0
  41. data/test/dummy/config/routes.rb +59 -0
  42. data/test/dummy/db/development.sqlite3 +0 -0
  43. data/test/dummy/log/development.log +23847 -0
  44. data/test/dummy/public/404.html +26 -0
  45. data/test/dummy/public/422.html +26 -0
  46. data/test/dummy/public/500.html +25 -0
  47. data/test/dummy/public/favicon.ico +0 -0
  48. data/test/dummy/script/rails +6 -0
  49. data/test/dummy/tmp/cache/assets/C79/790/sprockets%2Fde97a756642791010533233b267b1afe +0 -0
  50. data/test/dummy/tmp/cache/assets/C99/300/sprockets%2F3bd289b286c074ad66220ad20181b135 +0 -0
  51. data/test/dummy/tmp/cache/assets/C9E/CA0/sprockets%2F1252e234767209f94d0a41a0bc19e33c +0 -0
  52. data/test/dummy/tmp/cache/assets/CA1/9E0/sprockets%2F4c40d59c233952767c150cb8234cab22 +0 -0
  53. data/test/dummy/tmp/cache/assets/CA5/AC0/sprockets%2Fcc77204615926b95033b4fa044d7c61b +0 -0
  54. data/test/dummy/tmp/cache/assets/CAA/860/sprockets%2F6b1d0c998769132cda15f7c5d1283300 +0 -0
  55. data/test/dummy/tmp/cache/assets/CCD/830/sprockets%2F962b2259b044ca05005c14fd83ba982a +0 -0
  56. data/test/dummy/tmp/cache/assets/CD2/7B0/sprockets%2Fe9a64cf98e26a4719e64d747d7912893 +0 -0
  57. data/test/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
  58. data/test/dummy/tmp/cache/assets/CE4/A00/sprockets%2Fe589f936724000ba0e7f077a63b75d4e +0 -0
  59. data/test/dummy/tmp/cache/assets/CE7/D40/sprockets%2F888fd320a7c0125218313f1ef9d6d99d +0 -0
  60. data/test/dummy/tmp/cache/assets/CFD/650/sprockets%2F73de09397391979f4dcd67ce32a7816f +0 -0
  61. data/test/dummy/tmp/cache/assets/D07/710/sprockets%2F56976babbbfe475040b22885733ca41a +0 -0
  62. data/test/dummy/tmp/cache/assets/D0B/D70/sprockets%2F6c298d94014122a613afbebce90590f8 +0 -0
  63. data/test/dummy/tmp/cache/assets/D0F/6A0/sprockets%2F18414faf9678c2ab6d1d60891c6f06a1 +0 -0
  64. data/test/dummy/tmp/cache/assets/D15/C40/sprockets%2F5516d9e4e25aaefc72a78fc641962512 +0 -0
  65. data/test/dummy/tmp/cache/assets/D24/CD0/sprockets%2Fd0cd795c6f0b6074de96706e8bf70975 +0 -0
  66. data/test/dummy/tmp/cache/assets/D2E/980/sprockets%2Fded84d1bb4c31404691d106f4b8e120f +0 -0
  67. data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
  68. data/test/dummy/tmp/cache/assets/D33/120/sprockets%2F70d8b84b628400ffbf410b17b845bae2 +0 -0
  69. data/test/dummy/tmp/cache/assets/D35/8C0/sprockets%2F20424272a569797e71dbae1bdb33c2fc +0 -0
  70. data/test/dummy/tmp/cache/assets/D3F/FE0/sprockets%2Fbfc6e8a285039670d3ca0399bd3e633b +0 -0
  71. data/test/dummy/tmp/cache/assets/D44/E50/sprockets%2F5767c31d95e72a26ffb3c0092dc3cf94 +0 -0
  72. data/test/dummy/tmp/cache/assets/D48/8B0/sprockets%2F795b331f7ff4a59f0604c531e71af9de +0 -0
  73. data/test/dummy/tmp/cache/assets/D4D/420/sprockets%2Fab27b88913d73cdabe65891eeb884444 +0 -0
  74. data/test/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
  75. data/test/dummy/tmp/cache/assets/D5A/2A0/sprockets%2Fc379b9f699c8f614cefea762d376d224 +0 -0
  76. data/test/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
  77. data/test/dummy/tmp/cache/assets/D5D/330/sprockets%2Fce164ed2c0f3e91b338035376ad41bac +0 -0
  78. data/test/dummy/tmp/cache/assets/D6D/350/sprockets%2Fdac0344fd04f6fe00bce8983177635ea +0 -0
  79. data/test/dummy/tmp/cache/assets/D71/6E0/sprockets%2F07996d57b5cfca8a92b02613fdaae735 +0 -0
  80. data/test/dummy/tmp/cache/assets/D7C/210/sprockets%2Ff98c0ec43e4406aa3cec29837d9f618e +0 -0
  81. data/test/dummy/tmp/cache/assets/D7E/AF0/sprockets%2F1d4ffb9a3d536682ff7d185668dc2d4b +0 -0
  82. data/test/dummy/tmp/cache/assets/D96/9A0/sprockets%2Ff1caf4ea19ee66a13841eaf6f40a6433 +0 -0
  83. data/test/dummy/tmp/cache/assets/D97/760/sprockets%2Fd1c9a476956a0ccae81fa57dad53135a +0 -0
  84. data/test/dummy/tmp/cache/assets/DBB/F20/sprockets%2F03e6ee44546ae1ea1bfd3e0801b7ca7a +0 -0
  85. data/test/dummy/tmp/cache/assets/DC3/810/sprockets%2Fc86e769b165de4f9d49745ab9fee7b5f +0 -0
  86. data/test/dummy/tmp/cache/assets/DC5/3C0/sprockets%2Fc21f1ad7fdce18f5b7a5a581b1351fd6 +0 -0
  87. data/test/dummy/tmp/cache/assets/DCA/4A0/sprockets%2Fbdfe27f0efa249de171cbfb2970661c2 +0 -0
  88. data/test/dummy/tmp/cache/assets/DD3/AF0/sprockets%2Fb090f4fd52cbc2e5ccc815aa8999f2f3 +0 -0
  89. data/test/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
  90. data/test/dummy/tmp/cache/assets/DE7/920/sprockets%2Feea789b2f9e87ceaffa7757e32fd1192 +0 -0
  91. data/test/dummy/tmp/cache/assets/E02/FE0/sprockets%2F68a4afe37ab83b2d9fa1cf5e084fec81 +0 -0
  92. data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
  93. data/test/dummy/tmp/cache/assets/E2B/C80/sprockets%2Ff6f179d2c6207aabbc3cec5c260eee9e +0 -0
  94. data/test/dummy/tmp/cache/assets/E47/5F0/sprockets%2Fef5f1e3bcbc9baa50e54a15ba0e5ca91 +0 -0
  95. data/test/test_helper.rb +15 -0
  96. data/test/uploads_test.rb +7 -0
  97. metadata +276 -0
@@ -0,0 +1,173 @@
1
+ utils = @AnjLab.Uploads.Utils
2
+
3
+ class @AnjLab.Uploads.UploadHandlerForm extends @AnjLab.Uploads.UploadHandler
4
+
5
+ constructor: (options)->
6
+ super(options)
7
+ @inputs = []
8
+ @uuids = []
9
+ @detachLoadEvents = {}
10
+
11
+ attachLoadEvent: (iframe, callback)->
12
+ detach = =>
13
+ return if !@detachLoadEvents[iframe.id]
14
+ @log('Received response for ' + iframe.id)
15
+ # when we remove iframe from dom
16
+ # the request stops, but in IE load
17
+ # event fires
18
+ return if !iframe.parentNode
19
+
20
+ try
21
+ # fixing Opera 10.53
22
+ # In Opera event is fired second time
23
+ # when body.innerHTML changed from false
24
+ # to server response approx. after 1 sec
25
+ # when we upload file with iframe
26
+ return if iframe.contentDocument?.body?.innerHTML == 'false'
27
+
28
+ catch error
29
+ #IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
30
+ @log("Error when attempting to access iframe during handling of upload response (#{error})", 'error')
31
+
32
+ callback()
33
+ delete @detachLoadEvents[iframe.id]
34
+
35
+ @detachLoadEvents[iframe.id] = detach
36
+ $(iframe).on 'load', detach
37
+
38
+ # Returns json object received by iframe from server.
39
+ getIframeContentJson: (iframe)->
40
+ # IE may throw an "access is denied" error when attempting to access contentDocument on the iframe in some cases
41
+ try
42
+ # iframe.contentWindow.document - for IE<7
43
+ doc = iframe.contentDocument || iframe.contentWindow.document
44
+ innerHTML = doc.body.innerHTML
45
+
46
+ @log "converting iframe's innerHTML to JSON"
47
+ @log "innerHTML = #{innerHTML}"
48
+ # plain text response may be wrapped in <pre> tag
49
+ if innerHTML && innerHTML.match(/^<pre/i)
50
+ innerHTML = doc.body.firstChild.firstChild.nodeValue
51
+
52
+ utils.parseJson(innerHTML)
53
+ catch error
54
+ @log "Error when attempting to parse form upload response (#{error })", 'error'
55
+ {success: false}
56
+
57
+ # Creates iframe with unique name
58
+ createIframe: (id)->
59
+ # We can't use following code as the name attribute
60
+ # won't be properly registered in IE6, and new window
61
+ # on form submit will open
62
+ # var iframe = document.createElement('iframe');
63
+ # iframe.setAttribute('name', id);
64
+
65
+ iframe = $("<iframe src='javascript:false;' name='#{id}' />")[0]
66
+ # src="javascript:false;" removes ie6 prompt on https
67
+ iframe.setAttribute('id', id);
68
+ iframe.style.display = 'none';
69
+ document.body.appendChild(iframe)
70
+ iframe
71
+
72
+ # Creates form, that will be submitted to iframe
73
+ createForm: (id, iframe)->
74
+ params = @options.paramsStore.getParams(id)
75
+ protocol = if @options.demoMode then "GET" else "POST"
76
+ csrf_param = $("meta[name=csrf-param]").attr("content")
77
+ csrf_token = $("meta[name=csrf-token]").attr("content")
78
+ $form = $("<form method='#{protocol}' accept-charset='utf-8' enctype='multipart/form-data'>
79
+ <input type='hidden' name='#{csrf_param}' value='#{csrf_token}'>
80
+ </form>")
81
+ endpoint = @options.endpointStore.getEndpoint(id)
82
+ url = endpoint
83
+
84
+ params[@options.uuidParamName] = @uuids[id]
85
+ params['utf8'] = '✓'
86
+
87
+ url = endpoint + (if /\?/.test(url) then '&' else '?') + $.param(params)
88
+
89
+ $form.attr 'action', url
90
+ $form.attr 'target', iframe.name
91
+ $form.css {display: 'none'}
92
+ $form.appendTo(document.body)
93
+
94
+ $form
95
+
96
+ # api
97
+
98
+ add: (fileInput)->
99
+ fileInput.setAttribute('name', @options.inputName)
100
+
101
+ id = @inputs.push(fileInput) - 1
102
+ @uuids[id] = utils.getUniqueId()
103
+
104
+ # remove file input from DOM
105
+ if fileInput.parentNode
106
+ $(fileInput).remove()
107
+
108
+ id
109
+
110
+ getName: (id)->
111
+ # get input value and remove path to normalize
112
+ @inputs[id].value.replace(/.*(\/|\\)/, "")
113
+
114
+ isValid: (id)-> @inputs[id]?
115
+
116
+ reset: ->
117
+ super()
118
+ @inputs = []
119
+ @uuids = []
120
+ @detachLoadEvents = {}
121
+
122
+ getUuid: (id) -> @uuids[id]
123
+
124
+ cancelFile: (id) ->
125
+ @options.onCancel(id, @getName(id))
126
+
127
+ delete @inputs[id]
128
+ delete @uuids[id]
129
+ delete @detachLoadEvents[id]
130
+
131
+ iframe = document.getElementById(id)
132
+ if iframe
133
+ # to cancel request set src to something else
134
+ # we use src="javascript:false;" because it doesn't
135
+ # trigger ie6 prompt on https
136
+ iframe.setAttribute('src', 'java' + String.fromCharCode(115) + 'cript:false;'); # deal with "JSLint: javascript URL" warning, which apparently cannot be turned off
137
+ $(iframe).remove()
138
+
139
+ uploadFile: (id) ->
140
+ input = @inputs[id]
141
+ fileName = @getName(id)
142
+ iframe = @createIframe(id)
143
+ $form = @createForm(id, iframe);
144
+
145
+ if !input
146
+ throw new Error('file with passed id was not added, or already uploaded or cancelled')
147
+
148
+ @options.onUpload(id, this.getName(id))
149
+
150
+ $form.append(input)
151
+
152
+ @attachLoadEvent(iframe, ()=>
153
+ @log('iframe loaded')
154
+
155
+ response = @getIframeContentJson(iframe)
156
+ # timeout added to fix busy state in FF3.6
157
+ setTimeout(()->
158
+ $(iframe).remove()
159
+ , 1)
160
+
161
+ if !response.success
162
+ if @options.onAutoRetry(id, fileName, response)
163
+ return
164
+
165
+ @options.onComplete(id, fileName, response)
166
+ @uploadComplete(id)
167
+ )
168
+
169
+ @log("Sending upload request for #{id}")
170
+ $form.submit()
171
+ $form.remove()
172
+
173
+ id
@@ -0,0 +1,393 @@
1
+ utils = @AnjLab.Uploads.Utils
2
+
3
+ class @AnjLab.Uploads.UploadHandlerXhr extends @AnjLab.Uploads.UploadHandler
4
+
5
+ constructor: (options)->
6
+ super(options)
7
+ @fileState = []
8
+ @cookieItemDelimiter = "|"
9
+ @chunkFiles = @options.chunking.enabled && utils.isFileChunkingSupported()
10
+ @resumeEnabled = @options.resume.enabled && @chunkFiles && utils.areCookiesEnabled()
11
+ @resumeId = @getResumeId()
12
+ @multipart = @options.forceMultipart
13
+
14
+ addChunkingSpecificParams: (id, params, chunkData)->
15
+ size = @getSize(id)
16
+ name = @getName(id);
17
+
18
+ params[@options.chunking.paramNames.partIndex] = chunkData.part
19
+ params[@options.chunking.paramNames.partByteOffset] = chunkData.start
20
+ params[@options.chunking.paramNames.chunkSize] = chunkData.end - chunkData.start
21
+ params[@options.chunking.paramNames.totalParts] = chunkData.count
22
+ params[@options.totalFileSizeParamName] = size
23
+
24
+
25
+
26
+ # When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
27
+ # or an empty string. So, we will need to include the actual file name as a param in this case.
28
+ if @multipart
29
+ params[@options.chunking.paramNames.filename] = name
30
+
31
+ addResumeSpecificParams: (params) ->
32
+ params[@options.resume.paramNames.resuming] = true
33
+
34
+ getChunk: (file, startByte, endByte) ->
35
+ if file.slice
36
+ file.slice(startByte, endByte)
37
+ else if file.mozSlice
38
+ file.mozSlice(startByte, endByte)
39
+ else if file.webkitSlice
40
+ file.webkitSlice(startByte, endByte)
41
+
42
+ getChunkData: (id, chunkIndex) ->
43
+ chunkSize = @options.chunking.partSize
44
+ fileSize = @getSize(id)
45
+ file = @fileState[id].file
46
+ startBytes = chunkSize * chunkIndex
47
+ endBytes = startBytes+chunkSize >= if fileSize then fileSize else startBytes+chunkSize
48
+ totalChunks = @getTotalChunks(id)
49
+
50
+ {
51
+ part: chunkIndex
52
+ start: startBytes
53
+ end: endBytes
54
+ count: totalChunks,
55
+ blob: @getChunk(file, startBytes, endBytes)
56
+ }
57
+
58
+ getTotalChunks: (id) ->
59
+ Math.ceil(@getSize(id) / @options.chunking.partSize)
60
+
61
+ createXhr: (id) ->
62
+ @fileState[id].xhr = new XMLHttpRequest()
63
+ # fileState[id].xhr
64
+
65
+ setParamsAndGetEntityToSend: (params, xhr, fileOrBlob, id) ->
66
+ formData = new FormData()
67
+ protocol = if @options.demoMode then "GET" else "POST"
68
+ endpoint = @options.endpointStore.getEndpoint(id)
69
+ url = endpoint
70
+ name = @getName(id)
71
+ size = @getSize(id)
72
+
73
+ params[@options.uuidParamName] = @fileState[id].uuid;
74
+
75
+ params[@options.totalFileSizeParamName] = size if @multipart
76
+
77
+ params[@options.inputName] = name
78
+ params['_' + @options.inputName] = name
79
+ params['utf8'] = '✓'
80
+
81
+ csrf_param = $("meta[name=csrf-param]").attr("content")
82
+ csrf_token = $("meta[name=csrf-token]").attr("content")
83
+
84
+ params[csrf_param] = csrf_token
85
+
86
+ url = endpoint + (if /\?/.test(url) then '&' else '?') + $.param(params)
87
+
88
+ xhr.open protocol, url, true
89
+ fileOrBlob
90
+
91
+ setHeaders: (id, xhr) ->
92
+ extraHeaders = @options.customHeaders
93
+ name = @getName(id)
94
+ file = @fileState[id].file
95
+
96
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
97
+ xhr.setRequestHeader("Cache-Control", "no-cache");
98
+
99
+ xhr.setRequestHeader("Content-Type", "application/octet-stream");
100
+ # NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
101
+ xhr.setRequestHeader("X-Mime-Type", file.type);
102
+
103
+ for own name, val of extraHeaders
104
+ xhr.setRequestHeader(name, val)
105
+
106
+ handleCompletedFile: (id, response, xhr) ->
107
+ name = @getName(id)
108
+ size = @getSize(id)
109
+
110
+ @fileState[id].attemptingResume = false
111
+
112
+ @options.onProgress(id, name, size, size)
113
+
114
+ @options.onComplete(id, name, response, xhr)
115
+ delete @fileState[id].xhr
116
+ @uploadComplete(id)
117
+
118
+ handleSuccessfullyCompletedChunk: (id, response, xhr) ->
119
+ chunkIdx = @fileState[id].remainingChunkIdxs.shift()
120
+ chunkData = @getChunkData(id, chunkIdx)
121
+
122
+ @fileState[id].attemptingResume = false
123
+ @fileState[id].loaded += chunkData.end - chunkData.start
124
+
125
+ if @fileState[id].remainingChunkIdxs.length > 0
126
+ @uploadNextChunk(id)
127
+ else
128
+ @deletePersistedChunkData(id)
129
+ @handleCompletedFile(id, response, xhr)
130
+
131
+ isErrorResponse: (xhr, response) ->
132
+ xhr.status != 200 || !response.success || response.reset
133
+
134
+ parseResponse: (xhr) ->
135
+ try
136
+ utils.parseJson(xhr.responseText)
137
+ catch error
138
+ @log("Error when attempting to parse xhr response text (#{error})", 'error');
139
+ {}
140
+
141
+ handleResetResponse: (id)->
142
+ @log('Server has ordered chunking effort to be restarted on next attempt for file ID ' + id, 'error');
143
+
144
+ if @resumeEnabled
145
+ @deletePersistedChunkData(id)
146
+ @fileState[id].remainingChunkIdxs = []
147
+ delete @fileState[id].loaded
148
+
149
+ handleResetResponseOnResumeAttempt: (id) ->
150
+ @fileState[id].attemptingResume = false
151
+ @log("Server has declared that it cannot handle resume for file ID " + id + " - starting from the first chunk", 'error');
152
+ @uploadFile(id, true)
153
+
154
+ getChunkDataForCallback: (chunkData) ->
155
+ {
156
+ partIndex: chunkData.part
157
+ startByte: chunkData.start + 1
158
+ endByte: chunkData.end
159
+ totalParts: chunkData.count
160
+ }
161
+
162
+ getReadyStateChangeHandler: (id, xhr) ->
163
+ => @onComplete(id, xhr) if xhr.readyState == 4
164
+
165
+ persistChunkData: (id, chunkData) ->
166
+ fileUuid = @getUuid(id)
167
+ cookieName = @getChunkDataCookieName(id)
168
+ cookieValue = fileUuid + @cookieItemDelimiter + chunkData.part
169
+ cookieExpDays = @options.resume.cookiesExpireIn
170
+
171
+ utils.setCookie(cookieName, cookieValue, cookieExpDays)
172
+
173
+ deletePersistedChunkData: (id) ->
174
+ cookieName = @getChunkDataCookieName(id)
175
+
176
+ utils.deleteCookie(cookieName)
177
+
178
+ getPersistedChunkData: (id) ->
179
+ chunkCookieValue = utils.getCookie(@getChunkDataCookieName(id))
180
+
181
+ return if !chunkCookieValue
182
+
183
+ delimiterIndex = chunkCookieValue.indexOf(@cookieItemDelimiter)
184
+ uuid = chunkCookieValue.substr(0, delimiterIndex)
185
+ partIndex = parseInt(chunkCookieValue.substr(delimiterIndex + 1, chunkCookieValue.length - delimiterIndex), 10)
186
+
187
+ {
188
+ uuid: uuid
189
+ part: partIndex
190
+ }
191
+
192
+ handleNonResetErrorResponse: (id, response, xhr) ->
193
+ return if @options.onAutoRetry(id, @getName(id), response, xhr)
194
+
195
+ @handleCompletedFile(id, response, xhr)
196
+
197
+ getChunkDataCookieName: (id) ->
198
+ filename = @getName(id)
199
+ fileSize = @getSize(id)
200
+ maxChunkSize = @options.chunking.partSize
201
+
202
+ parts = ['qqfilechunk', encodeURIComponent(filename), fileSize, maxChunkSize]
203
+ parts << @resumeId if @resumeId?
204
+ parts.join(@cookieItemDelimiter)
205
+
206
+ getResumeId: ->
207
+ @options.resume.id if @options.resume.id? &&
208
+ !$.isFunction(@options.resume.id) &&
209
+ !$.isObject(@options.resume.id)
210
+
211
+ uploadNextChunk: (id) ->
212
+ chunkData = @getChunkData(id, @fileState[id].remainingChunkIdxs[0])
213
+ xhr = @createXhr(id)
214
+ size = @getSize(id)
215
+ name = @getName(id)
216
+
217
+ if @fileState[id].loaded?
218
+ @fileState[id].loaded = 0
219
+
220
+ @persistChunkData(id, chunkData)
221
+
222
+ xhr.onreadystatechange = @getReadyStateChangeHandler(id, xhr)
223
+
224
+ xhr.upload.onprogress = (e) =>
225
+ if e.lengthComputable
226
+ if @fileState[id].loaded < size
227
+ totalLoaded = e.loaded + @fileState[id].loaded
228
+ @options.onProgress(id, name, totalLoaded, size)
229
+
230
+ @options.onUploadChunk(id, name, @getChunkDataForCallback(chunkData))
231
+
232
+ params = @options.paramsStore.getParams(id)
233
+ @addChunkingSpecificParams(id, params, chunkData)
234
+
235
+ @addResumeSpecificParams(params) if @fileState[id].attemptingResume
236
+
237
+ toSend = @setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
238
+ @setHeaders(id, xhr)
239
+
240
+ @log('Sending chunked upload request for ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
241
+ xhr.send(toSend)
242
+
243
+ onComplete: (id, xhr) ->
244
+ # the request was aborted/cancelled
245
+ return if !@fileState[id]
246
+
247
+ @log("xhr - server response received for " + id)
248
+ @log("responseText = " + xhr.responseText)
249
+
250
+ response = @parseResponse(xhr)
251
+
252
+ if @isErrorResponse(xhr, response)
253
+ if response.reset
254
+ @handleResetResponse(id);
255
+
256
+ if @fileState[id].attemptingResume && response.reset
257
+ @handleResetResponseOnResumeAttempt(id)
258
+ else
259
+ @handleNonResetErrorResponse(id, response, xhr)
260
+ else if @chunkFiles
261
+ @handleSuccessfullyCompletedChunk(id, response, xhr)
262
+ else
263
+ @handleCompletedFile(id, response, xhr)
264
+
265
+ handleFileChunkingUpload: (id, retry) ->
266
+ name = @getName(id)
267
+ firstChunkIndex = 0
268
+
269
+ if !@fileState[id].remainingChunkIdxs || @fileState[id].remainingChunkIdxs.length == 0
270
+ @fileState[id].remainingChunkIdxs = []
271
+
272
+ if @resumeEnabled && !retry
273
+ persistedChunkInfoForResume = @getPersistedChunkData(id);
274
+ if persistedChunkInfoForResume
275
+ firstChunkDataForResume = @getChunkData(id, persistedChunkInfoForResume.part)
276
+ if @options.onResume(id, name, @getChunkDataForCallback(firstChunkDataForResume)) != false
277
+ firstChunkIndex = persistedChunkInfoForResume.part
278
+ @fileState[id].uuid = persistedChunkInfoForResume.uuid
279
+ @fileState[id].loaded = firstChunkDataForResume.start
280
+ @fileState[id].attemptingResume = true
281
+ @log('Resuming ' + name + " at partition index " + firstChunkIndex)
282
+
283
+ currentChunkIndex = @getTotalChunks(id) - 1
284
+ while currentChunkIndex >= firstChunkIndex
285
+ @fileState[id].remainingChunkIdxs.unshift(currentChunkIndex)
286
+ currentChunkIndex -= 1
287
+
288
+ @uploadNextChunk(id)
289
+
290
+ handleStandardFileUpload: (id)->
291
+ file = @fileState[id].file
292
+ name = @getName(id)
293
+
294
+ @fileState[id].loaded = 0
295
+
296
+ xhr = @createXhr(id)
297
+
298
+ xhr.upload.onprogress = (e) =>
299
+ if e.lengthComputable
300
+ @fileState[id].loaded = e.loaded
301
+ @options.onProgress(id, name, e.loaded, e.total)
302
+
303
+ xhr.onreadystatechange = @getReadyStateChangeHandler(id, xhr)
304
+
305
+ params = @options.paramsStore.getParams(id);
306
+ toSend = @setParamsAndGetEntityToSend(params, xhr, file, id)
307
+ @setHeaders(id, xhr)
308
+
309
+ @log('Sending upload request for ' + id)
310
+ xhr.send(toSend)
311
+
312
+ # Adds file to the queue
313
+ # Returns id to use with upload, cancel
314
+ add: (file) ->
315
+ if !(file instanceof File)
316
+ throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
317
+
318
+ id = @fileState.push(file: file) - 1
319
+ @fileState[id].uuid = utils.getUniqueId()
320
+
321
+ id
322
+
323
+ getName: (id) ->
324
+ file = @fileState[id].file
325
+ # fix missing name in Safari 4
326
+ #NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
327
+ file.fileName ? file.name
328
+
329
+ getSize: (id) ->
330
+ file = @fileState[id].file
331
+ file.fileSize ? file.size
332
+
333
+ getFile: (id) ->
334
+ return @fileState[id].file if @fileState[id]
335
+
336
+ # Returns uploaded bytes for file identified by id
337
+ getLoaded: (id) -> @fileState[id].loaded || 0
338
+
339
+ isValid: (id) -> @fileState[id]?
340
+
341
+ reset: =>
342
+ super()
343
+ @fileState = []
344
+
345
+ getUuid: (id) -> @fileState[id].uuid
346
+
347
+ # Sends the file identified by id to the server
348
+ uploadFile: (id, retry) ->
349
+ name = @getName(id)
350
+
351
+ @options.onUpload(id, name)
352
+
353
+ if @chunkFiles
354
+ @handleFileChunkingUpload(id, retry)
355
+ else
356
+ @handleStandardFileUpload(id)
357
+
358
+ cancelFile: (id) ->
359
+ @options.onCancel(id, @getName(id))
360
+
361
+ @fileState[id].xhr.abort() if @fileState[id].xhr
362
+
363
+ @deletePersistedChunkData(id) if @resumeEnabled
364
+
365
+ delete @fileState[id]
366
+
367
+ getResumableFilesData: ->
368
+ matchingCookieNames = []
369
+ resumableFilesData = []
370
+
371
+ if @chunkFiles && @resumeEnabled
372
+ if !@resumeId?
373
+ matchingCookieNames = utils.getCookieNames(new RegExp("^qqfilechunk\\" + @cookieItemDelimiter + ".+\\" +
374
+ @cookieItemDelimiter + "\\d+\\" + @cookieItemDelimiter + @options.chunking.partSize + "="))
375
+ else
376
+ matchingCookieNames = utils.getCookieNames(new RegExp("^qqfilechunk\\" + @cookieItemDelimiter + ".+\\" +
377
+ @cookieItemDelimiter + "\\d+\\" + @cookieItemDelimiter + @options.chunking.partSize + "\\" +
378
+ @cookieItemDelimiter + @resumeId + "="))
379
+
380
+
381
+ for cookieName in matchingCookieNames
382
+ cookiesNameParts = cookieName.split(@cookieItemDelimiter)
383
+ cookieValueParts = utils.getCookie(cookieName).split(@cookieItemDelimiter)
384
+
385
+ resumableFilesData.push
386
+ name: decodeURIComponent(cookiesNameParts[1])
387
+ size: cookiesNameParts[2]
388
+ uuid: cookieValueParts[0]
389
+ partIdx: cookieValueParts[1]
390
+
391
+ resumableFilesData
392
+ else
393
+ []