uploads 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+ []