SankeySheets/libraries/cUseful/FetchUtils.js
2016-06-24 18:11:06 +01:00

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 || {});