How to extract files from .tar archive with Google Apps Script - google-apps-script

Good day all,
I'm trying to get a tar.gz attachment from Gmail, extract the file and save it to Google Drive. It's a daily auto generated report which I'm getting, compressed due to >25mb raw size.
I got this so far:
var sheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("Setup");
var gmailLabels = sheet.getRange("B2:B2").getValue(); //I have my Gmail Label stored here
var driveFolder = sheet.getRange("B5:B5").getValue(); //I have my GDrive folder name stored here
// apply label filter, search only last 24hrs mail
var filter = "has:attachment label:" + gmailLabels + " after:" + Utilities.formatDate(new Date(new Date().getTime()-1*(24*60*60*1000)), "GMT", "yyyy/MM/dd");
var threads = GmailApp.search(filter, 0, 1); // check only 1 email at a time
var folder = DriveApp.getFoldersByName(driveFolder);
if (folder.hasNext()) {
folder = folder.next();
} else {
folder = DriveApp.createFolder(driveFolder);
}
var message = threads[0].getMessages()[0];
var desc = message.getSubject() + " #" + message.getId();
var att = message.getAttachments();
for (var z=0; z<att.length; z++) {
var attName = att[z].getName()
var attExt = attName.search('csv')
if (attExt > 0){ var fileType = "csv"; }
else {
var attExt = attName.search('tar.gz');
if (attExt > 0){ var fileType = "gzip"; }
else {
threads[x].addLabel(skipLabel);
continue;
}
}
// save the file to GDrive
try {
file = folder.createFile(att[z]);
file.setDescription(desc);
}
catch (e) {
Logger.log(e.toString());
}
// extract if gzip
if (fileType == 'gzip' ){
var ungzippedFile = Utilities.ungzip(file);
try {
gz_file = folder.createFile(ungzippedFile);
gz_file.setDescription(desc);
}
catch (e) {
Logger.log(e.toString());
}
}
}
Everything works fine, but in the last step it only decompresses the .gz file saving .tar file in the Drive. What can I do with it next? The .tar file contains a .csv file which I need to extract and process afterwards.
I should probably add that I'm limited to use GAS only.
Any help warmly appreciated.

How about this answer? Unfortunately, in the current stage, there are no methods for extracting files from a tar file in Google Apps Script, yet. But fortunately, from wiki of tar, we can retrieve the structure of the tar data. I implemented this method with Google Apps Script using this structure data.
1. Unarchive of tar data:
Before you run this script, please set the file ID of tar file to run(). Then, run run().
Sample script:
function tarUnarchiver(blob) {
var mimeType = blob.getContentType();
if (!mimeType || !~mimeType.indexOf("application/x-tar")) {
throw new Error("Inputted blob is not mimeType of tar. mimeType of inputted blob is " + mimeType);
}
var baseChunkSize = 512;
var byte = blob.getBytes();
var res = [];
do {
var headers = [];
do {
var chunk = byte.splice(0, baseChunkSize);
var headerStruct = {
filePath: function(b) {
var r = [];
for (var i = b.length - 1; i >= 0; i--) {
if (b[i] != 0) {
r = b.slice(0, i + 1);
break;
}
}
return r;
}(chunk.slice(0, 100)),
fileSize: chunk.slice(124, 124 + 11),
fileType: Utilities.newBlob(chunk.slice(156, 156 + 1)).getDataAsString(),
};
Object.keys(headerStruct).forEach(function(e) {
var t = Utilities.newBlob(headerStruct[e]).getDataAsString();
if (e == "fileSize") t = parseInt(t, 8);
headerStruct[e] = t;
});
headers.push(headerStruct);
} while (headerStruct.fileType == "5");
var lastHeader = headers[headers.length - 1];
var filePath = lastHeader.filePath.split("/");
var blob = Utilities.newBlob(byte.splice(0, lastHeader.fileSize)).setName(filePath[filePath.length - 1]).setContentTypeFromExtension();
byte.splice(0, Math.ceil(lastHeader.fileSize / baseChunkSize) * baseChunkSize - lastHeader.fileSize);
res.push({fileInf: lastHeader, file: blob});
} while (byte[0] != 0);
return res;
}
// Following function is a sample script for using tarUnarchiver().
// Please modify this to your situation.
function run() {
// When you want to extract the files from .tar.gz file, please use the following script.
var id = "### file ID of .tar.gz file ###";
var gz = DriveApp.getFileById(id).getBlob().setContentTypeFromExtension();
var blob = Utilities.ungzip(gz).setContentTypeFromExtension();
// When you want to extract the files from .tar file, please use the following script.
var id = "### file ID of .tar file ###";
var blob = DriveApp.getFileById(id).getBlob().setContentType("application/x-tar");
// Extract files from a tar data.
var res = tarUnarchiver(blob);
// If you want to create the extracted files to Google Drive, please use the following script.
res.forEach(function(e) {
DriveApp.createFile(e.file);
});
// You can see the file information by below script.
Logger.log(res);
}
2. Modification of your script:
If this script is used for your script, for example, how about this? tarUnarchiver() of above script is used. But I'm not sure how you want to use this script. So please think of this as a sample.
Sample script:
// extract if gzip
if (fileType == 'gzip' ){
var ungzippedFile = Utilities.ungzip(file);
try {
var blob = ungzippedFile.setContentType("application/x-tar"); // Added
tarUnarchiver(blob).forEach(function(e) {folder.createFile(e.file)}); // Added
}
catch (e) {
Logger.log(e.toString());
}
}
In this modified script, the blob of ungzippedFile (tar data) is put to my script and run tarUnarchiver(). Then, each file is created to the folder.
Note:
When you run this script, if an error related to mimeType occurs, please set the mimeType of "tar" to the input blob.
As the method for setting the mimeType, you can use as follows.
blob.setContentTypeFromExtension() Ref
blob.setContentType("application/x-tar") Ref
It might have already been got the mimeType in the blob. At that time, setContentTypeFromExtension() and setContentType() are not required.
If you want to retrieve the file path of each file, please check the response from tarUnarchiver(). You can see it as a property of fileInf from the response.
Limitations:
When this script is used, there is the limitations as follows. These limitations are due to Google's specification.
About the file size, when the size of tar data is over 50 MB (52,428,800 bytes), an error related to size limitation occurs.
When the size of extracted file is over 50 MB, an error occurs.
When a single file size of extracted file is near to 50 MB, there is a case that an error occurs.
In my environment, I could confirm that the size of 49 MB can be extracted. But in the case of just 50 MB
, an error occurred.
Reference:
tar (wiki)
In my environment, I could confirm that the script worked. But if this script didn't work, I apologize. At that time, can you provide a sample tar file? I would like to check it and modify the script.

Related

Google App Script timing out while removing Viewers, How to make the script more efficient?

I have a GAS that I run every month or so to remove Viewers and Editors from GoogleDocs and GoogleSheets that were created over 1 year ago. I have not found a way to return ONLY the documents which have the specific users I want to remove.
So the code is setup to loop thru all the documents in a specific folder and if the Viewers/Editors do not match the 2 owners, then it removes their access.
The problem is a few folders have a large number of files and it is timing out just reading thru to find out if any Viewers/Editors need to be removed.
Any ideas on how this code could be streamlined or if there is a way to query for only the documents not owned by a specific user?
var folder = folders.next(); //assume the match is the first one
folder = DriveApp.getFolderById(folder.getId()); //use the folderID of the year folder
processFolder(folder); //this starts in with the newest folder modified date under the Proposals/Year folder and works down thru the list until it times out after 5 minutes of running
function processFolder(folder) {
var asset;
var users;
var email;
var files = folder.getFiles();
var todaysDate = new Date();
while (files.hasNext()) {
var file = files.next();
var daysCreated = parseInt(((todaysDate - file.getDateCreated()) / 86400000)); //how many days since the document was created 24/3600/1000 = 86,400,000
if (daysCreated > RETENTION_DAYS) {
asset = DriveApp.getFileById(file.getId());
for (var i = 0; i < 2; i++) {
if (i == 0) {
users = asset.getEditors();
} else {
users = asset.getViewers();
}
for (var cnt = 0; cnt < users.length; cnt++) {
email = users[cnt].getEmail().toLowerCase();
if (email != "xxx1#gmail.com" && email != "xxx2#gmail.com") {
if (i == 0) { //Editors
asset.removeEditor(email);
} else { //Viewers
asset.removeViewer(email);
}
}
}
}
}
} //processFolder
About how this code could be streamlined or if there is a way to query for only the documents not owned by a specific user?, for example, if you want to retrieve only the files without including xxx1#gmail.com and xxx2#gmail.com as the writer and the viewer, how about using searchFiles instead of getFiles? When your script is modified, it becomes as follows.
Modified script:
function processFolder(folder) {
var emails = ["xxx1#gmail.com", "xxx2#gmail.com"]; // Please set the email addresses.
var query = emails.map(e => `not '${e}' in writers and not '${e}' in readers`).join(" and ") + " and trashed=false";
var files = folder.searchFiles(query);
var todaysDate = new Date();
while (files.hasNext()) {
var file = files.next();
var daysCreated = parseInt(((todaysDate - file.getDateCreated()) / 86400000));
if (daysCreated > RETENTION_DAYS) {
file.getEditors().forEach(e => file.removeEditor(e));
file.getViewers().forEach(e => file.removeViewer(e));
}
}
}
When this script is run, the writers and the viewers of the files without including "xxx1#gmail.com" and "xxx2#gmail.com" as the writer and the viewer are removed.
Note:
When this sample script is run, the writers and the viewers of the files without including "xxx1#gmail.com" and "xxx2#gmail.com" as the writer and the viewer are removed. So, I would like to recommend testing this script using the sample files. Please be careful about this.
Reference:
searchFiles(params)
Added:
From your replying, as another approach, in this case, how about the following sample script? In this sample, the following flow is used.
Retrieve all file IDs just under the specific folder using Drive API.
Retrieve permission IDs from the files using Drive API.
Create the requests for deleting the permissions except for "emails".
Delete permissions using Drive API.
Usage:
1. Install a Google Apps Script library.
In this sample, the batch request is used. In this case, I created a Google Apps Script library for this. So, please install the library. About the method for installing it, you can see it at here.
2. Enable Drive API.
This script uses Drive API. So, please enable Drive API at Advanced Google services.
3. Sample script:
Please copy and paste the following script to the script editor and set emails and folderId. And please run sample(). By this, the script is run.
function sample() {
var emails = ["xxx1#gmail.com", "xxx2#gmail.com"]; // Please set the email addresses.
var folderId = "###"; // Please set the folder ID.
// 1. Retrieve all file IDs just under the specific folder using Drive API.
var list = [];
var pageToken = "";
do {
var obj = Drive.Files.list({q: `'${folderId}' in parents`, maxResults: 1000, pageToken, fields: "items(id),nextPageToken"});
if (obj.items.length > 0) list = [...list, ...obj.items.map(({id}) => id)];
pageToken = obj.nextPageToken;
} while(pageToken);
// 2. Retrieve permission IDs from the files using Drive API.
var req1 = list.map(id => ({method: "GET", endpoint: `https://www.googleapis.com/drive/v3/files/${id}/permissions?pageSize=100&fields=permissions(id%2CemailAddress%2Crole)`}))
var token = ScriptApp.getOAuthToken();
var requests1 = {
batchPath: "batch/drive/v3", // batch path. This will be introduced in the near future.
requests: req1,
accessToken: token
};
var result1 = BatchRequest.EDo(requests1);
// 3. Create the requests for deleting the permissions except for "emails".
var req2 = list.reduce((ar, id, i) => {
var p = result1[i].permissions;
if (p.length > 0) {
p.forEach(e => {
if (e.role != "owner" && e.emailAddress && !emails.includes(e.emailAddress)) {
ar.push({method: "DELETE", endpoint: `https://www.googleapis.com/drive/v3/files/${id}/permissions/${e.id}`});
}
})
}
return ar;
}, []);
// 4. Delete permissions using Drive API.
var requests2 = {
batchPath: "batch/drive/v3",
requests: req2,
accessToken: token
};
var result2 = BatchRequest.EDo(requests2);
}
When this script is run, about all files just under the specific folder, all permissions except for the owner and emails are removed.
Note:
This script removes the permissions. Please be careful about this. So in this case, I would like to propose to test using a sample permitted files.
Reference:
BatchRequest

App script for pdf converter exceeded maximum execution time

This is my app script code that converts google docs in a folder into pdf format. The script stops after converting around 60 documents with maximum execution time error. I am converting around hundreds of files in a run. What can I do to avoid this error?
//Module to convert doc to pdf
function gdocToPDF() {
var documentRootfolder = DriveApp.getFolderById("xx") // replace this with the ID of the folder that contains the documents you want to convert
var pdfFolder = DriveApp.getFolderById("xx"); // replace this with the ID of the folder that the PDFs should be put in.
var documentRootFiles = documentRootfolder.getFiles()
while(documentRootFiles.hasNext()) {
createPDF(documentRootFiles.next().getId(), pdfFolder.getId(), function (fileID, folderID) {
if (fileID) createPDFfile(fileID, folderID);
})
}
}
function createPDF(fileID, folderID, callback) {
var templateFile = DriveApp.getFileById(fileID);
var templateName = templateFile.getName();
var existingPDFs = DriveApp.getFolderById(folderID).getFiles();
//in case no files exist
if (!existingPDFs.hasNext()) {
return callback(fileID, folderID);
}
for (; existingPDFs.hasNext();) {
var existingPDFfile = existingPDFs.next();
var existingPDFfileName = existingPDFfile.getName();
if (existingPDFfileName == templateName + ".pdf") {
Logger.log("PDF exists already. No PDF created")
return callback();
}
if (!existingPDFs.hasNext()) {
Logger.log("PDF is created")
return callback(fileID, folderID)
}
}
}
function createPDFfile(fileID, folderID) {
var templateFile = DriveApp.getFileById(fileID);
var folder = DriveApp.getFolderById(folderID);
var theBlob = templateFile.getBlob().getAs('application/pdf');
var newPDFFile = folder.createFile(theBlob);
var fileName = templateFile.getName().replace(".", ""); //otherwise filename will be shortened after full stop
newPDFFile.setName(fileName + ".pdf");
}
I generate a lot of files. What I do to avoid this error is open a dialog and run a function that creates one file at a time. I write the number of remaining files in a cell in the sheet, update it with every run. I use the success and failure handlers to decide whether to continue with file generation or close the dialog. You can generate files for hours in this way.

Drive.Files.Update - Uploading a new revision to maintain doc ID error

I'm trying to upload a new revision to an existing file (original file) and maintain the ID for that file.
I have the Google Drive API (v2) enabled in the Advanced Google Services. I have the script as a standalone script (not attached to a google doc or spreadsheet based on something I read that said the Drive.Files.Update doesn't work for those scripts).
On this line of code:
Drive.Files.update({
title: currentFile.getName(), mimeType: currentFile.getMimeType()
}, originalFileID, blobOfNewContent);
I get the following error:
Execution failed: We're sorry, a server error occurred. Please wait a bit and try again. (line 13, file "Code") [8.215 seconds total runtime]
Any clues as to what else I can try? Basically, I need to maintain the ID of the file and just update the contents every so often from other file.
function overwriteFile(blobOfNewContent, originalFileID)
{
var currentFile;
currentFile = DriveApp.getFileById(originalFileID);
if (originalFileID != "")
{//If original File exsts
Drive.Files.update({
title: currentFile.getName(), mimeType: currentFile.getMimeType()
}, originalFileID, blobOfNewContent);
}
}
function getNewestSlidesInFolderAndUploadToStaticFileID()
{
// fileID of file that is "Original File"
var mainfileID = "some id";
var folders = DriveApp.getFoldersByName('Testing Slides');
var arryFileDates = [];
var objFilesByDate = {};
//find most recent file
while (folders.hasNext())
{
var folder = folders.next();
var files = folder.getFilesByType("application/vnd.google-apps.presentation");
while (files.hasNext())
{
var file = files.next();
// Make sure the fileID is not the static file
if (file.getId() != mainfileID)
{
var fileDate = file.getLastUpdated();
objFilesByDate[fileDate] = file.getId(); //Create an object of file names by file ID
arryFileDates.push(file.getLastUpdated());
} // end if id = mainfileID
} //end while (files.hasNext())
arryFileDates.sort(function(a, b) { return b - a});
var newestDate = arryFileDates[0];
Logger.log('Newest date is: ' + newestDate);
var newestFileID = objFilesByDate[newestDate];
Logger.log('newestFile: ' + newestFileID);
//return newestFile;
}//while folders.hasNext()
//Get the contents of the most recent file
var newestFile = DriveApp.getFileById(newestFileID);
var blob = newestFile.getBlob();
overwriteFile(blob, mainfileID);
//delete most recent file so that only the main file exists
newestFile.setTrashed(true);
}
I did find all this code here on stackoverflow and I just modified (hardcoded) some pieces for my needs. Thanks to the original coders for their assistance.

Google Script - How to use unzip

I am downloading a .zip from a website. It contains one .txt file. I would like to access the data in the txt and write it to a spreadsheet. I'm open to either accessing it directly and not extracting the zip OR extracting the zip, saving the txt to a Google Drive Folder, and accessing it once it is saved.
When I use Utilities.unzip(), I can never get it to unzip the file and usually end up with an "Invalid argument" error. In the code below, the last section before else contains the unzip command. It successfully saves the file to the correct Google Folder but then I can't extract it.
function myFunction() {
// define where to gather data from
var url = '<insert url here>';
var filename = "ReportUploadTesting05.zip";
var response = UrlFetchApp.fetch(url, {
// muteHttpExceptions: true,
// validateHttpsCertificates: false,
followRedirects: true // Default is true anyway.
});
// get spreadsheet for follow up info
var Sp = SpreadsheetApp.getActiveSpreadsheet();
if (response.getResponseCode() === 200) {
// get folder details of spreadsheet for saving future files
var folderURL = getParentFolder(Sp);
var folderID = getIdFromUrl(folderURL);
var folder = DriveApp.getFolderById(folderID);
// save zip file
var blob = response.getBlob();
var file = folder.createFile(blob);
file.setName(filename);
file.setDescription("Downloaded from " + url);
var fileID = file.getId();
Logger.log(fileID);
Logger.log(blob)
// extract zip (not working)
file.setContent('application/zip')
var fileUnzippedBlob = Utilities.unzip(file); // invalid argument error occurs here
var filename = 'unzipped file'
var fileUnzipped = folder.createFile(fileUnzippedBlob)
fileUnzipped.setName(filename)
}
else {
Logger.log(response.getResponseCode());
}
}
I've followed the instructions on the Utilities page. I can get their exact example to work. I've tried creating a .zip on my computer, uploading it to Google Drive and attempted to open it unsuccessfully. Obviously there are some subtleties of using the unzip that I'm missing.
Could you help me understand this?
I was running into the same "Invalid arguments" error in my testing, so instead of using:
file.setContent('application/zip')
I used:
file.setContentTypeFromExtension();
And, that solved the problem for me. Also, as #tukusejssirs mentioned, a zip file can contain multiple files, so unzip() returns an array of blobs (as documented here). That means you either need to loop through the files, or if you know you only have one, explicitly reference it's position in the array, like this:
var fileUnzipped = folder.createFile(fileUnzippedBlob[0])
Here's my entire script, which covers both of these issues:
/**
* Fetches a zip file from a URL, unzips it, then uploads a new file to the user's Drive.
*/
function uploadFile() {
var url = '<url goes here>';
var zip = UrlFetchApp.fetch('url').getBlob();
zip.setContentTypeFromExtension();
var unzippedFile = Utilities.unzip(zip);
var filename = unzippedFile[0].getName();
var contentType = unzippedFile[0].getContentType();
var csv = unzippedFile[0];
var file = {
title: filename,
mimeType: contentType
};
file = Drive.Files.insert(file, csv);
Logger.log('ID: %s, File size (bytes): %s', file.id, file.fileSize);
var fileId = file.id;
// Move the file to a specific folder within Drive (Link: https://drive.google.com/drive/folders/<folderId>)
var folderId = '<folderId>';
var folder = DriveApp.getFolderById(folderId);
var driveFile = DriveApp.getFileById(fileId);
folder.addFile(driveFile);
}
I think the answer to your question may be found here. Is there a size limit to a blob for Utilities.unzip(blob) in Google Apps Script?
If the download is over 100 mb the full file cannot be downloaded. Due to that it will not be in the proper zip format. Throwing the cannot unzip file error.
I believe that the creation of the blob from a file (in this case the .zip file) requires the .next(); otherwise it did not work for me.
Also note that the .zip file might contain more than one file, therefore I included a for cycle.
Anyway, my working/tested solution/script is the following:
function unzip(folderName, fileZipName){
// Variables
// var folderName = "folder_name";
// var fileZipName = "file_name.zip";
var folderId = getFolderId(folderName);
var folder = DriveApp.getFolderById(folderId);
var fileZip = folder.getFilesByName(fileZipName);
var fileExtractedBlob, fileZipBlob, i;
// Decompression
fileZipBlob = fileZip.next().getBlob();
fileZipBlob.setContentType("application/zip");
fileExtractedBlob = Utilities.unzip(fileZipBlob);
for (i=0; i < fileExtractedBlob.length; i++){
folder.createFile(fileExtractedBlob[i]);
}
}

How To Download / Export Sheets In Spreadheet Via Google Apps Script

The task is to automate the manual process accomplished by the menu option "File | Download As | Plain Text"
I want to be able to control the saved file name, which cannot be done via the menu.
At the time this is invoked, the user would be sitting on the sheet in the spreadsheet. Ultimately, I'd make it a menu option, but for testing I'm just creating a function that I can run manually.
After reading several other threads for possible techniques, this is what I've come up with.
It builds a custom name for the file, makes the call, and the response code is 200.
Ideally, I'd like to avoid the open / save dialog. In other words, just save the file without additional user intervention. I'd want to save in a specific folder and I've tried it with a complete file spec, but the result is the same.
If I copy the URL displayed in the Logger and paste it into a browser, it initiates the open / save dialog, so that string works.
Here's the code as a function.
function testExportSheet() {
var ss = SpreadsheetApp.getActiveSpreadsheet();
var oSheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
var sId = ss.getId();
var ssID=sId + "&gid=" + oSheet.getSheetId();
var url = "https://spreadsheets.google.com/feeds/download/spreadsheets/Export?key="
+ ssID + "&exportFormat=tsv";
Logger.log(url);
var fn = ss.getName() + "-" + oSheet.getSheetName() + ".csv";
var sHeaders = {"Content-Disposition" : "attachment; filename=\"" + fn + "\""};
var sOptions = {"contentType" : "text/html", "headers" : sHeaders};
Logger.log(sOptions);
x = UrlFetchApp.fetch(url, sOptions)
Logger.log(x.getResponseCode());
}
I have exported a spreadsheet as CSV directly into a local hard drive as follows:
Get the CSV content from current sheet using a variation of function convertRangeToCsvFile_() from the tutorial on this page https://developers.google.com/apps-script/articles/docslist_tutorial#section3
var csvFile = convertRangeToCsvFile_(...);
Then select a drive folder that is syncing to a local computer using Drive
var localFolder = DocsList.getFolderById("055G...GM");
And finally save the CSV file into the "local" folder
localFolder.createFile("sample.csv", csvFile);
That's it.
This app script returns a file for download instead of web page to display:
function doGet(){
var outputDocument = DocumentApp.create('My custom csv file name');
var content = getCsv();
var textContent = ContentService.createTextOutput(content);
textContent.setMimeType(ContentService.MimeType.CSV);
textContent.downloadAsFile("4NocniMaraton.csv");
return textContent;
}
In case you are looking to export all of the sheets in s spreadsheet to csv without having to manually do it one by one, here's another thread about it:
Using the google drive API to download a spreadsheet in csv format
The download can be done. But not the "Write to the hard drive" of the computer.
Write issue:
You mean write a file to the hard drive of the computer, using Google Apps Script? Sorry, but you will need more than GAS to do this. For security reasons, I doubt this is possible with only GAS, have never seen anything like this in GAS.
Google Drive API will let you do a download, just needs OAuth and the URL you gave.
This is the function that I found when looking up the same question. This function was linked to by #Mogsdad and the link no longer exists.
function convertRangeToCsvFile_(csvFileName) {
// Get the selected range in the spreadsheet
var ws = SpreadsheetApp.getActiveSpreadsheet().getActiveSelection();
try {
var data = ws.getValues();
var csvFile = undefined;
// Loop through the data in the range and build a string with the CSV data
if (data.length > 1) {
var csv = "";
for (var row = 0; row < data.length; row++) {
for (var col = 0; col < data[row].length; col++) {
if (data[row][col].toString().indexOf(",") != -1) {
data[row][col] = "\"" + data[row][col] + "\"";
}
}
// Join each row's columns
// Add a carriage return to end of each row, except for the last one
if (row < data.length-1) {
csv += data[row].join(",") + "\r\n";
}
else {
csv += data[row];
}
}
csvFile = csv;
}
return csvFile;
}
catch(err) {
Logger.log(err);
Browser.msgBox(err);
}
}
I found it here and here