diff --git a/docs/GamUpdates.md b/docs/GamUpdates.md index 615273cc..ecb8ee6f 100644 --- a/docs/GamUpdates.md +++ b/docs/GamUpdates.md @@ -10,6 +10,10 @@ Add the `-s` option to the end of the above commands to suppress creating the `g See [Downloads](https://github.com/taers232c/GAMADV-XTD3/wiki/Downloads) for Windows or other options, including manual installation +### 6.66.11 + +Fixed/improved handling of shortcuts in `gam transfer drive`. + ### 6.66.10 Updated `gam create datatransfer` to handle the following error: diff --git a/docs/How-to-Upgrade-from-Standard-GAM.md b/docs/How-to-Upgrade-from-Standard-GAM.md index 9a9a4430..41f4fce8 100644 --- a/docs/How-to-Upgrade-from-Standard-GAM.md +++ b/docs/How-to-Upgrade-from-Standard-GAM.md @@ -334,7 +334,7 @@ writes the credentials into the file oauth2.txt. admin@server:/Users/admin/bin/gamadv-xtd3$ rm -f /Users/admin/GAMConfig/oauth2.txt admin@server:/Users/admin/bin/gamadv-xtd3$ ./gam version WARNING: Config File: /Users/admin/GAMConfig/gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: /Users/admin/GAMConfig/oauth2.txt, Not Found -GAMADV-XTD3 6.66.10 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.66.11 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.10.8 64-bit final MacOS High Sierra 10.13.6 x86_64 @@ -1002,7 +1002,7 @@ writes the credentials into the file oauth2.txt. C:\GAMADV-XTD3>del C:\GAMConfig\oauth2.txt C:\GAMADV-XTD3>gam version WARNING: Config File: C:\GAMConfig\gam.cfg, Section: DEFAULT, Item: oauth2_txt, Value: C:\GAMConfig\oauth2.txt, Not Found -GAMADV-XTD3 6.66.10 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.66.11 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.0 64-bit final Windows-10-10.0.17134 AMD64 diff --git a/docs/Users-Calendars-Events.md b/docs/Users-Calendars-Events.md index 551da5ef..56ff4ebe 100644 --- a/docs/Users-Calendars-Events.md +++ b/docs/Users-Calendars-Events.md @@ -585,13 +585,6 @@ gam delete events [doit] [` in conjunction with other `` and `` options. -``` -gam move events [] destination|to [] -``` - ## Empty calendar trash A user signed in to Google Calendar can empty the calendar trash but there is no direct API support for this operation. To empty the calendar trash a temporary calendar is created, the deleted events are moved to the temporary calendar and then the temporary calendar is deleted. diff --git a/docs/Version-and-Help.md b/docs/Version-and-Help.md index a93d4245..6208cdcf 100644 --- a/docs/Version-and-Help.md +++ b/docs/Version-and-Help.md @@ -4,7 +4,7 @@ Print the current version of Gam with details ``` gam version -GAMADV-XTD3 6.66.10 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.66.11 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.0 64-bit final MacOS Monterey 12.7 x86_64 @@ -16,7 +16,7 @@ Time: 2023-06-02T21:10:00-07:00 Print the current version of Gam with details and time offset information ``` gam version timeoffset -GAMADV-XTD3 6.66.10 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.66.11 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.0 64-bit final MacOS Monterey 12.7 x86_64 @@ -28,7 +28,7 @@ Your system time differs from www.googleapis.com by less than 1 second Print the current version of Gam with extended details and SSL information ``` gam version extended -GAMADV-XTD3 6.66.10 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource +GAMADV-XTD3 6.66.11 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource Ross Scroggs Python 3.12.0 64-bit final MacOS Monterey 12.7 x86_64 @@ -65,7 +65,7 @@ MacOS High Sierra 10.13.6 x86_64 Path: /Users/Admin/bin/gamadv-xtd3 Version Check: Current: 5.35.08 - Latest: 6.66.10 + Latest: 6.66.11 echo $? 1 ``` @@ -73,7 +73,7 @@ echo $? Print the current version number without details ``` gam version simple -6.66.10 +6.66.11 ``` In Linux/MacOS you can do: ``` @@ -83,7 +83,7 @@ echo $VER Print the current version of Gam and address of this Wiki ``` gam help -GAM 6.66.10 - https://github.com/taers232c/GAMADV-XTD3 +GAM 6.66.11 - https://github.com/taers232c/GAMADV-XTD3 Ross Scroggs Python 3.12.0 64-bit final MacOS Monterey 12.7 x86_64 diff --git a/src/GamUpdate.txt b/src/GamUpdate.txt index 9ba0a08b..122384ea 100644 --- a/src/GamUpdate.txt +++ b/src/GamUpdate.txt @@ -2,6 +2,10 @@ Merged GAM-Team version +6.66.11 + +Fixed/improved handling of shortcuts in `gam transfer drive`. + 6.66.10 Updated `gam create datatransfer` to handle the following error: diff --git a/src/gam/__init__.py b/src/gam/__init__.py index 9c462523..326c27ee 100755 --- a/src/gam/__init__.py +++ b/src/gam/__init__.py @@ -5107,30 +5107,30 @@ def checkGAPIError(e, softErrors=False, retryOnHttpError=False, mapNotFound=True error = makeErrorDict(http_status, GAPI.OPERATION_NOT_SUPPORTED, message) elif 'failed status in update settings response' in lmessage: error = makeErrorDict(http_status, GAPI.INVALID_INPUT, message) - elif status == 'INTERNAL': - error = makeErrorDict(http_status, GAPI.INTERNAL_ERROR, message) elif 'cannot delete a field in use.resource.fields' in lmessage: error = makeErrorDict(http_status, GAPI.FIELD_IN_USE, message) + elif status == 'INTERNAL': + error = makeErrorDict(http_status, GAPI.INTERNAL_ERROR, message) elif http_status == 502: if 'bad gateway' in lmessage: error = makeErrorDict(http_status, GAPI.BAD_GATEWAY, message) elif http_status == 503: - if status == 'UNAVAILABLE' or 'the service is currently unavailable' in lmessage: - error = makeErrorDict(http_status, GAPI.SERVICE_NOT_AVAILABLE, message) - elif message.startswith('quota exceeded for the current request'): + if message.startswith('quota exceeded for the current request'): error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, message) + elif status == 'UNAVAILABLE' or 'the service is currently unavailable' in lmessage: + error = makeErrorDict(http_status, GAPI.SERVICE_NOT_AVAILABLE, message) elif http_status == 504: if 'gateway timeout' in lmessage: error = makeErrorDict(http_status, GAPI.GATEWAY_TIMEOUT, message) elif http_status == 400: if '@attachmentnotvisible' in lmessage: error = makeErrorDict(http_status, GAPI.BAD_REQUEST, message) + elif 'does not match' in lmessage or 'invalid' in lmessage: + error = makeErrorDict(http_status, GAPI.INVALID, message) elif status == 'FAILED_PRECONDITION' or 'precondition check failed' in lmessage: error = makeErrorDict(http_status, GAPI.FAILED_PRECONDITION, message) elif status == 'INVALID_ARGUMENT': error = makeErrorDict(http_status, GAPI.INVALID_ARGUMENT, message) - elif 'does not match' in lmessage or 'invalid' in lmessage: - error = makeErrorDict(http_status, GAPI.INVALID, message) elif http_status == 401: if status == 'PERMISSION_DENIED': error = makeErrorDict(http_status, GAPI.PERMISSION_DENIED, message) @@ -57919,17 +57919,55 @@ def transferDrive(users): entityModifierItemValueListActionPerformed(kvList, Act.MODIFIER_IN, [Ent.DRIVE_FOLDER, newParentId, targetEntityType, f"{childName}({result['id']})"], j, jcount) - Act.Set(action) except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest, GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed, GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e: entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), j, jcount) + Act.Set(action) + +# Recreate source user shortcut in target user + def _transferShortcut(j, jcount, childEntryInfo, childId, childName, newParentId): + entityType = Ent.DRIVE_FOLDER_SHORTCUT if childEntryInfo['shortcutDetails']['targetMimeType'] == MIMETYPE_GA_FOLDER else Ent.DRIVE_FILE_SHORTCUT + kvList = [Ent.USER, sourceUser, entityType, f'{childName}({childId})'] + action = Act.Get() + body = {'name': childName, 'mimeType': MIMETYPE_GA_SHORTCUT, + 'parents': [newParentId], 'shortcutDetails': {'targetId': childEntryInfo['shortcutDetails']['targetId']}} + Act.Set(Act.RECREATE) + try: + result = callGAPI(targetDrive.files(), 'create', + throwReasons=GAPI.DRIVE_USER_THROW_REASONS+[GAPI.FORBIDDEN, GAPI.INSUFFICIENT_PERMISSIONS, GAPI.INSUFFICIENT_PARENT_PERMISSIONS, + GAPI.INVALID, GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND, GAPI.UNKNOWN_ERROR, + GAPI.STORAGE_QUOTA_EXCEEDED, GAPI.TEAMDRIVES_SHARING_RESTRICTION_NOT_ALLOWED, + GAPI.TEAMDRIVE_HIERARCHY_TOO_DEEP, GAPI.SHORTCUT_TARGET_INVALID], + body=body, fields='id', supportsAllDrives=True) + shortcutId = result['id'] + entityModifierNewValueItemValueListActionPerformed(kvList, Act.MODIFIER_IN, None, [Ent.USER, targetUser, + Ent.DRIVE_FOLDER, newParentId, entityType, f"{shortcutId})"], + j, jcount) + except (GAPI.forbidden, GAPI.insufficientFilePermissions, GAPI.insufficientParentPermissions, GAPI.invalid, GAPI.badRequest, + GAPI.fileNotFound, GAPI.unknownError, GAPI.storageQuotaExceeded, GAPI.teamDrivesSharingRestrictionNotAllowed, + GAPI.teamDriveHierarchyTooDeep, GAPI.shortcutTargetInvalid) as e: + entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), j, jcount) + Act.Set(action) + return + if ownerRetainRoleBody['role'] == 'none': + Act.Set(Act.DELETE_SHORTCUT) + kvList = [Ent.USER, sourceUser, entityType, f'{childName}({childId})'] + try: + callGAPI(sourceDrive.files(), 'delete', + throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.FILE_NEVER_WRITABLE], + fileId=childId, supportsAllDrives=True) + entityActionPerformed(kvList, j, jcount) + except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.insufficientFilePermissions, GAPI.unknownError, GAPI.fileNeverWritable) as e: + entityActionFailedWarning(kvList, str(e), j, jcount) + Act.Set(action) def _transferFile(childEntry, i, count, j, jcount, atSelectTop): childEntryInfo = childEntry['info'] childFileId = childEntryInfo['id'] childFileName = childEntryInfo['name'] childFileType = _getEntityMimeType(childEntryInfo) +# Owned files if childEntryInfo['ownedByMe']: childEntryInfo['sourcePermission'] = {'role': 'owner'} for permission in childEntryInfo.get('permissions', []): @@ -57975,20 +58013,25 @@ def transferDrive(users): GAPI.PERMISSION_NOT_FOUND, GAPI.SHARING_RATE_LIMIT_EXCEEDED], fileId=childFileId, permissionId=targetPermissionId, transferOwnership=True, body={'role': 'owner'}, fields='') - if removeSourceParents: - op = 'Remove Source Parents' - callGAPI(sourceDrive.files(), 'update', - throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS, retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3, - fileId=childFileId, removeParents=','.join(removeSourceParents), fields='') - actionUser = targetUser - if addTargetParent or removeTargetParents: - op = 'Add/Remove Target Parents' - callGAPI(targetDrive.files(), 'update', - throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INSUFFICIENT_PARENT_PERMISSIONS], - retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3, - fileId=childFileId, - addParents=addTargetParent, removeParents=','.join(removeTargetParents), fields='') - entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount) + if removeSourceParents: + op = 'Remove Source Parents' + callGAPI(sourceDrive.files(), 'update', + throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS, retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3, + fileId=childFileId, removeParents=','.join(removeSourceParents), fields='') + actionUser = targetUser + if addTargetParent or removeTargetParents: + op = 'Add/Remove Target Parents' + callGAPI(targetDrive.files(), 'update', + throwReasons=GAPI.DRIVE_ACCESS_THROW_REASONS+[GAPI.INSUFFICIENT_PARENT_PERMISSIONS], + retryReasons=[GAPI.BAD_REQUEST, GAPI.FILE_NOT_FOUND], triesLimit=3, + fileId=childFileId, + addParents=addTargetParent, removeParents=','.join(removeTargetParents), fields='') + entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount) + else: + if topSourceId in childParents: + _transferShortcut(j, jcount, childEntryInfo, childFileId, childFileName, addTargetParent) + else: + entityModifierNewValueItemValueListActionPerformed([Ent.USER, sourceUser, childFileType, childFileName], Act.MODIFIER_TO, None, [Ent.USER, targetUser], j, jcount) except (GAPI.fileNotFound, GAPI.forbidden, GAPI.internalError, GAPI.unknownError, GAPI.badRequest, GAPI.sharingRateLimitExceeded, GAPI.insufficientParentPermissions) as e: entityActionFailedWarning([Ent.USER, actionUser, childFileType, childFileName], f'{op}: {str(e)}', j, jcount) @@ -58003,6 +58046,7 @@ def transferDrive(users): entityActionFailedWarning([Ent.USER, actionUser, childFileType, childFileName], Ent.TypeNameMessage(Ent.PERMISSION_ID, targetPermissionId, str(e)), j, jcount) except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e: userSvcNotApplicableOrDriveDisabled(actionUser, str(e), i, count) +# Non-owned files else: Act.Set(Act.PROCESS) for permission in childEntryInfo.get('permissions', []): @@ -58145,6 +58189,10 @@ def transferDrive(users): childFileId = childEntryInfo['id'] childFileName = childEntryInfo['name'] childFileType = _getEntityMimeType(childEntryInfo) + if childEntryInfo['mimeType'] == MIMETYPE_GA_SHORTCUT: + if showRetentionMessages: + entityActionNotPerformedWarning([Ent.USER, sourceUser, childFileType, childFileName, Ent.ROLE, ownerRetainRoleBody['role']], Msg.NOT_APPROPRIATE, j, jcount) + return if childEntryInfo['ownedByMe']: try: if ownerRetainRoleBody['role'] != 'none': @@ -58293,7 +58341,7 @@ def transferDrive(users): throwReasons=GAPI.DRIVE_USER_THROW_REASONS, retryReasons=[GAPI.UNKNOWN_ERROR], orderBy=OBY.orderBy, q=WITH_PARENTS.format(fileId), - fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId),permissions(id,role))', + fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails)', pageSize=GC.Values[GC.DRIVE_MAX_RESULTS]) except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e: userSvcNotApplicableOrDriveDisabled(sourceUser, str(e), i, count) @@ -58512,6 +58560,7 @@ def transferDrive(users): return Ind.Increment() if buildTree: + topSourceId = sourceRootId parentIdMap = {sourceRootId: targetIds[TARGET_PARENT_ID]} printGettingAllEntityItemsForWhom(Ent.DRIVE_FILE_OR_FOLDER, Ent.TypeName(Ent.SOURCE_USER, user), i, count) feed = callGAPIpages(sourceDrive.files(), 'list', 'files', @@ -58519,7 +58568,7 @@ def transferDrive(users): throwReasons=GAPI.DRIVE_USER_THROW_REASONS, retryReasons=[GAPI.UNKNOWN_ERROR], orderBy=OBY.orderBy, q=NON_TRASHED, - fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,owners(emailAddress,permissionId),permissions(id,role))', + fields='nextPageToken,files(id,name,parents,mimeType,ownedByMe,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails)', pageSize=GC.Values[GC.DRIVE_MAX_RESULTS]) fileTree = buildFileTree(feed, sourceDrive) del feed @@ -58546,7 +58595,7 @@ def transferDrive(users): fileEntry = callGAPI(sourceDrive.files(), 'get', throwReasons=GAPI.DRIVE_GET_THROW_REASONS, fileId=fileId, - fields='id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId),permissions(id,role)') + fields='id,name,parents,mimeType,ownedByMe,trashed,owners(emailAddress,permissionId),permissions(id,role),shortcutDetails') entityType = _getEntityMimeType(fileEntry) if fileId in skipFileIdEntity['list']: entityActionNotPerformedWarning([Ent.USER, sourceUser, entityType, f'{fileEntry["name"]} ({fileId})'], @@ -58554,9 +58603,11 @@ def transferDrive(users): continue entityPerformActionItemValue([Ent.USER, sourceUser], entityType, f'{fileEntry["name"]} ({fileId})', j, jcount) if not mergeWithTarget: + topSourceId = None for parentId in fileEntry.get('parents', []): parentIdMap[parentId] = targetIds[TARGET_PARENT_ID] else: + topSourceId = fileId parentIdMap[fileId] = targetIds[TARGET_PARENT_ID] _identifyDriveFileAndChildren(fileEntry, i, count) filesTransferred = set() diff --git a/src/gam/gamlib/glaction.py b/src/gam/gamlib/glaction.py index 09512ec5..4e6dc192 100644 --- a/src/gam/gamlib/glaction.py +++ b/src/gam/gamlib/glaction.py @@ -49,6 +49,7 @@ class GamAction(): DELETE = 'dele' DELETE_EMPTY = 'delm' DELETE_PREVIEW = 'delp' + DELETE_SHORTCUT = 'desc' DEPROVISION = 'depr' DISABLE = 'disa' DOWNLOAD = 'down' @@ -160,12 +161,15 @@ class GamAction(): COPY_MERGE: ['Copied(Merge)', 'Copy(Merge)'], CREATE: ['Created', 'Create'], CREATE_PREVIEW: ['Created (Preview)', 'Create (Preview)'], - CREATE_SHORTCUT: ['Created Shortcut', 'Create SHORTCUT'], + CREATE_SHORTCUT: ['Created Shortcut', 'Create Shortcut'], DEDUP: ['Duplicates Deleted', 'Delete Duplicates'], DELETE: ['Deleted', 'Delete'], DELETE_EMPTY: ['Deleted', 'Delete Empty'], DELETE_PREVIEW: ['Deleted (Preview)', 'Delete (Preview)'], DEPROVISION: ['Deprovisioned', 'Deprovision'], + DELETE_SHORTCUT: ['Deleted Shortcut', 'Delete Shortcut'], + DISABLE: ['Disabled', 'Disable'], + DEPROVISION: ['Deprovisioned', 'Deprovision'], DISABLE: ['Disabled', 'Disable'], DOWNLOAD: ['Downloaded', 'Download'], DRAFT: ['Drafted', 'Draft'], diff --git a/src/gam/gamlib/glmsgs.py b/src/gam/gamlib/glmsgs.py index 5d462de2..2c7844e5 100644 --- a/src/gam/gamlib/glmsgs.py +++ b/src/gam/gamlib/glmsgs.py @@ -338,6 +338,7 @@ NOT_A_MEMBER = 'Not a member' NOT_ACTIVE = 'Not Active' NOT_ALLOWED = 'Not Allowed' NOT_AN_ENTITY = 'Not a {0}' +NOT_APPROPRIATE = 'Not Appropriate' NOT_COMPATIBLE = 'Not Compatible' NOT_COPYABLE = 'Not Copyable' NOT_COPYABLE_INTO_ITSELF = 'Not copyable into itself'