Skip to content

Commit 0baa278

Browse files
authored
feat: add caching infrastructure for Drive metadata (#83)
* ci: add test workflows, manual publish, and snap support - Add test-chocolatey.yml: validates Chocolatey packaging on PRs - Add test-winget.yml: validates Winget manifest schema on PRs - Add chocolatey-publish.yml: manual workflow to publish to Chocolatey - Add winget-publish.yml: manual workflow to publish to Winget - Add snap/snapcraft.yaml: Snap package definition - Add snap job to release.yml (disabled, awaiting personal-files approval) - Migrate Homebrew to goreleaser homebrew_casks (cleaner, includes xattr) - Remove manual update-homebrew job from release.yml * feat: add caching infrastructure for Drive metadata Add a caching layer to support fast repeated lookups for Drive API metadata (shared drive lists, etc). This is Phase 1 of shared drive support. New files: - internal/cache/cache.go: TTL-based cache implementation with GetDrives, SetDrives, Clear, and GetStatus methods - internal/cache/cache_test.go: Comprehensive tests for cache package - internal/cmd/config/cache.go: Cache management commands (show, clear, ttl) Modified files: - internal/config/config.go: Add Config struct, LoadConfig, SaveConfig, GetCacheTTL functions for persisting user settings - internal/cmd/config/config.go: Register cache subcommand - internal/cmd/initcmd/init.go: Add cache TTL prompt during setup - README.md: Document cache configuration and commands Features: - Configurable cache TTL (default 24 hours) set during `gro init` - `gro config cache show` - display cache status - `gro config cache clear` - clear all cached data - `gro config cache ttl <hours>` - update cache TTL - Cache stored in ~/.config/google-readonly/cache/ - Automatic expiration based on TTL
1 parent 56c73b0 commit 0baa278

7 files changed

Lines changed: 721 additions & 1 deletion

File tree

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ gro config test
167167
# Clear stored OAuth token
168168
gro config clear
169169

170+
# View cache status
171+
gro config cache show
172+
173+
# Clear cached data
174+
gro config cache clear
175+
176+
# Set cache TTL (in hours)
177+
gro config cache ttl 12
178+
170179
# Show version
171180
gro --version
172181
```
@@ -268,6 +277,33 @@ Remove stored OAuth token (forces re-authentication).
268277
Usage: gro config clear
269278
```
270279

280+
### gro config cache show
281+
282+
Display cache status including location, TTL, and cached data status.
283+
284+
```
285+
Usage: gro config cache show [flags]
286+
287+
Flags:
288+
-j, --json Output as JSON
289+
```
290+
291+
### gro config cache clear
292+
293+
Remove all cached data. Cache will be repopulated on next use.
294+
295+
```
296+
Usage: gro config cache clear
297+
```
298+
299+
### gro config cache ttl
300+
301+
Set the cache time-to-live in hours.
302+
303+
```
304+
Usage: gro config cache ttl <hours>
305+
```
306+
271307
### gro mail search
272308

273309
Search for Gmail messages using Gmail's search syntax.
@@ -685,6 +721,25 @@ Configuration files are stored in `~/.config/google-readonly/`:
685721
|------|-------------|
686722
| `credentials.json` | OAuth client credentials (from Google Cloud Console) |
687723
| `token.json` | OAuth access/refresh token (fallback if keychain unavailable) |
724+
| `config.json` | User settings (cache TTL, etc.) |
725+
| `cache/` | Cached API metadata for faster repeated lookups |
726+
727+
### Cache Settings
728+
729+
gro caches Drive metadata (like shared drive lists) to speed up repeated commands. The cache TTL is configured during `gro init` (default: 24 hours).
730+
731+
```bash
732+
# View cache status
733+
gro config cache show
734+
735+
# Clear cache
736+
gro config cache clear
737+
738+
# Change cache TTL
739+
gro config cache ttl 12 # Set to 12 hours
740+
```
741+
742+
The cache is automatically repopulated when stale or after being cleared.
688743

689744
## Security
690745

internal/cache/cache.go

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Package cache provides TTL-based caching for API metadata.
2+
package cache
3+
4+
import (
5+
"encoding/json"
6+
"os"
7+
"path/filepath"
8+
"time"
9+
10+
"github.com/open-cli-collective/google-readonly/internal/config"
11+
)
12+
13+
const (
14+
// DefaultTTLHours is the default cache TTL if not configured
15+
DefaultTTLHours = 24
16+
// CacheDir is the subdirectory within config for cache files
17+
CacheDir = "cache"
18+
// DrivesFile is the cache file for shared drives
19+
DrivesFile = "drives.json"
20+
)
21+
22+
// CachedDrive represents a cached shared drive entry
23+
type CachedDrive struct {
24+
ID string `json:"id"`
25+
Name string `json:"name"`
26+
}
27+
28+
// DriveCache represents the cached shared drives data
29+
type DriveCache struct {
30+
CachedAt time.Time `json:"cached_at"`
31+
TTLHours int `json:"ttl_hours"`
32+
Drives []*CachedDrive `json:"drives"`
33+
}
34+
35+
// Cache provides TTL-based caching for API metadata
36+
type Cache struct {
37+
dir string
38+
ttlHours int
39+
}
40+
41+
// New creates a new Cache instance
42+
func New(ttlHours int) (*Cache, error) {
43+
configDir, err := config.GetConfigDir()
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
cacheDir := filepath.Join(configDir, CacheDir)
49+
if err := os.MkdirAll(cacheDir, config.DirPerm); err != nil {
50+
return nil, err
51+
}
52+
53+
if ttlHours <= 0 {
54+
ttlHours = DefaultTTLHours
55+
}
56+
57+
return &Cache{
58+
dir: cacheDir,
59+
ttlHours: ttlHours,
60+
}, nil
61+
}
62+
63+
// GetDrives returns cached shared drives, or nil if cache is stale or missing
64+
func (c *Cache) GetDrives() ([]*CachedDrive, error) {
65+
path := filepath.Join(c.dir, DrivesFile)
66+
67+
data, err := os.ReadFile(path)
68+
if err != nil {
69+
if os.IsNotExist(err) {
70+
return nil, nil // Cache miss, not an error
71+
}
72+
return nil, err
73+
}
74+
75+
var cache DriveCache
76+
if err := json.Unmarshal(data, &cache); err != nil {
77+
// Corrupted cache, treat as miss
78+
return nil, nil
79+
}
80+
81+
// Check if cache is stale
82+
ttl := time.Duration(cache.TTLHours) * time.Hour
83+
if time.Since(cache.CachedAt) > ttl {
84+
return nil, nil // Cache expired
85+
}
86+
87+
return cache.Drives, nil
88+
}
89+
90+
// SetDrives updates the cached shared drives
91+
func (c *Cache) SetDrives(drives []*CachedDrive) error {
92+
cache := DriveCache{
93+
CachedAt: time.Now(),
94+
TTLHours: c.ttlHours,
95+
Drives: drives,
96+
}
97+
98+
data, err := json.MarshalIndent(cache, "", " ")
99+
if err != nil {
100+
return err
101+
}
102+
103+
path := filepath.Join(c.dir, DrivesFile)
104+
return os.WriteFile(path, data, config.TokenPerm)
105+
}
106+
107+
// Clear removes all cached data
108+
func (c *Cache) Clear() error {
109+
return os.RemoveAll(c.dir)
110+
}
111+
112+
// Status returns information about the cache state
113+
type Status struct {
114+
Dir string `json:"dir"`
115+
TTLHours int `json:"ttl_hours"`
116+
DrivesCache *FileInfo `json:"drives_cache,omitempty"`
117+
}
118+
119+
// FileInfo contains information about a cache file
120+
type FileInfo struct {
121+
Path string `json:"path"`
122+
CachedAt time.Time `json:"cached_at"`
123+
ExpiresAt time.Time `json:"expires_at"`
124+
IsStale bool `json:"is_stale"`
125+
Count int `json:"count"`
126+
}
127+
128+
// GetStatus returns the current cache status
129+
func (c *Cache) GetStatus() (*Status, error) {
130+
status := &Status{
131+
Dir: c.dir,
132+
TTLHours: c.ttlHours,
133+
}
134+
135+
// Check drives cache
136+
drivesPath := filepath.Join(c.dir, DrivesFile)
137+
data, err := os.ReadFile(drivesPath)
138+
if err == nil {
139+
var cache DriveCache
140+
if json.Unmarshal(data, &cache) == nil {
141+
ttl := time.Duration(cache.TTLHours) * time.Hour
142+
expiresAt := cache.CachedAt.Add(ttl)
143+
status.DrivesCache = &FileInfo{
144+
Path: drivesPath,
145+
CachedAt: cache.CachedAt,
146+
ExpiresAt: expiresAt,
147+
IsStale: time.Now().After(expiresAt),
148+
Count: len(cache.Drives),
149+
}
150+
}
151+
}
152+
153+
return status, nil
154+
}
155+
156+
// GetDir returns the cache directory path
157+
func (c *Cache) GetDir() string {
158+
return c.dir
159+
}

0 commit comments

Comments
 (0)