How to move a folder in Google Drive using Google Apps Script - javascript

EDIT
I have logged an issue with Google's Issue Tracker about the Folder.moveTo(destination) function not working, as it appears to be a bug. Feel free to star the issue to give it a bump!
https://issuetracker.google.com/issues/163080678
Background
I am trying to provide a "one click" way of moving folders in Google Drive via an add-on (which appears in both Drive and Gmail). For context, each "folder" in this case is a matter, and the "one click" will move the folder (containing all the relevant documents to that matter) from the "open" folder to the "closed" folder. However, I am getting confusing and unhelpful errors every way I try to achieve this.
Note: all of these folders are in a Shared Team Drive. I'm aware that this places some limitations on what the DriveApp Service and Drive API can do, but I'm crossing all my fingers and toes that it's not just an inherent limitation and that there is a workaround!
I have already read the following:
How can I use Google Apps Script to move files between personal Google Drives and Team Drives?.
Google Script Move Folders
Move a Google Drive folder to a new location using GAS
https://tanaikech.github.io/2019/11/20/moving-file-to-specific-folder-using-google-apps-script/
https://developers.google.com/apps-script/reference/drive/folder#movetodestination
https://developers.google.com/drive/api/v2/reference/files/update
https://developers.google.com/drive/api/v2/reference/parents/insert
But they did not solve my problem (noting that addFile(), addFolder(), removeFile() and removeFolder() have been deprecated recently and so are no longer available as the answers above would suggest).
Expected Result
My expectation is that when the user hits the "Close" button in the add-on, it will move the relevant folder (selected at a previous stage in the add-on) from the "open" folder, to the "closed" folder (which sit next to each other at the same level in the folder hierarchy). To be clear, I'm hoping that it will move the folder, not duplicate it in the target folder and then delete the original from the original parent folder.
Actual Result
See specific error messages below, but generally speaking, when the user clicks the "Close" button, I get an error right away, and nothing happens to the folder.
Attempt 1 - Folder.moveTo(destination)
Taking the Google documentation at face-value, I first attempted to use Folder.moveTo(destination) as below:
function moveFolder() {
var folderToMove = DriveApp.getFolderById(MOVE_FOLDER_ID);
var destinationFolder = DriveApp.getFolderById(DESTINATION_FOLDER_ID);
folderToMove.moveTo(destinationFolder);
}
However, on running this, I get the error message: Exception: Unexpected error while getting the method or property moveTo on object DriveApp.Folder.
Attempt 2 - Drive.Files.update()
Failing to fix the above, I tried the Advanced Drive feature of Google Apps Script, and attempted to use Drive.Files.update() (documentation here). My code looks like this:
function moveFolder() {
Drive.Files.update({parents: [{id: DESTINATION_FOLDER_ID}]}, MOVE_FOLDER_ID);
}
I also tried a variation including the optional paramter supportsAllDrives, as below:
function moveFolder() {
Drive.Files.update({supportsAllDrives: true, parents: [{id: DESTINATION_FOLDER_ID}]}, MOVE_FOLDER_ID);
}
However, both variations give the error: GoogleJsonResponseException: API call to drive.files.update failed with error: File not found: [fileId], where [fileId] is the actual Google Drive ID of the folder being moved.
I have confirmed in both attempts 1 and 2, by using various other functions I know do work (like adding customer properties with Drive.Properties and renaming the folder using DriveApp), that the folders themselves are definitely being passed through correctly and are able to be manipulated programmatically. Ie, the folder definitely exists, and using the URL in the browser, can definitely be found using that Drive ID.

Rather than moving a folder directly you could a) create an "equivalent" in the new location in your shared drive, b) move its files to the new folder from a using moveTo() and c) delete the original folder after completely migrated.
This will of course change the id of your folders, but it works for shared drives and also with folders from MyDrive that should be migrated to a shared drive
Please note this makes use of Advanced Drive Service v2 (enable it in Apps Script):
// function to copy a folder e.g. to a shared drive
function copyFolderToSharedDrive(folderTitle,folderId,targetParentId) {
var optionalArgs={supportsAllDrives: true};
var resource = {
title: folderTitle + '_copy', // adjust according to your needs
mimeType: 'application/vnd.google-apps.folder',
parents:[{
"id": targetParentId, // some folder in a shared drive
}],
description: folderId // optional, to back reference the source Folder
}
var migratedFolder = Drive.Files.insert(resource, null, optionalArgs);
Logger.log(migratedFolder.id);
// Optional: post-process original folder, e.g. trash it
// ...
}
Drawback when moving from My Drive until Single Parent Model has been enforced in My Drive: You need to check for multiple 'addings' of your subfolders so as to avoid creating duplicate folders.

Using DriveApp Methods only, you can also migrate a folder by
creating the folder in shared drive
move files to the created folders
recurse for any sub-folders
trash original folder that is moved.
hint: If multi-parenting applies to the subfolders within the folder that is migrated, you need to include a check in the code above in order to avoid creating duplicate folders.
// execute this function
function run_me() {
var folderId = 'ID_OF_FOLDER_TO_BE_MIGRATED'; // some folder (located in My Drive or Shared Drive)
var targetParentId = 'ID_OF_TARGET_PARENT_FOLDER'; // a folder in a Shared Drive
var folder = DriveApp.getFolderById(folderId);
var parent = DriveApp.getFolderById(targetParentId);
migrateFolderToSharedDrive(folder,parent); // copy folders and move files
folder.setTrashed(true); // trash original folder
}
// main function (recursive)
function migrateFolderToSharedDrive(folder,parent) {
var copiedFolder = parent.createFolder(folder.getName()).setDescription('previous Id: '+folder.getId());
Logger.log('Folder created with id %s in parent %s',copiedFolder.getId(),parent.getId());
// move files
var files = folder.getFiles();
while (files.hasNext()) {
Logger.log('File with id %s moved to %s', files.next().moveTo(copiedFolder).getId(), copiedFolder.getId())
}
// recurse any subfolders
var childfolders = folder.getFolders();
while (childfolders.hasNext()) {
var child = childfolders.next();
Logger.log('processing subfolder %s',child.getId());
migrateFolderToSharedDrive(child,copiedFolder); // recursive call
}
}

Related

How can I get an input folder and its files using Javacript for Automator?

I am writing an automator workflow to work with files and folders. I’m writing it in JavaScript as I’m more familiar with it.
I would like to receive a folder, and get the folder’s name as well as the files inside.
Here is roughly what I have tried:
Window receives current folders in Finder (I’m only interested in the first and only folder)
Get Folder Contents
JavaScript:
function run(input,parameters) {
var files = [];
for(let file of input) files.push(file.toString().replace(/.*\//,''));
// etc
}
This works, but I don’t have the folder name. Using this, I get the full path name of each file, which is why I run it through the replace() method.
If I omit step 2 above, I get the folder, but I don’t know how to access the contents of the folder.
I can fake the folder by getting the first file and stripping off the file name, but I wonder whether there is a more direct approach to getting both the folder and its contents.
I’ve got it working. In case anybody has a similar question:
// Window receives current folders in Finder
var app = Application.currentApplication()
app.includeStandardAdditions = true
function run(input, parameters) {
let directory = input.toString();
var directoryItems = app.listFolder(directory, { invisibles: false })
var files = [];
for(let file of directoryItems) files.push(file.toString().replace(/.*\//,'')) ;
// etc
}
I don’t include the Get Folder Contents step, but iterate through the folder using app.listFolder() instead. The replace() method is to trim off everything up to the last slash, giving the file’s base name.

How to move PDF files from a parent folder to a subfolder which has the same name as the file

I am working on a project which sends PDFs from a Wordpress website to a Google Drive. The files are PDFs of people who registered for certain events, which are held in certain cities. These PDFs are all sent to a parent folder "registrations". What I need to achieve is to automatically send these PDFs to subfolders like Amsterdam, Rotterdam, Woerden, Westland etc.
The PDFs are all sent to a parent folder "registrations". What I need to achieve is to automatically send these PDFs to subfolders like Amsterdam, Rotterdam. The names of the PDFs are the city names (e.g. Amsterdam).
In conclusion, the question basically is: how could I achieve to send a file with the name Amsterdam to a subfolder with the name Amsterdam, and a file with the name Rotterdam to a folder with the name Rotterdam?
EDIT: I am working on the script, but it is not complete yet. The script below searches for files in a parent folder that contains "woerden" in its name. Next, it moves these files to a folder called "woerden". This works, but the problem is that I will have to create about 300 different scripts to be able to automate the entire process. I am therefore wondering if it is possible to make this script more dynamic. For example, if a file with a name that contains -woerden- it is sent to a folder with the name woerden, and if a file with a name that contains -westland- it will be sent to a folder with the name westland.
function SearchFiles() {
// Parent folder
var parentFolder = DriveApp.getFolderById('15azR1-Fic_F_wPZHzXWKblIyxBB_GVsW'); //parent folder which will be scanned for duplicates
// Log the name of every file in the user's Drive whose name contains "westland".
var files = DriveApp.searchFiles(
'title contains "westland"');
while (files.hasNext()) {
var file = files.next();
// Remove the file from all parent folders
var parents = file.getParents();
while (parents.hasNext()) {
var parent = parents.next();
parent.removeFile(file);
}
DriveApp.getFolderById('1mSW3-uqv_F1m-TfLa_JyGRVK9-sGJYTj').addFile(file);
}
}
I hope that I asked my question clearly. Please let me know if you need some more information.
Cheers,

How to iterate over files in a directory within an extractionless add-on's .xpi file

My add-on, Color Source, needs to iterate files in a directory within my add-on (e.g., the content subdirectory) when the add-on is not unpacked (i.e., is kept zipped as an XPI).
We are using code like this to get the Addon object:
Cu.import("resource://gre/modules/AddonManager.jsm", null)
.AddonManager
.getAddonByID(
"color_source#brett.zamir",
function(addon) {
var uri = addon.getResourceURI();
}
);
In order to pass a path to new OS.File.DirectoryIterator, we have tried:
OS.Path.join(uri.path, 'content'); which apparently works in *nix but not Windows
Getting and then cloning uri.QueryInterface(Ci.nsIFileURL).file, and then calling append('content') on the clone and this works, but only when the add-on has the <em:unpack> directive set to true in install.rdf.
How do we get a path that works even when the add-on is zipped up?
(Note: I have added a Firefox add-on tag to this post because the issue should be similar there)
The .xpi file is a zip file and should be accessed using nsIZipReader.
The following should create a nsIUTF8StringEnumerator over which you can iterate. The contents of the zip file which match a pattern are obtained using findEntries()
Components.utils.import("resource://gre/modules/FileUtils.jsm");
var zipReader = Components.classes["#mozilla.org/libjar/zip-reader;1"]
.createInstance(Components.interfaces.nsIZipReader);
var addonFile = new FileUtils.File(uri.path);
zipReader.open(addonFile);
var contentEnumerator = zipReader.findEntries("content/*");
This should provide you with all the files and directories which are contained within the content directory. However, you are probably only interested in the files which are directly within the content directory:
for(contentFile of contentEnumerator) {
if(/content\/.*\//.test(contentFile) {
continue;
}
//If you get here contentFile should contain only direct descendants of
// the content directory.
}

Is there a way to exclude files not explicitly shared with the user from a script? ( aka files that are Shared with Everyone In Domain With The Link)

I'm trying to write a script that will return the name of every file that has been shared with the active user. However I'm having problems with the .getAccess() method.
EDIT:
So I've been tinkering with the code a little and it turns out that the code is breaking on files that are shared with "Everyone in the domain who has the link". So now the issue is that of avoiding files that are shared with everybody but are still popping up in the user's drive without having been explicitly shared with them.
Is there a way to say:
if (file.getAccess(user) == "VIEW" && file is explicitly shared with user)
or
&& file is not "Shared with everyone in the domain with the link"
OLD QUESTION:
When I use it on a file that was accessed from a specific ID, it works perfectly.
For example:
var id = "1HoJbszb1uLF1RZpU_IPv3O648pIeYs4IWK2kbbQ4IaA";
var file = DriveApp.getFileById(x);
Logger.log(file.getAccess(user));
Log:
EDIT
However, if I try to iterate through every file in a user's drive, I will get an error "No Item with the given ID could be found, or you do not have permission to access it".
I've tried it these two ways:
while (files.hasNext()){
var raw = files.next();
var id = raw.getId();
var file = DriveApp.getFileById(id);
if (file.getAccess(user) == "EDIT" ) Logger.log(file.getAccess(user));
}
and:
while (files.hasNext()){
var file = files.next();
if (file.getAccess(user) == "EDIT" ) Logger.log(file.getAccess(user));
}
I get the same error trying it both ways. I've even checked to make sure that the .hasNext() method is working and to be sure when I ask the log to only list the names of each file it works fine as long as .getAccess() is never called (so no if statement).
while (files.hasNext()){
var file = files.next();
Logger.log(file.getName());
}
any idea what's wrong?
The problem here is that getAccess() method - and the same applies to getSharingAccess() and getSharingPermissions() methods,- is only available to file owner and explicit editors. Files not shared with Can edit permission directly with a user throw the error you see.
There are several work-arounds available, though:
If you need to only get files explicitly shared with the user with "Can edit" permission:
Check getEditors().length for a file - it will be 0 if the user
does not have explicit "Can edit" permission to the file, and >0 if
the user can edit the file.
var f = DriveApp.getFileById(fileId);
if ( f.getEditors().length ) {
// this file has been explicitly shared with the user with "Can edit" permission
}
If you need to get files that the user has "Can edit" permission for, whether those files were shared explicitly with the user or
not:
First, enable Drive API in your script's Advanced Google services
(under menu Resources) AND in Google Developers Console - more
details on enabling advanced services can be found
here.
After that, you can check the user's permission to the file by
checking File.userPermission.role property:
var f = Drive.Files.get(fileId); // requires Drive API in Advanced Google services
var up = f.userPermission;
if (up.role=='writer') {
// 'writer' = user has Edit permission to the file
}
File.userPermission.role will be writer for files user has
"Can edit" permissions to, whichever way the files were shared with the
user (explicitly, via group/domain membership or for public files). Other possible values are reader (user has "Can view" or
"Can comment" permissions to the file) and owner (the user is the owner/creator of the file). More info on userPermission property
here.

Creating zip file only contains pdf

In my script I am trying to create a folder, create a date-stamped-document in said folder, create a sub folder, and copy some documents into that sub folder.
All of this works great. When I try to zip the parent folder via either of the methods found here: Creating a zip file inside google drive with apps script - it creates a zip file with a sole PDF file that has the same name as the date-stamped-document. The zipped PDF is blank, and the subfolder isn't there.
Any insight about why this is happening would be great.
var folder = DocsList.createFolder(folderTitle);
var subFolder = folder.createFolder('Attachments');
subfolder.createFile(attachments[]); //In a loop that creates a file from every
//attachment from messages in thread
var doc = DocumentApp.create(docTitle); //Google Doc
var docLocation = DocsList.getFileById(doc.getId());
docLocation.addToFolder(folder);
docLocation.removeFromFolder(DocsList.getRootFolder());
//Everything works fine, I can view file and subfolder, and subfolder's documents
//This is where the problem is:
var zippedFolder = DocsList.getFolder(folder.getName());
zippedFolder.createFile(Utilities.zip(zippedFolder.getFiles(), 'newFiles.zip'));
//this results in a zipped folder containing one blank pdf that has the same title as doc
The DocsList service has been deprecated so Phil Bozak's previous solution no longer works. However, refer to another SO question for solution that works with DriveApp class.
This is a great question. It does not seem that the zip function has the ability to zip sub folders. The reason that your script doesn't work is because you only select the files from the folder.
A solution would be to zip each of the subfolders and store that in the one zipped file. You can use the function that I wrote for this.
function zipFolder(folder) {
var zipped_folders = [];
var folders = folder.getFolders();
for(var i in folders)
zipped_folders.push(zipFolder(folders[i]));
return Utilities.zip(folder.getFiles().concat(zipped_folders),folder.getName()+".zip");
}
This recursively zips all subfolders. Then you need to create the file with something like
DocsList.getFolder("path/to/folder/to/store/zip/file").createFile(zipFolder(folderToZip));
I will put in a feature request to allow subfolders to be zipped.

Categories

Resources