-
Notifications
You must be signed in to change notification settings - Fork 11
/
Copy pathChangeOwnership,gs
375 lines (313 loc) · 14.8 KB
/
ChangeOwnership,gs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
// This file is part of the Minnesota Population Center's gas-change-ownership.
// For copyright and licensing information, see the NOTICE and LICENSE files
// in this project's top-level directory, and also on-line at:
// https://github.com/mnpopcenter/gas-change-ownership
// This script will traverse a folder hierarchy and make it appear as if the executing user grabbed ownership of anything in the tree that the
// script runner didn't own previously.
// It cannot directly take ownership of objects from another owner (only an owner can reassign ownership) so instead, the script
// accomplishes this by making a copy of each non-owned object, replicating all sharing permissions to the new object (except the old owner
// is optionally now an editor instead), and moves the old object aside to an archive. In the case of folders, it also moves all the folder's
// files into the new folder.
// The script first discovers all files in the tree that need adjustment and processes them. Then it goes back and processes folders.
// IMPORTANT!!! This script relies on enabling the Drive API in two places.
// See https://developers.google.com/drive/v3/web/enable-sdk to do it in the script developer console (this one may not be strictly necessary)
// and https://developers.google.com/apps-script/guides/services/advanced to do it in Google Advanced Services. (definitely necessary)
// I don't know why Google has this in two different places, but the Advanced Services version has more functionality than the
// built-in one, and I leverage that functionality. If you get a "Drive is not defined" error, you forgot to enable the Advanced
// Services version of the Drive API.
// ========== START OF CONFIGURATION ==========
// JOB SETTINGS
// top of the folder tree where I want to apply this magic
var parentFolderId = 'FOLDER ID GOES HERE';
// If you want to start from the top (root) folder
//var parentFolder = DriveApp.getRootFolder();
// person's files and folders I want to "steal" back
// will do all files and folders not owned by me if this is blank
var targetUserEmail = "";
//var targetUserEmail = "[email protected]";
// folder id where we want to move the old objects for archiving
var oldObjectFolderId = 'FOLDER ID GOES HERE';
// do we want the previous owner to be kept as an editor?
var retainAccess = 0;
// LOGGING SETTINGS
// a Google Sheet to log activity
var loggingSheetId = 'SPREADSHEET ID GOES HERE';
// 0-based sheet tab number (if you have many scripts logging to one Sheet, this may need to change)
var loggingSheetTabNumber = 0;
// also log messages via Logger.log
var alsoLogger = 1;
// should I be chatty in the log?
var verbose = 0;
// HELPER SETTINGS
// to help track execution time and avoid exceeding max runtime
var milliseconds_RunTime = 1000*350; //Run for 5 minutes 50 seconds (350 seconds) MAX TIME is 6 minutes (360 seconds)
var startTime = new Date();
var cutShort = 0;
// ========== END OF CONFIGURATION ==========
// set up some needed objects
var parentFolder = DriveApp.getFolderById(parentFolderId);
var oldObjectFolder = DriveApp.getFolderById(oldObjectFolderId);
var loggingSheet = SpreadsheetApp.openById(loggingSheetId).getSheets()[loggingSheetTabNumber];
var lastRow = loggingSheet.getLastRow() + 1;
// MAIN FUNCTION, RUN THIS ONE
function runCleanup() {
try {
// this seems like a bad idea - create empty list with globalish scope
// then it gets modified in the recursion - must be a better way?
// seed it with the parent folder, otherwise files in parent folder won't get fixed
var folders = [parentFolder];
getChildFolders(parentFolder, folders);
slog("***** Starting Cleanup Run at location " + parentFolder.getName() + " *****");
if (targetUserEmail) {
slog("Only looking for objects owned by " + targetUserEmail);
}
processAffectedFiles(folders);
// do we still have time left?
if (cutShort) {
slog("!!!!! Terminated Cleanup Run at location " + parentFolder.getName() + " due to max execution time. !!!!!");
return;
}
processAffectedFolders(folders);
if (cutShort) {
slog("!!!!! Terminated Cleanup Run at location " + parentFolder.getName() + " due to max execution time. !!!!!");
} else {
slog("***** Finished Cleanup Run at location " + parentFolder.getName() + " *****");
}
} catch (e) {
slog("CAUGHT EXCEPTION " + e.toString());
}
}
// build the folder tree structure for traversal
function getChildFolders(parent, folders) {
var childFolders = parent.getFolders();
while (childFolders.hasNext()) {
var childFolder = childFolders.next();
if (verbose) {slog("Visiting " + childFolder.getName());}
folders.push(childFolder);
// Recursive call for any sub-folders
getChildFolders(childFolder, folders);
}
}
// first traverse and handle any files not owned by me
function processAffectedFiles(folders) {
// loop over folders, examime files within each, process them if not owned
for (var i = 0; i < folders.length; i++) {
// check if approaching max runtime
var endTime = new Date();
if (endTime.getTime() > startTime.getTime() + milliseconds_RunTime){ cutShort = 1; break; }
var folder = folders[i];
slog("Checking files in folder " + folder.getName());
// any files here that I don't already own?
var files;
if (targetUserEmail) {
files = folder.searchFiles('"' + targetUserEmail + '" in owners');
} else {
files = folder.searchFiles('not "me" in owners');
}
while (files.hasNext()) {
try {
// check if approaching max runtime
var endTime = new Date();
if (endTime.getTime() > startTime.getTime() + milliseconds_RunTime){ cutShort = 1; break; }
var file = files.next();
if (verbose) { Logger.log("Processing file " + file.getName() + " in " + folder.getName()); }
slog(["Affected File", file.getName(), file.getId(), file.getOwner().getEmail()]);
// copy to a new file
// getName() because when some file types are copied they get a "Copy of" prefix otherwise
// Interestingly, only some types do - I think only native Google Drive types
newfile = file.makeCopy(file.getName());
newfileid = newfile.getId();
// start process of copying over sharing settings
// copies of objects will automatically inherit sharing from the container,
// of course, but we have to replicate any sharing that was directly applied to
// original object
// get the viewers and editors that were directly applied to the object
viewers = file.getViewers();
editors = file.getEditors();
// add old owner as editor on new copy, if desired
if (retainAccess) {
editors.push(file.getOwner());
if (verbose) { slog("Added old owner " + file.getOwner() + " as editor."); }
}
// TODO figure out how to handle commenters?
for (var j = 0; j < viewers.length; j++) {
// doing it this way will cause a notification to be sent to user
//newfile.addViewer(viewers[i]);
// do it this way if you don't want notifications
// requires Advanced Services / API enabled
Drive.Permissions.insert(
{
'role': 'reader',
'type': 'user',
'value': viewers[j].getEmail(),
},
newfileid,
{
'sendNotificationEmails': 'false'
});
if (verbose) { slog("Added " + viewers[j].getName() + " as viewer."); }
}
for (var k = 0; k < editors.length; k++) {
// doing it this way will cause a notification to be sent to user
//newfile.addEditor(editors[i]);
// do it this way if you don't want notifications
// requires Advanced Services / API enabled
Drive.Permissions.insert(
{
'role': 'writer',
'type': 'user',
'value': editors[k].getEmail(),
},
newfileid,
{
'sendNotificationEmails': 'false'
});
if (verbose) { slog("Added " + editors[k].getName() + " as editor."); }
}
// move old file out of the way
// as a last-resort back up
oldObjectFolder.addFile(file);
folder.removeFile(file);
// mark the old file as deprecated so users who have bookmarked/starred/currently open don't get fooled
file.setName("[DEPRECATED] " + file.getName() + " [MOVED TO " + newfile.getUrl() + "]" );
// Leaving a comment would be good, and this works, but it sends email notifications and the API has no way to disable that
//Drive.Comments.insert(
// {"content": "A cleanup script moved this file out of the way. The current copy of the document is now in the original location and can be found at " + newfile.getUrl() + "."
// },
// file.getId()
//);
} catch (e) {
slog("CAUGHT EXCEPTION " + e.toString());
}
}
}
}
// now traverse again, handling folders
function processAffectedFolders(folders) {
slog("----- Starting on Folders -----");
// loop over folders, examine subfolders within each, process them if not owned
for (var i = 0; i < folders.length; i++) {
// check if approaching max runtime
var endTime = new Date();
if (endTime.getTime() > startTime.getTime() + milliseconds_RunTime){ cutShort = 1; break; }
var folder = folders[i];
slog("Examining folder " + folder.getName());
// any folders here that I don't already own?
var affectedSubfolders;
if (targetUserEmail) {
affectedSubfolders = folder.searchFolders('"' + targetUserEmail + '" in owners');
} else {
affectedSubfolders = folder.searchFolders('not "me" in owners');
}
while (affectedSubfolders.hasNext()) {
try {
// check if approaching max runtime
var endTime = new Date();
if (endTime.getTime() > startTime.getTime() + milliseconds_RunTime){ cutShort = 1; break; }
var sf = affectedSubfolders.next();
slog(["Affected Subfolder", sf.getName(), sf.getId(), sf.getOwner().getEmail()]);
// locate or setup a new version of the subfolder which I own
// need to check if the folder was already created - this can happen if I
// hit maximum execution time on a previous run
var newsfid;
var newsf;
// double-escape any " in the folder name, once for Apps Script, once for Drive
newfolder = folder.searchFolders('title = "' + sf.getName().replace(/"/g,'\\"') + '" and "me" in owners');
if (!(newfolder.hasNext())) {
// create a new subfolder that I own to move everything into
newsf = folder.createFolder(sf.getName());
newsfid = newsf.getId();
// swap the old folder for the new one in the folders list, otherwise we won't descend into the new one looking for affected subfolders
// this is inefficient but the list shouldn't be enormous
// was pondering just adding this new folder to end of folders list, but I wasn't sure if folders.length in the enclosing loop was dynamic
for (var j = 0; j < folders.length; j++) {
if (folders[j].getId() == sf.getId()) {
folders[j] = newsf;
slog("Replaced old with new in folders list.");
break;
}
}
slog("Created new subfolder I own with the name " + newsf.getName() + " and id " + newsfid + " ." );
} else {
newsf = newfolder.next();
newsfid = newsf.getId();
slog("Found a subfolder I own with the name " + newsf.getName() + " and id " + newsfid + " already here. Using that one." );
}
// setup sharing on newsf
// get the viewers and editors that were directly applied to the object
viewers = sf.getViewers();
editors = sf.getEditors();
// set old owner as editor on new copy, if desired
editors.push(sf.getOwner());
if (verbose) { slog("Added old folder owner " + sf.getOwner().getEmail() + " as editor."); }
for (var j = 0; j < viewers.length; j++) {
// doing it this way will cause a notification to be sent to user
//newfile.addViewer(viewers[i]);
// do it this way if you don't want notifications
// requires Advanced Services / API enabled
Drive.Permissions.insert(
{
'role': 'reader',
'type': 'user',
'value': viewers[j].getEmail(),
},
newsfid,
{
'sendNotificationEmails': 'false'
});
if (verbose) { slog("Added " + viewers[j].getName() + " as viewer."); }
}
for (var j = 0; j < editors.length; j++) {
// doing it this way will cause a notification to be sent to user
//newfile.addEditor(editors[i]);
// do it this way if you don't want notifications
// requires Advanced Services / API enabled
Drive.Permissions.insert(
{
'role': 'writer',
'type': 'user',
'value': editors[j].getEmail(),
},
newsfid,
{
'sendNotificationEmails': 'false'
});
if (verbose) { slog("Added " + editors[j].getName() + " as editor."); }
}
//move over all files and folders from sf to newsf
files = sf.getFiles();
while (files.hasNext()) {
f = files.next();
newsf.addFile(f);
sf.removeFile(f);
}
subfolders = sf.getFolders();
while (subfolders.hasNext()) {
f = subfolders.next();
newsf.addFolder(f);
sf.removeFolder(f);
}
// remove old subfolder
oldObjectFolder.addFolder(sf);
folder.removeFolder(sf);
slog("Removed old folder with the name " + sf.getName() + " owned by " + sf.getOwner().getEmail() + ".");
} catch (e) {
slog("CAUGHT EXCEPTION " + e.toString());
}
}
}
}
// helper function to write messages to Google Sheet
// and optionally to log console
function slog(fields) {
if (typeof fields == 'string') fields = [fields];
var timestamp = "[" + Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "MM/dd/yyyy HH:mm:ss") + "]";
loggingSheet.getRange(lastRow, 1).setValue(timestamp);
for (var i = 2; i < fields.length + 2; i++) {
loggingSheet.getRange(lastRow, i).setValue(fields[i-2]);
}
lastRow++;
if (alsoLogger) {
Logger.log(fields.join("|"));
}
}