Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,29 @@ gro files tree <folder-id> --depth 3
gro drive tree --files # Include files, not just folders
```

#### Shared Drives

gro supports Google Shared Drives (formerly Team Drives). By default, search includes files from all drives you have access to.

```bash
# List available shared drives
gro drive drives
gro drive drives --json

# Search all drives (default)
gro drive search "quarterly report"

# Search only your personal drive
gro drive search "quarterly report" --my-drive

# Search a specific shared drive by name
gro drive search "budget" --drive "Finance Team"
gro drive list --drive "Engineering"
gro drive tree --drive "Marketing"
```

The `--my-drive` and `--drive` flags are mutually exclusive. Shared drive names are cached locally for fast lookups. Run `gro drive drives` to refresh the cache.

### gro drive list

List files in Google Drive root or a specific folder.
Expand All @@ -567,12 +590,14 @@ Aliases: gro files list
Flags:
-m, --max int Maximum number of files (default 25)
-t, --type string Filter by type (document, spreadsheet, presentation, folder, pdf, image, video, audio)
--my-drive List from My Drive only
--drive string List from specific shared drive (name or ID)
-j, --json Output as JSON
```

### gro drive search

Search for files in Google Drive.
Search for files in Google Drive. By default, searches all drives you have access to.

```
Usage: gro drive search [query] [flags]
Expand All @@ -586,6 +611,8 @@ Flags:
--modified-after string Modified after date (YYYY-MM-DD)
--modified-before string Modified before date (YYYY-MM-DD)
--in-folder string Search within folder ID
--my-drive Search only My Drive
--drive string Search specific shared drive (name or ID)
-m, --max int Maximum results (default 25)
-j, --json Output as JSON
```
Expand Down Expand Up @@ -636,9 +663,25 @@ Aliases: gro files tree
Flags:
-d, --depth int Maximum depth to traverse (default 2)
--files Include files in addition to folders
--my-drive Show My Drive only (default)
--drive string Show tree from specific shared drive
-j, --json Output as JSON
```

### gro drive drives

List all shared drives accessible to you.

```
Usage: gro drive drives [flags]

Aliases: gro files drives

Flags:
--refresh Force refresh from API (ignore cache)
-j, --json Output as JSON
```

## Search Query Reference

gro supports all Gmail search operators:
Expand Down
11 changes: 10 additions & 1 deletion internal/cmd/drive/drive.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,28 @@ This command group provides Google Drive functionality:
- get: Get detailed metadata for a file
- download: Download files or export Google Docs
- tree: Display folder structure
- drives: List accessible shared drives

Shared Drive Support:
By default, search includes files from all drives (My Drive + shared drives).
Use --my-drive to limit to personal drive, or --drive <name> to target a
specific shared drive.

Examples:
gro drive list
gro drive search "quarterly report"
gro drive search "budget" --drive "Finance Team"
gro drive get <file-id>
gro drive download <file-id> --format pdf`,
gro drive download <file-id> --format pdf
gro drive drives`,
}

cmd.AddCommand(newListCommand())
cmd.AddCommand(newSearchCommand())
cmd.AddCommand(newGetCommand())
cmd.AddCommand(newDownloadCommand())
cmd.AddCommand(newTreeCommand())
cmd.AddCommand(newDrivesCommand())

return cmd
}
179 changes: 179 additions & 0 deletions internal/cmd/drive/drives.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
package drive

import (
"fmt"
"os"
"strings"
"text/tabwriter"

"github.com/spf13/cobra"

"github.com/open-cli-collective/google-readonly/internal/cache"
"github.com/open-cli-collective/google-readonly/internal/config"
"github.com/open-cli-collective/google-readonly/internal/drive"
)

func newDrivesCommand() *cobra.Command {
var (
jsonOutput bool
refresh bool
)

cmd := &cobra.Command{
Use: "drives",
Short: "List shared drives",
Long: `List all Google Shared Drives accessible to you.

Results are cached locally for fast lookups. Use --refresh to force a refresh.

Examples:
gro drive drives # List shared drives (uses cache)
gro drive drives --refresh # Force refresh from API
gro drive drives --json # Output as JSON`,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
client, err := newDriveClient()
if err != nil {
return fmt.Errorf("failed to create Drive client: %w", err)
}

// Initialize cache
ttl := config.GetCacheTTLHours()
c, err := cache.New(ttl)
if err != nil {
return fmt.Errorf("failed to initialize cache: %w", err)
}

var drives []*drive.SharedDrive

// Try cache first unless refresh requested
if !refresh {
cached, err := c.GetDrives()
if err != nil {
return fmt.Errorf("failed to read cache: %w", err)
}
if cached != nil {
// Convert from cache type to drive type
drives = make([]*drive.SharedDrive, len(cached))
for i, d := range cached {
drives[i] = &drive.SharedDrive{
ID: d.ID,
Name: d.Name,
}
}
}
}

// Fetch from API if no cache hit
if drives == nil {
drives, err = client.ListSharedDrives(100)
if err != nil {
return fmt.Errorf("failed to list shared drives: %w", err)
}

// Update cache
cached := make([]*cache.CachedDrive, len(drives))
for i, d := range drives {
cached[i] = &cache.CachedDrive{
ID: d.ID,
Name: d.Name,
}
}
if err := c.SetDrives(cached); err != nil {
// Non-fatal: warn but continue
fmt.Fprintf(os.Stderr, "Warning: failed to update cache: %v\n", err)
}
}

if len(drives) == 0 {
fmt.Println("No shared drives found.")
return nil
}

if jsonOutput {
return printJSON(drives)
}

printSharedDrives(drives)
return nil
},
}

cmd.Flags().BoolVarP(&jsonOutput, "json", "j", false, "Output results as JSON")
cmd.Flags().BoolVar(&refresh, "refresh", false, "Force refresh from API (ignore cache)")

return cmd
}

// printSharedDrives prints shared drives in a formatted table
func printSharedDrives(drives []*drive.SharedDrive) {
w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0)
_, _ = fmt.Fprintln(w, "ID\tNAME")

for _, d := range drives {
_, _ = fmt.Fprintf(w, "%s\t%s\n", d.ID, d.Name)
}

_ = w.Flush()
}

// resolveDriveScope converts command flags to a DriveScope, resolving drive names via cache
func resolveDriveScope(client drive.DriveClientInterface, myDrive bool, driveFlag string) (drive.DriveScope, error) {
// --my-drive flag
if myDrive {
return drive.DriveScope{MyDriveOnly: true}, nil
}

// No --drive flag: search all drives
if driveFlag == "" {
return drive.DriveScope{AllDrives: true}, nil
}

// --drive flag provided: resolve name or use as ID
// If it looks like a drive ID (starts with 0A), use directly
if looksLikeDriveID(driveFlag) {
return drive.DriveScope{DriveID: driveFlag}, nil
}

// Try to resolve name via cache
ttl := config.GetCacheTTLHours()
c, err := cache.New(ttl)
if err != nil {
return drive.DriveScope{}, fmt.Errorf("failed to initialize cache: %w", err)
}

cached, _ := c.GetDrives()
if cached == nil {
// Cache miss - fetch from API
drives, err := client.ListSharedDrives(100)
if err != nil {
return drive.DriveScope{}, fmt.Errorf("failed to list shared drives: %w", err)
}

// Update cache
cached = make([]*cache.CachedDrive, len(drives))
for i, d := range drives {
cached[i] = &cache.CachedDrive{
ID: d.ID,
Name: d.Name,
}
}
_ = c.SetDrives(cached) // Ignore cache write errors
}

// Find by name (case-insensitive)
nameLower := strings.ToLower(driveFlag)
for _, d := range cached {
if strings.ToLower(d.Name) == nameLower {
return drive.DriveScope{DriveID: d.ID}, nil
}
}

return drive.DriveScope{}, fmt.Errorf("shared drive not found: %s", driveFlag)
}

// looksLikeDriveID returns true if the string appears to be a Drive ID
// Shared drive IDs typically start with "0A"
func looksLikeDriveID(s string) bool {
return len(s) > 10 && strings.HasPrefix(s, "0A")
}
Loading
Loading