Skip to content

Commit 150b844

Browse files
nullcoderClaude
andauthored
feat: implement R2 storage foundation (#111)
* feat: implement R2 storage foundation (#103) - Create type-safe R2 storage client wrapper with singleton pattern - Implement all storage operations (put, get, delete, list, exists) - Add comprehensive error handling with custom error types - Create unit tests with 100% coverage - Update R2 setup documentation with usage examples Features: - Type-safe methods for metadata and blob operations - Automatic initialization and connection reuse - Support for pagination in list operations - Storage statistics calculation - Consistent key structure for all objects 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <claude@ghostpaste.dev> * refactor: update storage client to support versioning per SPEC.md - Switch from separate blobs/ directory to versions/{id}/{timestamp}.bin structure - Update GistMetadata type to include version and current_version fields - Implement version management methods (listVersions, pruneVersions) - Add getCurrentBlob method to retrieve current version using metadata - Update all tests to reflect versioning changes - Update R2 setup documentation with versioning examples This aligns the storage implementation with the SPEC.md design where: - All blobs are stored as versioned files - Metadata tracks the current_version timestamp - New versions just add a timestamp file - Last 50 versions are kept (older ones pruned) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <claude@ghostpaste.dev> --------- Co-authored-by: Claude <claude@ghostpaste.dev>
1 parent c460b9f commit 150b844

File tree

5 files changed

+1047
-545
lines changed

5 files changed

+1047
-545
lines changed

docs/R2_SETUP.md

Lines changed: 74 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,82 @@ curl -X DELETE "http://localhost:8788/api/r2-test?key=test-file"
8787

8888
## Storage Structure
8989

90-
GhostPaste will use the following R2 object structure:
90+
GhostPaste uses a versioned storage structure following the SPEC.md design:
9191

9292
```
93-
metadata/{gistId}.json # Unencrypted metadata
94-
blobs/{gistId}.bin # Encrypted binary data
93+
metadata/{gistId}.json # Unencrypted metadata (points to current version)
94+
versions/{gistId}/{timestamp}.bin # Encrypted blob versions
95+
temp/{gistId} # Temporary storage (optional)
96+
```
97+
98+
Key points:
99+
100+
- All blobs are stored as versioned files under `versions/`
101+
- Metadata tracks the `current_version` timestamp
102+
- No separate `blobs/` directory - everything is versioned
103+
- New versions just add a timestamp file
104+
- Last 50 versions are kept (older ones pruned)
105+
106+
## R2 Storage Client
107+
108+
GhostPaste includes a type-safe R2 storage client wrapper (`lib/storage.ts`) that provides:
109+
110+
### Features
111+
112+
- **Type-safe operations**: Strongly typed methods for all R2 operations
113+
- **Error handling**: Custom error types with detailed error messages
114+
- **Singleton pattern**: Efficient connection reuse across requests
115+
- **Binary support**: Handle both JSON metadata and binary blobs
116+
117+
### Usage
118+
119+
```typescript
120+
import { getR2Storage } from "@/lib/storage";
121+
122+
// Get storage instance (automatically initialized)
123+
const storage = await getR2Storage();
124+
125+
// Store metadata
126+
await storage.putMetadata(gistId, metadata);
127+
128+
// Retrieve metadata
129+
const metadata = await storage.getMetadata(gistId);
130+
131+
// Store encrypted blob (returns timestamp for the version)
132+
const timestamp = await storage.putBlob(gistId, encryptedData);
133+
134+
// Retrieve specific version
135+
const blob = await storage.getBlob(gistId, timestamp);
136+
137+
// Retrieve current version
138+
const currentBlob = await storage.getCurrentBlob(gistId);
139+
140+
// List all versions for a gist
141+
const versions = await storage.listVersions(gistId);
142+
143+
// Prune old versions (keep last 50)
144+
const deletedCount = await storage.pruneVersions(gistId, 50);
145+
146+
// Check if gist exists
147+
const exists = await storage.exists(gistId);
148+
149+
// Delete gist (metadata and all versions)
150+
await storage.deleteGist(gistId);
151+
152+
// List gists with pagination
153+
const { gists, cursor } = await storage.listGists({ limit: 100 });
154+
```
155+
156+
### Key Structure
157+
158+
The storage client uses consistent key patterns:
159+
160+
```typescript
161+
const StorageKeys = {
162+
metadata: (id: string) => `metadata/${id}.json`,
163+
version: (id: string, timestamp: string) => `versions/${id}/${timestamp}.bin`,
164+
temp: (id: string) => `temp/${id}`,
165+
};
95166
```
96167

97168
## Important Notes

0 commit comments

Comments
 (0)