Comprehensive review covering: - Chunked upload implementation critique with fixes - API design improvements (batch ops, pagination, error format) - Error handling & observability (tracing, structured logging) - Reliability patterns (retry, circuit breaker, health checks) - AI & automation friendliness (idempotency, error codes) - Security considerations (audit logging, token scopes) - Performance optimizations (caching, compression) - Phased implementation roadmap
1014 lines
26 KiB
Markdown
1014 lines
26 KiB
Markdown
# Gitea Architecture Review & Suggestions
|
|
|
|
**Reviewer:** Senior Architect
|
|
**Date:** January 2026
|
|
**Scope:** Full codebase review with focus on API, services, and recent chunked upload implementation
|
|
|
|
---
|
|
|
|
## Executive Summary
|
|
|
|
Gitea demonstrates a **well-designed, pragmatic architecture** suitable for a mature self-hosted Git platform. The codebase follows clean layered patterns (routers → services → models) with good separation of concerns. However, there are opportunities to improve **operational observability**, **reliability**, **developer experience**, and **AI/automation friendliness**.
|
|
|
|
This document presents findings organized by priority and effort level.
|
|
|
|
---
|
|
|
|
## Table of Contents
|
|
|
|
1. [Chunked Upload Implementation Review](#1-chunked-upload-implementation-review)
|
|
2. [API Design Improvements](#2-api-design-improvements)
|
|
3. [Error Handling & Observability](#3-error-handling--observability)
|
|
4. [Reliability & Resilience](#4-reliability--resilience)
|
|
5. [Developer Experience](#5-developer-experience)
|
|
6. [AI & Automation Friendliness](#6-ai--automation-friendliness)
|
|
7. [Security Considerations](#7-security-considerations)
|
|
8. [Performance Optimizations](#8-performance-optimizations)
|
|
9. [Implementation Roadmap](#9-implementation-roadmap)
|
|
|
|
---
|
|
|
|
## 1. Chunked Upload Implementation Review
|
|
|
|
### What Was Done Well
|
|
|
|
- Clean separation: model (`upload_session.go`), service (`chunked.go`), handler (`upload.go`)
|
|
- Resumable uploads with session status tracking
|
|
- Automatic cleanup via cron job
|
|
- Follows existing Gitea patterns for API handlers and models
|
|
|
|
### Issues to Address
|
|
|
|
#### 1.1 Missing Chunk Verification (HIGH)
|
|
|
|
**Problem:** No checksum verification for uploaded chunks. Corrupted chunks will produce corrupted files.
|
|
|
|
**Suggestion:**
|
|
```go
|
|
// Add to UploadSession model
|
|
type UploadSession struct {
|
|
// ... existing fields
|
|
ChecksumAlgorithm string `xorm:"DEFAULT 'sha256'"` // sha256, md5, none
|
|
ExpectedChecksum string `xorm:""` // Full file checksum
|
|
}
|
|
|
|
// Add to chunk upload
|
|
type ChunkMetadata struct {
|
|
ChunkNumber int64 `json:"chunk_number"`
|
|
Checksum string `json:"checksum"` // Chunk checksum
|
|
Size int64 `json:"size"`
|
|
}
|
|
```
|
|
|
|
#### 1.2 No Concurrent Upload Protection (MEDIUM)
|
|
|
|
**Problem:** Same chunk can be uploaded multiple times concurrently, causing race conditions.
|
|
|
|
**Suggestion:**
|
|
```go
|
|
// Add file locking or atomic operations
|
|
func SaveChunk(ctx context.Context, session *UploadSession, chunkNum int64, ...) error {
|
|
lockPath := session.GetChunkPath(chunkNum) + ".lock"
|
|
lock, err := acquireFileLock(lockPath)
|
|
if err != nil {
|
|
return fmt.Errorf("chunk %d is being uploaded by another request", chunkNum)
|
|
}
|
|
defer lock.Release()
|
|
// ... save chunk
|
|
}
|
|
```
|
|
|
|
#### 1.3 Missing Progress Webhook (MEDIUM)
|
|
|
|
**Problem:** No way to notify external systems of upload progress.
|
|
|
|
**Suggestion:**
|
|
```go
|
|
// Emit webhook on upload milestones
|
|
if session.ChunksReceived % 10 == 0 || session.IsComplete() {
|
|
notify_service.UploadProgress(ctx, session)
|
|
}
|
|
```
|
|
|
|
#### 1.4 No Rate Limiting (MEDIUM)
|
|
|
|
**Problem:** Unlimited chunk uploads could exhaust server resources.
|
|
|
|
**Suggestion:**
|
|
```go
|
|
// Add rate limiting middleware for upload endpoints
|
|
m.Group("/uploads", func() {
|
|
// ... routes
|
|
}, rateLimit(100, time.Minute)) // 100 chunks per minute per user
|
|
```
|
|
|
|
#### 1.5 Temp File Cleanup on Failure (LOW)
|
|
|
|
**Problem:** If `AssembleChunks` fails partway through, temp files may not be cleaned up.
|
|
|
|
**Suggestion:**
|
|
```go
|
|
func AssembleChunks(...) (*Attachment, error) {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
cleanupTempFiles(session)
|
|
panic(r)
|
|
}
|
|
}()
|
|
// ... existing logic
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 2. API Design Improvements
|
|
|
|
### 2.1 Standardized Error Response Format (HIGH)
|
|
|
|
**Current State:** Error responses vary in format across endpoints.
|
|
|
|
**Suggestion:** Adopt RFC 7807 Problem Details format:
|
|
|
|
```go
|
|
// modules/structs/error.go
|
|
type APIError struct {
|
|
Type string `json:"type"` // URI reference
|
|
Title string `json:"title"` // Short summary
|
|
Status int `json:"status"` // HTTP status code
|
|
Detail string `json:"detail"` // Explanation
|
|
Instance string `json:"instance,omitempty"` // Request URI
|
|
Code string `json:"code"` // Machine-readable code
|
|
Errors []ValidationError `json:"errors,omitempty"` // Field-level errors
|
|
}
|
|
|
|
// Example response:
|
|
{
|
|
"type": "https://gitea.io/errors/validation",
|
|
"title": "Validation Failed",
|
|
"status": 422,
|
|
"detail": "The provided repository name is invalid",
|
|
"code": "ERR_VALIDATION_FAILED",
|
|
"errors": [
|
|
{"field": "name", "message": "must be alphanumeric", "code": "ERR_INVALID_CHARS"}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 2.2 Batch Operations API (HIGH)
|
|
|
|
**Current State:** No batch endpoints for bulk operations.
|
|
|
|
**Suggestion:** Add batch endpoints for common operations:
|
|
|
|
```
|
|
POST /api/v1/repos/{owner}/{repo}/issues/batch
|
|
POST /api/v1/repos/{owner}/{repo}/labels/batch
|
|
POST /api/v1/admin/users/batch
|
|
```
|
|
|
|
```go
|
|
// Example batch request
|
|
{
|
|
"operations": [
|
|
{"action": "close", "issue_ids": [1, 2, 3]},
|
|
{"action": "add_label", "issue_ids": [1, 2], "label_id": 5}
|
|
]
|
|
}
|
|
|
|
// Example batch response
|
|
{
|
|
"results": [
|
|
{"index": 0, "success": true, "affected": 3},
|
|
{"index": 1, "success": false, "error": {"code": "ERR_NOT_FOUND", "detail": "Label 5 not found"}}
|
|
],
|
|
"summary": {"total": 2, "succeeded": 1, "failed": 1}
|
|
}
|
|
```
|
|
|
|
### 2.3 Cursor-Based Pagination (MEDIUM)
|
|
|
|
**Current State:** Offset-based pagination can be slow for large datasets.
|
|
|
|
**Suggestion:** Add cursor-based pagination option:
|
|
|
|
```
|
|
GET /api/v1/repos/{owner}/{repo}/issues?cursor=eyJpZCI6MTAwfQ&limit=50
|
|
```
|
|
|
|
```go
|
|
type PaginatedResponse struct {
|
|
Data []any `json:"data"`
|
|
NextCursor string `json:"next_cursor,omitempty"`
|
|
PrevCursor string `json:"prev_cursor,omitempty"`
|
|
HasMore bool `json:"has_more"`
|
|
Total int64 `json:"total,omitempty"` // Optional, expensive to compute
|
|
}
|
|
```
|
|
|
|
### 2.4 Field Selection (GraphQL-lite) (MEDIUM)
|
|
|
|
**Current State:** All fields returned on every request.
|
|
|
|
**Suggestion:** Add field selection parameter:
|
|
|
|
```
|
|
GET /api/v1/repos/{owner}/{repo}?fields=id,name,description,stars_count
|
|
```
|
|
|
|
```go
|
|
// Reduces payload and database load
|
|
func filterFields(obj any, fields []string) map[string]any {
|
|
// Return only requested fields
|
|
}
|
|
```
|
|
|
|
### 2.5 API Versioning Header (LOW)
|
|
|
|
**Current State:** No API version negotiation.
|
|
|
|
**Suggestion:** Add version header support:
|
|
|
|
```
|
|
Accept: application/vnd.gitea.v2+json
|
|
X-Gitea-API-Version: 2
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Error Handling & Observability
|
|
|
|
### 3.1 Centralized Error Catalog (HIGH)
|
|
|
|
**Current State:** 100+ custom error types scattered across packages.
|
|
|
|
**Suggestion:** Create centralized error registry:
|
|
|
|
```go
|
|
// modules/errors/catalog.go
|
|
package errors
|
|
|
|
type ErrorCode string
|
|
|
|
const (
|
|
// Authentication errors (1xxx)
|
|
ErrAuthRequired ErrorCode = "ERR_1001"
|
|
ErrInvalidToken ErrorCode = "ERR_1002"
|
|
ErrTokenExpired ErrorCode = "ERR_1003"
|
|
|
|
// Authorization errors (2xxx)
|
|
ErrForbidden ErrorCode = "ERR_2001"
|
|
ErrNotRepoMember ErrorCode = "ERR_2002"
|
|
|
|
// Resource errors (3xxx)
|
|
ErrUserNotFound ErrorCode = "ERR_3001"
|
|
ErrRepoNotFound ErrorCode = "ERR_3002"
|
|
ErrIssueNotFound ErrorCode = "ERR_3003"
|
|
|
|
// Validation errors (4xxx)
|
|
ErrInvalidInput ErrorCode = "ERR_4001"
|
|
ErrNameTooLong ErrorCode = "ERR_4002"
|
|
|
|
// Upload errors (5xxx)
|
|
ErrUploadSessionExpired ErrorCode = "ERR_5001"
|
|
ErrChunkMissing ErrorCode = "ERR_5002"
|
|
ErrFileTooLarge ErrorCode = "ERR_5003"
|
|
)
|
|
|
|
var errorMessages = map[ErrorCode]string{
|
|
ErrAuthRequired: "Authentication required",
|
|
ErrInvalidToken: "Invalid or malformed token",
|
|
// ...
|
|
}
|
|
|
|
func (e ErrorCode) Error() string { return errorMessages[e] }
|
|
func (e ErrorCode) HTTPStatus() int { /* return appropriate status */ }
|
|
```
|
|
|
|
### 3.2 Request Tracing (HIGH)
|
|
|
|
**Current State:** No correlation IDs for request tracing.
|
|
|
|
**Suggestion:** Add OpenTelemetry integration:
|
|
|
|
```go
|
|
// modules/web/middleware/tracing.go
|
|
func TracingMiddleware() func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
traceID := r.Header.Get("X-Request-ID")
|
|
if traceID == "" {
|
|
traceID = uuid.New().String()
|
|
}
|
|
|
|
ctx := context.WithValue(r.Context(), TraceIDKey, traceID)
|
|
w.Header().Set("X-Request-ID", traceID)
|
|
|
|
// Add to structured logging
|
|
log := log.WithField("trace_id", traceID)
|
|
ctx = context.WithValue(ctx, LoggerKey, log)
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3.3 Structured Logging (MEDIUM)
|
|
|
|
**Current State:** Mix of structured and unstructured logging.
|
|
|
|
**Suggestion:** Standardize on JSON structured logs:
|
|
|
|
```go
|
|
// All log entries should include:
|
|
{
|
|
"timestamp": "2024-01-08T12:00:00Z",
|
|
"level": "info",
|
|
"trace_id": "abc-123",
|
|
"user_id": 42,
|
|
"repo_id": 100,
|
|
"action": "create_issue",
|
|
"duration_ms": 45,
|
|
"message": "Issue created successfully"
|
|
}
|
|
```
|
|
|
|
### 3.4 Metrics Endpoint Enhancement (MEDIUM)
|
|
|
|
**Current State:** Basic Prometheus metrics.
|
|
|
|
**Suggestion:** Add business metrics:
|
|
|
|
```go
|
|
// Metrics to add:
|
|
gitea_api_requests_total{method, endpoint, status}
|
|
gitea_api_request_duration_seconds{method, endpoint}
|
|
gitea_upload_sessions_active
|
|
gitea_upload_bytes_total
|
|
gitea_webhook_deliveries_total{status, hook_type}
|
|
gitea_webhook_delivery_duration_seconds
|
|
gitea_repository_operations_total{operation}
|
|
```
|
|
|
|
---
|
|
|
|
## 4. Reliability & Resilience
|
|
|
|
### 4.1 Webhook Retry with Exponential Backoff (HIGH)
|
|
|
|
**Current State:** Limited retry logic visible.
|
|
|
|
**Suggestion:**
|
|
|
|
```go
|
|
// services/webhook/deliver.go
|
|
type RetryConfig struct {
|
|
MaxRetries int // Default: 5
|
|
InitialBackoff time.Duration // Default: 1s
|
|
MaxBackoff time.Duration // Default: 5m
|
|
BackoffFactor float64 // Default: 2.0
|
|
}
|
|
|
|
func DeliverWithRetry(ctx context.Context, task *HookTask, cfg RetryConfig) error {
|
|
var lastErr error
|
|
backoff := cfg.InitialBackoff
|
|
|
|
for attempt := 0; attempt <= cfg.MaxRetries; attempt++ {
|
|
err := Deliver(ctx, task)
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
lastErr = err
|
|
|
|
// Log retry
|
|
log.Warn("Webhook delivery failed, retrying",
|
|
"task_id", task.ID,
|
|
"attempt", attempt+1,
|
|
"next_backoff", backoff)
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
case <-time.After(backoff):
|
|
}
|
|
|
|
backoff = time.Duration(float64(backoff) * cfg.BackoffFactor)
|
|
if backoff > cfg.MaxBackoff {
|
|
backoff = cfg.MaxBackoff
|
|
}
|
|
}
|
|
|
|
// Move to dead letter queue
|
|
if err := moveToDeadLetterQueue(ctx, task, lastErr); err != nil {
|
|
log.Error("Failed to move to DLQ", "task_id", task.ID, "error", err)
|
|
}
|
|
|
|
return fmt.Errorf("webhook delivery failed after %d attempts: %w", cfg.MaxRetries, lastErr)
|
|
}
|
|
```
|
|
|
|
### 4.2 Circuit Breaker for External Services (MEDIUM)
|
|
|
|
**Current State:** No circuit breaker pattern.
|
|
|
|
**Suggestion:**
|
|
|
|
```go
|
|
// modules/circuitbreaker/breaker.go
|
|
type CircuitBreaker struct {
|
|
name string
|
|
maxFailures int
|
|
resetTimeout time.Duration
|
|
state State // Closed, Open, HalfOpen
|
|
failures int
|
|
lastFailure time.Time
|
|
}
|
|
|
|
func (cb *CircuitBreaker) Execute(fn func() error) error {
|
|
if cb.state == Open {
|
|
if time.Since(cb.lastFailure) > cb.resetTimeout {
|
|
cb.state = HalfOpen
|
|
} else {
|
|
return ErrCircuitOpen
|
|
}
|
|
}
|
|
|
|
err := fn()
|
|
if err != nil {
|
|
cb.recordFailure()
|
|
return err
|
|
}
|
|
|
|
cb.recordSuccess()
|
|
return nil
|
|
}
|
|
|
|
// Usage for webhook delivery, OAuth providers, etc.
|
|
var webhookBreaker = NewCircuitBreaker("webhook", 5, time.Minute)
|
|
```
|
|
|
|
### 4.3 Graceful Degradation (MEDIUM)
|
|
|
|
**Current State:** Services fail hard on dependency failures.
|
|
|
|
**Suggestion:** Add fallback behaviors:
|
|
|
|
```go
|
|
// Example: Search indexer fallback
|
|
func SearchIssues(ctx context.Context, query string) ([]Issue, error) {
|
|
if indexer.IsHealthy() {
|
|
return indexer.Search(ctx, query)
|
|
}
|
|
|
|
// Fallback to database search (slower but works)
|
|
log.Warn("Search indexer unavailable, falling back to DB search")
|
|
return db.SearchIssuesByTitle(ctx, query)
|
|
}
|
|
```
|
|
|
|
### 4.4 Health Check Endpoints (LOW)
|
|
|
|
**Current State:** Basic health check.
|
|
|
|
**Suggestion:** Add detailed health checks:
|
|
|
|
```
|
|
GET /api/v1/health
|
|
GET /api/v1/health/live # Kubernetes liveness
|
|
GET /api/v1/health/ready # Kubernetes readiness
|
|
```
|
|
|
|
```json
|
|
{
|
|
"status": "healthy",
|
|
"version": "1.24.0",
|
|
"checks": {
|
|
"database": {"status": "up", "latency_ms": 2},
|
|
"redis": {"status": "up", "latency_ms": 1},
|
|
"search_indexer": {"status": "degraded", "message": "High latency"},
|
|
"storage": {"status": "up", "free_space_gb": 450}
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 5. Developer Experience
|
|
|
|
### 5.1 OpenAPI Spec Improvements (HIGH)
|
|
|
|
**Current State:** Swagger docs generated from code comments.
|
|
|
|
**Suggestion:** Enhance with examples and better descriptions:
|
|
|
|
```go
|
|
// swagger:operation POST /repos/{owner}/{repo}/issues repository createIssue
|
|
// ---
|
|
// summary: Create an issue
|
|
// description: |
|
|
// Creates a new issue in the repository. The authenticated user must have
|
|
// write access to the repository.
|
|
//
|
|
// ## Example
|
|
// ```json
|
|
// {"title": "Bug report", "body": "Description here", "labels": [1, 2]}
|
|
// ```
|
|
// consumes:
|
|
// - application/json
|
|
// produces:
|
|
// - application/json
|
|
// parameters:
|
|
// - name: body
|
|
// in: body
|
|
// schema:
|
|
// "$ref": "#/definitions/CreateIssueOption"
|
|
// example:
|
|
// title: "Bug: Login fails"
|
|
// body: "When I try to login..."
|
|
// labels: [1, 2]
|
|
// responses:
|
|
// "201":
|
|
// description: Issue created
|
|
// schema:
|
|
// "$ref": "#/definitions/Issue"
|
|
// examples:
|
|
// application/json:
|
|
// id: 1
|
|
// title: "Bug: Login fails"
|
|
// state: "open"
|
|
```
|
|
|
|
### 5.2 SDK Generation (MEDIUM)
|
|
|
|
**Current State:** No official SDK libraries.
|
|
|
|
**Suggestion:** Generate SDKs from OpenAPI spec:
|
|
|
|
```bash
|
|
# Generate SDKs
|
|
openapi-generator generate -i api/v1/swagger.json -g python -o sdk/python
|
|
openapi-generator generate -i api/v1/swagger.json -g typescript-axios -o sdk/typescript
|
|
openapi-generator generate -i api/v1/swagger.json -g go -o sdk/go
|
|
```
|
|
|
|
### 5.3 CLI Tool Enhancement (MEDIUM)
|
|
|
|
**Current State:** Basic CLI for Git operations.
|
|
|
|
**Suggestion:** Add API CLI tool:
|
|
|
|
```bash
|
|
# Proposed gitea-cli commands
|
|
gitea-cli auth login --server https://gitea.example.com
|
|
gitea-cli repo list
|
|
gitea-cli repo create myrepo --private
|
|
gitea-cli issue create --title "Bug" --body "Description"
|
|
gitea-cli release create v1.0.0 --attach ./binary.zip
|
|
gitea-cli upload --chunked ./large-file.zip # Uses chunked upload
|
|
```
|
|
|
|
### 5.4 Webhook Debugging Tools (LOW)
|
|
|
|
**Current State:** Limited visibility into webhook delivery.
|
|
|
|
**Suggestion:** Add webhook inspection endpoints:
|
|
|
|
```
|
|
GET /api/v1/repos/{owner}/{repo}/hooks/{id}/deliveries
|
|
GET /api/v1/repos/{owner}/{repo}/hooks/{id}/deliveries/{delivery_id}
|
|
POST /api/v1/repos/{owner}/{repo}/hooks/{id}/deliveries/{delivery_id}/redeliver
|
|
```
|
|
|
|
---
|
|
|
|
## 6. AI & Automation Friendliness
|
|
|
|
### 6.1 Machine-Readable Error Codes (HIGH)
|
|
|
|
**Current State:** Error messages are human-readable strings.
|
|
|
|
**Suggestion:** Always include machine-readable codes:
|
|
|
|
```json
|
|
{
|
|
"code": "ERR_REPO_ARCHIVED",
|
|
"message": "Repository is archived and cannot be modified",
|
|
"documentation_url": "https://docs.gitea.io/errors/ERR_REPO_ARCHIVED"
|
|
}
|
|
```
|
|
|
|
### 6.2 Idempotency Keys (HIGH)
|
|
|
|
**Current State:** No idempotency support for POST requests.
|
|
|
|
**Suggestion:** Add idempotency key header:
|
|
|
|
```go
|
|
// Header: Idempotency-Key: <client-generated-uuid>
|
|
|
|
func IdempotencyMiddleware() func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "POST" && r.Method != "PUT" {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
key := r.Header.Get("Idempotency-Key")
|
|
if key == "" {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Check cache
|
|
if cached, found := idempotencyCache.Get(key); found {
|
|
w.Header().Set("X-Idempotency-Replayed", "true")
|
|
writeResponse(w, cached)
|
|
return
|
|
}
|
|
|
|
// Capture response
|
|
rec := httptest.NewRecorder()
|
|
next.ServeHTTP(rec, r)
|
|
|
|
// Cache successful responses
|
|
if rec.Code >= 200 && rec.Code < 300 {
|
|
idempotencyCache.Set(key, rec, 24*time.Hour)
|
|
}
|
|
|
|
copyResponse(w, rec)
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### 6.3 Webhook Event Catalog (MEDIUM)
|
|
|
|
**Current State:** Events documented but not discoverable via API.
|
|
|
|
**Suggestion:** Add event discovery endpoint:
|
|
|
|
```
|
|
GET /api/v1/events
|
|
```
|
|
|
|
```json
|
|
{
|
|
"events": [
|
|
{
|
|
"name": "push",
|
|
"description": "Triggered when commits are pushed",
|
|
"payload_schema": {"$ref": "#/definitions/PushPayload"},
|
|
"example": {...}
|
|
},
|
|
{
|
|
"name": "issues",
|
|
"description": "Triggered for issue events",
|
|
"actions": ["opened", "closed", "edited", "labeled"],
|
|
"payload_schema": {"$ref": "#/definitions/IssuePayload"}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### 6.4 Async Operation Pattern (MEDIUM)
|
|
|
|
**Current State:** Long operations block HTTP requests.
|
|
|
|
**Suggestion:** Add async operation pattern for long-running tasks:
|
|
|
|
```
|
|
POST /api/v1/repos/{owner}/{repo}/mirror-sync
|
|
Response: 202 Accepted
|
|
{
|
|
"operation_id": "op_abc123",
|
|
"status": "pending",
|
|
"status_url": "/api/v1/operations/op_abc123",
|
|
"estimated_completion": "2024-01-08T12:05:00Z"
|
|
}
|
|
|
|
GET /api/v1/operations/op_abc123
|
|
{
|
|
"operation_id": "op_abc123",
|
|
"status": "running",
|
|
"progress": 45,
|
|
"started_at": "2024-01-08T12:00:00Z"
|
|
}
|
|
```
|
|
|
|
### 6.5 Rate Limit Headers (MEDIUM)
|
|
|
|
**Current State:** Limited rate limit visibility.
|
|
|
|
**Suggestion:** Add standard rate limit headers:
|
|
|
|
```
|
|
X-RateLimit-Limit: 5000
|
|
X-RateLimit-Remaining: 4990
|
|
X-RateLimit-Reset: 1704720000
|
|
X-RateLimit-Resource: api
|
|
```
|
|
|
|
### 6.6 Conditional Requests (LOW)
|
|
|
|
**Current State:** Limited ETag/If-Match support.
|
|
|
|
**Suggestion:** Add conditional request support:
|
|
|
|
```
|
|
GET /api/v1/repos/owner/repo
|
|
Response Headers:
|
|
ETag: "abc123"
|
|
Last-Modified: Wed, 08 Jan 2024 12:00:00 GMT
|
|
|
|
GET /api/v1/repos/owner/repo
|
|
Request Headers:
|
|
If-None-Match: "abc123"
|
|
Response: 304 Not Modified
|
|
|
|
PUT /api/v1/repos/owner/repo
|
|
Request Headers:
|
|
If-Match: "abc123" # Prevents lost updates
|
|
```
|
|
|
|
---
|
|
|
|
## 7. Security Considerations
|
|
|
|
### 7.1 Audit Logging (HIGH)
|
|
|
|
**Current State:** Limited audit trail.
|
|
|
|
**Suggestion:** Add comprehensive audit logging:
|
|
|
|
```go
|
|
type AuditEvent struct {
|
|
ID int64 `json:"id"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
ActorID int64 `json:"actor_id"`
|
|
ActorIP string `json:"actor_ip"`
|
|
Action string `json:"action"` // "repo.create", "user.delete"
|
|
Resource string `json:"resource"` // "repository", "user"
|
|
ResourceID int64 `json:"resource_id"`
|
|
OldValue string `json:"old_value,omitempty"` // JSON
|
|
NewValue string `json:"new_value,omitempty"` // JSON
|
|
UserAgent string `json:"user_agent"`
|
|
TraceID string `json:"trace_id"`
|
|
}
|
|
|
|
// Store in separate audit_log table
|
|
// Expose via admin API: GET /api/v1/admin/audit-log
|
|
```
|
|
|
|
### 7.2 Token Scopes Enhancement (MEDIUM)
|
|
|
|
**Current State:** Basic token scopes.
|
|
|
|
**Suggestion:** Add granular scopes:
|
|
|
|
```
|
|
repo:read # Read repository data
|
|
repo:write # Push to repository
|
|
repo:delete # Delete repository
|
|
issues:read # Read issues
|
|
issues:write # Create/edit issues
|
|
releases:read # Read releases
|
|
releases:write # Create releases
|
|
releases:upload # Upload release assets (for CI)
|
|
admin:users # Manage users
|
|
admin:repos # Manage all repos
|
|
webhook:manage # Manage webhooks
|
|
```
|
|
|
|
### 7.3 Request Signing (LOW)
|
|
|
|
**Current State:** Token-based auth only.
|
|
|
|
**Suggestion:** Add HMAC request signing for webhooks:
|
|
|
|
```go
|
|
// For high-security integrations
|
|
signature := hmac.New(sha256.New, []byte(secret))
|
|
signature.Write([]byte(timestamp + "." + requestBody))
|
|
expectedSig := hex.EncodeToString(signature.Sum(nil))
|
|
|
|
// Header: X-Gitea-Signature: sha256=<signature>
|
|
// Header: X-Gitea-Timestamp: <unix-timestamp>
|
|
```
|
|
|
|
---
|
|
|
|
## 8. Performance Optimizations
|
|
|
|
### 8.1 Response Compression (HIGH)
|
|
|
|
**Current State:** Gzip available but not always used.
|
|
|
|
**Suggestion:** Ensure compression for all JSON responses:
|
|
|
|
```go
|
|
// Middleware to compress responses > 1KB
|
|
func CompressionMiddleware(next http.Handler) http.Handler {
|
|
return gziphandler.GzipHandler(next)
|
|
}
|
|
```
|
|
|
|
### 8.2 Database Query Optimization (MEDIUM)
|
|
|
|
**Current State:** Some N+1 query patterns.
|
|
|
|
**Suggestion:** Add eager loading for common associations:
|
|
|
|
```go
|
|
// Instead of loading issues then loading users one by one
|
|
issues, _ := GetIssues(ctx, opts)
|
|
for _, issue := range issues {
|
|
issue.Poster, _ = GetUser(issue.PosterID) // N+1!
|
|
}
|
|
|
|
// Use eager loading
|
|
issues, _ := GetIssuesWithUsers(ctx, opts) // Single JOIN query
|
|
```
|
|
|
|
### 8.3 Caching Layer (MEDIUM)
|
|
|
|
**Current State:** Limited caching.
|
|
|
|
**Suggestion:** Add Redis/memory caching for hot paths:
|
|
|
|
```go
|
|
// Cache frequently accessed data
|
|
func GetRepository(ctx context.Context, id int64) (*Repository, error) {
|
|
cacheKey := fmt.Sprintf("repo:%d", id)
|
|
|
|
if cached, found := cache.Get(cacheKey); found {
|
|
return cached.(*Repository), nil
|
|
}
|
|
|
|
repo, err := db.GetRepository(ctx, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
cache.Set(cacheKey, repo, 5*time.Minute)
|
|
return repo, nil
|
|
}
|
|
```
|
|
|
|
### 8.4 Connection Pooling Tuning (LOW)
|
|
|
|
**Current State:** Default connection pool settings.
|
|
|
|
**Suggestion:** Add configurable pool settings:
|
|
|
|
```ini
|
|
[database]
|
|
MAX_OPEN_CONNS = 100
|
|
MAX_IDLE_CONNS = 10
|
|
CONN_MAX_LIFETIME = 1h
|
|
CONN_MAX_IDLE_TIME = 10m
|
|
```
|
|
|
|
---
|
|
|
|
## 9. Implementation Roadmap
|
|
|
|
### Phase 1: Quick Wins (1-2 weeks)
|
|
|
|
| Item | Priority | Effort | Impact |
|
|
|------|----------|--------|--------|
|
|
| Add request tracing (X-Request-ID) | HIGH | Low | High |
|
|
| Add rate limit headers | MEDIUM | Low | Medium |
|
|
| Add chunk checksum verification | HIGH | Low | High |
|
|
| Standardize error response format | HIGH | Medium | High |
|
|
|
|
### Phase 2: Foundation (2-4 weeks)
|
|
|
|
| Item | Priority | Effort | Impact |
|
|
|------|----------|--------|--------|
|
|
| Centralized error catalog | HIGH | Medium | High |
|
|
| Webhook retry with backoff | HIGH | Medium | High |
|
|
| Idempotency key support | HIGH | Medium | High |
|
|
| Audit logging | HIGH | Medium | High |
|
|
| Health check endpoints | LOW | Low | Medium |
|
|
|
|
### Phase 3: Enhancement (4-8 weeks)
|
|
|
|
| Item | Priority | Effort | Impact |
|
|
|------|----------|--------|--------|
|
|
| Batch operations API | HIGH | High | High |
|
|
| Cursor-based pagination | MEDIUM | Medium | Medium |
|
|
| SDK generation pipeline | MEDIUM | Medium | Medium |
|
|
| Circuit breaker pattern | MEDIUM | Medium | Medium |
|
|
| Event discovery endpoint | MEDIUM | Low | Medium |
|
|
|
|
### Phase 4: Advanced (8+ weeks)
|
|
|
|
| Item | Priority | Effort | Impact |
|
|
|------|----------|--------|--------|
|
|
| Async operation pattern | MEDIUM | High | Medium |
|
|
| Field selection (GraphQL-lite) | MEDIUM | High | Medium |
|
|
| API versioning | LOW | High | Low |
|
|
| Full OpenTelemetry integration | MEDIUM | High | High |
|
|
|
|
---
|
|
|
|
## Appendix A: Chunked Upload Quick Fixes
|
|
|
|
```go
|
|
// 1. Add checksum to chunk upload (upload.go)
|
|
func UploadChunk(ctx *context.APIContext) {
|
|
// Get expected checksum from header
|
|
expectedChecksum := ctx.Req.Header.Get("X-Chunk-Checksum")
|
|
|
|
// ... save chunk ...
|
|
|
|
// Verify checksum if provided
|
|
if expectedChecksum != "" {
|
|
actualChecksum := sha256sum(chunkData)
|
|
if actualChecksum != expectedChecksum {
|
|
ctx.APIError(http.StatusBadRequest, api.APIError{
|
|
Code: "ERR_CHECKSUM_MISMATCH",
|
|
Detail: "Chunk checksum verification failed",
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Add final file checksum verification (chunked.go)
|
|
func AssembleChunks(...) (*Attachment, error) {
|
|
// ... assemble chunks ...
|
|
|
|
// Verify final checksum if expected
|
|
if session.ExpectedChecksum != "" {
|
|
actualChecksum := computeFileChecksum(finalPath)
|
|
if actualChecksum != session.ExpectedChecksum {
|
|
return nil, fmt.Errorf("file checksum mismatch: expected %s, got %s",
|
|
session.ExpectedChecksum, actualChecksum)
|
|
}
|
|
}
|
|
|
|
// ... create attachment ...
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Appendix B: Sample Error Catalog
|
|
|
|
```go
|
|
// modules/errors/codes.go
|
|
|
|
package errors
|
|
|
|
// Error codes organized by category
|
|
const (
|
|
// 1xxx - Authentication
|
|
ErrCodeAuthRequired = "ERR_1001"
|
|
ErrCodeInvalidToken = "ERR_1002"
|
|
ErrCodeTokenExpired = "ERR_1003"
|
|
ErrCodeInvalidPassword = "ERR_1004"
|
|
ErrCode2FARequired = "ERR_1005"
|
|
|
|
// 2xxx - Authorization
|
|
ErrCodeForbidden = "ERR_2001"
|
|
ErrCodeNotRepoMember = "ERR_2002"
|
|
ErrCodeNotOrgMember = "ERR_2003"
|
|
ErrCodeInsufficientPerm = "ERR_2004"
|
|
|
|
// 3xxx - Resource Not Found
|
|
ErrCodeUserNotFound = "ERR_3001"
|
|
ErrCodeRepoNotFound = "ERR_3002"
|
|
ErrCodeIssueNotFound = "ERR_3003"
|
|
ErrCodeReleaseNotFound = "ERR_3004"
|
|
ErrCodeBranchNotFound = "ERR_3005"
|
|
|
|
// 4xxx - Validation
|
|
ErrCodeInvalidInput = "ERR_4001"
|
|
ErrCodeNameTooLong = "ERR_4002"
|
|
ErrCodeInvalidEmail = "ERR_4003"
|
|
ErrCodeDuplicateName = "ERR_4004"
|
|
|
|
// 5xxx - Upload
|
|
ErrCodeSessionExpired = "ERR_5001"
|
|
ErrCodeChunkMissing = "ERR_5002"
|
|
ErrCodeFileTooLarge = "ERR_5003"
|
|
ErrCodeChecksumMismatch = "ERR_5004"
|
|
ErrCodeInvalidFileType = "ERR_5005"
|
|
|
|
// 6xxx - Rate Limiting
|
|
ErrCodeRateLimited = "ERR_6001"
|
|
ErrCodeTooManyRequests = "ERR_6002"
|
|
|
|
// 7xxx - Server
|
|
ErrCodeInternal = "ERR_7001"
|
|
ErrCodeServiceDown = "ERR_7002"
|
|
ErrCodeTimeout = "ERR_7003"
|
|
)
|
|
```
|
|
|
|
---
|
|
|
|
*Document prepared for discussion. All suggestions should be evaluated against project priorities and resource constraints.*
|