feat(drive): add --domain flag to share command (#192)
* feat(drive): add --domain flag to share command Allow sharing files/folders with an entire Google Workspace domain (e.g. `gog drive share <id> --domain=example.com --role=writer`). This creates a "domain" type permission in the Drive API, enabling the "Anyone in <org> with the link" sharing mode. Also updates `permissions` output to display domain names. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(drive): add --to target for share (#192) (thanks @Danielkweber) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
parent
60762e3619
commit
e3cb940780
@ -5,7 +5,7 @@
|
||||
### Added
|
||||
|
||||
- Gmail: add `--exclude-labels` to `watch serve` (defaults: `SPAM,TRASH`). (#194) — thanks @salmonumbrella.
|
||||
|
||||
- Drive: share files with an entire Workspace domain via `drive share --to domain`. (#192) — thanks @Danielkweber.
|
||||
## 0.9.0 - 2026-01-22
|
||||
|
||||
### Highlights
|
||||
|
||||
10
README.md
10
README.md
@ -777,11 +777,11 @@ gog drive rename <fileId> "New Name"
|
||||
gog drive move <fileId> --parent <destinationFolderId>
|
||||
gog drive delete <fileId> # Move to trash
|
||||
|
||||
# Permissions
|
||||
gog drive permissions <fileId>
|
||||
gog drive share <fileId> --email user@example.com --role reader
|
||||
gog drive share <fileId> --email user@example.com --role writer
|
||||
gog drive unshare <fileId> --permission-id <permissionId>
|
||||
# Permissions
|
||||
gog drive permissions <fileId>
|
||||
gog drive share <fileId> --to user --email user@example.com --role reader
|
||||
gog drive share <fileId> --to user --email user@example.com --role writer
|
||||
gog drive unshare <fileId> --permission-id <permissionId>
|
||||
|
||||
# Shared drives (Team Drives)
|
||||
gog drive drives --max 100
|
||||
|
||||
@ -174,7 +174,7 @@ Flag aliases:
|
||||
- `gog drive delete <fileId>`
|
||||
- `gog drive move <fileId> --parent ID`
|
||||
- `gog drive rename <fileId> <newName>`
|
||||
- `gog drive share <fileId> [--anyone | --email addr] [--role reader|writer] [--discoverable]`
|
||||
- `gog drive share <fileId> --to anyone|user|domain [--email addr] [--domain example.com] [--role reader|writer] [--discoverable]`
|
||||
- `gog drive permissions <fileId> [--max N] [--page TOKEN]`
|
||||
- `gog drive unshare <fileId> <permissionId>`
|
||||
- `gog drive url <fileIds...>`
|
||||
|
||||
@ -40,6 +40,13 @@ const (
|
||||
extPptx = ".pptx"
|
||||
extPNG = ".png"
|
||||
extTXT = ".txt"
|
||||
|
||||
driveShareToAnyone = "anyone"
|
||||
driveShareToUser = "user"
|
||||
driveShareToDomain = "domain"
|
||||
|
||||
drivePermRoleReader = "reader"
|
||||
drivePermRoleWriter = "writer"
|
||||
)
|
||||
|
||||
type DriveCmd struct {
|
||||
@ -572,8 +579,10 @@ func (c *DriveRenameCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
|
||||
type DriveShareCmd struct {
|
||||
FileID string `arg:"" name:"fileId" help:"File ID"`
|
||||
Anyone bool `name:"anyone" help:"Make publicly accessible"`
|
||||
Email string `name:"email" help:"Share with specific user"`
|
||||
To string `name:"to" help:"Share target: anyone|user|domain"`
|
||||
Anyone bool `name:"anyone" hidden:"" help:"(deprecated) Use --to=anyone"`
|
||||
Email string `name:"email" help:"User email (for --to=user)"`
|
||||
Domain string `name:"domain" help:"Domain (for --to=domain; e.g. example.com)"`
|
||||
Role string `name:"role" help:"Permission: reader|writer" default:"reader"`
|
||||
Discoverable bool `name:"discoverable" help:"Allow file discovery in search (anyone/domain only)"`
|
||||
}
|
||||
@ -589,14 +598,58 @@ func (c *DriveShareCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
return usage("empty fileId")
|
||||
}
|
||||
|
||||
if !c.Anyone && strings.TrimSpace(c.Email) == "" {
|
||||
return usage("must specify --anyone or --email")
|
||||
to := strings.TrimSpace(c.To)
|
||||
email := strings.TrimSpace(c.Email)
|
||||
domain := strings.TrimSpace(c.Domain)
|
||||
|
||||
// Back-compat: allow legacy target flags without --to, but keep it unambiguous.
|
||||
// New UX: prefer explicit --to + matching parameter.
|
||||
if to == "" {
|
||||
switch {
|
||||
case c.Anyone && email == "" && domain == "":
|
||||
to = driveShareToAnyone
|
||||
case !c.Anyone && email != "" && domain == "":
|
||||
to = driveShareToUser
|
||||
case !c.Anyone && email == "" && domain != "":
|
||||
to = driveShareToDomain
|
||||
case !c.Anyone && email == "" && domain == "":
|
||||
return usage("must specify --to (anyone|user|domain)")
|
||||
default:
|
||||
return usage("ambiguous share target (use --to=anyone|user|domain)")
|
||||
}
|
||||
}
|
||||
|
||||
switch to {
|
||||
case driveShareToAnyone:
|
||||
if email != "" || domain != "" {
|
||||
return usage("--to=anyone cannot be combined with --email or --domain")
|
||||
}
|
||||
case driveShareToUser:
|
||||
if email == "" {
|
||||
return usage("missing --email for --to=user")
|
||||
}
|
||||
if domain != "" || c.Anyone {
|
||||
return usage("--to=user cannot be combined with --anyone or --domain")
|
||||
}
|
||||
if c.Discoverable {
|
||||
return usage("--discoverable is only valid for --to=anyone or --to=domain")
|
||||
}
|
||||
case driveShareToDomain:
|
||||
if domain == "" {
|
||||
return usage("missing --domain for --to=domain")
|
||||
}
|
||||
if email != "" || c.Anyone {
|
||||
return usage("--to=domain cannot be combined with --anyone or --email")
|
||||
}
|
||||
default:
|
||||
// Should be guarded by enum, but keep a friendly message for future changes.
|
||||
return usage("invalid --to (expected anyone|user|domain)")
|
||||
}
|
||||
role := strings.TrimSpace(c.Role)
|
||||
if role == "" {
|
||||
role = "reader"
|
||||
role = drivePermRoleReader
|
||||
}
|
||||
if role != "reader" && role != "writer" {
|
||||
if role != drivePermRoleReader && role != drivePermRoleWriter {
|
||||
return usage("invalid --role (expected reader|writer)")
|
||||
}
|
||||
|
||||
@ -606,18 +659,23 @@ func (c *DriveShareCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
}
|
||||
|
||||
perm := &drive.Permission{Role: role}
|
||||
if c.Anyone {
|
||||
switch to {
|
||||
case driveShareToAnyone:
|
||||
perm.Type = "anyone"
|
||||
perm.AllowFileDiscovery = c.Discoverable
|
||||
} else {
|
||||
case driveShareToDomain:
|
||||
perm.Type = "domain"
|
||||
perm.Domain = domain
|
||||
perm.AllowFileDiscovery = c.Discoverable
|
||||
default:
|
||||
perm.Type = "user"
|
||||
perm.EmailAddress = strings.TrimSpace(c.Email)
|
||||
perm.EmailAddress = email
|
||||
}
|
||||
|
||||
created, err := svc.Permissions.Create(fileID, perm).
|
||||
SupportsAllDrives(true).
|
||||
SendNotificationEmail(false).
|
||||
Fields("id, type, role, emailAddress").
|
||||
Fields("id, type, role, emailAddress, domain, allowFileDiscovery").
|
||||
Context(ctx).
|
||||
Do()
|
||||
if err != nil {
|
||||
@ -713,7 +771,7 @@ func (c *DrivePermissionsCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
|
||||
call := svc.Permissions.List(fileID).
|
||||
SupportsAllDrives(true).
|
||||
Fields("nextPageToken, permissions(id, type, role, emailAddress)").
|
||||
Fields("nextPageToken, permissions(id, type, role, emailAddress, domain)").
|
||||
Context(ctx)
|
||||
if c.Max > 0 {
|
||||
call = call.PageSize(c.Max)
|
||||
@ -744,6 +802,9 @@ func (c *DrivePermissionsCmd) Run(ctx context.Context, flags *RootFlags) error {
|
||||
fmt.Fprintln(w, "ID\tTYPE\tROLE\tEMAIL")
|
||||
for _, p := range resp.Permissions {
|
||||
email := p.EmailAddress
|
||||
if email == "" && p.Domain != "" {
|
||||
email = p.Domain
|
||||
}
|
||||
if email == "" {
|
||||
email = "-"
|
||||
}
|
||||
|
||||
@ -107,13 +107,63 @@ func TestDriveCommands_MoreCoverage(t *testing.T) {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
case r.Method == http.MethodPost && strings.HasSuffix(path, "/permissions"):
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "perm1",
|
||||
"type": "user",
|
||||
"role": "reader",
|
||||
"emailAddress": "share@example.com",
|
||||
})
|
||||
return
|
||||
var req map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "bad json", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
typ, _ := req["type"].(string)
|
||||
role, _ := req["role"].(string)
|
||||
if role == "" {
|
||||
role = "reader"
|
||||
}
|
||||
|
||||
switch typ {
|
||||
case "user":
|
||||
email, _ := req["emailAddress"].(string)
|
||||
if email == "" {
|
||||
http.Error(w, "missing emailAddress", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"id": "perm1",
|
||||
"type": "user",
|
||||
"role": role,
|
||||
"emailAddress": email,
|
||||
})
|
||||
return
|
||||
case "domain":
|
||||
domain, _ := req["domain"].(string)
|
||||
if domain == "" {
|
||||
http.Error(w, "missing domain", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
resp := map[string]any{
|
||||
"id": "perm1",
|
||||
"type": "domain",
|
||||
"role": role,
|
||||
"domain": domain,
|
||||
}
|
||||
if afd, ok := req["allowFileDiscovery"].(bool); ok {
|
||||
resp["allowFileDiscovery"] = afd
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
case "anyone":
|
||||
resp := map[string]any{
|
||||
"id": "perm1",
|
||||
"type": "anyone",
|
||||
"role": role,
|
||||
}
|
||||
if afd, ok := req["allowFileDiscovery"].(bool); ok {
|
||||
resp["allowFileDiscovery"] = afd
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
return
|
||||
default:
|
||||
http.Error(w, "invalid type", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
case r.Method == http.MethodDelete && strings.Contains(path, "/permissions/"):
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
@ -191,11 +241,16 @@ func TestDriveCommands_MoreCoverage(t *testing.T) {
|
||||
t.Fatalf("unexpected rename output: %q", out)
|
||||
}
|
||||
|
||||
out = run("--json", "--account", "a@b.com", "drive", "share", "file1", "--email", "share@example.com")
|
||||
out = run("--json", "--account", "a@b.com", "drive", "share", "file1", "--to", "user", "--email", "share@example.com")
|
||||
if !strings.Contains(out, "\"permissionId\"") {
|
||||
t.Fatalf("unexpected share json: %q", out)
|
||||
}
|
||||
|
||||
out = run("--json", "--account", "a@b.com", "drive", "share", "file1", "--to", "domain", "--domain", "example.com", "--role", "writer")
|
||||
if !strings.Contains(out, "\"permissionId\"") {
|
||||
t.Fatalf("unexpected domain share json: %q", out)
|
||||
}
|
||||
|
||||
out = run("--force", "--account", "a@b.com", "drive", "unshare", "file1", "perm1")
|
||||
if !strings.Contains(out, "removed") {
|
||||
t.Fatalf("unexpected unshare output: %q", out)
|
||||
|
||||
@ -15,14 +15,39 @@ func TestDriveCommand_ValidationErrors(t *testing.T) {
|
||||
}
|
||||
|
||||
shareCmd := &DriveShareCmd{}
|
||||
if err := runKong(t, shareCmd, []string{"file1"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "must specify") {
|
||||
t.Fatalf("expected share validation error, got %v", err)
|
||||
if err := runKong(t, shareCmd, []string{"file1"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "must specify --to") {
|
||||
t.Fatalf("expected share target error, got %v", err)
|
||||
}
|
||||
|
||||
shareCmd = &DriveShareCmd{}
|
||||
if err := runKong(t, shareCmd, []string{"file1", "--anyone", "--role", "owner"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "invalid --role") {
|
||||
if err := runKong(t, shareCmd, []string{"file1", "--to", "domain", "--domain", "example.com", "--role", "owner"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "invalid --role") {
|
||||
t.Fatalf("expected role error for domain share, got %v", err)
|
||||
}
|
||||
|
||||
shareCmd = &DriveShareCmd{}
|
||||
if err := runKong(t, shareCmd, []string{"file1", "--to", "anyone", "--role", "owner"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "invalid --role") {
|
||||
t.Fatalf("expected role error, got %v", err)
|
||||
}
|
||||
|
||||
shareCmd = &DriveShareCmd{}
|
||||
if err := runKong(t, shareCmd, []string{"file1", "--to", "user"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "missing --email") {
|
||||
t.Fatalf("expected missing email error, got %v", err)
|
||||
}
|
||||
|
||||
shareCmd = &DriveShareCmd{}
|
||||
if err := runKong(t, shareCmd, []string{"file1", "--to", "domain"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "missing --domain") {
|
||||
t.Fatalf("expected missing domain error, got %v", err)
|
||||
}
|
||||
|
||||
shareCmd = &DriveShareCmd{}
|
||||
if err := runKong(t, shareCmd, []string{"file1", "--to", "user", "--email", "a@b.com", "--discoverable"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "discoverable") {
|
||||
t.Fatalf("expected discoverable error, got %v", err)
|
||||
}
|
||||
|
||||
shareCmd = &DriveShareCmd{}
|
||||
if err := runKong(t, shareCmd, []string{"file1", "--email", "a@b.com", "--domain", "example.com"}, context.Background(), flags); err == nil || !strings.Contains(err.Error(), "ambiguous") {
|
||||
t.Fatalf("expected ambiguous target error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDriveDeleteUnshare_NoInput(t *testing.T) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user