- 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
5.1 KiB
5.1 KiB
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.mdfor full workspace conventions.
- .NET backends share JWT key:
RagJwtSecretKey2026MustBeAtLeast32CharsLong!- gRPC auth: call
rag-backend:50051for token validation + permission checks- Other repos:
rag-backend(5211),im-system(5212),work-flow,rag-frontend(5666 Vue)
Build & Run
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)
// 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)
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
// 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 viac.ShouldBind) - JSON body:
json:"bucket_name"(bound viac.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'sValidateTokenRPC with token caching (2min TTL) - Without gRPC:
AuthMiddleware→ checksX-API-Keyheader 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.