fine_uploader 3.1.1 → 3.4.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.
@@ -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
+ };