Compare commits

...

12 Commits

Author SHA1 Message Date
Ross Scroggs
ae46ae8738 Added option convertcrnl to gam update chromepolicy 2023-12-20 19:59:18 -08:00
Ross Scroggs
06a4c7a8c9 Added option copysubfilesownedby any|me|others to `gam <UserTypeEntity> copy drivefile 2023-12-20 12:32:10 -08:00
Ross Scroggs
f89f730957 Handle issues in update alias/message 2023-12-19 20:04:06 -08:00
Ross Scroggs
80fc40a9c7 Updated functionality of option preservefiletimes in gam <UserTypeEntity> update drivefile <DriveFileEntity>. 2023-12-14 10:06:46 -08:00
Ross Scroggs
2bb0088ade Updated all drive commands to handle the following error:
```
ERROR: 401: Active session is invalid. Error code: 4 - authError
```
2023-12-12 10:25:59 -08:00
Jay Lee
d113b3ec8e flush cache to pickup Python 3.12.1 2023-12-12 06:57:08 -05:00
Ross Scroggs
97e13b92be Fixed/improved handling of shortcuts in gam <UserTypeEntity> transfer drive. 2023-12-11 15:56:00 -08:00
Ross Scroggs
dc832b8c7f Updated gam create datatransfer to handle the following error:
ERROR: 401: Active session is invalid. Error code: 4 - authError
2023-12-09 10:16:40 -08:00
Ross Scroggs
56c33fec87 Fixed bug in gam <UserTypeEntity> print filelist ... allfields that caused a trap 2023-12-07 18:26:59 -08:00
Ross Scroggs
48862997b0 Added additional columns isBase and baseId' to gam <UserTypeEntity> print fileparenttree` 2023-12-07 08:29:11 -08:00
Ross Scroggs
59dd01f1e8 Update and fix
Fixed bug in `gam <UserTypeEntity> print diskusage` that caused a trap.

Added a command the print the parent tree of file/folder.
2023-12-06 14:18:33 -08:00
Ross Scroggs
d639e8e728 Two small updates 2023-12-04 11:57:56 -08:00
20 changed files with 1092 additions and 222 deletions

View File

@@ -114,7 +114,7 @@ jobs:
path: |
bin.tar.xz
src/cpython
key: gam-${{ matrix.jid }}-202311118
key: gam-${{ matrix.jid }}-20231212
- name: Untar Cache archive
if: matrix.goal == 'build' && steps.cache-python-ssl.outputs.cache-hit == 'true'

View File

@@ -55,13 +55,18 @@ gam create alias bob[@yourdomain.com] user robert[@yourdomain.com]
The existing alias is deleted and a new alias is created.
```
gam update alias|aliases <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
[notargetverify]
[notargetverify] [waitafterdelete <Integer>]
```
`<EmailAddressEntity>` are the aliases, `<EmailAddress>` is the target.
By default, GAM makes additional API calls to verify that the target email address exists before updating the alias;
if you know that the target exists, you can suppress the verification with `notargetverify.
GAM updates an alias to point to a new target by deleting the alias and then recreates the alias pointing to the new target.
Unfortunately, if these commands are executed back-to-back; Google generates the `Update Failed: Duplicate` error.
Now, GAM waits 2 seconds between the delete and the insert which seems to eliminate the problem. If the problem persists,
use the option `waitafterdelete <Integer>` to increase the wait time to a maximum of 10 seconds.
## Delete an alias regardless of the target
```
gam delete alias|aliases [user|group|target] <EmailAddressEntity>

File diff suppressed because it is too large Load Diff

View File

@@ -10,6 +10,104 @@ 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.16
Added option `convertcrnl` to `gam update chromepolicy` to properly handle carriage returns (\r) and line feeds (\n)
in value strings entered on the command line in the `<Field> <Value>` form.
```
gam update chromepolicy convertcrnl chrome.devices.DisabledDeviceReturnInstructions
deviceDisabledMessage "Please return device to:\nSchool\n123 Main Street\nAnytown US" ou /Path/to/OU
```
### 6.66.15
Added option `copysubfilesownedby any|me|others` to `gam <UserTypeEntity> copy drivefile` that allows
specification of which source folder sub files to copy based on file ownership; the default is `any`.
This only applies when files are being copied from a 'My Drive'.
### 6.66.14
Updated `gam <UserTypeEntity> modify messages` to recognize the following error:
```
ERROR: 400: invalid - Invalid label: SENT
```
Updated `gam update alias <EmailAddressEntity> user|group|target <EmailAddress>`
to avoid the following problem.
```
$ gam update alias testalias@domain.com user testuser
User Alias: testalias@domain.com, Deleted
User Alias: testalias@domain.com, User: testuser@domain.com, Update Failed: Duplicate, Email Address: testalias@domain.com
```
GAM updates an alias to point to a new target by deleting the alias and then recreating the alias pointing to the new target.
Unfortunately, if these commands are executed back-to-back; Google generates the `Update Failed: Duplicate` error.
Now, GAM waits 2 seconds between the delete and the insert which seems to eliminate the problem. If the problem persists,
the option `waitafterdelete <Integer>` can be used to increase the wait time to a maximum of 10 seconds.
### 6.66.13
Updated functionality of option `preservefiletimes` in `gam <UserTypeEntity> update drivefile <DriveFileEntity>`.
* Current
* `preservefiletimes localfile <FileName>` - `modifiedTime` of `<DriveFileEntity>` is set to that of `localfile <FileName>`
* `preservefiletimes` - No effect
* Updated
* `preservefiletimes localfile <FileName>` - `modifiedTime` of `<DriveFileEntity>` is set to that of `localfile <FileName>`
* `preservefiletimes` - `modifiedTime` of `<DriveFileEntity>` retains its current value
### 6.66.12
Upgraded to Python 3.12.1 where possible.
Updated all drive commands to handle the following error:
```
ERROR: 401: Active session is invalid. Error code: 4 - authError
```
This is due to the Drive SDK API being disabled in the user's OU.
* See: https://support.google.com/a/answer/6105699
### 6.66.11
Fixed/improved handling of shortcuts in `gam <UserTypeEntity> transfer drive`.
### 6.66.10
Updated `gam create datatransfer` to handle the following error:
```
ERROR: 401: Active session is invalid. Error code: 4 - authError
```
### 6.66.09
Fixed bug in `gam <UserTypeEntity> print filelist ... allfields` that caused a trap
when `gam.cfg` contained `drive_v3_native_names = False`.
### 6.66.08
Added additional columns `isBase` and `baseId` to `gam <UserTypeEntity> print fileparenttree`
to simplify processing the output in a script.
### 6.66.07
Fixed bug in `gam <UserTypeEntity> print diskusage` that caused a trap.
### 6.66.06
Added a command the print the parent tree of file/folder.
```
gam <UserTypeEntity> print fileparenttree <DriveFileEntity> [todrive <ToDriveAttribute>*]
[stripcrsfromname]
```
* See: https://github.com/taers232c/GAMADV-XTD3/wiki/Users-Drive-Files-Display#display-file-parent-tree
### 6.66.05
Added column `space.name` to `gam <UserTypeEntity> print chatmembers`.
### 6.66.04
Updated Chat info|show|print commands to display all time fields in local time if specified in `gam.cfg`.
### 6.66.03
Fixed bug in `gam <UserTypeEntity> print filelist select <DriveFileEntity>` where `stripcrsfromname` was not being

View File

@@ -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.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.66.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
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.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.66.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.0 64-bit final
Windows-10-10.0.17134 AMD64

View File

@@ -585,13 +585,6 @@ gam <UserTypeEntity> delete events <UserCalendarEntity> [doit] [<EventNotificati
```
No events are deleted unless you specify the `doit` option; omit `doit` to verify that you properly selected the events to delete.
## Move calendar events to another calendar
Generally you won't move all events from one calendar to another; typically, you'll move events created by the event creator
using `matchfield creatoremail <RegularExpression>` in conjunction with other `<EventSelectProperty>` and `<EventMatchProperty>` options.
```
gam <UserTypeEntity> move events <UserCalendarEntity> [<EventEntity>] destination|to <CalendarItem> [<EventNotificationAttribute>]
```
## 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.

View File

@@ -68,6 +68,7 @@ gam <UserTypeEntity> copy drivefile <DriveFileEntity>
[mergewithparent [<Boolean>]] [recursive [depth <Number>]]
[copysubfiles [<Boolean>]] [filenamematchpattern <RegularExpression>]
[filemimetype [not] <MimeTypeList>]
[copysubfilesownedby any|me|others]
[copysubfolders [<Boolean>]] [foldernamematchpattern <RegularExpression>]
[copysubshortcuts [<Boolean>]] [shortcutnamematchpattern <RegularExpression>]
<DriveFileCopyAttribute>*
@@ -148,6 +149,11 @@ You can specify `<RegularExpression>` patterns that limit the items copied based
* `foldernamematchpattern <RegularExpression>` - Only folders whose name matches `<RegularExpression>` are copied
* `shortcutnamematchpattern <RegularExpression>` - Only shortcuts whose name matches `<RegularExpression>` are copied
### By default, when copying sub files, all files, regardless of ownership, are copied.
* `copysubfilesownedby any` - All files, regardless of ownership, are copied.
* `copysubfilesownedby me` - Only files owned by `<UserTypeEntity>` are copied.
* `copysubfilesownedby others` - Only files not owned by `<UserTypeEntity>` are copied.
### Specify a new name for the file/folder
* `newfilename <DriveFileName>` - The copied file/folder will be named `<DriveFileName>`
* If `stripnameprefix <String>` is specified, `<String>` will be stripped from the front of `<DriveFileName>`

View File

@@ -24,6 +24,7 @@
- [Display file share counts](#display-file-share-counts)
- [Display file tree](#display-file-tree)
- [File selection starting point for Display file tree](#file-selection-starting-point-for-display-file-tree)
- [Display file parent tree](#display-file-parent-tree)
- [Display file list](#display-file-list)
- [File selection by name and entity shortcuts for Display file list](#file-selection-by-name-and-entity-shortcuts-for-display-file-list)
- [File selection starting point for Display file list](#file-selection-starting-point-for-display-file-list)
@@ -33,6 +34,7 @@
## API documentation
* https://developers.google.com/drive/api/v3/reference/files
* https://support.google.com/a/answer/6105699
## Definitions
* [`<DriveFileEntity>`](Drive-File-Selection)
@@ -79,6 +81,7 @@
canaddfolderfromanotherdrive|
canaddmydriveparent|
canchangecopyrequireswriterpermission|
canchangecopyrequireswriterpermissionrestriction|
canchangedomainusersonlyrestriction|
canchangedrivebackground|
canchangedrivemembersonlyrestriction|
@@ -96,11 +99,14 @@
canmanagemembers|
canmodifycontent|
canmodifycontentrestriction|
canmodifyeditorcontentrestriction|
canmodifylabels|
canmodifyownercontentrestriction|
canmovechildrenoutofdrive|
canmovechildrenoutofteamdrive|
canmovechildrenwithindrive|
canmovechildrenwithinteamdrive|
canmoveitemintodrive|
canmoveitemintoteamdrive|
canmoveitemoutofdrive|
canmoveitemoutofteamdrive|
@@ -112,6 +118,7 @@
canreadrevisions|
canreadteamdrive|
canremovechildren|
canremovecontentrestriction|
canremovemydriveparent|
canrename|
canrenamedrive|
@@ -946,6 +953,40 @@ Show file tree starting at the folder named "Middle Folder" and 2 levels deeper
```
gam user testuser show filetree select drivefilename "Middle Folder" depth 2
```
## Display file parent tree
Print the parent tree of file/folder.
```
gam <UserTypeEntity> print fileparenttree <DriveFileEntity> [todrive <ToDriveAttribute>*]
[stripcrsfromname]
```
### Examples
```
# My Drive file
$ gam user user@domain.com print fileparenttree 1tDGtnaBXc1qx_9NjBSZOUUNZ7FoRc2u6
User: user@domain.com, Print 1 File Parent Tree
Owner,id,name,parentId,depth,isRoot
user@domain.com,1tDGtnaBXc1qx_9NjBSZOUUNZ7FoRc2u6,Bottom Folder,1HvAJtmQ2KZrKJhzY8aeZVScHhZ3HBJLp,4,False
user@domain.com,1HvAJtmQ2KZrKJhzY8aeZVScHhZ3HBJLp,Middle Folder,1CVqOJJLNQtxX4QEPdpDfbkjiq1oUsxne,3,False
user@domain.com,1CVqOJJLNQtxX4QEPdpDfbkjiq1oUsxne,TopCopy,0AHYenC8f12ALUk9PVA,2,False
user@domain.com,0AHYenC8f12ALUk9PVA,My Drive,,1,True
# Shared Drive file
$ gam user user@domain.com print fileparenttree 1kAHa7Q801KXRF1DfoofNlW05UWDzddhVP_u_L2xGfFQ
User: user@domain.com, Print 1 File Parent Tree
Owner,id,name,parentId,depth,isRoot
user@domain.com,1kAHa7Q801KXRF1DfoofNlW05UWDzddhVP_u_L2xGfFQ,Middle Doc,1DShPJ6iG1TnNsgiBn-Oy1OVE2BahYlPr,4,False
user@domain.com,1DShPJ6iG1TnNsgiBn-Oy1OVE2BahYlPr,Middle Folder,1s3g64uWfuQrpXRPf82B-bWCB5VuyrOmQ,3,False
user@domain.com,1s3g64uWfuQrpXRPf82B-bWCB5VuyrOmQ,Top Folder,0AL5LiIe4dqxZUk9PVA,2,False
user@domain.com,0AL5LiIe4dqxZUk9PVA,TS Shared Drive 1,,1,True
# Shared with Me file
$ gam user user@domain.com print fileparenttree 1S2D97pyG1vAil4hgNnGGLD2ldCwTOzXUM9D7XbeUv0s
User: user@domain.com, Print 1 File Parent Tree
Owner,id,name,parentId,depth,isRoot
user@domain.com,1S2D97pyG1vAil4hgNnGGLD2ldCwTOzXUM9D7XbeUv0s,GooGoo,0B0NlVEBUkz-hfjVudlF4VHlYYWlmOEdCUUxDaHdLdXhJTF84YWQwbmpRWmZ3Qm0wZnpHSGs,2,False
user@domain.com,0B0NlVEBUkz-hfjVudlF4VHlYYWlmOEdCUUxDaHdLdXhJTF84YWQwbmpRWmZ3Qm0wZnpHSGs,FooBar,,1,False
```
## Display file list
Display a list of file/folder details in CSV format.
```
@@ -1041,6 +1082,12 @@ Use the following option to select a subset of files based on their permissions.
* `<PermissionMatch>* [<PermissionMatchAction>]` - Use permission matching to select files
## File selection starting point for Display file list
You can limit the selection for files on a specific Shared drive.
Any query will be applied to the Shared drive.
```
select <SharedDriveEntity>
```
You can specify a specific folder from which to select files.
```
select <DriveFileEntity> [selectsubquery <QueryDriveFile>]

View File

@@ -25,6 +25,7 @@
* https://developers.google.com/drive/api/v3/ref-single-parent
* https://developers.google.com/drive/api/v3/shared-drives-diffs
* https://developers.google.com/drive/api/v3/shortcuts
* https://support.google.com/a/answer/6105699
* https://support.google.com/a/answer/7374057
* https://developers.google.com/docs/api/reference/rest
@@ -492,7 +493,8 @@ From the Google Drive API documentation.
By default, Google assigns the current time to the attribute `modifiedTime`; you can assign your own value
with `modifiedtime <Time>`.
The option `preservefiletimes`, when used with `localfile <FileName>`, will set the `modifiedTime` attribute from the local file.
* `preservefiletimes localfile <FileName>` - `modifiedTime` of `<DriveFileEntity>` is set to that of `localfile <FileName>`
* `preservefiletimes` - `modifiedTime` of `<DriveFileEntity>` retains its current value
These are the naming rules when updating from a local file:
* `update drivefile drivefilename "GoogleFile.csv" localfile "NewLocalFile.csv"` - Google Drive file "GoogleFile.csv" is renamed "NewLocalFile.csv"

View File

@@ -17,7 +17,7 @@
- [Display a selected set of messages](#display-a-selected-set-of-messages)
- [Choose information to display](#choose-information-to-display)
- [Display message content](#display-message-content)
- [Display message count](#display-message-count)
- [Display message counts](#display-message-counts)
- [Display label counts](#display-label-counts)
- [Print only options](#print-only-options)
- [Show only options](#show-only-options)
@@ -204,6 +204,9 @@ You can also replace ` ` with `-` but it doesn't seem to be required.
* `query "label:Foo -Bar-"` - Select messages with label `Foo (Bar)`
You can have GAM do the substitutions for you with the `matchlabel <LabelName>` option.
* `matchlabel "Foo (Bar)"` is converted to `query "label:Foo -Bar-"`
## Draft messages
Add a draft message to a user's mailbox.
```
@@ -539,7 +542,7 @@ The `dateheaderconverttimezone [<Boolean>]>` option converts `<SMTPDateHeader>`
* `showsize` - Display the message size
* `showsnippet` - Display the message snippet
### Display message count and optionally cumulative message size
### Display message counts
* `countsonly` - Display the count of the number of messages
* `showsize` - Display the cumulative message size

View File

@@ -13,6 +13,7 @@
## API documentation
* https://developers.google.com/gmail/api/reference/rest/v1/users.settings.sendAs
* https://developers.google.com/gmail/api/v1/reference/users/settings
* https://support.google.com/a/answer/1710338
## Definitions
* [`<UserTypeEntity>`](Collections-of-Users)
@@ -87,6 +88,8 @@ of the sendas address.
The `default` option sets `<EmailAddress>` as the default sendas address for the user.
For `treatasalias`, see: https://support.google.com/a/answer/1710338
You can allow users to send mail through an external SMTP server when configuring a sendas address hosted outside your email domains. You must enable
this capability in Admin Console/Apps/Google Workspace/Gmail/Advanced settings/End User Access/Allow per-user outbound gateways.
@@ -145,6 +148,8 @@ gam <UserTypeEntity> signature|sig
The `default` option sets `<EmailAddress>` as the default sendas address for the user.
For `treatasalias`, see: https://support.google.com/a/answer/1710338
When `<UserTypeEntity>` specifies an alias, the `primary` option causes the primary
email address signature rather than the alias signature to be set.

View File

@@ -1,10 +1,10 @@
\
# Version and Help
Print the current version of Gam with details
```
gam version
GAMADV-XTD3 6.66.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.66.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
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.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.66.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
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.03 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
GAMADV-XTD3 6.66.16 - https://github.com/taers232c/GAMADV-XTD3 - pythonsource
Ross Scroggs <ross.scroggs@gmail.com>
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.03
Latest: 6.66.16
echo $?
1
```
@@ -73,7 +73,7 @@ echo $?
Print the current version number without details
```
gam version simple
6.66.03
6.66.16
```
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.03 - https://github.com/taers232c/GAMADV-XTD3
GAM 6.66.16 - https://github.com/taers232c/GAMADV-XTD3
Ross Scroggs <ross.scroggs@gmail.com>
Python 3.12.0 64-bit final
MacOS Monterey 12.7 x86_64

View File

@@ -1457,7 +1457,7 @@ gam print alertfeedback [todrive <ToDriveAttribute>*] [alert <AlertID>] [filter
gam create|add alias|aliases <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
[verifynotinvitable]
gam update alias|aliases <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
[notargetverify]
[notargetverify] [waitafterdelete <Integer>]
gam delete alias|aliases [user|group|target] <EmailAddressEntity>
gam remove aliases|nicknames <EmailAddress> user|group <EmailAddressEntity>
gam <UserTypeEntity> delete alias|aliases
@@ -2475,7 +2475,7 @@ gam print crostelemetry [todrive <ToDriveAttribute>*]
gam create chromepolicyimage <ChromePolicyImageSchemaName> <FileName>
gam update chromepolicy
gam update chromepolicy [convertcrnl]
(<SchemaName> ((<Field> <Value>)+ | <JSONData>))+
ou|org|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
gam delete chromepolicy
@@ -6006,7 +6006,9 @@ gam <UserTypeEntity> copy drivefile <DriveFileEntity>
[summary [<Boolean>]] [showpermissionmessages [<Boolean>]]
[<DriveFileParentAttribute>]
[mergewithparent [<Boolean>]] [recursive [depth <Number>]]
[copysubfiles [<Boolean>]] [filenamematchpattern <RegularExpression>] [filemimetype [not] <MimeTypeList>]
[copysubfiles [<Boolean>]] [filenamematchpattern <RegularExpression>]
[filemimetype [not] <MimeTypeList>]
[copysubfilesownedby any|me|others]
[copysubfolders [<Boolean>]] [foldernamematchpattern <RegularExpression>]
[copysubshortcuts [<Boolean>]] [shortcutnamematchpattern <RegularExpression>]
<DriveFileCopyAttribute>*
@@ -6284,6 +6286,7 @@ gam <UserTypeEntity> collect orphans
canaddfolderfromanotherdrive|
canaddmydriveparent|
canchangecopyrequireswriterpermission|
canchangecopyrequireswriterpermissionrestriction|
canchangedomainusersonlyrestriction|
canchangedrivebackground|
canchangedrivemembersonlyrestriction|
@@ -6301,11 +6304,14 @@ gam <UserTypeEntity> collect orphans
canmanagemembers|
canmodifycontent|
canmodifycontentrestriction|
canmodifyeditorcontentrestriction|
canmodifylabels|
canmodifyownercontentrestriction|
canmovechildrenoutofdrive|
canmovechildrenoutofteamdrive|
canmovechildrenwithindrive|
canmovechildrenwithinteamdrive|
canmoveitemintodrive|
canmoveitemintoteamdrive|
canmoveitemoutofdrive|
canmoveitemoutofteamdrive|
@@ -6317,6 +6323,7 @@ gam <UserTypeEntity> collect orphans
canreadrevisions|
canreadteamdrive|
canremovechildren|
canremovecontentrestriction|
canremovemydriveparent|
canrename|
canrenamedrive|
@@ -6608,6 +6615,9 @@ gam <UserTypeEntity> show filetree
(orderby <DriveFileOrderByFieldName> [ascending|descending])* [delimiter <Character>]
[stripcrsfromname]
gam <UserTypeEntity> print fileparenttree <DriveFileEntity> [todrive <ToDriveAttribute>*]
[stripcrsfromname]
gam <UserTypeEntity> print filelist [todrive <ToDriveAttribute>*]
[((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>)
(querytime<String> <Time>)*]

View File

@@ -2,6 +2,104 @@
Merged GAM-Team version
6.66.16
Added option `convertcrnl` to `gam update chromepolicy` to properly handle carriage returns (\r) and line feeds (\n)
in value strings entered on the command line in the `<Field> <Value>` form.
```
gam update chromepolicy convertcrnl chrome.devices.DisabledDeviceReturnInstructions
deviceDisabledMessage "Please return device to:\nSchool\n123 Main Street\nAnytown US" ou /Path/to/OU
```
6.66.15
Added option `copysubfilesownedby any|me|others` to `gam <UserTypeEntity> copy drivefile` that allows
specification of which source folder sub files to copy based on file ownership; the default is `any`.
This only applies when files are being copied from a 'My Drive'.
6.66.14
Updated `gam <UserTypeEntity> modify messages` to recognize the following error:
```
ERROR: 400: invalid - Invalid label: SENT
```
Updated `gam update alias <EmailAddressEntity> user|group|target <EmailAddress>`
to avoid the following problem.
```
$ gam update alias testalias@domain.com user testuser
User Alias: testalias@domain.com, Deleted
User Alias: testalias@domain.com, User: testuser@domain.com, Update Failed: Duplicate, Email Address: testalias@domain.com
```
GAM updates an alias to point to a new target by deleting the alias and then recreating the alias pointing to the new target.
Unfortunately, if these commands are executed back-to-back; Google generates the `Update Failed: Duplicate` error.
Now, GAM waits 2 seconds between the delete and the insert which seems to eliminate the problem. If the problem persists,
the option `waitafterdelete <Integer>` can be used to increase the wait time to a maximum of 10 seconds.
6.66.13
Updated functionality of option `preservefiletimes` in `gam <UserTypeEntity> update drivefile <DriveFileEntity>`.
* Current
* `preservefiletimes localfile <FileName>` - `modifiedTime` of `<DriveFileEntity>` is set to that of `localfile <FileName>`
* `preservefiletimes` - No effect
* Updated
* `preservefiletimes localfile <FileName>` - `modifiedTime` of `<DriveFileEntity>` is set to that of `localfile <FileName>`
* `preservefiletimes` - `modifiedTime` of `<DriveFileEntity>` retains its current value
6.66.12
Upgraded to Python 3.12.1 where possible.
Updated all drive commands to handle the following error:
```
ERROR: 401: Active session is invalid. Error code: 4 - authError
```
This is due to the Drive SDK API being disabled in the user's OU.
* See: https://support.google.com/a/answer/6105699
6.66.11
Fixed/improved handling of shortcuts in `gam <UserTypeEntity> transfer drive`.
6.66.10
Updated `gam create datatransfer` to handle the following error:
```
ERROR: 401: Active session is invalid. Error code: 4 - authError
```
6.66.09
Fixed bug in `gam <UserTypeEntity> print filelist ... allfields` that caused a trap
when `gam.cfg` contained `drive_v3_native_names = False`.
6.66.08
Added additional columns `isBase` and `baseId` to `gam <UserTypeEntity> print fileparenttree`
to simplify processing the output in a script.
6.66.07
Fixed bug in `gam <UserTypeEntity> print diskusage` that caused a trap.
6.66.06
Added a command the print the parent tree of file/folder.
```
gam <UserTypeEntity> print fileparenttree <DriveFileEntity> [todrive <ToDriveAttribute>*]
[stripcrsfromname]
```
* See: https://github.com/taers232c/GAMADV-XTD3/wiki/Users-Drive-Files-Display#display-file-parent-tree
6.66.05
Added column `space.name` to `gam <UserTypeEntity> print chatmembers`.
6.66.04
Updated Chat info|show|print commands to display all time fields in local time if specified in `gam.cfg`.
6.66.03
Fixed bug in `gam <UserTypeEntity> print filelist select <DriveFileEntity>` where `stripcrsfromname` was not being

View File

@@ -370,7 +370,7 @@ YUBIKEY_VALUE_ERROR_RC = 85
YUBIKEY_MULTIPLE_CONNECTED_RC = 86
YUBIKEY_NOT_FOUND_RC = 87
# Multiprocessing lock
# Multiprocessing lock
mplock = None
# stdin/stdout/stderr
@@ -2521,7 +2521,7 @@ def entityBadRequestWarning(entityValueList, errMessage, i=0, count=0):
currentCountNL(i, count)))
def userSvcNotApplicableOrDriveDisabled(user, errMessage, i=0, count=0):
if errMessage.find('Drive apps') == -1:
if errMessage.find('Drive apps') == -1 and errMessage.find('Active session is invalid') == -1:
entityServiceNotApplicableWarning(Ent.USER, user, i, count)
else:
entityActionNotPerformedWarning([Ent.USER, user], errMessage, i, count)
@@ -5082,15 +5082,16 @@ def checkGAPIError(e, softErrors=False, retryOnHttpError=False, mapNotFound=True
systemErrorExit(HTTP_ERROR_RC, eContent)
if 'error' in error:
http_status = error['error']['code']
reason = ''
if 'errors' in error['error'] and 'message' in error['error']['errors'][0]:
message = error['error']['errors'][0]['message']
status = ''
if 'reason' in error['error']['errors'][0]:
reason = error['error']['errors'][0]['reason']
elif 'errors' in error['error'] and 'Unknown Error' in error['error']['message'] and 'reason' in error['error']['errors'][0]:
message = error['error']['errors'][0]['reason']
status = error['error'].get('status', '')
else:
message = error['error']['message']
status = error['error'].get('status', '')
status = error['error'].get('status', '')
lmessage = message.lower() if message is not None else ''
if http_status == 500:
if not lmessage or status == 'UNKNOWN':
@@ -5109,30 +5110,36 @@ 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 'active session is invalid' in lmessage and reason == 'authError':
message += ' Drive SDK API access disabled'
error = makeErrorDict(http_status, GAPI.AUTH_ERROR, message)
elif status == 'PERMISSION_DENIED':
error = makeErrorDict(http_status, GAPI.PERMISSION_DENIED, message)
elif http_status == 403:
if 'quota exceeded for quota metric' in lmessage:
error = makeErrorDict(http_status, GAPI.QUOTA_EXCEEDED, message)
@@ -7695,7 +7702,6 @@ class CSVPrintFile():
def MapDrive3TitlesToDrive2(self):
_mapDrive3TitlesToDrive2(self.titlesList, API.DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP)
_mapDrive3TitlesToDrive2(self.titlesList, API.DRIVE3_TO_DRIVE2_CAPABILITIES_TITLES_MAP)
self.titlesSet = set(self.titlesList)
def AddJSONTitle(self, title):
@@ -17327,7 +17333,7 @@ ALIAS_TARGET_TYPES = ['user', 'group', 'target']
# gam create aliases|nicknames <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
# [verifynotinvitable]
# gam update aliases|nicknames <EmailAddressEntity> user|group|target <UniqueID>|<EmailAddress>
# [notargetverify]
# [notargetverify] [waitafterdelete <Integer>]
def doCreateUpdateAliases():
def verifyAliasTargetExists():
if targetType != 'group':
@@ -17349,6 +17355,42 @@ def doCreateUpdateAliases():
GAPI.badRequest, GAPI.invalid, GAPI.systemError):
return None
def deleteAliasOnUpdate():
# User alias
if targetType != 'group':
try:
callGAPI(cd.users().aliases(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
userKey=aliasEmail, alias=aliasEmail)
printEntityKVList([Ent.USER_ALIAS, aliasEmail], [Act.PerformedName(Act.DELETE)], i, count)
time.sleep(waitAfterDelete)
return True
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail], str(e), i, count)
return False
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource):
if targetType == 'user':
entityUnknownWarning(Ent.USER_ALIAS, aliasEmail, i, count)
return False
# Group alias
try:
callGAPI(cd.groups().aliases(), 'delete',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
groupKey=aliasEmail, alias=aliasEmail)
time.sleep(waitAfterDelete)
return True
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail], str(e), i, count)
return False
except GAPI.forbidden:
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
return False
except (GAPI.groupNotFound, GAPI.badRequest, GAPI.invalid, GAPI.invalidResource):
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
return False
cd = buildGAPIObject(API.DIRECTORY)
ci = None
updateCmd = Act.Get() == Act.UPDATE
@@ -17358,12 +17400,15 @@ def doCreateUpdateAliases():
entityLists = targetEmails if isinstance(targetEmails, dict) else None
verifyNotInvitable = False
verifyTarget = updateCmd
waitAfterDelete = 2
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if (not updateCmd) and myarg == 'verifynotinvitable':
verifyNotInvitable = True
elif updateCmd and myarg == 'notargetverify':
verifyTarget = False
elif updateCmd and myarg == 'waitafterdelete':
waitAfterDelete = getInteger(minVal=2, maxVal=10)
else:
unknownArgumentExit()
i = 0
@@ -17388,30 +17433,9 @@ def doCreateUpdateAliases():
if targetType is None:
entityUnknownWarning(Ent.ALIAS_TARGET, targetEmail, i, count)
continue
if updateCmd:
try:
callGAPI(cd.users().aliases(), 'delete',
throwReasons=[GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
userKey=aliasEmail, alias=aliasEmail)
printEntityKVList([Ent.USER_ALIAS, aliasEmail], [Act.PerformedName(Act.DELETE)], i, count)
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.USER_ALIAS, aliasEmail], str(e), i, count)
continue
except (GAPI.userNotFound, GAPI.badRequest, GAPI.invalid, GAPI.forbidden, GAPI.invalidResource):
try:
callGAPI(cd.groups().aliases(), 'delete',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.BAD_REQUEST, GAPI.INVALID, GAPI.FORBIDDEN, GAPI.INVALID_RESOURCE,
GAPI.CONDITION_NOT_MET],
groupKey=aliasEmail, alias=aliasEmail)
except GAPI.conditionNotMet as e:
entityActionFailedWarning([Ent.GROUP_ALIAS, aliasEmail], str(e), i, count)
continue
except GAPI.forbidden:
entityUnknownWarning(Ent.GROUP_ALIAS, aliasEmail, i, count)
continue
except (GAPI.groupNotFound, GAPI.badRequest, GAPI.invalid, GAPI.invalidResource):
entityUnknownWarning(Ent.ALIAS, aliasEmail, i, count)
if updateCmd and not deleteAliasOnUpdate():
continue
# User alias
if targetType != 'group':
try:
callGAPI(cd.users().aliases(), 'insert',
@@ -17434,6 +17458,7 @@ def doCreateUpdateAliases():
if targetType == 'user':
entityUnknownWarning(Ent.ALIAS_TARGET, targetEmail, i, count)
continue
# Group alias
try:
callGAPI(cd.groups().aliases(), 'insert',
throwReasons=[GAPI.GROUP_NOT_FOUND, GAPI.USER_NOT_FOUND, GAPI.BAD_REQUEST,
@@ -24578,6 +24603,8 @@ def doPrintShowBrowsers():
csvPF.SetSortTitles(['deviceId'])
csvPF.writeCSVfile('Browsers')
BROWSER_TOKEN_TIME_OBJECTS = {'createTime', 'expireTime', 'revokeTime'}
def _showBrowserToken(browser, FJQC, i=0, count=0):
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(browser), ensure_ascii=False, sort_keys=True))
@@ -24633,8 +24660,6 @@ def doRevokeBrowserToken():
except GAPI.forbidden:
accessErrorExit(None)
BROWSER_TOKEN_TIME_OBJECTS = {'createTime', 'expireTime', 'revokeTime'}
BROWSER_TOKEN_FIELDS_CHOICE_MAP = {
'createtime': 'createTime',
'creatorid': 'creatorId',
@@ -24788,14 +24813,17 @@ def _cleanChatSpace(space):
space.pop('type', None)
space.pop('threaded', None)
CHAT_SPACE_TIME_OBJECTS = {'createTime'}
def _showChatSpace(space, FJQC, i=0, count=0):
_cleanChatSpace(space)
if FJQC.formatJSON:
printLine(json.dumps(cleanJSON(space), ensure_ascii=False, sort_keys=True))
printLine(json.dumps(cleanJSON(space, timeObjects=CHAT_SPACE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True))
return
printEntity([Ent.CHAT_SPACE, space['name']], i, count)
Ind.Increment()
showJSON(None, space)
showJSON(None, space, timeObjects=CHAT_SPACE_TIME_OBJECTS)
Ind.Decrement()
def getChatSpaceParameters(myarg, body, typeChoicesMap):
@@ -25059,7 +25087,7 @@ CHAT_PAGE_SIZE = 1000
def printShowChatSpaces(users):
def _printChatSpace(user, space):
_cleanChatSpace(space)
row = flattenJSON(space)
row = flattenJSON(space, timeObjects=CHAT_SPACE_TIME_OBJECTS)
if user is not None:
row['User'] = user
if not FJQC.formatJSON:
@@ -25067,7 +25095,7 @@ def printShowChatSpaces(users):
elif csvPF.CheckRowTitles(row):
row = {'User': user} if user is not None else {}
row.update({'name': space['name'],
'JSON': json.dumps(cleanJSON(space),
'JSON': json.dumps(cleanJSON(space, timeObjects=CHAT_SPACE_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
csvPF.WriteRowNoFilter(row)
@@ -25160,16 +25188,18 @@ def createChatMember(users):
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
if jcount == 0:
return
kvList.extend([entityType, ''])
Ind.Increment()
j = 0
for body in members:
j += 1
kvList[-1] = body[field]['name']
try:
member = callGAPI(chat.spaces().members(), 'create',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
parent=parent, body=body)
if role != 'ROLE_MEMBER':
if role != 'ROLE_MEMBER' and entityType == Ent.CHAT_MANAGER_USER:
member = callGAPI(chat.spaces().members(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
@@ -25184,7 +25214,7 @@ def createChatMember(users):
Ind.Decrement()
else:
writeStdout(f'{member["name"]}\n')
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
except (GAPI.alreadyExists, GAPI.notFound, GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning(kvList, str(e))
Ind.Decrement()
@@ -25221,6 +25251,7 @@ def createChatMember(users):
missingArgumentExit('space')
if not userList and not groupList:
missingArgumentExit('user|members|group|groups')
userEntityType = Ent.CHAT_MEMBER_USER if role == 'ROLE_MEMBER' else Ent.CHAT_MANAGER_USER
userMembers = []
for user in userList:
name = normalizeEmailAddressOrUID(user)
@@ -25232,13 +25263,15 @@ def createChatMember(users):
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MEMBERSHIPS, user, i, count, [Ent.CHAT_SPACE, parent, Ent.CHAT_MEMBER, ''])
user, chat, kvList = buildChatServiceObject(API.CHAT_MEMBERSHIPS, user, i, count, [Ent.CHAT_SPACE, parent])
if not chat:
continue
Ind.Increment()
if userMembers:
addMembers(userMembers, 'member', Ent.USER, i, count)
addMembers(userMembers, 'member', userEntityType, i, count)
if groupMembers:
addMembers(groupMembers, 'groupMember', Ent.GROUP, i, count)
addMembers(groupMembers, 'groupMember', Ent.CHAT_MEMBER_GROUP, i, count)
Ind.Decrement()
def _deleteChatMembers(chat, kvList, jcount, memberNames, i, count):
j = 0
@@ -25314,7 +25347,7 @@ def deleteUpdateChatMember(users):
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MEMBERSHIPS, user, i, count)
user, chat, kvList = buildChatServiceObject(API.CHAT_MEMBERSHIPS, user, i, count, [Ent.CHAT_SPACE, parent] if parent is not None else None)
if not chat:
continue
jcount = len(memberNames)
@@ -25351,12 +25384,12 @@ CHAT_SYNC_PREVIEW_TITLES = ['space', 'member', 'role', 'action', 'message']
# [preview [actioncsv]]
# (users <UserTypeEntity>)* (groups <GroupEntity>)*
def syncChatMembers(users):
def _previewAction(members, jcount, action):
def _previewAction(members, entityType, jcount, action):
Ind.Increment()
j = 0
for member in members:
j += 1
entityActionPerformed([Ent.CHAT_SPACE, parent, Ent.CHAT_MEMBER, member, Ent.ROLE, role], j, jcount)
entityActionPerformed([Ent.CHAT_SPACE, parent, entityType, member, Ent.ROLE, role], j, jcount)
Ind.Decrement()
if csvPF:
for member in members:
@@ -25364,11 +25397,11 @@ def syncChatMembers(users):
def addMembers(memberNames, members, entityType, i, count):
jcount = len(memberNames)
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_MEMBER, i, count)
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
if jcount == 0:
return
if preview:
_previewAction(memberNames, jcount, Act.REMOVE)
_previewAction(memberNames, entityType, jcount, Act.REMOVE)
return
kvList.extend([entityType, ''])
Ind.Increment()
@@ -25380,26 +25413,26 @@ def syncChatMembers(users):
try:
callGAPI(chat.spaces().members(), 'create',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
throwReasons=[GAPI.ALREADY_EXISTS, GAPI.NOT_FOUND, GAPI.INVALID, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
parent=parent, body=body)
if role != 'ROLE_MEMBER':
if role != 'ROLE_MEMBER' and entityType == Ent.CHAT_MANAGER_USER:
callGAPI(chat.spaces().members(), 'patch',
bailOnInternalError=True,
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED, GAPI.INTERNAL_ERROR],
name=memberName, updateMask='role', body={'role': role})
entityActionPerformed(kvList, j, jcount)
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
except (GAPI.alreadyExists, GAPI.notFound, GAPI.invalid, GAPI.invalidArgument, GAPI.permissionDenied, GAPI.internalError) as e:
entityActionFailedWarning(kvList, str(e), j, jcount)
Ind.Decrement()
del kvList[-2:]
def deleteMembers(memberNames, entityType, i, count):
jcount = len(memberNames)
entityPerformActionNumItems(kvList, jcount, Ent.CHAT_MEMBER, i, count)
entityPerformActionNumItems(kvList, jcount, entityType, i, count)
if jcount == 0:
return
if preview:
_previewAction(memberNames, jcount, Act.ADD)
_previewAction(memberNames, entityType, jcount, Act.ADD)
return
kvList.extend([entityType, ''])
Ind.Increment()
@@ -25408,7 +25441,6 @@ def syncChatMembers(users):
del kvList[-2:]
cd = buildGAPIObject(API.DIRECTORY)
kwargs = {}
parent = None
role = CHAT_MEMBER_ROLE_MAP['member']
mtype = CHAT_MEMBER_TYPE_MAP['human']
@@ -25447,6 +25479,7 @@ def syncChatMembers(users):
unknownArgumentExit()
if not parent:
missingArgumentExit('space')
userEntityType = Ent.CHAT_MEMBER_USER if role == 'ROLE_MEMBER' else Ent.CHAT_MANAGER_USER
userMembers = {}
syncUsersSet = set()
for user in userList:
@@ -25461,12 +25494,11 @@ def syncChatMembers(users):
memberName = f'{parent}/members/{name}'
groupMembers[memberName] = {'groupMember': {'name': f'groups/{name}'}}
syncGroupsSet.add(memberName)
kwargs['filter'] = f'member.type = "{mtype}" AND role = "{role}"'
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parent}, {kwargs["filter"]}'
qfilter = f'{Ent.Singular(Ent.CHAT_SPACE)}: {parent}'
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, chat, kvList = buildChatServiceObject(API.CHAT_MEMBERSHIPS, user, i, count)
user, chat, kvList = buildChatServiceObject(API.CHAT_MEMBERSHIPS, user, i, count, [Ent.CHAT_SPACE, parent])
if not chat:
continue
currentUsersSet = set()
@@ -25475,12 +25507,14 @@ def syncChatMembers(users):
members = callGAPIpages(chat.spaces().members(), 'list', 'memberships',
pageMessage=_getChatPageMessage(Ent.CHAT_MEMBER, user, i, count, qfilter),
throwReasons=[GAPI.NOT_FOUND, GAPI.INVALID_ARGUMENT, GAPI.PERMISSION_DENIED],
pageSize=CHAT_PAGE_SIZE, parent=parent, **kwargs)
pageSize=CHAT_PAGE_SIZE, parent=parent, showGroups=groupsSpecified)
for member in members:
_getChatMemberEmail(cd, member)
if 'member' in member:
currentUsersSet.add(f"{parent}/members/{member['member']['email']}")
if member['member']['type'] == mtype and member['role'] == role:
_getChatMemberEmail(cd, member)
currentUsersSet.add(f"{parent}/members/{member['member']['email']}")
elif 'groupMember' in member:
_getChatMemberEmail(cd, member)
currentGroupsSet.add(f"{parent}/members/{member['groupMember']['email']}")
except (GAPI.notFound, GAPI.invalidArgument, GAPI.permissionDenied) as e:
exitIfChatNotConfigured(chat, kvList, str(e), i, count)
@@ -25488,15 +25522,15 @@ def syncChatMembers(users):
if syncOperation != 'addonly':
Act.Set([Act.REMOVE, Act.REMOVE_PREVIEW][preview])
if usersSpecified:
deleteMembers(currentUsersSet-syncUsersSet, Ent.USER, i, count)
deleteMembers(currentUsersSet-syncUsersSet, userEntityType, i, count)
if groupsSpecified:
deleteMembers(currentGroupsSet-syncGroupsSet, Ent.GROUP, i, count)
deleteMembers(currentGroupsSet-syncGroupsSet, Ent.CHAT_MEMBER_GROUP, i, count)
if syncOperation != 'removeonly':
Act.Set([Act.ADD, Act.ADD_PREVIEW][preview])
if usersSpecified:
addMembers(syncUsersSet-currentUsersSet, userMembers, Ent.USER, i, count)
addMembers(syncUsersSet-currentUsersSet, userMembers, userEntityType, i, count)
if groupsSpecified:
addMembers(syncGroupsSet-currentGroupsSet, groupMembers, Ent.GROUP, i, count)
addMembers(syncGroupsSet-currentGroupsSet, groupMembers, Ent.CHAT_MEMBER_GROUP, i, count)
if csvPF:
csvPF.writeCSVfile('Chat Member Updates')
@@ -25554,17 +25588,18 @@ def printShowChatMembers(users):
row = flattenJSON(member, timeObjects=CHAT_MEMBER_TIME_OBJECTS)
if user is not None:
row['User'] = user
row['space.name'] = parent
if not FJQC.formatJSON:
csvPF.WriteRowTitles(row)
elif csvPF.CheckRowTitles(row):
row = {'User': user} if user is not None else {}
row = {'User': user, 'space.name': parent} if user is not None else {'space.name': parent}
row.update({'name': member['name'],
'JSON': json.dumps(cleanJSON(member, timeObjects=CHAT_MEMBER_TIME_OBJECTS),
ensure_ascii=False, sort_keys=True)})
csvPF.WriteRowNoFilter(row)
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['User', 'name'] if not isinstance(users, list) else ['name']) if Act.csvFormat() else None
csvPF = CSVPrintFile(['User', 'space.name', 'name'] if not isinstance(users, list) else ['space.name', 'name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
kwargs = {}
parent = None
@@ -25773,7 +25808,7 @@ def doDeleteChatMessage():
def _cleanChatMessage(message):
message.pop('cards', None)
CHAT_MESSAGE_TIME_OBJECTS = {'createTime'}
CHAT_MESSAGE_TIME_OBJECTS = {'createTime', 'deleteTime', 'lastUpdateTime'}
def _showChatMessage(message, FJQC, i=0, count=0):
_cleanChatMessage(message)
@@ -25844,7 +25879,7 @@ def printShowChatMessages(users):
csvPF.WriteRowNoFilter(row)
cd = buildGAPIObject(API.DIRECTORY)
csvPF = CSVPrintFile(['User', 'name'] if not isinstance(users, list) else ['name']) if Act.csvFormat() else None
csvPF = CSVPrintFile(['User', 'space.name', 'name'] if not isinstance(users, list) else ['space.name', 'name']) if Act.csvFormat() else None
FJQC = FormatJSONQuoteChar(csvPF)
parent = pfilter = None
showDeleted = False
@@ -26116,7 +26151,7 @@ CHROME_SCHEMA_TYPE_MESSAGE = {
CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN = re.compile(r'^([a-z]+)-(\d+)$')
CHROME_TARGET_VERSION_PATTERN = re.compile(r'^(\d{1,4}\.){1,4}$')
# gam update chromepolicy
# gam update chromepolicy [convertcrnl]
# (<SchemaName> ((<Field> <Value>)+ | <JSONData>))+
# ou|orgunit <OrgUnitItem> [(printerid <PrinterID>)|(appid <AppID>)]
def doUpdateChromePolicy():
@@ -26137,6 +26172,7 @@ def doUpdateChromePolicy():
app_id = channelMap = orgUnit = printer_id = None
body = {'requests': []}
schemaNameList = []
convertCRsNLs = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg in {'ou', 'org', 'orgunit'}:
@@ -26145,6 +26181,8 @@ def doUpdateChromePolicy():
printer_id = getString(Cmd.OB_PRINTER_ID)
elif myarg == 'appid':
app_id = getString(Cmd.OB_APP_ID)
elif myarg == 'convertcrnl':
convertCRsNLs = True
else:
schemaName, schema = simplifyChromeSchema(_getChromePolicySchema(cp, Cmd.Previous(), '*'))
body['requests'].append({'policyValue': {'policySchema': schemaName, 'value': {}},
@@ -26183,11 +26221,11 @@ def doUpdateChromePolicy():
value = field['value']
if vtype in ['TYPE_INT64', 'TYPE_INT32', 'TYPE_UINT64']:
value = int(value)
elif vtype in ['TYPE_BOOL']:
elif vtype == 'TYPE_BOOL':
pass
elif vtype in ['TYPE_ENUM']:
elif vtype == 'TYPE_ENUM':
value = f"{schema['settings'][lowerField]['enum_prefix']}{value}"
elif vtype in ['TYPE_LIST']:
elif vtype == 'TYPE_LIST':
value = value.split(',') if value else []
if myarg == 'chrome.users.chromebrowserupdates' and casedField == 'targetVersionPrefixSetting':
mg = CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN.match(value)
@@ -26237,7 +26275,7 @@ def doUpdateChromePolicy():
Cmd.Backup()
invalidArgumentExit(integerLimits(None, None))
value = int(value)
elif vtype in ['TYPE_BOOL']:
elif vtype == 'TYPE_BOOL':
value = value.lower()
if value in TRUE_VALUES:
value = True
@@ -26245,7 +26283,7 @@ def doUpdateChromePolicy():
value = False
else:
invalidChoiceExit(value, TRUE_FALSE, True)
elif vtype in ['TYPE_ENUM']:
elif vtype == 'TYPE_ENUM':
value = value.upper()
prefix = schema['settings'][field]['enum_prefix']
enum_values = schema['settings'][field]['enums']
@@ -26255,8 +26293,10 @@ def doUpdateChromePolicy():
pass
else:
invalidChoiceExit(value, enum_values, True)
elif vtype in ['TYPE_LIST']:
elif vtype == 'TYPE_LIST':
value = value.split(',') if value else []
elif vtype == 'TYPE_STRING' and convertCRsNLs:
value = unescapeCRsNLs(value)
if myarg == 'chrome.users.chromebrowserupdates' and casedField == 'targetVersionPrefixSetting':
mg = CHROME_TARGET_VERSION_CHANNEL_MINUS_PATTERN.match(value)
if mg:
@@ -50975,6 +51015,7 @@ DRIVE_CAPABILITIES_SUBFIELDS_CHOICE_MAP = {
'canaddfolderfromanotherdrive': 'canAddFolderFromAnotherDrive',
'canaddmydriveparent': 'canAddMyDriveParent',
'canchangecopyrequireswriterpermission': 'canChangeCopyRequiresWriterPermission',
'canchangecopyrequireswriterpermissionrestriction': 'canChangeCopyRequiresWriterPermissionRestriction',
'canchangedomainusersonlyrestriction': 'canChangeDomainUsersOnlyRestriction',
'canchangedrivebackground': 'canChangeDriveBackground',
'canchangedrivemembersonlyrestriction': 'canChangeDriveMembersOnlyRestriction',
@@ -50992,22 +51033,26 @@ DRIVE_CAPABILITIES_SUBFIELDS_CHOICE_MAP = {
'canmanagemembers': 'canManageMembers',
'canmodifycontent': 'canModifyContent',
'canmodifycontentrestriction': 'canModifyContentRestriction',
'canmodifyeditorcontentrestriction': 'canModifyEditorContentRestriction',
'canmodifylabels': 'canModifyLabels',
'canmodifyownercontentrestriction': 'canModifyOwnerContentRestriction',
'canmovechildrenoutofdrive': 'canMoveChildrenOutOfDrive',
'canmovechildrenoutofteamdrive': 'canMoveChildrenOutOfDrive',
'canmovechildrenwithindrive': 'canMoveChildrenWithinDrive',
'canmovechildrenwithinteamdrive': 'canMoveChildrenWithinDrive',
'canmoveitemintoteamdrive': 'canMoveItemOutOfDrive',
'canmoveitemintodrive': 'canMoveItemIntoDrive',
'canmoveitemintoteamdrive': 'canMoveItemIntoDrive',
'canmoveitemoutofdrive': 'canMoveItemOutOfDrive',
'canmoveitemoutofteamdrive': 'canMoveItemOutOfDrive',
'canmoveitemwithindrive': 'canMoveItemWithinDrive',
'canmoveitemwithinteamdrive': 'canMoveItemWithinDrive',
'canmoveteamdriveitem': ['canMoveItemOutOfDrive', 'canMoveItemWithinDrive'],
'canmoveteamdriveitem': 'canMoveTeamDriveItem',
'canreaddrive': 'canReadDrive',
'canreadlabels': 'canReadLabels',
'canreadrevisions': 'canReadRevisions',
'canreadteamdrive': 'canReadDrive',
'canremovechildren': 'canRemoveChildren',
'canremovecontentrestriction': 'canRemoveContentRestriction',
'canremovemydriveparent': 'canRemoveMyDriveParent',
'canrename': 'canRename',
'canrenamedrive': 'canRenameDrive',
@@ -53336,6 +53381,76 @@ def printShowFilePaths(users):
if csvPF:
csvPF.writeCSVfile('Drive File Paths')
# gam <UserTypeEntity> print fileparenttree <DriveFileEntity> [todrive <ToDriveAttribute>*]
# [stripcrsfromname]
def printFileParentTree(users):
fileNameTitle = 'title' if not GC.Values[GC.DRIVE_V3_NATIVE_NAMES] else 'name'
csvPF = CSVPrintFile(['Owner', 'isBase', 'baseId', 'id', fileNameTitle, 'parentId', 'depth', 'isRoot'], 'sortall')
fileIdEntity = getDriveFileEntity()
stripCRsFromName = False
while Cmd.ArgumentsRemaining():
myarg = getArgument()
if myarg == 'todrive':
csvPF.GetTodriveParameters()
elif myarg == 'stripcrsfromname':
stripCRsFromName = True
else:
unknownArgumentExit()
i, count, users = getEntityArgument(users)
for user in users:
i += 1
user, drive, jcount = _validateUserGetFileIDs(user, i, count, fileIdEntity, entityType=Ent.FILE_PARENT_TREE)
if jcount == 0:
continue
try:
rootId = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_USER_THROW_REASONS,
fileId=ROOT, fields='id')['id']
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
continue
j = 0
for fileId in fileIdEntity['list']:
j += 1
fileList = []
baseId = fileId
while True:
try:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='id,name,mimeType,parents,driveId', supportsAllDrives=True)
if stripCRsFromName:
result['name'] = _stripControlCharsFromName(result['name'])
result['isRoot'] = False
if not result.get('parents', []):
if fileId == rootId:
result['isRoot'] = True
else:
driveId = result.get('driveId')
if driveId:
if result['mimeType'] == MIMETYPE_GA_FOLDER and result['name'] == TEAM_DRIVE:
result['name'] = _getSharedDriveNameFromId(drive, driveId)
result['isRoot'] = True
result['parents'] = ['']
fileList.append(result)
break
fileList.append(result)
fileId = result['parents'][0]
except GAPI.fileNotFound:
entityActionFailedWarning([Ent.USER, user, Ent.DRIVE_FILE_OR_FOLDER_ID, fileId], Msg.DOES_NOT_EXIST, j, jcount)
break
except (GAPI.serviceNotAvailable, GAPI.authError, GAPI.domainPolicy) as e:
userSvcNotApplicableOrDriveDisabled(user, str(e), i, count)
break
kcount = len(fileList)
isBase = True
for result in fileList:
csvPF.WriteRow({'Owner': user, 'isBase': isBase, 'baseId': baseId, 'id': result['id'], fileNameTitle: result['name'],
'parentId': result['parents'][0], 'depth': kcount, 'isRoot': result['isRoot']})
isBase = False
kcount -= 1
csvPF.writeCSVfile('Drive File Parent Tree')
# gam <UserTypeEntity> print filecounts [todrive <ToDriveAttribute>*]
# [((query <QueryDriveFile>) | (fullquery <QueryDriveFile>) | <DriveFileQueryShortcut>) (querytime<String> <Time>)*]
# [corpora <CorporaAttribute>]
@@ -53588,6 +53703,7 @@ def printDiskUsage(users):
if stripCRsFromName:
childEntryInfo['name'] = _stripControlCharsFromName(childEntryInfo['name'])
childEntryInfo['path'] = fileEntry['path']+pathDelimiter+childEntryInfo['name']
childEntryInfo.pop(sizeField, None)
foldersList.append(childEntryInfo)
_getChildDriveFolderInfo(drive, childEntryInfo, user, i, count)
fileEntry['totalFileCount'] += childEntryInfo['totalFileCount']
@@ -53710,6 +53826,7 @@ def printDiskUsage(users):
topFolder.pop('ownedByMe', None)
topFolder.pop('parents', None)
topFolder.update(zeroFolderInfo)
topFolder.pop(sizeField, None)
foldersList.append(topFolder)
_getChildDriveFolderInfo(drive, topFolder, user, i, count)
except GAPI.fileNotFound:
@@ -54738,6 +54855,7 @@ def updateDriveFile(users):
addSheetEntity = None
updateSheetEntity = None
clearFilter = False
preserveModifiedTime = False
encoding = GC.Values[GC.CHARSET]
columnDelimiter = GC.Values[GC.CSV_INPUT_COLUMN_DELIMITER]
returnIdLink = None
@@ -54790,6 +54908,8 @@ def updateDriveFile(users):
addSheetEntity = updateSheetEntity = None
media_body = getMediaBody(parameters)
body['mimeType'] = parameters[DFA_LOCALMIMETYPE]
elif operation == 'update' and parameters[DFA_PRESERVE_FILE_TIMES]:
preserveModifiedTime = True
i, count, users = getEntityArgument(users)
for user in users:
i += 1
@@ -54811,15 +54931,17 @@ def updateDriveFile(users):
try:
addParents = addParentsBase[:]
removeParents = removeParentsBase[:]
if newParents or (not newName and parameters[DFA_REPLACEFILENAME]):
if newParents or (not newName and parameters[DFA_REPLACEFILENAME]) or preserveModifiedTime:
result = callGAPI(drive.files(), 'get',
throwReasons=GAPI.DRIVE_GET_THROW_REASONS,
fileId=fileId, fields='name,parents', supportsAllDrives=True)
fileId=fileId, fields='name,parents,modifiedTime', supportsAllDrives=True)
if newParents:
addParents.extend(newParents)
removeParents.extend(result.get('parents', []))
if not newName and parameters[DFA_REPLACEFILENAME]:
body['name'] = processFilenameReplacements(result['name'], parameters[DFA_REPLACEFILENAME])
if preserveModifiedTime:
body['modifiedTime'] = result['modifiedTime']
if newName:
if parameters[DFA_REPLACEFILENAME]:
body['name'] = processFilenameReplacements(newName, parameters[DFA_REPLACEFILENAME])
@@ -55146,6 +55268,7 @@ def initCopyMoveOptions(copyCmd):
'shortcutNameMatchPattern': None,
'fileMimeTypes': set(),
'notMimeTypes': False,
'copySubFilesOwnedBy': None,
}
DUPLICATE_FILE_CHOICES = {
@@ -55261,6 +55384,8 @@ def getCopyMoveOptions(myarg, copyMoveOptions):
copyMoveOptions['notMimeTypes'] = checkArgumentPresent('not')
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)
else:
return False
return True
@@ -55983,7 +56108,7 @@ def copyDriveFile(users):
entityActionFailedWarning(kvList+[Ent.DRIVE_FILE_SHORTCUT, childName], str(e), k, kcount)
_incrStatistic(statistics, STAT_FILE_FAILED)
def _checkChildCopyAllowed(childMimeType, childName):
def _checkChildCopyAllowed(childMimeType, childName, child):
if childMimeType == MIMETYPE_GA_FOLDER:
if not copyMoveOptions['copySubFolders']:
return False
@@ -55995,6 +56120,9 @@ 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['fileMimeTypes']:
if not copyMoveOptions['notMimeTypes']:
if childMimeType not in copyMoveOptions['fileMimeTypes']:
@@ -56026,7 +56154,7 @@ def copyDriveFile(users):
q=WITH_PARENTS.format(folderId),
orderBy='folder desc,name,modifiedTime desc',
fields='nextPageToken,files(id,name,parents,appProperties,capabilities,contentHints,copyRequiresWriterPermission,'\
'description,folderColorRgb,mimeType,modifiedTime,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare,'\
'description,folderColorRgb,mimeType,modifiedTime,ownedByMe,properties,starred,driveId,trashed,viewedByMeTime,writersCanShare,'\
'shortcutDetails(targetId,targetMimeType))',
pageSize=GC.Values[GC.DRIVE_MAX_RESULTS], **sourceSearchArgs)
kcount = len(sourceChildren)
@@ -56052,10 +56180,11 @@ def copyDriveFile(users):
if childId in copiedTargetFiles: # Don't recopy file/folder copied into a sub-folder
continue
kvList = [Ent.USER, user, _getEntityMimeType(child), childNameId]
if not _checkChildCopyAllowed(childMimeType, childName):
if not _checkChildCopyAllowed(childMimeType, childName, child):
if not suppressNotSelectedMessages:
entityActionNotPerformedWarning(kvList, Msg.NOT_SELECTED, k, kcount)
continue
child.pop('ownedByMe', None)
trashed = child.pop('trashed', False)
if (childId == newFolderId) or (excludeTrashed and trashed):
entityActionNotPerformedWarning(kvList,
@@ -57838,17 +57967,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', []):
@@ -57894,20 +58061,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)
@@ -57922,6 +58094,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', []):
@@ -58064,6 +58237,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':
@@ -58212,7 +58389,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)
@@ -58431,6 +58608,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',
@@ -58438,7 +58616,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
@@ -58465,7 +58643,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})'],
@@ -58473,9 +58651,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()
@@ -65341,20 +65521,27 @@ def _processMessagesThreads(users, entityType):
bcount = min(jcount-mcount, GC.Values[GC.MESSAGE_BATCH_SIZE])
while bcount > 0:
body['ids'] = messageIds[mcount:mcount+bcount]
idsCount = min(5, bcount)
idsList = ','.join(body['ids'][0:idsCount])
if bcount > 5:
idsList += ',...'
try:
callGAPI(gmail.users().messages(), function,
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID_MESSAGE_ID, GAPI.FAILED_PRECONDITION],
throwReasons=GAPI.GMAIL_THROW_REASONS+[GAPI.INVALID_MESSAGE_ID, GAPI.INVALID, GAPI.FAILED_PRECONDITION],
userId='me', body=body)
for messageId in body['ids']:
mcount += 1
entityActionPerformed([Ent.USER, user, entityType, messageId], mcount, jcount)
except (GAPI.serviceNotAvailable, GAPI.badRequest):
mcount += bcount
except GAPI.invalid as e:
entityActionFailedWarning([Ent.USER, user, entityType, idsList], f'{str(e)} ({mcount+1}-{mcount+bcount}/{jcount})')
mcount += bcount
except GAPI.invalidMessageId:
entityActionFailedWarning([Ent.USER, user, entityType, Msg.BATCH], f'{Msg.INVALID_MESSAGE_ID} ({mcount+1}-{mcount+bcount}/{jcount})')
entityActionFailedWarning([Ent.USER, user, entityType, idsList], f'{Msg.INVALID_MESSAGE_ID} ({mcount+1}-{mcount+bcount}/{jcount})')
mcount += bcount
except GAPI.failedPrecondition:
entityActionFailedWarning([Ent.USER, user, entityType, Msg.BATCH], f'{Msg.FAILED_PRECONDITION} ({mcount+1}-{mcount+bcount}/{jcount})')
entityActionFailedWarning([Ent.USER, user, entityType, idsList], f'{Msg.FAILED_PRECONDITION} ({mcount+1}-{mcount+bcount}/{jcount})')
mcount += bcount
bcount = min(jcount-mcount, GC.Values[GC.MESSAGE_BATCH_SIZE])
@@ -66752,7 +66939,8 @@ def printShowMessagesThreads(users, entityType):
if senderMatchPattern:
row['Sender'] = sender
if not show_size:
labelsMap.pop('size', None)
for label in labelsMap.values():
label.pop('size', None)
csvPF.WriteRowTitles(flattenJSON({'Labels': sorted(iter(labelsMap.values()), key=lambda k: k['name'])}, flattened=row))
elif not senderMatchPattern:
if not csvPF:
@@ -71848,6 +72036,7 @@ USER_COMMANDS_WITH_OBJECTS = {
Cmd.ARG_NOTE: printShowNotes,
Cmd.ARG_OTHERCONTACT: printShowUserPeopleOtherContacts,
Cmd.ARG_OUTOFOFFICE: printShowOutOfOffice,
Cmd.ARG_FILEPARENTTREE: printFileParentTree,
Cmd.ARG_PEOPLECONTACT: printShowUserPeopleContacts,
Cmd.ARG_PEOPLECONTACTGROUP: printShowUserPeopleContactGroups,
Cmd.ARG_PEOPLEPROFILE: printShowUserPeopleProfiles,

View File

@@ -49,6 +49,7 @@ class GamAction():
DELETE = 'dele'
DELETE_EMPTY = 'delm'
DELETE_PREVIEW = 'delp'
DELETE_SHORTCUT = 'desc'
DEPROVISION = 'depr'
DISABLE = 'disa'
DOWNLOAD = 'down'
@@ -160,11 +161,12 @@ 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)'],
DELETE_SHORTCUT: ['Deleted Shortcut', 'Delete Shortcut'],
DEPROVISION: ['Deprovisioned', 'Deprovision'],
DISABLE: ['Disabled', 'Disable'],
DOWNLOAD: ['Downloaded', 'Download'],

View File

@@ -691,14 +691,6 @@ DRIVE3_TO_DRIVE2_CAPABILITIES_NAMES_MAP = {
'canChangeViewersCanCopyContent': 'canChangeRestrictedDownload',
}
DRIVE3_TO_DRIVE2_CAPABILITIES_TITLES_MAP = {
'capabilities.canComment': 'canComment',
'capabilities.canReadRevisions': 'canReadRevisions',
'capabilities.canCopy': 'copyable',
'capabilities.canEdit': 'editable',
'capabilities.canShare': 'shareable',
}
DRIVE3_TO_DRIVE2_FILES_FIELDS_MAP = {
'allowFileDiscovery': 'withLink',
'createdTime': 'createdDate',

View File

@@ -577,6 +577,7 @@ class GamCLArgs():
ARG_FILEDRIVELABELS = 'filedrivelabels'
ARG_FILEINFO = 'fileinfo'
ARG_FILELIST = 'filelist'
ARG_FILEPARENTTREE = 'fileparenttree'
ARG_FILEPATH = 'filepath'
ARG_FILEPATHS = 'filepaths'
ARG_FILEREVISION = 'filerevision'

View File

@@ -84,7 +84,10 @@ class GamEntity():
CHANNEL_PRODUCT = 'chpr'
CHANNEL_SKU = 'chsk'
CHAT_BOT = 'chbo'
CHAT_MANAGER_USER = 'chgu'
CHAT_MEMBER = 'chme'
CHAT_MEMBER_GROUP = 'chmg'
CHAT_MEMBER_USER = 'chmu'
CHAT_MESSAGE = 'chms'
CHAT_MESSAGE_ID = 'chmi'
CHAT_SPACE = 'chsp'
@@ -207,6 +210,7 @@ class GamEntity():
FEATURE = 'feat'
FIELD = 'fiel'
FILE = 'file'
FILE_PARENT_TREE = 'fptr'
FILTER = 'filt'
FORM = 'form'
FORM_RESPONSE = 'frmr'
@@ -413,9 +417,12 @@ class GamEntity():
CHANNEL_PRODUCT: ['Channel Products', 'Channel Product'],
CHANNEL_SKU: ['Channel SKUs', 'Channel SKU'],
CHAT_BOT: ['Chat BOTs', 'Chat BOT'],
CHAT_MANAGER_USER: ['Chat User Managers', 'Chat User Manager'],
CHAT_MESSAGE: ['Chat Messages', 'Chat Message'],
CHAT_MESSAGE_ID: ['Chat Message IDs', 'Chat Message ID'],
CHAT_MEMBER: ['Chat Members', 'Chat Member'],
CHAT_MEMBER_GROUP: ['Chat Group Members', 'Chat Group Member'],
CHAT_MEMBER_USER: ['Chat User Members', 'Chat User Member'],
CHAT_SPACE: ['Chat Spaces', 'Chat Space'],
CHAT_THREAD: ['Chat Threads', 'Chat Thread'],
CHROME_APP: ['Chrome Applications', 'Chrome Application'],
@@ -536,6 +543,7 @@ class GamEntity():
FEATURE: ['Features', 'Feature'],
FIELD: ['Fields', 'Field'],
FILE: ['Files', 'File'],
FILE_PARENT_TREE: ['File Parent Trees', 'File Parent Tree'],
FILTER: ['Filters', 'Filter'],
FORM: ['Forms', 'Form'],
FORM_RESPONSE: ['Form Responses', 'Form Response'],

View File

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