file-system/CLAUDE.md
向宁 3a18ca0579 feat: add directory structure and file sharing support
- 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
2026-05-20 20:26:19 +08:00

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`.