fine_uploader 3.1.1 → 3.4.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,168 +1,630 @@
1
- /**
2
- * Class for uploading files using xhr
3
- * @inherits qq.UploadHandlerAbstract
4
- */
5
- qq.UploadHandlerXhr = function(o){
6
- qq.UploadHandlerAbstract.apply(this, arguments);
7
-
8
- this._files = [];
9
- this._xhrs = [];
10
-
11
- // current loaded size in bytes for each file
12
- this._loaded = [];
13
- };
1
+ /*globals qq, File, XMLHttpRequest, FormData, Blob*/
2
+ qq.UploadHandlerXhr = function(o, uploadCompleteCallback, logCallback) {
3
+ "use strict";
4
+
5
+ var options = o,
6
+ uploadComplete = uploadCompleteCallback,
7
+ log = logCallback,
8
+ fileState = [],
9
+ cookieItemDelimiter = "|",
10
+ chunkFiles = options.chunking.enabled && qq.isFileChunkingSupported(),
11
+ resumeEnabled = options.resume.enabled && chunkFiles && qq.areCookiesEnabled(),
12
+ resumeId = getResumeId(),
13
+ multipart = options.forceMultipart || options.paramsInBody,
14
+ api;
14
15
 
15
- // @inherits qq.UploadHandlerAbstract
16
- qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype)
17
-
18
- qq.extend(qq.UploadHandlerXhr.prototype, {
19
- /**
20
- * Adds file to the queue
21
- * Returns id to use with upload, cancel
22
- **/
23
- add: function(file){
24
- if (!(file instanceof File)){
25
- throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)');
26
- }
27
-
28
- return this._files.push(file) - 1;
29
- },
30
- getName: function(id){
31
- var file = this._files[id];
32
- // fix missing name in Safari 4
33
- //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
34
- return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
35
- },
36
- getSize: function(id){
37
- var file = this._files[id];
38
- return file.fileSize != null ? file.fileSize : file.size;
39
- },
40
- /**
41
- * Returns uploaded bytes for file identified by id
42
- */
43
- getLoaded: function(id){
44
- return this._loaded[id] || 0;
45
- },
46
- isValid: function(id) {
47
- return this._files[id] !== undefined;
48
- },
49
- reset: function() {
50
- qq.UploadHandlerAbstract.prototype.reset.apply(this, arguments);
51
- this._files = [];
52
- this._xhrs = [];
53
- this._loaded = [];
54
- },
55
- /**
56
- * Sends the file identified by id to the server
57
- */
58
- _upload: function(id){
59
- var file = this._files[id],
60
- name = this.getName(id),
61
- size = this.getSize(id),
62
- self = this,
63
- url = this._options.endpoint,
64
- protocol = this._options.demoMode ? "GET" : "POST",
65
- xhr, formData, paramName, key, params;
66
-
67
- this._options.onUpload(id, this.getName(id), true);
68
-
69
- this._loaded[id] = 0;
70
-
71
- xhr = this._xhrs[id] = new XMLHttpRequest();
72
16
 
73
- xhr.upload.onprogress = function(e){
74
- if (e.lengthComputable){
75
- self._loaded[id] = e.loaded;
76
- self._options.onProgress(id, name, e.loaded, e.total);
77
- }
78
- };
17
+ function addChunkingSpecificParams(id, params, chunkData) {
18
+ var size = api.getSize(id),
19
+ name = api.getName(id);
79
20
 
80
- xhr.onreadystatechange = function(){
81
- if (xhr.readyState === 4){
82
- self._onComplete(id, xhr);
83
- }
21
+ params[options.chunking.paramNames.partIndex] = chunkData.part;
22
+ params[options.chunking.paramNames.partByteOffset] = chunkData.start;
23
+ params[options.chunking.paramNames.chunkSize] = chunkData.size;
24
+ params[options.chunking.paramNames.totalParts] = chunkData.count;
25
+ params[options.totalFileSizeParamName] = size;
26
+
27
+ /**
28
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
29
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
30
+ */
31
+ if (multipart) {
32
+ params[options.chunking.paramNames.filename] = name;
33
+ }
34
+ }
35
+
36
+ function addResumeSpecificParams(params) {
37
+ params[options.resume.paramNames.resuming] = true;
38
+ }
39
+
40
+ function getChunk(fileOrBlob, startByte, endByte) {
41
+ if (fileOrBlob.slice) {
42
+ return fileOrBlob.slice(startByte, endByte);
43
+ }
44
+ else if (fileOrBlob.mozSlice) {
45
+ return fileOrBlob.mozSlice(startByte, endByte);
46
+ }
47
+ else if (fileOrBlob.webkitSlice) {
48
+ return fileOrBlob.webkitSlice(startByte, endByte);
49
+ }
50
+ }
51
+
52
+ function getChunkData(id, chunkIndex) {
53
+ var chunkSize = options.chunking.partSize,
54
+ fileSize = api.getSize(id),
55
+ fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
56
+ startBytes = chunkSize * chunkIndex,
57
+ endBytes = startBytes+chunkSize >= fileSize ? fileSize : startBytes+chunkSize,
58
+ totalChunks = getTotalChunks(id);
59
+
60
+ return {
61
+ part: chunkIndex,
62
+ start: startBytes,
63
+ end: endBytes,
64
+ count: totalChunks,
65
+ blob: getChunk(fileOrBlob, startBytes, endBytes),
66
+ size: endBytes - startBytes
84
67
  };
68
+ }
69
+
70
+ function getTotalChunks(id) {
71
+ var fileSize = api.getSize(id),
72
+ chunkSize = options.chunking.partSize;
73
+
74
+ return Math.ceil(fileSize / chunkSize);
75
+ }
76
+
77
+ function createXhr(id) {
78
+ var xhr = new XMLHttpRequest();
79
+
80
+ fileState[id].xhr = xhr;
85
81
 
86
- params = this._options.paramsStore.getParams(id);
82
+ return xhr;
83
+ }
84
+
85
+ function setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id) {
86
+ var formData = new FormData(),
87
+ method = options.demoMode ? "GET" : "POST",
88
+ endpoint = options.endpointStore.getEndpoint(id),
89
+ url = endpoint,
90
+ name = api.getName(id),
91
+ size = api.getSize(id),
92
+ blobData = fileState[id].blobData;
93
+
94
+ params[options.uuidParamName] = fileState[id].uuid;
95
+
96
+ if (multipart) {
97
+ params[options.totalFileSizeParamName] = size;
98
+
99
+ if (blobData) {
100
+ /**
101
+ * When a Blob is sent in a multipart request, the filename value in the content-disposition header is either "blob"
102
+ * or an empty string. So, we will need to include the actual file name as a param in this case.
103
+ */
104
+ params[options.blobs.paramNames.name] = blobData.name;
105
+ }
106
+ }
87
107
 
88
108
  //build query string
89
- if (!this._options.paramsInBody) {
90
- params[this._options.inputName] = name;
91
- url = qq.obj2url(params, this._options.endpoint);
109
+ if (!options.paramsInBody) {
110
+ if (!multipart) {
111
+ params[options.inputName] = name;
112
+ }
113
+ url = qq.obj2url(params, endpoint);
92
114
  }
93
115
 
94
- xhr.open(protocol, url, true);
95
- xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
96
- xhr.setRequestHeader("X-File-Name", encodeURIComponent(name));
97
- xhr.setRequestHeader("Cache-Control", "no-cache");
98
- if (this._options.forceMultipart || this._options.paramsInBody) {
99
- formData = new FormData();
116
+ xhr.open(method, url, true);
117
+
118
+ if (options.cors.expected && options.cors.sendCredentials) {
119
+ xhr.withCredentials = true;
120
+ }
100
121
 
101
- if (this._options.paramsInBody) {
122
+ if (multipart) {
123
+ if (options.paramsInBody) {
102
124
  qq.obj2FormData(params, formData);
103
125
  }
104
126
 
105
- formData.append(this._options.inputName, file);
106
- file = formData;
107
- } else {
127
+ formData.append(options.inputName, fileOrBlob);
128
+ return formData;
129
+ }
130
+
131
+ return fileOrBlob;
132
+ }
133
+
134
+ function setHeaders(id, xhr) {
135
+ var extraHeaders = options.customHeaders,
136
+ fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
137
+
138
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
139
+ xhr.setRequestHeader("Cache-Control", "no-cache");
140
+
141
+ if (!multipart) {
108
142
  xhr.setRequestHeader("Content-Type", "application/octet-stream");
109
143
  //NOTE: return mime type in xhr works on chrome 16.0.9 firefox 11.0a2
110
- xhr.setRequestHeader("X-Mime-Type", file.type);
144
+ xhr.setRequestHeader("X-Mime-Type", fileOrBlob.type);
111
145
  }
112
146
 
113
- for (key in this._options.customHeaders){
114
- if (this._options.customHeaders.hasOwnProperty(key)) {
115
- xhr.setRequestHeader(key, this._options.customHeaders[key]);
147
+ qq.each(extraHeaders, function(name, val) {
148
+ xhr.setRequestHeader(name, val);
149
+ });
150
+ }
151
+
152
+ function handleCompletedItem(id, response, xhr) {
153
+ var name = api.getName(id),
154
+ size = api.getSize(id);
155
+
156
+ fileState[id].attemptingResume = false;
157
+
158
+ options.onProgress(id, name, size, size);
159
+
160
+ options.onComplete(id, name, response, xhr);
161
+ delete fileState[id].xhr;
162
+ uploadComplete(id);
163
+ }
164
+
165
+ function uploadNextChunk(id) {
166
+ var chunkIdx = fileState[id].remainingChunkIdxs[0],
167
+ chunkData = getChunkData(id, chunkIdx),
168
+ xhr = createXhr(id),
169
+ size = api.getSize(id),
170
+ name = api.getName(id),
171
+ toSend, params;
172
+
173
+ if (fileState[id].loaded === undefined) {
174
+ fileState[id].loaded = 0;
175
+ }
176
+
177
+ if (resumeEnabled && fileState[id].file) {
178
+ persistChunkData(id, chunkData);
179
+ }
180
+
181
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
182
+
183
+ xhr.upload.onprogress = function(e) {
184
+ if (e.lengthComputable) {
185
+ var totalLoaded = e.loaded + fileState[id].loaded,
186
+ estTotalRequestsSize = calcAllRequestsSizeForChunkedUpload(id, chunkIdx, e.total);
187
+
188
+ options.onProgress(id, name, totalLoaded, estTotalRequestsSize);
116
189
  }
190
+ };
191
+
192
+ options.onUploadChunk(id, name, getChunkDataForCallback(chunkData));
193
+
194
+ params = options.paramsStore.getParams(id);
195
+ addChunkingSpecificParams(id, params, chunkData);
196
+
197
+ if (fileState[id].attemptingResume) {
198
+ addResumeSpecificParams(params);
117
199
  }
118
200
 
119
- this.log('Sending upload request for ' + id);
120
- xhr.send(file);
121
- },
122
- _onComplete: function(id, xhr){
123
- "use strict";
124
- // the request was aborted/cancelled
125
- if (!this._files[id]) { return; }
201
+ toSend = setParamsAndGetEntityToSend(params, xhr, chunkData.blob, id);
202
+ setHeaders(id, xhr);
126
203
 
127
- var name = this.getName(id);
128
- var size = this.getSize(id);
129
- var response; //the parsed JSON response from the server, or the empty object if parsing failed.
204
+ log('Sending chunked upload request for item ' + id + ": bytes " + (chunkData.start+1) + "-" + chunkData.end + " of " + size);
205
+ xhr.send(toSend);
206
+ }
130
207
 
131
- this._options.onProgress(id, name, size, size);
208
+ function calcAllRequestsSizeForChunkedUpload(id, chunkIdx, requestSize) {
209
+ var chunkData = getChunkData(id, chunkIdx),
210
+ blobSize = chunkData.size,
211
+ overhead = requestSize - blobSize,
212
+ size = api.getSize(id),
213
+ chunkCount = chunkData.count,
214
+ initialRequestOverhead = fileState[id].initialRequestOverhead,
215
+ overheadDiff = overhead - initialRequestOverhead;
132
216
 
133
- this.log("xhr - server response received for " + id);
134
- this.log("responseText = " + xhr.responseText);
217
+ fileState[id].lastRequestOverhead = overhead;
135
218
 
136
- try {
137
- if (typeof JSON.parse === "function") {
138
- response = JSON.parse(xhr.responseText);
139
- } else {
140
- response = eval("(" + xhr.responseText + ")");
219
+ if (chunkIdx === 0) {
220
+ fileState[id].lastChunkIdxProgress = 0;
221
+ fileState[id].initialRequestOverhead = overhead;
222
+ fileState[id].estTotalRequestsSize = size + (chunkCount * overhead);
223
+ }
224
+ else if (fileState[id].lastChunkIdxProgress !== chunkIdx) {
225
+ fileState[id].lastChunkIdxProgress = chunkIdx;
226
+ fileState[id].estTotalRequestsSize += overheadDiff;
227
+ }
228
+
229
+ return fileState[id].estTotalRequestsSize;
230
+ }
231
+
232
+ function getLastRequestOverhead(id) {
233
+ if (multipart) {
234
+ return fileState[id].lastRequestOverhead;
235
+ }
236
+ else {
237
+ return 0;
238
+ }
239
+ }
240
+
241
+ function handleSuccessfullyCompletedChunk(id, response, xhr) {
242
+ var chunkIdx = fileState[id].remainingChunkIdxs.shift(),
243
+ chunkData = getChunkData(id, chunkIdx);
244
+
245
+ fileState[id].attemptingResume = false;
246
+ fileState[id].loaded += chunkData.size + getLastRequestOverhead(id);
247
+
248
+ if (fileState[id].remainingChunkIdxs.length > 0) {
249
+ uploadNextChunk(id);
250
+ }
251
+ else {
252
+ if (resumeEnabled) {
253
+ deletePersistedChunkData(id);
141
254
  }
142
- } catch(error){
143
- this.log('Error when attempting to parse xhr response text (' + error + ')', 'error');
255
+
256
+ handleCompletedItem(id, response, xhr);
257
+ }
258
+ }
259
+
260
+ function isErrorResponse(xhr, response) {
261
+ return xhr.status !== 200 || !response.success || response.reset;
262
+ }
263
+
264
+ function parseResponse(xhr) {
265
+ var response;
266
+
267
+ try {
268
+ response = qq.parseJson(xhr.responseText);
269
+ }
270
+ catch(error) {
271
+ log('Error when attempting to parse xhr response text (' + error + ')', 'error');
144
272
  response = {};
145
273
  }
146
274
 
147
- if (xhr.status !== 200 || !response.success){
148
- if (this._options.onAutoRetry(id, name, response, xhr)) {
149
- return;
275
+ return response;
276
+ }
277
+
278
+ function handleResetResponse(id) {
279
+ log('Server has ordered chunking effort to be restarted on next attempt for item ID ' + id, 'error');
280
+
281
+ if (resumeEnabled) {
282
+ deletePersistedChunkData(id);
283
+ fileState[id].attemptingResume = false;
284
+ }
285
+
286
+ fileState[id].remainingChunkIdxs = [];
287
+ delete fileState[id].loaded;
288
+ delete fileState[id].estTotalRequestsSize;
289
+ delete fileState[id].initialRequestOverhead;
290
+ }
291
+
292
+ function handleResetResponseOnResumeAttempt(id) {
293
+ fileState[id].attemptingResume = false;
294
+ log("Server has declared that it cannot handle resume for item ID " + id + " - starting from the first chunk", 'error');
295
+ handleResetResponse(id);
296
+ api.upload(id, true);
297
+ }
298
+
299
+ function handleNonResetErrorResponse(id, response, xhr) {
300
+ var name = api.getName(id);
301
+
302
+ if (options.onAutoRetry(id, name, response, xhr)) {
303
+ return;
304
+ }
305
+ else {
306
+ handleCompletedItem(id, response, xhr);
307
+ }
308
+ }
309
+
310
+ function onComplete(id, xhr) {
311
+ var response;
312
+
313
+ // the request was aborted/cancelled
314
+ if (!fileState[id]) {
315
+ return;
316
+ }
317
+
318
+ log("xhr - server response received for " + id);
319
+ log("responseText = " + xhr.responseText);
320
+ response = parseResponse(xhr);
321
+
322
+ if (isErrorResponse(xhr, response)) {
323
+ if (response.reset) {
324
+ handleResetResponse(id);
325
+ }
326
+
327
+ if (fileState[id].attemptingResume && response.reset) {
328
+ handleResetResponseOnResumeAttempt(id);
150
329
  }
330
+ else {
331
+ handleNonResetErrorResponse(id, response, xhr);
332
+ }
333
+ }
334
+ else if (chunkFiles) {
335
+ handleSuccessfullyCompletedChunk(id, response, xhr);
336
+ }
337
+ else {
338
+ handleCompletedItem(id, response, xhr);
151
339
  }
340
+ }
341
+
342
+ function getChunkDataForCallback(chunkData) {
343
+ return {
344
+ partIndex: chunkData.part,
345
+ startByte: chunkData.start + 1,
346
+ endByte: chunkData.end,
347
+ totalParts: chunkData.count
348
+ };
349
+ }
350
+
351
+ function getReadyStateChangeHandler(id, xhr) {
352
+ return function() {
353
+ if (xhr.readyState === 4) {
354
+ onComplete(id, xhr);
355
+ }
356
+ };
357
+ }
358
+
359
+ function persistChunkData(id, chunkData) {
360
+ var fileUuid = api.getUuid(id),
361
+ lastByteSent = fileState[id].loaded,
362
+ initialRequestOverhead = fileState[id].initialRequestOverhead,
363
+ estTotalRequestsSize = fileState[id].estTotalRequestsSize,
364
+ cookieName = getChunkDataCookieName(id),
365
+ cookieValue = fileUuid +
366
+ cookieItemDelimiter + chunkData.part +
367
+ cookieItemDelimiter + lastByteSent +
368
+ cookieItemDelimiter + initialRequestOverhead +
369
+ cookieItemDelimiter + estTotalRequestsSize,
370
+ cookieExpDays = options.resume.cookiesExpireIn;
371
+
372
+ qq.setCookie(cookieName, cookieValue, cookieExpDays);
373
+ }
374
+
375
+ function deletePersistedChunkData(id) {
376
+ if (fileState[id].file) {
377
+ var cookieName = getChunkDataCookieName(id);
378
+ qq.deleteCookie(cookieName);
379
+ }
380
+ }
381
+
382
+ function getPersistedChunkData(id) {
383
+ var chunkCookieValue = qq.getCookie(getChunkDataCookieName(id)),
384
+ filename = api.getName(id),
385
+ sections, uuid, partIndex, lastByteSent, initialRequestOverhead, estTotalRequestsSize;
386
+
387
+ if (chunkCookieValue) {
388
+ sections = chunkCookieValue.split(cookieItemDelimiter);
389
+
390
+ if (sections.length === 5) {
391
+ uuid = sections[0];
392
+ partIndex = parseInt(sections[1], 10);
393
+ lastByteSent = parseInt(sections[2], 10);
394
+ initialRequestOverhead = parseInt(sections[3], 10);
395
+ estTotalRequestsSize = parseInt(sections[4], 10);
396
+
397
+ return {
398
+ uuid: uuid,
399
+ part: partIndex,
400
+ lastByteSent: lastByteSent,
401
+ initialRequestOverhead: initialRequestOverhead,
402
+ estTotalRequestsSize: estTotalRequestsSize
403
+ };
404
+ }
405
+ else {
406
+ log('Ignoring previously stored resume/chunk cookie for ' + filename + " - old cookie format", "warn");
407
+ }
408
+ }
409
+ }
410
+
411
+ function getChunkDataCookieName(id) {
412
+ var filename = api.getName(id),
413
+ fileSize = api.getSize(id),
414
+ maxChunkSize = options.chunking.partSize,
415
+ cookieName;
416
+
417
+ cookieName = "qqfilechunk" + cookieItemDelimiter + encodeURIComponent(filename) + cookieItemDelimiter + fileSize + cookieItemDelimiter + maxChunkSize;
418
+
419
+ if (resumeId !== undefined) {
420
+ cookieName += cookieItemDelimiter + resumeId;
421
+ }
422
+
423
+ return cookieName;
424
+ }
425
+
426
+ function getResumeId() {
427
+ if (options.resume.id !== null &&
428
+ options.resume.id !== undefined &&
429
+ !qq.isFunction(options.resume.id) &&
430
+ !qq.isObject(options.resume.id)) {
431
+
432
+ return options.resume.id;
433
+ }
434
+ }
152
435
 
153
- this._options.onComplete(id, name, response, xhr);
436
+ function handleFileChunkingUpload(id, retry) {
437
+ var name = api.getName(id),
438
+ firstChunkIndex = 0,
439
+ persistedChunkInfoForResume, firstChunkDataForResume, currentChunkIndex;
154
440
 
155
- this._xhrs[id] = null;
156
- this._dequeue(id);
157
- },
158
- _cancel: function(id){
159
- this._options.onCancel(id, this.getName(id));
441
+ if (!fileState[id].remainingChunkIdxs || fileState[id].remainingChunkIdxs.length === 0) {
442
+ fileState[id].remainingChunkIdxs = [];
160
443
 
161
- this._files[id] = null;
444
+ if (resumeEnabled && !retry && fileState[id].file) {
445
+ persistedChunkInfoForResume = getPersistedChunkData(id);
446
+ if (persistedChunkInfoForResume) {
447
+ firstChunkDataForResume = getChunkData(id, persistedChunkInfoForResume.part);
448
+ if (options.onResume(id, name, getChunkDataForCallback(firstChunkDataForResume)) !== false) {
449
+ firstChunkIndex = persistedChunkInfoForResume.part;
450
+ fileState[id].uuid = persistedChunkInfoForResume.uuid;
451
+ fileState[id].loaded = persistedChunkInfoForResume.lastByteSent;
452
+ fileState[id].estTotalRequestsSize = persistedChunkInfoForResume.estTotalRequestsSize;
453
+ fileState[id].initialRequestOverhead = persistedChunkInfoForResume.initialRequestOverhead;
454
+ fileState[id].attemptingResume = true;
455
+ log('Resuming ' + name + " at partition index " + firstChunkIndex);
456
+ }
457
+ }
458
+ }
162
459
 
163
- if (this._xhrs[id]){
164
- this._xhrs[id].abort();
165
- this._xhrs[id] = null;
460
+ for (currentChunkIndex = getTotalChunks(id)-1; currentChunkIndex >= firstChunkIndex; currentChunkIndex-=1) {
461
+ fileState[id].remainingChunkIdxs.unshift(currentChunkIndex);
462
+ }
166
463
  }
464
+
465
+ uploadNextChunk(id);
466
+ }
467
+
468
+ function handleStandardFileUpload(id) {
469
+ var fileOrBlob = fileState[id].file || fileState[id].blobData.blob,
470
+ name = api.getName(id),
471
+ xhr, params, toSend;
472
+
473
+ fileState[id].loaded = 0;
474
+
475
+ xhr = createXhr(id);
476
+
477
+ xhr.upload.onprogress = function(e){
478
+ if (e.lengthComputable){
479
+ fileState[id].loaded = e.loaded;
480
+ options.onProgress(id, name, e.loaded, e.total);
481
+ }
482
+ };
483
+
484
+ xhr.onreadystatechange = getReadyStateChangeHandler(id, xhr);
485
+
486
+ params = options.paramsStore.getParams(id);
487
+ toSend = setParamsAndGetEntityToSend(params, xhr, fileOrBlob, id);
488
+ setHeaders(id, xhr);
489
+
490
+ log('Sending upload request for ' + id);
491
+ xhr.send(toSend);
167
492
  }
168
- });
493
+
494
+
495
+ api = {
496
+ /**
497
+ * Adds File or Blob to the queue
498
+ * Returns id to use with upload, cancel
499
+ **/
500
+ add: function(fileOrBlobData){
501
+ var id;
502
+
503
+ if (fileOrBlobData instanceof File) {
504
+ id = fileState.push({file: fileOrBlobData}) - 1;
505
+ }
506
+ else if (fileOrBlobData.blob instanceof Blob) {
507
+ id = fileState.push({blobData: fileOrBlobData}) - 1;
508
+ }
509
+ else {
510
+ throw new Error('Passed obj in not a File or BlobData (in qq.UploadHandlerXhr)');
511
+ }
512
+
513
+ fileState[id].uuid = qq.getUniqueId();
514
+ return id;
515
+ },
516
+ getName: function(id){
517
+ if (api.isValid(id)) {
518
+ var file = fileState[id].file,
519
+ blobData = fileState[id].blobData;
520
+
521
+ if (file) {
522
+ // fix missing name in Safari 4
523
+ //NOTE: fixed missing name firefox 11.0a2 file.fileName is actually undefined
524
+ return (file.fileName !== null && file.fileName !== undefined) ? file.fileName : file.name;
525
+ }
526
+ else {
527
+ return blobData.name;
528
+ }
529
+ }
530
+ else {
531
+ log(id + " is not a valid item ID.", "error");
532
+ }
533
+ },
534
+ getSize: function(id){
535
+ /*jshint eqnull: true*/
536
+ var fileOrBlob = fileState[id].file || fileState[id].blobData.blob;
537
+
538
+ if (qq.isFileOrInput(fileOrBlob)) {
539
+ return fileOrBlob.fileSize != null ? fileOrBlob.fileSize : fileOrBlob.size;
540
+ }
541
+ else {
542
+ return fileOrBlob.size;
543
+ }
544
+ },
545
+ getFile: function(id) {
546
+ if (fileState[id]) {
547
+ return fileState[id].file || fileState[id].blobData.blob;
548
+ }
549
+ },
550
+ /**
551
+ * Returns uploaded bytes for file identified by id
552
+ */
553
+ getLoaded: function(id){
554
+ return fileState[id].loaded || 0;
555
+ },
556
+ isValid: function(id) {
557
+ return fileState[id] !== undefined;
558
+ },
559
+ reset: function() {
560
+ fileState = [];
561
+ },
562
+ getUuid: function(id) {
563
+ return fileState[id].uuid;
564
+ },
565
+ /**
566
+ * Sends the file identified by id to the server
567
+ */
568
+ upload: function(id, retry){
569
+ var name = this.getName(id);
570
+
571
+ options.onUpload(id, name);
572
+
573
+ if (chunkFiles) {
574
+ handleFileChunkingUpload(id, retry);
575
+ }
576
+ else {
577
+ handleStandardFileUpload(id);
578
+ }
579
+ },
580
+ cancel: function(id){
581
+ var xhr = fileState[id].xhr;
582
+
583
+ options.onCancel(id, this.getName(id));
584
+
585
+ if (xhr) {
586
+ xhr.onreadystatechange = null;
587
+ xhr.abort();
588
+ }
589
+
590
+ if (resumeEnabled) {
591
+ deletePersistedChunkData(id);
592
+ }
593
+
594
+ delete fileState[id];
595
+ },
596
+ getResumableFilesData: function() {
597
+ var matchingCookieNames = [],
598
+ resumableFilesData = [];
599
+
600
+ if (chunkFiles && resumeEnabled) {
601
+ if (resumeId === undefined) {
602
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
603
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "="));
604
+ }
605
+ else {
606
+ matchingCookieNames = qq.getCookieNames(new RegExp("^qqfilechunk\\" + cookieItemDelimiter + ".+\\" +
607
+ cookieItemDelimiter + "\\d+\\" + cookieItemDelimiter + options.chunking.partSize + "\\" +
608
+ cookieItemDelimiter + resumeId + "="));
609
+ }
610
+
611
+ qq.each(matchingCookieNames, function(idx, cookieName) {
612
+ var cookiesNameParts = cookieName.split(cookieItemDelimiter);
613
+ var cookieValueParts = qq.getCookie(cookieName).split(cookieItemDelimiter);
614
+
615
+ resumableFilesData.push({
616
+ name: decodeURIComponent(cookiesNameParts[1]),
617
+ size: cookiesNameParts[2],
618
+ uuid: cookieValueParts[0],
619
+ partIdx: cookieValueParts[1]
620
+ });
621
+ });
622
+
623
+ return resumableFilesData;
624
+ }
625
+ return [];
626
+ }
627
+ };
628
+
629
+ return api;
630
+ };