Updated copy drivefile to limit copying to those files owned by selected users

This commit is contained in:
Ross Scroggs
2026-01-07 16:42:57 -08:00
parent aab8db0b84
commit a8f9fb7b81
4 changed files with 93 additions and 24 deletions

View File

@@ -6848,7 +6848,12 @@ gam <UserTypeEntity> copy drivefile <DriveFileEntity>
[skipids <DriveFileEntity>]
[copysubfiles [<Boolean>]] [filenamematchpattern <REMatchPattern>]
[filemimetype [not] <MimeTypeList>]
[copysubfilesownedby any|me|others]
[copysubfilesownedby
any|me|others|
users <EmailAddressList>|
notusers <EmailAddressList>|
regex <REMatchPattern>|
notregex <REMatchPattern>]
[copysubfolders [<Boolean>]] [foldernamematchpattern <REMatchPattern>]
[copysubshortcuts [<Boolean>]] [shortcutnamematchpattern <REMatchPattern>]
[duplicatefiles overwriteolder|overwriteall|duplicatename|uniquename|skip]

View File

@@ -1,3 +1,12 @@
7.31.02
Added the following options to `gam <UserTypeEntity> copy drivefile`
to limit copying to those files owned by selected users.
* `copysubfilesownedby users <EmailAddressList>` - Only files owned by users in `<EmailAddressList>` are copied.
* `copysubfilesownedby notusers <EmailAddressList>` - Only files not owned by users in `<EmailAddressList>` are copied.
* `copysubfilesownedby regex <REMatchPattern>` - Only files owned by users whose email addresses match `<REMatchPattern>` are copied.
* `copysubfilesownedby notregex <REMatchPattern>` - Only files owned by users whose email addresses do not match `<REMatchPattern>` are copied.
7.31.01
Code cleanup for `addcsvdata <FieldName> <String>`.

View File

@@ -25,7 +25,7 @@ https://github.com/GAM-team/GAM/wiki
"""
__author__ = 'GAM Team <google-apps-manager@googlegroups.com>'
__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 = {
# <DriveFileCopyAttribute>*
# [skipids <DriveFileEntity>]
# [copysubfiles [<Boolean>]] [filenamematchpattern <REMatchPattern>] [filemimetype [not] <MimeTypeList>]
# [copysubfilesownedby
# any|me|others|
# users <EmailAddressList>|
# notusers <EmailAddressList>|
# regex <REMatchPattern>|
# notregex <REMatchPattern>]
# [copysubfolders [<Boolean>]] [foldernamematchpattern <REMatchPattern>]
# [copysubshortcuts [<Boolean>]] [shortcutnamematchpattern <REMatchPattern>]
# [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,

View File

@@ -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,