-
Notifications
You must be signed in to change notification settings - Fork 25
/
Copy pathFetchUtils.gs
251 lines (206 loc) · 7.72 KB
/
FetchUtils.gs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
/**
* Utils contains useful functions for working with urlfetchapp
* @namespace FetchUtils
*/
var FetchUtils = (function (ns) {
/**
* to keep this namespace dependency free
* this must be run to set the driveappservice before it can be used
* @param {fetchapp} dap the fetchapp
* @return {FetchUtils} self
*/
ns.setService = function (dap) {
ns.service = dap;
return ns;
};
ns.checkService = function () {
if(!ns.service) {
throw 'please do a FetchUtils.setService (yoururlfetchapp) to inialise namespace';
}
};
/**
* restart a resumable upload
* @param {string} accessToken the token
* @param {blob} contblobent the content
* @param {string} location the location url
* @param {string} start the start position
* @param {function} [func] a func to call after each chunk
* @return {object} the status from the last request
*/
ns.resumeUpload = function (accessToken,blob,location,start,func) {
var MAXPOSTSIZE = 1024*1024*8;
ns.checkService();
//get the content and make the resource
var content = blob.getBytes();
var file = {
title: blob.getName(),
mimeType:blob.getContentType()
};
var chunkFunction = func || function ( status) {
// you can replace this function with your own.
// it gets called after each chunk
// do something on completion
if (status.done) {
Logger.log (
status.resource.title + '(' + status.resource.id + ')' + '\n' +
' is finished uploading ' +
status.content.length +
' bytes in ' + (status.index+1) + ' chunks ' +
' (overall transfer rate ' + Math.round(content.length*1000/(new Date().getTime() - status.startTime)) + ' bytes per second)'
);
}
// do something on successful completion of a chunk
else if (status.success) {
Logger.log (
status.resource.title +
' is ' + Math.round(status.ratio*100) + '% complete ' +
' (chunk transfer rate ' + Math.round(status.size*1000/(new Date().getTime() - status.startChunkTime)) + ' bytes per second)' +
' for chunk ' + (status.index+1)
);
}
// decide what to do on an error
else if (response.getResponseCode() === 503 ) {
throw 'error 503 - you can try restarting using ' + status.location;
}
// its some real error
else {
throw response.getContentText() + ' you might be able to restart using ' + location;
}
// if you want to cancel return true
return false;
};
var startTime = new Date().getTime();
// now do the chunks
var pos = 0, index = 0 ;
do {
// do it in bits
var startChunkTime = new Date().getTime();
var chunk = content.slice (pos , Math.min(pos+MAXPOSTSIZE, content.length));
var options = {
contentType:blob.getContentType(),
method:"PUT",
muteHttpExceptions:true,
headers: {
"Authorization":"Bearer " + accessToken,
"Content-Range": "bytes "+pos+"-"+(pos+chunk.length-1)+"/"+content.length
}
};
// load this chunk of data
options.payload = chunk;
// now we can send the file
// but .... UrlFetch failed because too much upload bandwidth was used
var response = Utils.expBackoff (function () {
return ns.service.fetch (location, options) ;
});
// the actual data size transferred
var size = chunk.length;
if (response.getResponseCode() === 308 ) {
var ranges = response.getHeaders().Range.split('=')[1].split('-');
var size = parseInt (ranges[1],10) - pos + 1;
if (size !== chunk.length ) {
Logger.log ('chunk length mismatch - sent:' + chunk.length + ' but confirmed:' + size + ':recovering by resending the difference');
}
};
// catch the file id
if (!file.id) {
try {
file.id = JSON.parse(response.getContentText()).id;
}
catch (err) {
// this is just in case the contenttext is not a proper object
}
}
var status = {
start:pos,
size:size,
index:index,
location:location,
response:response,
content:content,
success:response.getResponseCode() === 200 || response.getResponseCode() === 308,
done:response.getResponseCode() === 200,
ratio:(size + pos) / content.length,
resource:file,
startTime:startTime,
startChunkTime:startChunkTime
};
index++;
pos += size;
// now call the chunk completefunction
var cancel = chunkFunction ( status );
} while ( !cancel && status.success && !status.done);
return status;
};
/**
* resumable upload
* @param {string} accessToken an accesstoken with Drive scope
* @param {blob} blob containg the data, type and name
* @param {string} [folderId] the folderId to be the parent
* @param {function} func a func to call after each chunk
* @return {object} the status from the last request
*/
ns.resumableUpload = function (accessToken,blob,folderId,func) {
ns.checkService();
/**
* @param {object} status the status of the transfer
* status.start the start position in the content just processed
* status.size the size of the chunk
* status.index the index number (0 base) of the chunk
* status.location the restartable url
* status.content the total content
* status.response the httr response of this attempt
* status.success whether this worked (see response.getResponseCode() for more
* status.done whether its all done successfully
* status.ratio ratio complete
* status.resource the file resource
* status.startTime timestamp of when it all started
* status.startChunkTime timestamp of when this chunk started
* @return {boolean} whether to cancel (true means cancel the upload)
*/
//get the content and make the resource
var content = blob.getBytes();
var file = {
title: blob.getName(),
mimeType:blob.getContentType()
};
// assign to a folder if given
if (folderId) {
file.parents = [{id:folderId}];
}
// this sends the metadata and gets back a url
var resourceBody = JSON.stringify(file);
var headers = {
"X-Upload-Content-Type":blob.getContentType(),
"X-Upload-Content-Length":content.length ,
"Authorization":"Bearer " + accessToken,
};
var response = Utils.expBackoff( function () {
return ns.service.fetch ("https://www.googleapis.com/upload/drive/v2/files?uploadType=resumable", {
headers:headers,
method:"POST",
muteHttpExceptions:true,
payload:resourceBody,
contentType: "application/json; charset=UTF-8",
contentLengthxx:resourceBody.length
});
});
if (response.getResponseCode() !== 200) {
throw 'failed on initial upload ' + response.getResponseCode();
}
// get the resume location
var location = getLocation (response);
return ns.resumeUpload (accessToken,blob,location,0,func);
function getLocation (resp) {
if(resp.getResponseCode()!== 200) {
throw 'failed in setting up resumable upload ' + resp.getContentText();
}
// the location we need comes back as a header
var location = resp.getHeaders().Location;
if (!location) {
throw 'failed to get location for resumable uploade';
}
return location;
}
};
return ns;
})(FetchUtils || {});