From a8f9fb7b81eb35018fe4b71117742c52f7e9f521 Mon Sep 17 00:00:00 2001 From: Ross Scroggs Date: Wed, 7 Jan 2026 16:42:57 -0800 Subject: [PATCH] Updated copy drivefile to limit copying to those files owned by selected users --- src/GamCommands.txt | 7 ++- src/GamUpdate.txt | 9 ++++ src/gam/__init__.py | 97 ++++++++++++++++++++++++++++++---------- src/gam/gamlib/glgapi.py | 4 ++ 4 files changed, 93 insertions(+), 24 deletions(-) diff --git a/src/GamCommands.txt b/src/GamCommands.txt index e475baaa..674ba37a 100644 --- a/src/GamCommands.txt +++ b/src/GamCommands.txt @@ -6848,7 +6848,12 @@ gam copy drivefile [skipids ] [copysubfiles []] [filenamematchpattern ] [filemimetype [not] ] - [copysubfilesownedby any|me|others] + [copysubfilesownedby + any|me|others| + users | + notusers | + regex | + notregex ] [copysubfolders []] [foldernamematchpattern ] [copysubshortcuts []] [shortcutnamematchpattern ] [duplicatefiles overwriteolder|overwriteall|duplicatename|uniquename|skip] diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index ee1bd21c..134fb851 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -1,3 +1,12 @@ +7.31.02 + +Added the following options to `gam copy drivefile` +to limit copying to those files owned by selected users. +* `copysubfilesownedby users ` - Only files owned by users in `` are copied. +* `copysubfilesownedby notusers ` - Only files not owned by users in `` are copied. +* `copysubfilesownedby regex ` - Only files owned by users whose email addresses match `` are copied. +* `copysubfilesownedby notregex ` - Only files owned by users whose email addresses do not match `` are copied. + 7.31.01 Code cleanup for `addcsvdata `. diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 63f07381..e1ee4d4c 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki """ __author__ = 'GAM Team ' -__version__ = '7.31.01' +__version__ = '7.31.02' __license__ = 'Apache License 2.0 (http://www.apache.org/licenses/LICENSE-2.0)' #pylint: disable=wrong-import-position @@ -5307,6 +5307,9 @@ def checkGAPIError(e, softErrors=False, retryOnHttpError=False, mapNotFound=True error = makeErrorDict(http_status, GAPI.FIELD_IN_USE, message) elif status == 'INTERNAL': error = makeErrorDict(http_status, GAPI.INTERNAL_ERROR, message) + elif http_status == 501: + if status == 'UNIMPLEMENTED': + error = makeErrorDict(http_status, GAPI.UNIMPLEMENTED_ERROR, message) elif http_status == 502: if 'bad gateway' in lmessage: error = makeErrorDict(http_status, GAPI.BAD_GATEWAY, message) @@ -37297,14 +37300,16 @@ def doCreateUpdateCIPolicy(): if updateCmd: pname = jsonData.pop('name', None) else: + jsonData.pop('name', None) pname = 'New Policy' if 'policyQuery' in jsonData: jsonData['policyQuery'].pop('orgUnitPath', None) jsonData['policyQuery'].pop('groupEmail', None) jsonData['policyQuery'].pop('sortOrder', None) - if 'setting' in jsonData and 'value' in jsonData['setting']: - jsonData['setting']['value'].pop('createTime', None) - jsonData['setting']['value'].pop('updateTime', None) + if 'setting' in jsonData: + if 'value' in jsonData['setting']: + jsonData['setting']['value'].pop('createTime', None) + jsonData['setting']['value'].pop('updateTime', None) while Cmd.ArgumentsRemaining(): myarg = getArgument() if myarg in {'ou', 'org', 'orgunit'}: @@ -37324,12 +37329,15 @@ def doCreateUpdateCIPolicy(): if updateCmd: result = callGAPI(ci.policies(), 'patch', bailOnInternalError=True, - throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], + throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.UNIMPLEMENTED_ERROR, + GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], + name=pname, body=jsonData) else: result = callGAPI(ci.policies(), 'create', bailOnInternalError=True, - throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], + throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.UNIMPLEMENTED_ERROR, + GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], body=jsonData) if result['done']: if 'error' not in result: @@ -37340,7 +37348,8 @@ def doCreateUpdateCIPolicy(): entityActionFailedWarning([Ent.POLICY, pname], result['error']['message']) else: entityActionPerformedMessage([Ent.POLICY, pname], Msg.ACTION_IN_PROGRESS.format('delete')) - except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e: + except (GAPI.invalid, GAPI.invalidArgument, GAPI.unimplementedError, + GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e: entityActionFailedWarning([Ent.POLICY, pname], str(e)) @@ -37358,10 +37367,10 @@ def doDeleteCIPolicies(): try: policies = [callGAPI(ci.policies(), 'get', bailOnInternalError=True, - throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], - name=pname, - fields='name')] - except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e: + throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, + GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], + name=pname, fields='name')] + except (GAPI.invalid, GAPI.invalidArgument, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e: entityActionFailedWarning([Ent.POLICY, pname], str(e), i, count) continue else: @@ -37380,7 +37389,8 @@ def doDeleteCIPolicies(): try: result = callGAPI(ci.policies(), 'delete', bailOnInternalError=True, - throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], + throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, + GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], name=pname) if result['done']: if 'error' not in result: @@ -37389,7 +37399,7 @@ def doDeleteCIPolicies(): entityActionFailedWarning([Ent.POLICY, pname], result['error']['message'], j, jcount) else: entityActionPerformedMessage([Ent.POLICY, pname], Msg.ACTION_IN_PROGRESS.format('delete'), j, jcount) - except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e: + except (GAPI.invalid, GAPI.invalidArgument, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e: entityActionFailedWarning([Ent.POLICY, pname], str(e), j, jcount) Ind.Decrement() @@ -37421,10 +37431,10 @@ def doInfoCIPolicies(): try: policies = [callGAPI(ci.policies(), 'get', bailOnInternalError=True, - throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], - name=pname, - fields='name,policyQuery(group,orgUnit,sortOrder),type,setting')] - except (GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e: + throwReasons=[GAPI.INVALID, GAPI.INVALID_ARGUMENT, + GAPI.NOT_FOUND, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR], + name=pname, fields='name,policyQuery(group,orgUnit,sortOrder),type,setting')] + except (GAPI.invalid, GAPI.invalidArgument, GAPI.notFound, GAPI.permissionDenied, GAPI.internalError) as e: entityActionFailedWarning([Ent.POLICY, pname], str(e), i, count) continue else: @@ -61579,7 +61589,7 @@ def initCopyMoveOptions(copyCmd): 'shortcutNameMatchPattern': None, 'fileMimeTypes': set(), 'notMimeTypes': False, - 'copySubFilesOwnedBy': None, + 'copySubFilesOwnedBy': {}, 'copyPermissionRoles': set(DRIVEFILE_ACL_ROLES_MAP.values()), 'copyPermissionTypes': set(DRIVEFILE_ACL_PERMISSION_TYPES), } @@ -61598,6 +61608,16 @@ DUPLICATE_FOLDER_CHOICES = { 'skip': DUPLICATE_FOLDER_SKIP, } +COPY_OWNED_BY_CHOICE_MAP = { + 'any': {}, + 'me': {'mode': 'bool', 'value': True}, + 'others': {'mode': 'bool', 'value': False}, + 'users': {'mode': 'users', 'value': set()}, + 'notusers': {'mode': 'notusers', 'value': set()}, + 'regex': {'mode': 'regex', 'value': ''}, + 'notregex': {'mode': 'notregex', 'value': ''} +} + def getCopyMoveOptions(myarg, copyMoveOptions): # Copy/Move arguments if myarg == 'newfilename': @@ -61726,7 +61746,12 @@ def getCopyMoveOptions(myarg, copyMoveOptions): for mimeType in getString(Cmd.OB_MIMETYPE_LIST).lower().replace(',', ' ').split(): copyMoveOptions['fileMimeTypes'].add(validateMimeType(mimeType)) elif myarg == 'copysubfilesownedby': - copyMoveOptions['copySubFilesOwnedBy'] = getChoice(SHOW_OWNED_BY_CHOICE_MAP, mapChoice=True) + copyMoveOptions['copySubFilesOwnedBy'] = getChoice(COPY_OWNED_BY_CHOICE_MAP, mapChoice=True) + if copyMoveOptions['copySubFilesOwnedBy']: + if copyMoveOptions['copySubFilesOwnedBy']['mode'] in {'users', 'notusers'}: + copyMoveOptions['copySubFilesOwnedBy']['value'] = set(getString(Cmd.OB_EMAIL_ADDRESS_LIST).replace(',', ' ').lower().split()) + elif copyMoveOptions['copySubFilesOwnedBy']['mode'] in {'regex', 'notregex'}: + copyMoveOptions['copySubFilesOwnedBy']['value'] = getREPattern(re.IGNORECASE) else: return False return True @@ -62277,6 +62302,12 @@ copyReturnItemMap = { # * # [skipids ] # [copysubfiles []] [filenamematchpattern ] [filemimetype [not] ] +# [copysubfilesownedby +# any|me|others| +# users | +# notusers | +# regex | +# notregex ] # [copysubfolders []] [foldernamematchpattern ] # [copysubshortcuts []] [shortcutnamematchpattern ] # [duplicatefiles overwriteolder|overwriteall|duplicatename|uniquename|skip] @@ -62489,9 +62520,28 @@ def copyDriveFile(users): else: if not copyMoveOptions['copySubFiles']: return False - if copyMoveOptions['copySubFilesOwnedBy'] is not None: - if child.get('driveId', None) is None and child.get('ownedByMe', False) != copyMoveOptions['copySubFilesOwnedBy']: - return False + if copyMoveOptions['copySubFilesOwnedBy'] and child.get('driveId', None) is None: + if copyMoveOptions['copySubFilesOwnedBy']['mode'] == 'bool': + if child.get('ownedByMe', False) != copyMoveOptions['copySubFilesOwnedBy']['value']: + return False + else: + childOwner = child.get('owners', []) + if childOwner: + childOwner = childOwner[0].get('emailAddress', '').lower() + else: + childOwner = '' + if copyMoveOptions['copySubFilesOwnedBy']['mode'] == 'users': + if childOwner not in copyMoveOptions['copySubFilesOwnedBy']['value']: + return False + elif copyMoveOptions['copySubFilesOwnedBy']['mode'] == 'notusers': + if childOwner in copyMoveOptions['copySubFilesOwnedBy']['value']: + return False + elif copyMoveOptions['copySubFilesOwnedBy']['mode'] == 'regex': + if not copyMoveOptions['copySubFilesOwnedBy']['value'].match(childOwner): + return False + else: # elif copyMoveOptions['copySubFilesOwnedBy']['mode'] == 'notregex': + if copyMoveOptions['copySubFilesOwnedBy']['value'].match(childOwner): + return False if copyMoveOptions['fileMimeTypes']: if not copyMoveOptions['notMimeTypes']: if childMimeType not in copyMoveOptions['fileMimeTypes']: @@ -62524,7 +62574,7 @@ def copyDriveFile(users): orderBy='folder desc,name,modifiedTime desc', fields='nextPageToken,files(id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\ 'description,folderColorRgb,mimeType,modifiedTime,ownedByMe,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare,'\ - 'shortcutDetails(targetId,targetMimeType))', + 'shortcutDetails(targetId,targetMimeType),owners(emailAddress))', pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **sourceSearchArgs) kcount = len(sourceChildren) if kcount > 0: @@ -62559,6 +62609,7 @@ def copyDriveFile(users): entityActionNotPerformedWarning(kvList, Msg.NOT_SELECTED, k, kcount) continue child.pop('ownedByMe', None) + child.pop('owners', None) trashed = child.pop('trashed', False) if (childId == newFolderId) or (excludeTrashed and trashed): entityActionNotPerformedWarning(kvList, diff --git a/src/gam/gamlib/glgapi.py b/src/gam/gamlib/glgapi.py index 56c1f2f1..3aefdb2e 100644 --- a/src/gam/gamlib/glgapi.py +++ b/src/gam/gamlib/glgapi.py @@ -179,6 +179,7 @@ TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED = 'teamDrivesSharingRestrictionNotAll TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED = 'teamDrivesShortcutFileNotSupported' TIME_RANGE_EMPTY = 'timeRangeEmpty' TRANSIENT_ERROR = 'transientError' +UNIMPLEMENTED_ERROR = 'unimplementedError' UNKNOWN_ERROR = 'unknownError' UNSUPPORTED_LANGUAGE_CODE = 'unsupportedLanguageCode' UNSUPPORTED_SUPERVISED_ACCOUNT = 'unsupportedSupervisedAccount' @@ -671,6 +672,8 @@ class timeRangeEmpty(Exception): pass class transientError(Exception): pass +class unimplementedError(Exception): + pass class unknownError(Exception): pass class unsupportedLanguageCode(Exception): @@ -843,6 +846,7 @@ REASON_EXCEPTION_MAP = { TEAMDRIVES_SHORTCUT_FILE_NOT_SUPPORTED: teamDrivesShortcutFileNotSupported, TIME_RANGE_EMPTY: timeRangeEmpty, TRANSIENT_ERROR: transientError, + UNIMPLEMENTED_ERROR: unimplementedError, UNKNOWN_ERROR: unknownError, UNSUPPORTED_LANGUAGE_CODE: unsupportedLanguageCode, UNSUPPORTED_SUPERVISED_ACCOUNT: unsupportedSupervisedAccount,