the urlfetch has a limited data allowed so in order to load a large CSV or TSV file (50K) I've created a proxy server (REST)
which makes the request under the hood, map it to list and cache it for 6 hours.
then, I added an endpoint to fetch data based on a range (see signature below)
signature:
var PROXY = 'http://lagoja-services.rcb.co.il/api/adwordsProxy/feed/{base64URL}/{seperator}/{from}/{to}'
when someone reaches the range endpoint, I create a CSV on-the-fly.
the first loading of this large file on the server takes between 60-80 seconds, other followed requests take the content from the cache so it takes only 2 seconds.
the problem I have is that I'm getting a timeout after 60 seconds using the urlfetch built-in class so it Alternately works (sometimes it works and sometimes getting timeout exception)
there is no way of reducing the first load!
it seems that I can't control the client timeout (from some unknown reason)
any other solution for me?
thanks, Roby (-:
hi, I've created a dedicated "test" endpoint for you to reproduce the issue. this endpoint tries to load the massive file every time (no cache)
function main() {
var response = UrlFetchApp.fetch('http://lagoja-services.rcb.co.il/api/adwordsProxy/test');
var status_code = response.getResponseCode();
Logger.log('status code %s', status_code);
}
endpoint:
http://lagoja-services.rcb.co.il/api/adwordsProxy/test
exception:
Timeout: http://lagoja-services.rcb.co.il/api/adwordsProxy/test
hi all,
i found a solution (which i don't like of course)
but i didn't have any better suggestion.
the idea is to ping the server to fetch the file, wait till it loaded into the cache and then request the loaded content... a shitty solution but at least it works lol
api:
[HttpGet]
[Route("api/fire")]
public HttpResponseMessage FireGetCSVFeed(...) {
HostingEnvironment.QueueBackgroundWorkItem(async ct => {
// make a request to the original CSV file - takes 60-80 seconds
});
return Request.CreateResponse(HttpStatusCode.OK, "Working...");
}
adwords script:
function sleep(milliseconds) {
var start = new Date().getTime();
while (new Date().getTime() < start + milliseconds);
}
function main() {
var response = UrlFetchApp.fetch(baseURL + 'fire');
sleep(1000 * 120); // yes i know, i'm blocking everything )-:
response = UrlFetchApp.fetch(baseURL + 'getCSV');
status_code = response.getResponseCode();
Logger.log('status code %s', status_code);
}
Related
I am building a user interface thanks to a GAS web app.
This script is connected to a spreadsheet to read and record data.
I am able to record data from the user entry to the spreadsheet thanks to my current code but my concern is about the reliability of this app when multiple users will be connected to this app and will try to record data.
In order to test the tool, I have created a 10 iterations loop on client side which send pre formatted data to the server side for recording into the spreadsheet.
When I launch the function on one computer it works (10 lines are properly recorded) but when a second user activate the same function on its session the total number of lines recorded is random (15 or 17 instead of 20).
When having a look to the spreadsheet when scriptS are running, I see that sometimes values on a line is overwritten by an other value (just like if the new row was not recorded at the proper place).
The Web app is share as execute as me (so the server side code is executed with my account)
In order to control what s going on, a lockservice has been implemented on the server side script (the part which records the data on the spreadsheet) and a new promise / async function on the client side.
function recordDataOnSheet(data){ // server side function to record data in the spreadsheet
var lock='';
lock = LockService.getScriptLock();
var success = lock.tryLock(10000);//Throws exception if fail wait 10s max to get a lock
if (success){
var dat =dataSheet.getDataRange().getValues();
lsRow++;
data[0].length);
dat.push(data);
dataSheet.getRange(1, 1, dat.length, dat[0].length).setValues(dat);
Logger.log(dat.length);
} else {Logger.log("Lock failed")};
lock.releaseLock();
} ```
and client side piece of script:
function runGoogleSript(serverFunc,dat){ // function to mange the asynchronous call of a server sdide function
return new Promise((resolve, reject) => {
google.script.run.withSuccessHandler(data => {
resolve(data)
}).withFailureHandler(er => {
reject(er)
})[serverFunc](dat)
});
}
async function test (){
for (i=0;i<10;i++){
let d = new Date();
setTimeout(() => { console.log(i) }, 2000);
try {
const data = await runGoogleSript("recordDataOnSheet",["06323","XX:",user.id,"2022-02-07", d.getMinutes() , d.getSeconds() ] );
}
catch (er) {alert(er);}
}
}
After dozen of test and tests, I have discovered that it was only an issue with the spreadsheet call.
As I am calling the same file/sheet in a loop sometimes the previous activity (to update the data in the sheet) were not over. This was at the origin of the issue.
I have simply added a SpreadsheetApp.flush(); at the end of my function recordDataOnSheet and it works and it is reliable
I have problem with my function in Google Sheets. I am getting every day this error: "Exception: Service invoked too many times for one day: urlfetch." I have about 1000 urls in document. I am looked for solution at google. I find some topics where is recommended to add cache to function but I dont know how to do it. Does somebody have any idea? My function:
function ImportCeny(url, HTMLClass) {
var output = '';
var fetchedUrl = UrlFetchApp.fetch(url, {muteHttpExceptions: true});
if (fetchedUrl) {
var html = fetchedUrl.getContentText();
}
// Grace period to avoid call limit
Utilities.sleep(1000);
var priceposition = html.search(HTMLClass);
return html.slice(priceposition,priceposition + 70).match(/(-\d+|\d+)(,\d+)*(\.\d+)*/g);
}
You may try to add a randomly generated number, for example 6 digits and add this number to the URL as a parameter each time before calling "UrlFetchApp"
i.e.;
url = url & "?t=458796"
You can certainly use Utilities.sleep() to force the program to stop for some time before making the next call. However, using the built-in Cache class (you can see the docs here) is much more suitable as it is specially designed for these scenarios.
So, if you wanted to leave one second you could replace:
Utilities.sleep(1000); //In milliseconds
with
var cache = CacheService.getScriptCache(); //Create a cache instance
cache.put("my_content", html, 1); // In seconds
I need to create a lot of individual files on Google Drive in one folder. Currently the only way I see it can be done is using folderName.createFile(). Creating files one by one with this command is tediously slow.
Is there a faster way to do it like a batch creation tool from an array of files?
Yes, the latest Google Drive API (v3) supports any 100 actions in a single http call. The default endpoint is /batch/api_name/api_version. Most of the Drive client libraries have functions built-in to work with it.
This page gives the details, and here is an example in Java. Notice how all of the queries, after being constructed, are queue()-ed into the batch call, which is executed as a single HTTP connection.
Update from comments: Google Drive API does not currently support batch file upload or download queries. Consider looking into Google Cloud Platform's Storage Buckets (or similar services) for more large-scale file options.
Ok pleased to confirm it can be done with UrlFetchApp.fetchAll().
Here is how I did it. I am not a professional programmer so please forgive my transgressions in the code :) Special thanks to #TheMaster and #Tanaike who put me on the right track.
On the web app you need to apply the following code:
//doPost (note I updated the code on stackoverflow after posting my answer as there was an error)
const doPost = e => {
try{
var diskPath = getFolder(e.parameter.diskPath)[0];
const file = diskPath.createFile(e.parameter.fileName,Utilities.newBlob(Utilities.base64DecodeWebSafe(e.parameter.file),e.parameter.contentType));
}
catch(e){
return ContentService.createTextOutput(e)
}
return ContentService.createTextOutput( 'Saved:' + 'e.parameter.fileName: '+e.parameter.fileName + 'e.parameter.contentType '+e.parameter.contentType);
}
The code must be published as a web app. within the script editor click "Publish" and then "Deploy as a web app" and select "Project version" as "New". "Who has access to the app:" should be set to "Anyone, even anonymous" and then save. You will be given a public URL and you enter that in the script below:
//save files function
function saveFiles(fileArr){ //note file blobs must have setName and have contentType set so we save them in the fetch array: arr = [[file1/Blob2,'image/jpeg',name1],[file2/Blob2,'text/html',name2],............];
var url = 'INSERT YOUR WEB APP PUBLIC URL HERE'
var fetchArr = [];
for (var i in fileArr){
var file = fileArr[i][0];
var bytes
try{ //if file is already blob prevent error getting Blob
bytes = file.getBlob().getBytes()
}
catch(e){
bytes = file.getBytes();
}
var fileName = fileArr[i][2];
var contentType = fileArr[i][1];
var payload = {
fileName: fileName,
contentType : contentType,
file: Utilities.base64EncodeWebSafe(bytes)
};
var options = {
url: url,
method: "POST",
payload: payload,
muteHttpExceptions : true,
};
fetchArr.push(options)
}
UrlFetchApp.fetchAll(fetchArr);
}
The above code creates a fetch array of URLs with files attached, and then posts them to the web app. The web app then saves them asynchronously saving you a heap of time if you have a lot of files you want to save to disk at once.
In order to run the code you need to call saveFiles(fileArr) from another script which builds the array. The array should have the following format:
[[file or blob, content type, name for the new file as a string],[file or blob, content type, name for the new file as a string],.....]
The content type is a MIME type and can be derived from the old file (if you are not making a new Blob) like so: fileName.getContentType(). If you are making a new Blob you should set the contentType according to the MIME type for the file type you are creating.
Edited: Just a note about rate limiting. I hit a rate limit when I tried to save over 13 files in one go, but I was able to send batches of 10 files every few seconds without any problems.
I setup a custom slash command to store data into a Google Spreadsheet. Everything works perfectly (the script gets triggered and does its magic) except the respond takes too long (more than the given max of 3000ms) and Slack throws me a timeout error.
simplified google script:
function doPost(request) {
//// get data from slack payload
var params = request.parameters;
//// call a function with given parameters
custom_function(params);
////
var output = {"text":"SUCCESS"};
//// respond to slacks POST request
return ContentService.createTextOutput(JSON.stringify(output)).setMimeType(ContentService.MimeType.JSON);
}
Result:
Due to the long execution time of custom_function(); the end return ContentService. ... comes too late (past 3000ms timelimit) = timeout error in slack
Additional Info: I setup delayed responses with UrlFetchApp.fetch(url,options); with the code of custom_function(); - I am receiving those responses in Slack together with the timeout error.
Question: Is there any way I DON'T have to wait until custom_function(); is finished and send some HTTP 200 OK back immediately ? The doPost(); in my case doesn't need anything from custom_function in return to finish so why wait ... ?
THANKS !
You can create a time based trigger to run the code in the future. Specifically the after method of ClockTriggerBuilder can run the code x milliseconds in the future.
https://developers.google.com/apps-script/reference/script/clock-trigger-builder#after(Integer)
function doPost(){
//execute the the script function "myFunction" 100 ms in the future
ScriptApp.newTrigger("myFunction")
.timeBased()
.after(100)
.create();
return ContentService.createTextOutput("OK");
}
My idea was to setup a script with GAS to record videos of my ip camera to google drive. Currently it can access the camera videostream, take data and save it do drive. It's not working because instead of getting a limited amount of data due to the http request header range parameter it takes the maximum size the http request could receive. Furthermore the asf video seems to get corrupted, and can't be played on VLC.
Any idea for making the script download a defined video size and in the correct format?
function myFunction() {
var URL = 'http://201.17.122.01:82/videostream.asf?user=xxxx&pwd=xxxxxx&resolution=32&rate=1'; // file to backup
var chunkSize = 1048576; // read the file in pieces of 1MB
var chunkStart = 0, chunkEnd = chunkStart + chunkSize;
var chunkHTTP = UrlFetchApp.fetch(URL, {
method: "get",
contentType: "video/x-ms-asf",
headers: {
"Range": "bytes=" + chunkStart + "-" + chunkEnd
}
})
var chunk = chunkHTTP.getContentText();
// write to Drive
try {
var folder = DocsList.getFolder('bakfolder');
} catch(err) {
var folder = DocsList.createFolder('bakfolder');
}
fileOnDrive = folder.createFile('ipcamera.asf', chunk);
Logger.log(" %s bytes written to drive", chunk.length);
}
I do not have an ip camera, so can't test and help you with the chunking of received data. But I suspect that you are using wrong methods for extracting received video data and saving it to Drive:
a) .getContentText() method gets the content of an HTTP response encoded as a string - probably wrong for working with video stream data. Try using .getBlob() or .getContent() instead;
b) folder.createFile(name, content) method creates a text file in the current folder. Use .createFile(blob) method instead.
You will most likely have t experiment with the stream bytes to get it as a blob Apps Script can work with. If I find an ip camera or another video stream I can access, I will test it out and update this.