uploads 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +3 -0
- data/Rakefile +38 -0
- data/app/assets/javascripts/anjlab/uploads.js +7 -0
- data/app/assets/javascripts/anjlab/uploads/button.js.coffee +80 -0
- data/app/assets/javascripts/anjlab/uploads/dnd.js.coffee +215 -0
- data/app/assets/javascripts/anjlab/uploads/handler.base.js.coffee +101 -0
- data/app/assets/javascripts/anjlab/uploads/handler.form.js.coffee +173 -0
- data/app/assets/javascripts/anjlab/uploads/handler.xhr.js.coffee +393 -0
- data/app/assets/javascripts/anjlab/uploads/uploader.js.coffee +487 -0
- data/app/assets/javascripts/anjlab/uploads/utils.js.coffee +116 -0
- data/lib/tasks/uploads_tasks.rake +4 -0
- data/lib/uploads.rb +10 -0
- data/lib/uploads/engine.rb +6 -0
- data/lib/uploads/version.rb +3 -0
- data/test/dummy/README.rdoc +261 -0
- data/test/dummy/Rakefile +7 -0
- data/test/dummy/app/assets/javascripts/application.js +16 -0
- data/test/dummy/app/assets/stylesheets/application.css +13 -0
- data/test/dummy/app/controllers/application_controller.rb +3 -0
- data/test/dummy/app/controllers/uploads_controller.rb +15 -0
- data/test/dummy/app/controllers/welcome_controller.rb +4 -0
- data/test/dummy/app/helpers/application_helper.rb +2 -0
- data/test/dummy/app/views/layouts/application.html.erb +14 -0
- data/test/dummy/app/views/welcome/index.html.slim +53 -0
- data/test/dummy/config.ru +4 -0
- data/test/dummy/config/application.rb +59 -0
- data/test/dummy/config/boot.rb +10 -0
- data/test/dummy/config/database.yml +25 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +37 -0
- data/test/dummy/config/environments/production.rb +67 -0
- data/test/dummy/config/environments/test.rb +37 -0
- data/test/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/test/dummy/config/initializers/inflections.rb +15 -0
- data/test/dummy/config/initializers/mime_types.rb +5 -0
- data/test/dummy/config/initializers/secret_token.rb +7 -0
- data/test/dummy/config/initializers/session_store.rb +8 -0
- data/test/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/test/dummy/config/locales/en.yml +5 -0
- data/test/dummy/config/routes.rb +59 -0
- data/test/dummy/db/development.sqlite3 +0 -0
- data/test/dummy/log/development.log +23847 -0
- data/test/dummy/public/404.html +26 -0
- data/test/dummy/public/422.html +26 -0
- data/test/dummy/public/500.html +25 -0
- data/test/dummy/public/favicon.ico +0 -0
- data/test/dummy/script/rails +6 -0
- data/test/dummy/tmp/cache/assets/C79/790/sprockets%2Fde97a756642791010533233b267b1afe +0 -0
- data/test/dummy/tmp/cache/assets/C99/300/sprockets%2F3bd289b286c074ad66220ad20181b135 +0 -0
- data/test/dummy/tmp/cache/assets/C9E/CA0/sprockets%2F1252e234767209f94d0a41a0bc19e33c +0 -0
- data/test/dummy/tmp/cache/assets/CA1/9E0/sprockets%2F4c40d59c233952767c150cb8234cab22 +0 -0
- data/test/dummy/tmp/cache/assets/CA5/AC0/sprockets%2Fcc77204615926b95033b4fa044d7c61b +0 -0
- data/test/dummy/tmp/cache/assets/CAA/860/sprockets%2F6b1d0c998769132cda15f7c5d1283300 +0 -0
- data/test/dummy/tmp/cache/assets/CCD/830/sprockets%2F962b2259b044ca05005c14fd83ba982a +0 -0
- data/test/dummy/tmp/cache/assets/CD2/7B0/sprockets%2Fe9a64cf98e26a4719e64d747d7912893 +0 -0
- data/test/dummy/tmp/cache/assets/CD8/370/sprockets%2F357970feca3ac29060c1e3861e2c0953 +0 -0
- data/test/dummy/tmp/cache/assets/CE4/A00/sprockets%2Fe589f936724000ba0e7f077a63b75d4e +0 -0
- data/test/dummy/tmp/cache/assets/CE7/D40/sprockets%2F888fd320a7c0125218313f1ef9d6d99d +0 -0
- data/test/dummy/tmp/cache/assets/CFD/650/sprockets%2F73de09397391979f4dcd67ce32a7816f +0 -0
- data/test/dummy/tmp/cache/assets/D07/710/sprockets%2F56976babbbfe475040b22885733ca41a +0 -0
- data/test/dummy/tmp/cache/assets/D0B/D70/sprockets%2F6c298d94014122a613afbebce90590f8 +0 -0
- data/test/dummy/tmp/cache/assets/D0F/6A0/sprockets%2F18414faf9678c2ab6d1d60891c6f06a1 +0 -0
- data/test/dummy/tmp/cache/assets/D15/C40/sprockets%2F5516d9e4e25aaefc72a78fc641962512 +0 -0
- data/test/dummy/tmp/cache/assets/D24/CD0/sprockets%2Fd0cd795c6f0b6074de96706e8bf70975 +0 -0
- data/test/dummy/tmp/cache/assets/D2E/980/sprockets%2Fded84d1bb4c31404691d106f4b8e120f +0 -0
- data/test/dummy/tmp/cache/assets/D32/A10/sprockets%2F13fe41fee1fe35b49d145bcc06610705 +0 -0
- data/test/dummy/tmp/cache/assets/D33/120/sprockets%2F70d8b84b628400ffbf410b17b845bae2 +0 -0
- data/test/dummy/tmp/cache/assets/D35/8C0/sprockets%2F20424272a569797e71dbae1bdb33c2fc +0 -0
- data/test/dummy/tmp/cache/assets/D3F/FE0/sprockets%2Fbfc6e8a285039670d3ca0399bd3e633b +0 -0
- data/test/dummy/tmp/cache/assets/D44/E50/sprockets%2F5767c31d95e72a26ffb3c0092dc3cf94 +0 -0
- data/test/dummy/tmp/cache/assets/D48/8B0/sprockets%2F795b331f7ff4a59f0604c531e71af9de +0 -0
- data/test/dummy/tmp/cache/assets/D4D/420/sprockets%2Fab27b88913d73cdabe65891eeb884444 +0 -0
- data/test/dummy/tmp/cache/assets/D4E/1B0/sprockets%2Ff7cbd26ba1d28d48de824f0e94586655 +0 -0
- data/test/dummy/tmp/cache/assets/D5A/2A0/sprockets%2Fc379b9f699c8f614cefea762d376d224 +0 -0
- data/test/dummy/tmp/cache/assets/D5A/EA0/sprockets%2Fd771ace226fc8215a3572e0aa35bb0d6 +0 -0
- data/test/dummy/tmp/cache/assets/D5D/330/sprockets%2Fce164ed2c0f3e91b338035376ad41bac +0 -0
- data/test/dummy/tmp/cache/assets/D6D/350/sprockets%2Fdac0344fd04f6fe00bce8983177635ea +0 -0
- data/test/dummy/tmp/cache/assets/D71/6E0/sprockets%2F07996d57b5cfca8a92b02613fdaae735 +0 -0
- data/test/dummy/tmp/cache/assets/D7C/210/sprockets%2Ff98c0ec43e4406aa3cec29837d9f618e +0 -0
- data/test/dummy/tmp/cache/assets/D7E/AF0/sprockets%2F1d4ffb9a3d536682ff7d185668dc2d4b +0 -0
- data/test/dummy/tmp/cache/assets/D96/9A0/sprockets%2Ff1caf4ea19ee66a13841eaf6f40a6433 +0 -0
- data/test/dummy/tmp/cache/assets/D97/760/sprockets%2Fd1c9a476956a0ccae81fa57dad53135a +0 -0
- data/test/dummy/tmp/cache/assets/DBB/F20/sprockets%2F03e6ee44546ae1ea1bfd3e0801b7ca7a +0 -0
- data/test/dummy/tmp/cache/assets/DC3/810/sprockets%2Fc86e769b165de4f9d49745ab9fee7b5f +0 -0
- data/test/dummy/tmp/cache/assets/DC5/3C0/sprockets%2Fc21f1ad7fdce18f5b7a5a581b1351fd6 +0 -0
- data/test/dummy/tmp/cache/assets/DCA/4A0/sprockets%2Fbdfe27f0efa249de171cbfb2970661c2 +0 -0
- data/test/dummy/tmp/cache/assets/DD3/AF0/sprockets%2Fb090f4fd52cbc2e5ccc815aa8999f2f3 +0 -0
- data/test/dummy/tmp/cache/assets/DDC/400/sprockets%2Fcffd775d018f68ce5dba1ee0d951a994 +0 -0
- data/test/dummy/tmp/cache/assets/DE7/920/sprockets%2Feea789b2f9e87ceaffa7757e32fd1192 +0 -0
- data/test/dummy/tmp/cache/assets/E02/FE0/sprockets%2F68a4afe37ab83b2d9fa1cf5e084fec81 +0 -0
- data/test/dummy/tmp/cache/assets/E04/890/sprockets%2F2f5173deea6c795b8fdde723bb4b63af +0 -0
- data/test/dummy/tmp/cache/assets/E2B/C80/sprockets%2Ff6f179d2c6207aabbc3cec5c260eee9e +0 -0
- data/test/dummy/tmp/cache/assets/E47/5F0/sprockets%2Fef5f1e3bcbc9baa50e54a15ba0e5ca91 +0 -0
- data/test/test_helper.rb +15 -0
- data/test/uploads_test.rb +7 -0
- 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
|
+
[]
|