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