Objective:
as a google workspace domain admin for a school that uses google workspace education, I want to create a google apps script that given a google workspace user's email address (the current owner), the scritp should be able to get a list of all the user's folders and files in their google drive and then it should also be able to transfer the ownership of those folders and files to domain user and add the current owner as a viewer so they can only see the folders/files but can't modify them in any way.
things I tried:
DriveApp can access files/folders and change the ownership of the file/folder but only if you are the owner, and I want to do this as the domain admin, regardless which user owns the google drive and respective files/folders.
Drive API, seems to do the same as DriveApp as far you're the owner, I couldn't figure out how to give Drive API admin permissions so I can see every domain user google drive file list, if that's even possible.
GAM advance: I found this as management tool, I set it and it migh do what I need but it's bit complex for me, plus I was really hoping to be able to build the tool myself.
What worked halfway:
I found this: https://github.com/googleworkspace/apps-script-oauth2#using-service-accounts which refers to using a service account. It took a while but I manage to get a list of items that exist on a user's google drive with the script below. but I can't figure out how to access those files/folders so I can change the ownership or set viewers on them. I think I read that the service account will only give me read-only access so I'm doubting this is even possible.
Here's what I got so far:
function main(){
// Private key and client email of the service account.
var key = getJsonKey()
var clientEmail = 'service_account_email_setup_in_google_dev_console';
// Email address of the user to impersonate.
var userEmail = 'a_regular_domain_user#my_google_workspace_domain.com';
try{
var drive = getDriveService_(key,userEmail,clientEmail);
if (drive.hasAccess()) {
// this code gets me a json response with items that list id's and urls and other
//file metadata of the
// files that belongs to the domain user, this is as far as i got.
var url = 'https://www.googleapis.com/drive/v2/files';
var response = UrlFetchApp.fetch(url, {
headers: {
Authorization: 'Bearer ' + drive.getAccessToken()
}
});
var result = JSON.parse(response.getContentText());
//the following code returns a fileid in the user's google
//drive not shared with the admin
var fileid = JSON.stringify(result.items[0].id)
Logger.log(fileid);
//but the following code returns an error indicating that the
//file is not found (in reality it's not accessible by the
//admin account)
var file = Drive.Files.get(fileid);
//access a list of items and as I traverse it I'd like to
//change the ownership and
//add the the current user as a file viewer
//??
} else {
Logger.log(drive.getLastError());
}
}catch (e){
Logger.log(e)
}
}
// Load the JSON key file with private key for service account
function getJsonKey(){
var keyFile = DriveApp.getFileById("json_fileid_in_drive_obtained_from_googledevcons");
var key = JSON.parse(keyFile.getBlob().getDataAsString()).private_key;
return key
}
function reset() {
getDriveService__().reset();
}
//get the google drive from the domain user's email address
function getDriveService_(key,userEmail,clientEmail) {
return OAuth2.createService('GoogleDrive:' + userEmail)
.setTokenUrl('https://oauth2.googleapis.com/token')
.setPrivateKey(key)
.setIssuer(clientEmail)
.setSubject(userEmail)
.setPropertyStore(PropertiesService.getUserProperties())
.setCache(CacheService.getUserCache())
.setScope('https://www.googleapis.com/auth/drive');
}
Any help is appreciated :)
You are going in the right direction, the only part you are missing currently is setting up Domain Wide Delegation this will allow you to impersonate the users in your domain so you can make the changes on behalf of them by granting the service account permissions through the above mentioned DWD.
Since you have already created the Oauth2Service you will just need to send the user to impersonate through the OauthParams:
const oauthParams = {
serviceName: 'Nameofyourservice',
serviceAccount,
scopes: ['https://www.googleapis.com/auth/appsmarketplace.license', 'https://www.googleapis.com/auth/userinfo.email', 'https://mail.google.com', 'https://www.googleapis.com/auth/iam'],
userToImpersonate: 'usertoimpersonate#test.com',
};
The scopes were from the Marketplace API as an example.
I am creating a workflow for Shared Drive (Team Drive) where I have 3 folders under team drive:
TO BE APPROVED
APPROVED
REJECTED
I am sending a document from TO BE APPROVED folder for approval, if user approves it then this document should move to APPROVED folder. Same logic for REJECTED.
Now my question is how can I move a document between Shared Drive folders. DriveApp.getFolderById(folderId).addFile() is not working as I can not have more than one parent in Team Drive. DriveApp.getFolderById(folderId).createFile() is working but it is creating a whole new file with new ID which is not fulfilling my purpose of approval workflow as this is a whole new file.
Is there any way to move file or copy/replace any operations which will not change my file's ID? I tried for REST APIs as well but couldn't found any.
Okay looks like I found an answer, via REST API I can update file's parents. I've made that call and it's working.
Here's the sample.
var apiUrl = "https://www.googleapis.com/drive/v3/files/fileId?addParents=newFolderId&removeParents=oldFolderId&supportsTeamDrives=true";
var token = ScriptApp.getOAuthToken();
var header = {"Authorization":"Bearer " + token};
var options = {
"method":"PATCH",
"headers": header
};
var res = UrlFetchApp.fetch(apiUrl, options);
UPDATE
Using Advance Services API we can achieve the same, here's the answer I've received from Aliaksei Ivaneichyk
function moveFileToFolder(fileId, newFolderId) {
var file = Drive.Files.get(fileId, {supportsTeamDrives: true});
Drive.Files.patch(file, fileId, {
removeParents: file.parents.map(function(f) { return f.id; }),
addParents: [newFolderId],
supportsTeamDrives: true
});
}
Here you need to Enable Drive SDK advance services if you are using Appscript. In case of Appmaker Add Drive SDK as Service in Settings Option.
I'm using Google Apps Script and V2 of the Drive API (I don't think V3 is available in scripts yet) to automate file creation inside of a Team Drive. I'd like to add editors with the script with no success.
I can access the Team Drive and child folders using the FolderIterator in the standard DriveApp methods.
Attempt 1
function addUserToTeam(email, folders) {
// Open the team drive and get all the folders
var teamFolders = DriveApp.getFolderById('TEAMDRIVEIDSTRING').getFolders();
var folders = ["folderIdToMatch"] // This may hold multiple folders
try {
// Loop an array of folder IDs
for(var i=0; i<folders.length; i++) {
// Check the team drive folders for a matching name
while(teamFolders.hasNext()) {
var teamFolder = teamFolders.next();
if(folders[i] == teamFolder.getId()) {
teamFolder.addEditor(email);
}
}
}
} catch(e) {
Logger.log(e);
}
}
This failed with Exception: Cannot use this operation on a Team Drive item.
Attempt 2
I tried the Drive API by substituting teamFolder.addEditor(email) a Permissions resource:
if(folders[i] == teamFolder.getId()) {
var resource = {
"type":"user",
"role":"writer",
"value": email
}
Drive.Permissions.insert(resource, teamFolder.getId());
}
This fails with a File not found error.
I can find the folder (or file) with DriveApp methods. Any attempt at the same with the Drive API fails.
I cannot find any documentation saying Team Drive files are inaccessible with the API. Is there something wrong with my approach?
After more research and digging, here's the solution for people with similar use cases.
You can access files to an entire Team Drive or to files inside the Drive, but not folders. This is done on purpose to prevent accidentally giving access to directories of sensitive information to people who shouldn't have access.
To give access, supportsTeamDrives is an optional argument in the request body that takes a boolean value. Set this to true and pass in the API call. A successful function is below.
The only way to achieve the outcome I described is to use multiple Team Drives and give access to users based on some event. Another option would be to promote a user to Full permissions (from edit or view) for the duration of the project and then revoke when completed.
Add a user to a Team Drive
(This also works for single files in a Drive)
// Using Google Apps Script with v2 of the Drive API enabled
function addToTeamDrive() {
var resource = {
'value': emailString,
'type': 'user',
'role': 'writer'
}
// If you have several Team Drives, loop through and give access
try {
var TeamDrive = Drive.Teamdrives.list();
for(var i = 0; i < TeamDrive.items.length; i++) {
if(TeamDrive.items[i].name === "Team Drive String") {
// This ID may also be a single file inside a Team Drive
var id = TeamDrive.items[i].id;
}
}
// Add user permissions to the matched Drive
Drive.Permissions.insert(resource, id, {"supportsTeamDrives": true});
} catch(e) {
Logger.log(e);
}
}
You can access but there's alot you can't do like removeFile() or getUrl() even when you have full access. You will still get the
{error: "Exception: Cannot use this operation on a Team Drive item."}
Workaround is to use setTrashed() instead of removeFile() on files/folders.
How do you make an app script which attaches a spreadsheet as an excel file and emails it to a certain email address?
There are some older posts on Stackoverflow on how to do this however they seem to be outdated now and do not seem to work.
Thank you.
It looks like #Christiaan Westerbeek's answer is spot on but its been a year now since his post and I think there needs to be a bit of a modification in the script he has given above.
var url = file.exportLinks[MimeType.MICROSOFT_EXCEL];
There is something wrong with this line of code, maybe that exportLinks has now depreciated. When I executed his code it gave an error to the following effect:
TypeError: Cannot read property "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" from undefined.
The workaround is as follows:
The URL in the above line of code is basically the "download as xlsx" URL that can be used to directly download the spreadsheet as an xlsx file that you get from File> Download as > Microsoft Excel (.xlsx)
This is the format:
https://docs.google.com/spreadsheets/d/<<<ID>>>/export?format=xlsx&id=<<<ID>>>
where <<>> should be replaced by the ID of your file.
Check here to easily understand how to extract the ID from the URL of your google sheet.
Here's an up-to-date and working version. One prerequisite for this Google Apps script to work is that the Drive API v2 Advanced Google Service must be enabled. Enable it in your Google Apps script via Resources -> Advanced Google Services... -> Drive API v2 -> on. Then, that window will tell you that you must also enabled this service in the Google Developers Console. Follow the link and enable the service there too! When you're done, just use this script.
/**
* Thanks to a few answers that helped me build this script
* Explaining the Advanced Drive Service must be enabled: http://stackoverflow.com/a/27281729/1385429
* Explaining how to convert to a blob: http://ctrlq.org/code/20009-convert-google-documents
* Explaining how to convert to zip and to send the email: http://ctrlq.org/code/19869-email-google-spreadsheets-pdf
* New way to set the url to download from by #tera
*/
function emailAsExcel(config) {
if (!config || !config.to || !config.subject || !config.body) {
throw new Error('Configure "to", "subject" and "body" in an object as the first parameter');
}
var spreadsheet = SpreadsheetApp.getActiveSpreadsheet();
var spreadsheetId = spreadsheet.getId()
var file = Drive.Files.get(spreadsheetId);
var url = 'https://docs.google.com/spreadsheets/d/'+spreadsheetId+'/export?format=xlsx';
var token = ScriptApp.getOAuthToken();
var response = UrlFetchApp.fetch(url, {
headers: {
'Authorization': 'Bearer ' + token
}
});
var fileName = (config.fileName || spreadsheet.getName()) + '.xlsx';
var blobs = [response.getBlob().setName(fileName)];
if (config.zip) {
blobs = [Utilities.zip(blobs).setName(fileName + '.zip')];
}
GmailApp.sendEmail(
config.to,
config.subject,
config.body,
{
attachments: blobs
}
);
}
Update: I updated the way to set the url to download from. Doing it through the file.exportLinks collection is not working anymore. Thanks to #tera for pointing that out in his answer.
There are four ways to create a new file:
DocsList - Shown as DocsList in the Main List. Built in to Apps Script.
DriveApp - Shown as Drive in the Main List. Built in to Apps Script.
Drive API - Also shown as Drive in the Main List. Must be added to Apps Script.
DocumentApp - Shown as Document in the Main List. Built in, but only creates a document file.
They are all called services. Drive API is called an advanced service. So, which one should you use? I don't know, it depends. This question is about the Drive API Advanced Service.
I don't want to use 2 or 3 of the services to get the job done. I'd like to just use one of them. But to decide which one to use, I need to know the capabilities and options of all of them. If the simplest and easiest one to use will do everything I want, then I'll use that.
If I can create a new file with Drive API, but then I need to use the DriveApp service to move the file I created with Drive API, to the folder I want it in, then using Drive API in that particular situation is pointless.
I can create a new file in my Google Drive from a Google Apps Script .gs code, but the file gets written to the main 'My Drive'. I want to write the file directly to a sub-folder. My current code is:
var fileNameSetA = 'someFile.jpg';
var uploadedBlobA = an image uploaded with a file picker;
var fileTestDrive = {
title: fileNameSetA,
mimeType: 'image/jpeg'
};
fileTestDrive = Drive.Files.insert(fileTestDrive, uploadedBlobA);
Even though the code works, I have no idea why the syntax is the way it is, and I can't find documentation that tells me why. I can find a list of properties:
The title: and mimeType: are Optional Properties as part of the Request Body. From the example, the Optional Properties are obviously put in a key:value paired object. So, is the syntax:
Drive.Files.insert(optional properties, content);
There are also Required query parameters of:
uploadType --> media, multipart, resumable
But I don't see any required uploadType parameter designated anywhere in the example code. So, I don't understand Google's documentation.
Google Documentation Insert
Is it possible to write directly to a specific drive with Google Advanced Drive service in a Apps Script .gs code file? How do I do it?
The easiest way to create a new file is to use DriveApp which comes with pure Google Apps Script:
var dir = DriveApp.getFolderById("{dir_id}");
var file = dir.createFile(name, content);
If you do not know exact directory's id you can get the folder by its name:
var dir = DriveApp.getFoldersByName(name).next();
The next() is there because getFoldersByName() returns collection of all directories whose names match given value.
Also check DriveApp docs: https://developers.google.com/apps-script/reference/drive/drive-app
Maybe this is a bit late, but by looking at the REST API docs, it shows that you can use Drive.Files.insert to insert into any folder. You simply have to add the folder's ID in the properties of the file you are inserting as such:
var file = {
title: 'myFile',
"parents": [{'id':folder.getId()}], //<--By setting this parent ID to the folder's ID, it creates this file in the correct folder.
mimeType: 'image/png'
};
Folder ID can be obtained from the shareable link using the Google Drive GUI or as shown here. (e.g. Use the Execute function on the right.)
Alternatively, you can access the folder by name by replacing the folder.getID() with Drive.getFoldersByName('name of folder').
This is helpful because Drive.Files.insert() accepts arguments while Drive.createFile() and Drive.createFolder() do not.
The documentation for INSERT for the Drive API is found at this link:
Drive API for INSERT
There is a section for Request body. One of the Optional Properties for Insert is parents[]. The brackets [] indicate that a list of parents can be designated. The documentation for parents[] states this:
Collection of parent folders which contain this file. Setting this
field will put the file in all of the provided folders. On insert, if
no folders are provided, the file will be placed in the default root
folder.
So, . . . using Insert in Drive API, . . . . CAN write a new file directly to a subfolder. It's possible.
Now, the nomenclature and syntax for the Google Drive SDK, HTTP request is different than what is inside of Apps Script.
The syntax for invoking the Drive API HTTP Request inside of a .gs file is one of the following three:
Drive.Files.insert(FILE resource)
Drive.Files.insert(FILE resource, BLOB mediaData)
Drive.Files.insert(FILE resource, BLOB mediaData, OBJECT optionalArgs)
The syntax shown in the list above is from the auto-complete drop down list inside the Apps Script code editor. If you type Drive.Files. a list of possible methods will appear. I can't find information about the syntax anywhere in the online documentation.
So, where does the parents[] optional property go? Well, it's not a Blob, so we can rule that out. It's either FILE resource, or OBJECT optionalArgs. optionalArgs indicates that it's an object, but FILE resource is actually also an object.
In the examples, the FILE resource is constructed as key:value pair object.
Uploading Files - Advanced Drive Service - Google Documentation
Direct Answer to Question
This summary from https://developers.google.com/apps-script/advanced/drive sums things up pretty well:
The advanced Drive service allows you to use the Google Drive web API
in Apps Script. Much like Apps Script's built-in Drive service, this
API allows scripts to create, find, and modify files and folders in
Google Drive. In most cases, the built-in service is easier to
use, but this advanced service provides a few extra features,
including access to custom file properties as well as revisions for
files and folders.
Like all advanced services in Apps Script, the advanced Drive
service uses the same objects, methods, and parameters as the public
API.
Essentially DriveApp is easier to use than Drive, but Drive gives you more functionality since it shares the same functionality of the public API. I was not able to see how to save a file to a Shared/Team drive using DriveApp, so I ended up using Drive. The pain came around lack of documentation for the Google Apps Script implementation of Drive.
Explanation of My Solution and Code Sample:
A specific implementation of saving a file to Google drive, but this will likely be useful for others. It took me a whole day to figure this out since the documentation and code examples for Google Apps scripts is severely lacking. My use case was for saving a JSON file to a shared Google Drive (Team Drive).
There are three parameters that I did not have at first and my files were not uploading. I am not sure if all are necessary. One was the "kind": "drive#parentReference" part of the parents metadata. The next was "teamDriveId": teamDriveId which is also in the metadata. The last parameter was "supportsAllDrives": true which I passed in the optional parameter location of Drive.Files.insert().
I found the API explorer on https://developers.google.com/drive/api/v2/reference/files/insert to be very useful in figuring out which parameters were needed and how they needed to be formatted. I basically edited values in the explorer till I got a network request that worked. I then pulled the parameters I used into my Google Apps script.
/**
* Creates a JSON file in the designated Google Drive location
* #param {String} jsonString - A JS string from the result of a JSON.stringify(jsObject)
* #param {String} filename - The filename. Be sure to include the .json extension
* #param {String} folderId - The ID of the Google Drive folder where the file will be created
* #param {String} teamDriveId - The ID of the team drive
* #return {void}
*/
function createJSONFileInDriveFolder(jsonString, filename, folderId, teamDriveId) {
var metadata = {
"title": filename,
"mimeType": "application/json",
"parents": [
{
"id": folderId,
"kind": "drive#parentReference"
}
],
"teamDriveId": teamDriveId
};
var optionalParams = {
"supportsAllDrives": true
};
try {
var jsonBlob = Utilities.newBlob(jsonString, 'application/vnd.google-apps.script+json');
Drive.Files.insert(metadata, jsonBlob, optionalParams);
} catch (error) {
Logger.log(error);
}
}
var searchthreads = GmailApp.search('in:inbox AND after:2020/11/30 AND has:attachment');//"in:all -in:trash category:social older_than:15d
Logger.log("GMAIL thread 0:"+ searchthreads[0].getId());
Logger.log("GMAIL thread 1:"+ searchthreads[1].getId());
Logger.log("GMAIL thread 2:"+ searchthreads[2].getId());
Logger.log("Active User: " + me);
Logger.log("Search Thread: " + searchthreads.length);
Logger.log("Gmail lenght" + gmailthread.length);
//Logger.log("Gmail lenght" + gmailMessages.length);
for (var i in searchthreads){
var messageCOunt = searchthreads[i].getMessageCount();
Logger.log("messageCOunt :" + messageCOunt);
var messages = searchthreads[i].getMessages();
for (var m in messages){
var messagesender = messages[m].getFrom();
var messageDate = messages[m].getDate();
var messageReplyTo = messages[m].getReplyTo();
var messagesubject = messages[m].getSubject();
var messagebody = messages[m].getSubject();
var messagephoneNo = messages[m].getSubject();
//messages[m].isInInbox();
var messageid = messages[m].getId();
var messageplainbody = messages[m].getSubject();//messages[0].getPlainBody();
var EmailStatus ='N';
var ApptStatus = "CVReceived";// Tracking till candidate offer and payout
var messageattachement = messages[m].getAttachments();
//var png=UrlFetchApp.fetch(messageattachement).getBlob();
//https://drive.google.com/drive/folders/1RY4i6FwUvfy5OxrJ1pZTxJAOxjFFXbhz?usp=sharing
var folder = DriveApp.getFolderById("1RY4i6FwUvfy5OxrJ1pZTxJAOxjFFXbhz");
// DriveApp.getFolderById("1RY4i6FwUvfy5OxrJ1pZTxJAOxjFFXbhz").createFile(png);
//DriveApp.createFile();
for (var k in messageattachement){
var filename = messageattachement[k].getName();
var filesize = messageattachement[k].getSize();
var filecontent = messageattachement[k].getContentType();
var fileBlob = messageattachement[k].getAs(filecontent);
var filecpblob = messageattachement[k].copyBlob();
//folder.createFile(filename, messageattachement);
var file = {
title: filename,
"parents": [{'id':folder.getId()}],
mimeType: filecontent
};
file = Drive.Files.insert(file, filecpblob);
//DataStudioApp
Logger.log('ID: %s, File size (bytes): %s', file.id, file.fileSize);
//folder.createFile(filecpblob);
}
var processeddate = new Date();
I know it's been a while since this question was posted. But here is the solution to help other readers. When using Drive.Files.insert() method, in order to specify a location for the inserted file, you must specify the parents[] property within the FILE resource. So expanding on #Alan Wells response here is the syntax for writing a blob as Goggle Spreadsheet format in a specific folder.
let newFile = {title: 'Title goes here', parents: [{id: targetFolderId}]};
let savedFile = Drive.Files.insert(
newFile,
blobGoesHere,
{mimeType: MimeType.GOOGLE_SHEETS, convert: true});
Please note that parents: takes an array of objects. You can specify multiple locations for a single file (it will be created in all the specified folders). Even if you want to use a single location you still have to provide this object in a list.
I was able to use the DriveApp to create a file in a specified folder this way.
var driveFolder = DriveApp.getFolderByName("MyDriveFolder");
var file = driveFolder.createFile(formObject.txtReceipt);
file.setName("MyFile");
PS: formObject.txtReceipt is coming from a file upload control on a form in the html and this returns a blob