Skip to content

Commit 213c762

Browse files
"Added sample: javascript/cors_upload.js"
1 parent c1d67fd commit 213c762

1 file changed

Lines changed: 284 additions & 0 deletions

File tree

javascript/cors_upload.js

Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
/*
2+
Copyright 2015 Google Inc. All Rights Reserved.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
var DRIVE_UPLOAD_URL = 'https://www.googleapis.com/upload/drive/v2/files/';
18+
19+
20+
/**
21+
* Helper for implementing retries with backoff. Initial retry
22+
* delay is 1 second, increasing by 2x (+jitter) for subsequent retries
23+
*
24+
* @constructor
25+
*/
26+
var RetryHandler = function() {
27+
this.interval = 1000; // Start at one second
28+
this.maxInterval = 60 * 1000; // Don't wait longer than a minute
29+
};
30+
31+
/**
32+
* Invoke the function after waiting
33+
*
34+
* @param {function} fn Function to invoke
35+
*/
36+
RetryHandler.prototype.retry = function(fn) {
37+
setTimeout(fn, this.interval);
38+
this.interval = this.nextInterval_();
39+
};
40+
41+
/**
42+
* Reset the counter (e.g. after successful request.)
43+
*/
44+
RetryHandler.prototype.reset = function() {
45+
this.interval = 1000;
46+
};
47+
48+
/**
49+
* Calculate the next wait time.
50+
* @return {number} Next wait interval, in milliseconds
51+
*
52+
* @private
53+
*/
54+
RetryHandler.prototype.nextInterval_ = function() {
55+
var interval = this.interval * 2 + this.getRandomInt_(0, 1000);
56+
return Math.min(interval, this.maxInterval);
57+
};
58+
59+
/**
60+
* Get a random int in the range of min to max. Used to add jitter to wait times.
61+
*
62+
* @param {number} min Lower bounds
63+
* @param {number} max Upper bounds
64+
* @private
65+
*/
66+
RetryHandler.prototype.getRandomInt_ = function(min, max) {
67+
return Math.floor(Math.random() * (max - min + 1) + min);
68+
};
69+
70+
71+
/**
72+
* Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether
73+
* files or in-memory constructs.
74+
*
75+
* @example
76+
* var content = new Blob(["Hello world"], {"type": "text/plain"});
77+
* var uploader = new MediaUploader({
78+
* file: content,
79+
* token: accessToken,
80+
* onComplete: function(data) { ... }
81+
* onError: function(data) { ... }
82+
* });
83+
* uploader.upload();
84+
*
85+
* @constructor
86+
* @param {object} options Hash of options
87+
* @param {string} options.token Access token
88+
* @param {blob} options.file Blob-like item to upload
89+
* @param {string} [options.fileId] ID of file if replacing
90+
* @param {object} [options.params] Additional query parameters
91+
* @param {string} [options.contentType] Content-type, if overriding the type of the blob.
92+
* @param {object} [options.metadata] File metadata
93+
* @param {function} [options.onComplete] Callback for when upload is complete
94+
* @param {function} [options.onProgress] Callback for status for the in-progress upload
95+
* @param {function} [options.onError] Callback if upload fails
96+
*/
97+
var MediaUploader = function(options) {
98+
var noop = function() {};
99+
this.file = options.file;
100+
this.contentType = options.contentType || this.file.type || 'application/octet-stream';
101+
this.metadata = options.metadata || {
102+
'title': this.file.name,
103+
'mimeType': this.contentType
104+
};
105+
this.token = options.token;
106+
this.onComplete = options.onComplete || noop;
107+
this.onProgress = options.onProgress || noop;
108+
this.onError = options.onError || noop;
109+
this.offset = options.offset || 0;
110+
this.chunkSize = options.chunkSize || 0;
111+
this.retryHandler = new RetryHandler();
112+
113+
this.url = options.url;
114+
if (!this.url) {
115+
var params = options.params || {};
116+
params.uploadType = 'resumable';
117+
this.url = this.buildUrl_(options.fileId, params, options.baseUrl);
118+
}
119+
this.httpMethod = options.fileId ? 'PUT' : 'POST';
120+
};
121+
122+
/**
123+
* Initiate the upload.
124+
*/
125+
MediaUploader.prototype.upload = function() {
126+
var self = this;
127+
var xhr = new XMLHttpRequest();
128+
129+
xhr.open(this.httpMethod, this.url, true);
130+
xhr.setRequestHeader('Authorization', 'Bearer ' + this.token);
131+
xhr.setRequestHeader('Content-Type', 'application/json');
132+
xhr.setRequestHeader('X-Upload-Content-Length', this.file.size);
133+
xhr.setRequestHeader('X-Upload-Content-Type', this.contentType);
134+
135+
xhr.onload = function(e) {
136+
if (e.target.status < 400) {
137+
var location = e.target.getResponseHeader('Location');
138+
this.url = location;
139+
this.sendFile_();
140+
} else {
141+
this.onUploadError_(e);
142+
}
143+
}.bind(this);
144+
xhr.onerror = this.onUploadError_.bind(this);
145+
xhr.send(JSON.stringify(this.metadata));
146+
};
147+
148+
/**
149+
* Send the actual file content.
150+
*
151+
* @private
152+
*/
153+
MediaUploader.prototype.sendFile_ = function() {
154+
var content = this.file;
155+
var end = this.file.size;
156+
157+
if (this.offset || this.chunkSize) {
158+
// Only bother to slice the file if we're either resuming or uploading in chunks
159+
if (this.chunkSize) {
160+
end = Math.min(this.offset + this.chunkSize, this.file.size);
161+
}
162+
content = content.slice(this.offset, end);
163+
}
164+
165+
var xhr = new XMLHttpRequest();
166+
xhr.open('PUT', this.url, true);
167+
xhr.setRequestHeader('Content-Type', this.contentType);
168+
xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size);
169+
xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
170+
if (xhr.upload) {
171+
xhr.upload.addEventListener('progress', this.onProgress);
172+
}
173+
xhr.onload = this.onContentUploadSuccess_.bind(this);
174+
xhr.onerror = this.onContentUploadError_.bind(this);
175+
xhr.send(content);
176+
};
177+
178+
/**
179+
* Query for the state of the file for resumption.
180+
*
181+
* @private
182+
*/
183+
MediaUploader.prototype.resume_ = function() {
184+
var xhr = new XMLHttpRequest();
185+
xhr.open('PUT', this.url, true);
186+
xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size);
187+
xhr.setRequestHeader('X-Upload-Content-Type', this.file.type);
188+
if (xhr.upload) {
189+
xhr.upload.addEventListener('progress', this.onProgress);
190+
}
191+
xhr.onload = this.onContentUploadSuccess_.bind(this);
192+
xhr.onerror = this.onContentUploadError_.bind(this);
193+
xhr.send();
194+
};
195+
196+
/**
197+
* Extract the last saved range if available in the request.
198+
*
199+
* @param {XMLHttpRequest} xhr Request object
200+
*/
201+
MediaUploader.prototype.extractRange_ = function(xhr) {
202+
var range = xhr.getResponseHeader('Range');
203+
if (range) {
204+
this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1;
205+
}
206+
};
207+
208+
/**
209+
* Handle successful responses for uploads. Depending on the context,
210+
* may continue with uploading the next chunk of the file or, if complete,
211+
* invokes the caller's callback.
212+
*
213+
* @private
214+
* @param {object} e XHR event
215+
*/
216+
MediaUploader.prototype.onContentUploadSuccess_ = function(e) {
217+
if (e.target.status == 200 || e.target.status == 201) {
218+
this.onComplete(e.target.response);
219+
} else if (e.target.status == 308) {
220+
this.extractRange_(e.target);
221+
this.retryHandler.reset();
222+
this.sendFile_();
223+
}
224+
};
225+
226+
/**
227+
* Handles errors for uploads. Either retries or aborts depending
228+
* on the error.
229+
*
230+
* @private
231+
* @param {object} e XHR event
232+
*/
233+
MediaUploader.prototype.onContentUploadError_ = function(e) {
234+
if (e.target.status && e.target.status < 500) {
235+
this.onError(e.target.response);
236+
} else {
237+
this.retryHandler.retry(this.resume_.bind(this));
238+
}
239+
};
240+
241+
/**
242+
* Handles errors for the initial request.
243+
*
244+
* @private
245+
* @param {object} e XHR event
246+
*/
247+
MediaUploader.prototype.onUploadError_ = function(e) {
248+
this.onError(e.target.response); // TODO - Retries for initial upload
249+
};
250+
251+
/**
252+
* Construct a query string from a hash/object
253+
*
254+
* @private
255+
* @param {object} [params] Key/value pairs for query string
256+
* @return {string} query string
257+
*/
258+
MediaUploader.prototype.buildQuery_ = function(params) {
259+
params = params || {};
260+
return Object.keys(params).map(function(key) {
261+
return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]);
262+
}).join('&');
263+
};
264+
265+
/**
266+
* Build the drive upload URL
267+
*
268+
* @private
269+
* @param {string} [id] File ID if replacing
270+
* @param {object} [params] Query parameters
271+
* @return {string} URL
272+
*/
273+
MediaUploader.prototype.buildUrl_ = function(id, params, baseUrl) {
274+
var url = baseUrl || DRIVE_UPLOAD_URL;
275+
if (id) {
276+
url += id;
277+
}
278+
var query = this.buildQuery_(params);
279+
if (query) {
280+
url += '?' + query;
281+
}
282+
return url;
283+
};
284+

0 commit comments

Comments
 (0)