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

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

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: domaininfrastructureapi.

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