251 lines
No EOL
7.7 KiB
JavaScript
251 lines
No EOL
7.7 KiB
JavaScript
/**
|
|
* 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 || {}); |