- PostgreSQL metadata overlay layer on top of existing S3 storage - 3 new tables: folders, files, share_links - Folder CRUD: create, get with children, tree, rename, delete (cascade) - File operations: upload to folder, move between folders - Share links: create with optional password/expiry/download limit, public access - S3 compensation on PG write failure - Existing 14 endpoints untouched
140 lines
5.1 KiB
Markdown
140 lines
5.1 KiB
Markdown
# CLAUDE.md
|
|
|
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
|
|
> **Cross-repo rules** — see `/Users/wen/project/rag/CLAUDE.md` for full workspace conventions.
|
|
> - .NET backends share JWT key: `RagJwtSecretKey2026MustBeAtLeast32CharsLong!`
|
|
> - gRPC auth: call `rag-backend:50051` for token validation + permission checks
|
|
> - Other repos: `rag-backend` (5211), `im-system` (5212), `work-flow`, `rag-frontend` (5666 Vue)
|
|
|
|
## Build & Run
|
|
|
|
```bash
|
|
go run cmd/server/main.go # HTTP :8080
|
|
go test ./... # All tests
|
|
docker compose up -d # Via private registry
|
|
buf generate # Regenerate proto code
|
|
```
|
|
|
|
## Architecture
|
|
|
|
Go 1.25 microservice. Clean Architecture: `domain` ← `infrastructure` ← `api`.
|
|
|
|
```
|
|
cmd/server/main.go → Entry point, manual wiring
|
|
internal/domain/repository/ → Pure interfaces (FileRepository)
|
|
internal/infrastructure/
|
|
s3/ → S3 adapter (AWS SDK v2, RustFS/MinIO-compatible)
|
|
grpc/ → gRPC auth client (token validation via rag-backend)
|
|
mediator/ → Generic CQRS mediator (Go generics + reflect)
|
|
internal/api/
|
|
endpoints/ → Gin HTTP handlers (thin controllers)
|
|
handlers/ → CQRS command/query handlers (business logic)
|
|
requests/ → Request DTOs (form/json struct tags)
|
|
validators/ → Input validation + sanitization
|
|
internal/middleware/ → Auth, CORS, logging, rate limit, request ID, timeout
|
|
internal/common/ → Config (env-only), errors, logger, OTel, sanitization
|
|
api/proto/ → Protobuf definitions + generated Go code
|
|
```
|
|
|
|
## Code Patterns
|
|
|
|
### Generic Mediator (CQRS)
|
|
|
|
```go
|
|
// Define command/query struct
|
|
type UploadFileCommand struct { BucketName string; FileName string; Data io.Reader }
|
|
|
|
// Handler implements generic interface
|
|
type UploadFileHandler struct { Repo repository.FileRepository }
|
|
func (h *UploadFileHandler) Handle(ctx context.Context, cmd UploadFileCommand) (string, error) { ... }
|
|
|
|
// Register: mediator.Register[RequestType, ResponseType](m, handler)
|
|
mediator.Register[UploadFileCommand, string](m, uploadHandler)
|
|
|
|
// Dispatch: mediator.Send[RequestType, ResponseType](m, ctx, cmd)
|
|
result, err := mediator.Send[UploadFileCommand, string](e.Mediator, c.Request.Context(), cmd)
|
|
```
|
|
|
|
Commands named `XxxCommand`, queries named `XxxQuery`. Split across files: `file_commands.go`, `file_queries.go`, `bucket_commands.go`.
|
|
|
|
### Endpoint Pattern (4 steps)
|
|
|
|
```go
|
|
func (e *FileEndpoint) UploadFile(c *gin.Context) {
|
|
// 1. Bind request
|
|
var req requests.UploadFileRequest
|
|
if err := c.ShouldBind(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
// 2. Validate
|
|
if err := e.FileValidator.ValidateUpload(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
// 3. Send through mediator
|
|
result, err := mediator.Send[XxxCommand, string](e.Mediator, ctx, cmd)
|
|
if err != nil { handleError(c, err); return }
|
|
// 4. Respond
|
|
c.JSON(http.StatusOK, gin.H{"message": result})
|
|
}
|
|
```
|
|
|
|
### Error Handling
|
|
|
|
```go
|
|
// Domain errors
|
|
type BusinessException struct { Message string; Code int }
|
|
func NewBusinessError(msg string) *BusinessException // 400
|
|
func NewNotFoundError(msg string) *BusinessException // 404
|
|
|
|
// Endpoint error handler
|
|
func handleError(c *gin.Context, err error) {
|
|
if be, ok := err.(*common.BusinessException); ok {
|
|
c.JSON(be.Code, gin.H{"error": be.Message})
|
|
} else {
|
|
common.Logger.Error("unhandled error", "error", err, "path", c.Request.URL.Path)
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Request Struct Tags
|
|
|
|
- GET query params: `form:"bucket_name"` (bound via `c.ShouldBind`)
|
|
- JSON body: `json:"bucket_name"` (bound via `c.ShouldBindJSON`)
|
|
- File upload: `form:"file"` with `*multipart.FileHeader`
|
|
|
|
### Auth Middleware
|
|
|
|
Dual-mode (configured by `GRPC_AUTH_ADDR` env var):
|
|
- **With gRPC**: `JWTAuthMiddleware` → calls rag-backend's `ValidateToken` RPC with token caching (2min TTL)
|
|
- **Without gRPC**: `AuthMiddleware` → checks `X-API-Key` header against configured key
|
|
|
|
### Middleware Chain
|
|
|
|
```
|
|
gin.Default (Logger + Recovery)
|
|
→ OTel tracing
|
|
→ RequestID (X-Request-ID)
|
|
→ Logging (structured: method, path, status, duration_ms, request_id)
|
|
→ Timeout (30s context deadline)
|
|
→ CORS
|
|
→ Auth (API key or JWT)
|
|
```
|
|
|
|
### Input Sanitization
|
|
|
|
- `SanitizeObjectKey(key)` — blocks `..`, `//`, leading `/`
|
|
- `SanitizeBucketName(name)` — regex: `^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$`
|
|
- `SanitizeFilename(name)` — escapes quotes, strips CR/LF
|
|
|
|
### Testing
|
|
|
|
Standard `testing` package (no testify). Table-driven tests for sanitization. `httptest.NewRecorder` + `gin.TestMode` for middleware tests.
|
|
|
|
### Config
|
|
|
|
Environment variables only (12-factor). No YAML/JSON config files. Loaded in `internal/common/config.go`.
|