# Tanabata File Manager — Go Project Structure

## Stack

- **Router**: Gin
- **Database**: pgx v5 (pgxpool)
- **Migrations**: goose v3 + go:embed (auto-migrate on startup)
- **Auth**: JWT (golang-jwt/jwt/v5)
- **Config**: environment variables via .env (joho/godotenv)
- **Logging**: slog (stdlib, Go 1.21+)
- **Validation**: go-playground/validator/v10
- **EXIF**: rwcarlsen/goexif or dsoprea/go-exif
- **Image processing**: disintegration/imaging (thumbnails, previews)
- **Architecture**: Clean Architecture (domain → service → repository/handler)

## Monorepo Layout

```
tanabata/
├── backend/                            ← Go project
├── frontend/                           ← SvelteKit project
├── openapi.yaml                        ← Shared API contract
├── docker-compose.yml
├── Dockerfile
├── .env.example
└── README.md
```

## Backend Directory Layout

```
backend/
├── cmd/
│   └── server/
│       └── main.go                     # Entrypoint: config → DB → migrate → wire → run
│
├── internal/
│   │
│   ├── domain/                         # Pure business entities & value objects
│   │   ├── file.go                     # File, FileFilter, FilePage
│   │   ├── tag.go                      # Tag, TagRule
│   │   ├── category.go                 # Category
│   │   ├── pool.go                     # Pool, PoolFile
│   │   ├── user.go                     # User, Session
│   │   ├── acl.go                      # Permission, ObjectType
│   │   ├── audit.go                    # AuditEntry, ActionType
│   │   └── errors.go                   # Domain error types (ErrNotFound, ErrForbidden, etc.)
│   │
│   ├── port/                           # Interfaces (ports) — contracts between layers
│   │   ├── repository.go              # FileRepo, TagRepo, CategoryRepo, PoolRepo,
│   │   │                               # UserRepo, SessionRepo, ACLRepo, AuditRepo,
│   │   │                               # MimeRepo, TagRuleRepo
│   │   └── storage.go                 # FileStorage interface (disk operations)
│   │
│   ├── service/                        # Business logic (use cases)
│   │   ├── file_service.go             # Upload, update, delete, trash/restore, replace,
│   │   │                               # import, filter/list, duplicate detection
│   │   ├── tag_service.go              # CRUD + auto-tag application logic
│   │   ├── category_service.go         # CRUD (thin, delegates to repo + ACL + audit)
│   │   ├── pool_service.go             # CRUD + file ordering, add/remove files
│   │   ├── auth_service.go             # Login, logout, JWT issue/refresh, session management
│   │   ├── acl_service.go              # Permission checks, grant/revoke
│   │   ├── audit_service.go            # Log actions, query audit log
│   │   └── user_service.go             # Profile update, admin CRUD, block/unblock
│   │
│   ├── handler/                        # HTTP layer (Gin handlers)
│   │   ├── router.go                   # Route registration, middleware wiring
│   │   ├── middleware.go               # Auth middleware (JWT extraction → context)
│   │   ├── request.go                  # Common request parsing helpers
│   │   ├── response.go                 # Error/success response builders
│   │   ├── file_handler.go             # /files endpoints
│   │   ├── tag_handler.go              # /tags endpoints
│   │   ├── category_handler.go         # /categories endpoints
│   │   ├── pool_handler.go             # /pools endpoints
│   │   ├── auth_handler.go             # /auth endpoints
│   │   ├── acl_handler.go              # /acl endpoints
│   │   ├── user_handler.go             # /users endpoints
│   │   └── audit_handler.go            # /audit endpoints
│   │
│   ├── db/                             # Database adapters
│   │   ├── db.go                       # Common helpers: pagination, repo factory, transactor base
│   │   └── postgres/                   # PostgreSQL implementation
│   │       ├── postgres.go             # pgxpool init, tx-from-context helpers
│   │       ├── file_repo.go            # FileRepo implementation
│   │       ├── tag_repo.go             # TagRepo + TagRuleRepo implementation
│   │       ├── category_repo.go        # CategoryRepo implementation
│   │       ├── pool_repo.go            # PoolRepo implementation
│   │       ├── user_repo.go            # UserRepo implementation
│   │       ├── session_repo.go         # SessionRepo implementation
│   │       ├── acl_repo.go             # ACLRepo implementation
│   │       ├── audit_repo.go           # AuditRepo implementation
│   │       ├── mime_repo.go            # MimeRepo implementation
│   │       └── filter_parser.go        # DSL → SQL WHERE clause builder
│   │
│   ├── storage/                        # File storage adapter
│   │   └── disk.go                     # FileStorage implementation (read/write/delete on disk)
│   │
│   └── config/                         # Configuration
│       └── config.go                   # Struct + loader from env vars
│
├── migrations/                         # SQL migration files (goose format)
│   ├── 001_init_schemas.sql
│   ├── 002_core_tables.sql
│   ├── 003_data_tables.sql
│   ├── 004_acl_tables.sql
│   ├── 005_activity_tables.sql
│   ├── 006_indexes.sql
│   └── 007_seed_data.sql
│
├── go.mod
└── go.sum
```

## Layer Dependency Rules

```
handler  →  service  →  port (interfaces)  ←  db/postgres / storage
                ↓
              domain (entities, value objects, errors)
```

- **domain/**: zero imports from other internal packages. Only stdlib.
- **port/**: imports only domain/. Defines interfaces.
- **service/**: imports domain/ and port/. Never imports db/ or handler/.
- **handler/**: imports domain/ and service/. Never imports db/.
- **db/postgres/**: imports domain/, port/, and db/ (common helpers). Implements port interfaces.
- **db/**: imports domain/ and port/. Shared utilities for all DB adapters.
- **storage/**: imports domain/ and port/. Implements FileStorage.

No layer may import a layer above it. No circular dependencies.

## Key Design Decisions

### Dependency Injection (Wiring)

Manual wiring in `cmd/server/main.go`. No DI frameworks.

```go
// Pseudocode
pool := postgres.NewPool(cfg.DatabaseURL)
goose.Up(pool, migrations)

// Repos (all from internal/db/postgres/)
fileRepo   := postgres.NewFileRepo(pool)
tagRepo    := postgres.NewTagRepo(pool)
// ...

// Storage
diskStore  := storage.NewDiskStorage(cfg.FilesPath)

// Services
aclSvc     := service.NewACLService(aclRepo, objectTypeRepo)
auditSvc   := service.NewAuditService(auditRepo, actionTypeRepo)
fileSvc    := service.NewFileService(fileRepo, mimeRepo, tagRepo, diskStore, aclSvc, auditSvc)
tagSvc     := service.NewTagService(tagRepo, tagRuleRepo, aclSvc, auditSvc)
// ...

// Handlers
fileHandler := handler.NewFileHandler(fileSvc, tagSvc)
// ...

router := handler.NewRouter(cfg, fileHandler, tagHandler, ...)
router.Run(cfg.ListenAddr)
```

### Context Propagation

Every service method receives `context.Context` as the first argument.
The handler extracts user info from JWT (via middleware) and puts it
into context. Services read the current user from context for ACL checks
and audit logging.

```go
// middleware.go
func (m *AuthMiddleware) Handle(c *gin.Context) {
    claims := parseJWT(c.GetHeader("Authorization"))
    ctx := domain.WithUser(c.Request.Context(), claims.UserID, claims.IsAdmin)
    c.Request = c.Request.WithContext(ctx)
    c.Next()
}

// domain/context.go
type ctxKey int
const userKey ctxKey = iota
func WithUser(ctx context.Context, userID int16, isAdmin bool) context.Context { ... }
func UserFromContext(ctx context.Context) (userID int16, isAdmin bool) { ... }
```

### Transaction Management

Repository interfaces include a `Transactor`:

```go
// port/repository.go
type Transactor interface {
    WithTx(ctx context.Context, fn func(ctx context.Context) error) error
}
```

The postgres implementation wraps `pgxpool.Pool.BeginTx`. Inside `fn`,
all repo calls use the transaction from context. This allows services
to compose multiple repo calls in a single transaction:

```go
// service/file_service.go
func (s *FileService) Upload(ctx context.Context, input UploadInput) (*domain.File, error) {
    return s.tx.WithTx(ctx, func(ctx context.Context) error {
        file, err := s.fileRepo.Create(ctx, ...)  // uses tx
        if err != nil { return err }
        for _, tagID := range input.TagIDs {
            s.tagRepo.AddFileTag(ctx, file.ID, tagID)  // same tx
        }
        s.auditRepo.Log(ctx, ...)  // same tx
        return nil
    })
}
```

### ACL Check Pattern

ACL logic is centralized in `ACLService`. Other services call it before
any data mutation or retrieval:

```go
// service/acl_service.go
func (s *ACLService) CanView(ctx context.Context, objectType string, objectID uuid.UUID) error {
    userID, isAdmin := domain.UserFromContext(ctx)
    if isAdmin { return nil }
    // Check is_public on the object
    // If not public, check creator_id == userID
    // If not creator, check acl.permissions
    // Return domain.ErrForbidden if none match
}
```

### Error Mapping

Domain errors → HTTP status codes (handled in handler/response.go):

| Domain Error          | HTTP Status | Error Code        |
|-----------------------|-------------|-------------------|
| ErrNotFound           | 404         | not_found         |
| ErrForbidden          | 403         | forbidden         |
| ErrUnauthorized       | 401         | unauthorized      |
| ErrConflict           | 409         | conflict          |
| ErrValidation         | 400         | validation_error  |
| ErrUnsupportedMIME    | 415         | unsupported_mime  |
| (unexpected)          | 500         | internal_error    |

### Filter DSL

The DSL parser lives in `db/postgres/filter_parser.go` because it produces
SQL WHERE clauses — it is a PostgreSQL-specific adapter concern.
The service layer passes the raw DSL string to the repository; the
repository parses it and builds the query.

For a different DBMS, a corresponding parser would live in
`db/<dbms>/filter_parser.go`.

The interface:
```go
// port/repository.go
type FileRepo interface {
    List(ctx context.Context, params FileListParams) (*domain.FilePage, error)
    // ...
}

// domain/file.go
type FileListParams struct {
    Filter    string    // raw DSL string
    Sort      string
    Order     string
    Cursor    string
    Anchor    *uuid.UUID
    Direction string    // "forward" or "backward"
    Limit     int
    Trash     bool
    Search    string
}
```

### JWT Structure

```go
type Claims struct {
    jwt.RegisteredClaims
    UserID   int16 `json:"uid"`
    IsAdmin  bool  `json:"adm"`
    SessionID int  `json:"sid"`
}
```

Access token: short-lived (15 min). Refresh token: long-lived (30 days),
stored as hash in `activity.sessions.token_hash`.

### Configuration (.env)

```env
# Server
LISTEN_ADDR=:8080
JWT_SECRET=<random-32-bytes>
JWT_ACCESS_TTL=15m
JWT_REFRESH_TTL=720h

# Database
DATABASE_URL=postgres://user:pass@host:5432/tanabata?sslmode=disable

# Storage
FILES_PATH=/data/files
THUMBS_CACHE_PATH=/data/thumbs

# Thumbnails
THUMB_WIDTH=160
THUMB_HEIGHT=160
PREVIEW_WIDTH=1920
PREVIEW_HEIGHT=1080

# Import
IMPORT_PATH=/data/import
```
