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:
Daniel Weber 2026-02-06 19:42:39 -05:00 committed by GitHub
parent 60762e3619
commit e3cb940780
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 170 additions and 29 deletions

View File

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

View File

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

View File

@ -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...>`

View File

@ -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 = "-"
}

View File

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

View File

@ -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) {