No description
  • Go 90.7%
  • HTML 7.5%
  • Makefile 1.3%
  • Dockerfile 0.4%
Find a file
Jesus Marin 34fdd373f7
All checks were successful
CI / test (push) Successful in 1m27s
CI / lint (push) Successful in 1m43s
CI / docker (push) Successful in 4m48s
fix
2026-06-01 11:14:58 -04:00
.forgejo/workflows fix 2026-06-01 11:03:16 -04:00
cmd fist commit 2026-06-01 10:57:31 -04:00
docs/superpowers/specs fist commit 2026-06-01 10:57:31 -04:00
internal fist commit 2026-06-01 10:57:31 -04:00
migrations fist commit 2026-06-01 10:57:31 -04:00
.env.example fist commit 2026-06-01 10:57:31 -04:00
.gitignore fist commit 2026-06-01 10:57:31 -04:00
docker-compose.yml fist commit 2026-06-01 10:57:31 -04:00
Dockerfile fix 2026-06-01 11:14:58 -04:00
entrypoint.sh fist commit 2026-06-01 10:57:31 -04:00
go.mod fist commit 2026-06-01 10:57:31 -04:00
go.sum fist commit 2026-06-01 10:57:31 -04:00
Makefile fist commit 2026-06-01 10:57:31 -04:00
README.md fist commit 2026-06-01 10:57:31 -04:00

Goartesory

Modern image gallery web application built in Go with SQLite, S3-compatible storage, and Bootstrap 5.

Features

  • Masonry grid layout with responsive design (any screen size)
  • Tag-based filtering: click tags to filter images
  • Image detail page with full metadata (title, description, dimensions, tags)
  • Pagination: 20 images per page
  • Image proxy: images served through Go app from S3 (with caching headers)
  • CDN support: rewrite image URLs to CDN domain when configured

Admin Dashboard

  • Secure login with bcrypt password hashing (cost 12) and rate limiting
  • Role-based access: Admin (full CRUD) and Viewer (read-only)
  • Image management: upload, edit metadata, assign tags, delete
  • Automatic variants: micro (160x160) and mini (350x350) generated on upload
  • Tag management: create, edit, delete tags (CRUD with image count)
  • User management: Admin can create/edit/delete users (Admin only)
  • CSRF protection on all state-changing requests
  • Session management stored in SQLite

Image Processing

  • Upload validation (type: JPEG, PNG, WebP, GIF; max 20MB)
  • SHA256 checksums for integrity
  • Automatic variant generation using libvips (via bimg)
  • Variants preserve aspect ratio without cropping or distortion
  • Metadata extraction (width, height)
  • Images stored in S3-compatible storage (MinIO, AWS S3, Backblaze B2, etc.)

System Requirements

Requirement Version
Go 1.26+
golang-migrate CLI 4.15+
libvips any recent version

Installing libvips

# Ubuntu / Debian
sudo apt install libvips-dev

# macOS
brew install vips

# Arch Linux
sudo pacman -S libvips

Installing golang-migrate CLI

go install -tags 'sqlite' github.com/golang-migrate/migrate/v4/cmd/migrate@latest

The -tags 'sqlite' flag includes the pure-Go SQLite driver (modernc.org/sqlite, no CGO needed).

Quick Start (Development)

# 1. Clone and enter project
git clone git.jesusmarin.dev/angel/goartesory
cd goartesory

# 2. Configure environment
cp .env.example .env
# Edit .env with your values (see Configuration section below)

# 3. Full setup (build + migrate + create admin)
make dev

# 4. Start the server
make run

The app will be available at http://localhost:8080.

Important: The Makefile automatically loads environment variables from .env. You do NOT need to manually source .env or export variables. Everything is handled by make.

Configuration (.env)

All configuration is done via environment variables. Copy .env.example to .env and adjust:

Variable Default Description
PORT 8080 HTTP server port
ENV development development or production
BASE_URL http://localhost:8080 Public base URL
DATABASE_PATH ./data/goartesory.db SQLite database file path
SESSION_SECRET required 64-char random string for session encryption
SESSION_LIFETIME 24h Session cookie duration
S3_ENDPOINT (optional) S3-compatible endpoint URL (required for image uploads)
S3_REGION us-east-1 S3 region
S3_BUCKET goartesory S3 bucket name
S3_ACCESS_KEY (optional) S3 access key (required for image uploads)
S3_SECRET_KEY (optional) S3 secret key (required for image uploads)
S3_USE_PATH_STYLE true Use path-style addressing (required for MinIO)
CDN_BASE_URL (empty) CDN base URL for image URLs (optional)
CSRF_SECRET required 32-byte random string for CSRF protection

Generating secrets

# Session secret (64 chars)
openssl rand -base64 48

# CSRF secret (32 bytes)
openssl rand -base64 32

Makefile Commands

Command Description
make dev Full setup: build binaries, run migrations, create admin user
make build Build both binaries (goartesory + goartesory-setup)
make run Start the web server
make setup Create admin user (interactive CLI)
make migrate-up Run all pending migrations
make migrate-down Rollback all migrations
make migrate-new Create a new migration file
make reset Clean database + binaries, then recreate from scratch
make clean Remove bin/ and data/ directories
make vet Run Go static analysis
make fmt Format Go source code
make test Run test suite (without libvips-dependent tests)
make test-race Run tests with race detector
make test-cover Run tests with coverage report
make test-all Run tests with race detector and coverage report

Typical Development Workflow

# First time
make dev        # build + migrate + create admin
make run        # start server

# After pulling changes
make build      # rebuild binaries
make migrate-up # apply new migrations
make run        # restart

# Reset everything
make reset      # clean + build + migrate + setup

Testing

The test suite uses only Go's built-in testing package and net/http/httptest. No external test frameworks.

Summary

Metric Value
Test files 34
Total test functions 243
Strategy In-memory SQLite repos + hand-written fakes + httptest handlers

Test Structure

internal/
  images/
    domain/             errors_test.go            # Sentinel error values
    application/
      service_test.go    # ImageService (Create, Update, Delete, ServeProxy, validators)
      queue_test.go      # VariantQueue lifecycle
      bimg_timeout_test.go  # CGO timeout wrappers (build tag: !nobimg)
    infrastructure/
      sqlite/
        image_repo_test.go  # Real in-memory SQLite: CRUD, tags, pagination
        blob_repo_test.go   # Real in-memory SQLite: CRUD, unique key
      s3/
        blob_store_adapter_test.go  # Adapter delegation to storage.BlobStore
      http/
        handler_test.go   # Gallery, detail, dashboard CRUD, proxy
        routes_test.go    # Route registration verification
  users/
    domain/             user_test.go, errors_test.go  # IsAdmin(), sentinel errors
    application/        auth_test.go      # Authenticate, CreateUser, UpdateUser (bcrypt)
    infrastructure/
      sqlite/           user_repo_test.go # Real in-memory SQLite: CRUD, UNIQUE email
      http/
        handler_test.go   # Login, logout, user CRUD, self-role/self-delete guards
        routes_test.go    # Route registration: login public, logout auth
  tags/
    application/        service_test.go   # TagService: CRUD, slug generation
    infrastructure/
      sqlite/           tag_repo_test.go  # Real in-memory SQLite: CRUD, FindAllWithCount
      http/
        handler_test.go   # Tag CRUD with flash messages
        routes_test.go    # Route registration
  storage/
    domain/             errors_test.go    # Sentinel error values
    infrastructure/
      s3/               s3_store_test.go  # URL generation (cdn/no-cdn)
  platform/
    config/             config_test.go    # Load() from env, defaults, validation
    database/
      sqlite_test.go    # Open(), pragmas, OpenTestDB()
      testdb.go         # Test helper: in-memory SQLite with full schema
    middleware/
      auth_test.go      # RequireAuth, RequireAdmin, context helpers
      csrf_test.go      # Token generation, GET/POST validation
      logging_test.go   # realIP extraction, responseWriter
      recovery_test.go  # Panic recovery
      security_test.go  # Security headers
      ratelimit/
        limiter_test.go     # Rate limiter middleware
        sqlite_store_test.go  # SQLite-backed UPSERT (including concurrent)
    response/
      errors_test.go    # NotFound, ServerError
      pages_test.go     # Render, RenderLogin, RenderDashboard, SetFlash
    filesystem/         templates_test.go  # ParseTemplates, FuncMap
    router/             router_test.go     # New() ServeMux
  testutil/             mocks.go           # Hand-written fakes for all domain interfaces

Coverage

Layer Packages Coverage
Domain users, images, storage, tags 100% (entity methods + sentinel errors)
Application users, tags, images 89-100% (services + validators)
Infrastructure SQLite users, images, tags 84-86% (repos against in-memory DB)
Infrastructure HTTP users, images, tags 79-97% (handlers via httptest)
Platform config, middleware, response, etc. 77-100%

What Is NOT Tested (and why)

Area Reason
cmd/web/main.go, cmd/setup/main.go main() entry points — tested via integration/manual testing
ImageService.Create() image processing path Depends on h2non/bimg (CGO/libvips). Tests use -tags nobimg to skip. Pre-processing validations (type, size) are still tested.
VariantQueue.processJob() bimg calls Same as above — CGO dependency not available in all environments.
storage/infrastructure/s3/ S3 operations Requires a real S3-compatible endpoint. Only URL() method is tested.
testutil/mocks.go Test helpers — self-testing is redundant.
internal/tags/domain No testable logic (Tag struct only has fields, no methods).

Race Detector

Go's race detector (-race) detects data races: concurrent access to shared memory without synchronization. It's essential for code with goroutines (like VariantQueue and ratelimit.SQLiteStore).

Command What it does
make test Basic tests, fast, no race checks
make test-race Tests with race detector (~10x slower but catches concurrency bugs)
make test-cover Coverage report showing % of statements exercised
make test-all Race detector + coverage (most thorough)

Performance note: The race detector slows execution significantly because it instruments every memory access. For rapid TDD cycles, use make test. Run make test-race or make test-all before commits.

Project Structure

goartesory/
  cmd/
    web/main.go              # Server entry point (wiring, DI, startup)
    setup/main.go            # CLI: create admin user
  internal/
    images/                  # Images module
      domain/                #   Image, Blob entities, repository interfaces
      application/           #   ImageService (CRUD, variants, proxy)
      infrastructure/
        sqlite/              #   SQLite repositories
        http/                #   HTTP handlers (public + dashboard)
        s3/                  #   BlobStore adapter (bridges storage module)
    tags/                    # Tags module
      domain/                #   Tag entity, repository interface
      application/           #   TagService (CRUD, slug generation)
      infrastructure/
        sqlite/              #   SQLite repository
        http/                #   HTTP handlers (dashboard CRUD)
    users/                   # Users module
      domain/                #   User entity, repository interface
      application/           #   AuthService (login, user CRUD, bcrypt)
      infrastructure/
        sqlite/              #   SQLite repository
        http/                #   HTTP handlers (login, logout, user CRUD)
    storage/                 # Storage module (shared port)
      domain/                #   BlobStore interface
      infrastructure/
        s3/                  #   S3 adapter (AWS SDK v2)
    platform/                # Cross-cutting platform layer
      config/                #   Configuration from env vars
      database/              #   SQLite connection + pragmas
      middleware/             #   auth, csrf, session, logging, recovery, security, ratelimit
      router/                #   Route registrar interface
      response/              #   Template rendering + error pages
      filesystem/            #   Embedded template filesystem
        templates/           #   All HTML templates (embedded at build time)
  migrations/                # Database migrations (7 migration pairs)
  .env.example               # Environment configuration template
  Makefile                   # Build and development commands

Architecture

Hexagonal Monolith

The application follows a modular monolith with hexagonal architecture (ports & adapters):

  • Domain layer: entities, value objects, repository interfaces (no external dependencies)
  • Application layer: use cases, services (depends only on domain)
  • Infrastructure layer: SQLite repos, HTTP handlers, S3 client (implements domain interfaces)

Module Boundaries

Each module defines its own interfaces. Cross-module dependencies are wired at cmd/web/main.go:

images module ──(needs)──▶ BlobStore interface (defined locally)
                               ▲
                               │ implements
                               │
storage module ──(provides)──▶ S3 BlobStore adapter

No module accesses another module's internal/ directly.

Database Schema

Tables:

  • sessions — session storage (SCS/sqlite3store)
  • users — authentication (email, password_hash, role)
  • blobs — file metadata (ActiveStorage-style: key, filename, content_type, byte_size, checksum, metadata)
  • images — image entity (title, description, blob_id FK)
  • tags — tag labels (name, slug)
  • image_tags — many-to-many relationship
  • rate_limits — rate limiting counters

Routes

Public (no authentication)

Method Path Description
GET / Gallery homepage (masonry grid)
GET /images/{id} Image detail page
GET /blobs/{key}/{variant} Image proxy from S3
GET /dashboard/login Login form
POST /dashboard/login Process login

Dashboard (authenticated)

Method Path Access
GET /dashboard Any user
GET /dashboard/images Any user
GET /dashboard/images/new Admin
POST /dashboard/images Admin
GET /dashboard/images/{id} Any user
GET /dashboard/images/{id}/edit Admin
POST /dashboard/images/{id}/edit Admin
POST /dashboard/images/{id}/delete Admin
GET /dashboard/tags Any user
POST /dashboard/tags Admin
GET /dashboard/tags/{id}/edit Admin
POST /dashboard/tags/{id}/edit Admin
POST /dashboard/tags/{id}/delete Admin
GET /dashboard/users Admin
POST /dashboard/users Admin
GET /dashboard/users/{id}/edit Admin
POST /dashboard/users/{id}/edit Admin
POST /dashboard/users/{id}/delete Admin
POST /dashboard/logout Any user

Health

Method Path Description
GET /health/live Liveness probe
GET /health/ready Readiness probe (checks DB)

Security

  • Password hashing: bcrypt with cost 12 (golang.org/x/crypto/bcrypt)
  • Session management: SCS with SQLite backend, HttpOnly + SameSite=Lax cookies
  • CSRF protection: nosurf on all state-changing requests
  • Rate limiting: configurable per-route, SQLite backend (login: 10 req/min per IP)
  • Security headers: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy
  • X-Forwarded-For support: real IP extraction for reverse proxy setups
  • Secrets: all sensitive values via environment variables, never committed

Tech Stack

Component Technology
Language Go 1.26
Database SQLite (modernc.org/sqlite - pure Go, no CGO)
Migrations golang-migrate (CLI tool)
HTTP Router Go 1.22+ http.ServeMux (pattern matching)
Sessions alexedwards/scs + sqlite3store
CSRF justinas/nosurf
Middleware justinas/alice
Forms go-playground/form
Image Processing h2non/bimg (libvips - CGO required)
S3 Storage AWS SDK Go v2
Password Hashing golang.org/x/crypto/bcrypt
Frontend Bootstrap 5.3.8 + Bootstrap Icons (CDN)
UUID google/uuid

Deployment

Single Binary

The app compiles to a single static binary (with CGO for libvips). Deploy alongside:

  1. The binary (goartesory)
  2. A .env file with production configuration
  3. An SQLite database file in a persistent volume
  4. Run migrations before starting: make migrate-up

Proxy Setup

The app supports reverse proxies. Set ENV=production to enable:

  • Secure cookies (Secure + HttpOnly)
  • HSTS headers (via Strict-Transport-Security)

S3 Storage

Works with any S3-compatible service:

  • AWS S3: set S3_ENDPOINT=https://s3.amazonaws.com, S3_USE_PATH_STYLE=false
  • MinIO: set S3_ENDPOINT=http://localhost:9000, S3_USE_PATH_STYLE=true
  • Cloudflare R2: set S3_ENDPOINT=https://<account>.r2.cloudflarestorage.com
  • Backblaze B2: set S3_ENDPOINT=https://s3.us-west-004.backblazeb2.com

CDN

Set CDN_BASE_URL=https://cdn.example.com to rewrite all image URLs to use your CDN. When empty, images are served through the Go app proxy from S3.

Production Deployment with Docker

The project provides a pre-built Docker image available in the Forgejo Container Registry at git.jesusmarin.dev/angel/goartesory. The image is automatically built and pushed by Forgejo Actions on every push to the master branch.

How It Works

  1. The CI workflow (.forgejo/workflows/ci.yml) runs tests and linting on every push and pull request.
  2. On push to master, it builds a multi-stage Docker image and pushes it to the registry with two tags:
    • git.jesusmarin.dev/angel/goartesory:latest
    • git.jesusmarin.dev/angel/goartesory:sha-<commit-sha>
  3. The docker-compose.yml file uses the pre-built image from the registry (no local build needed).

Deploy

# 1. Copy and configure environment
cp .env.example .env
# Edit .env with your production values

# 2. Start the service
docker compose up -d
# Or with Podman:
podman compose up -d

The app will be available at http://<host>:8080.

Docker Image Details

  • Base image: Debian Bookworm Slim (minimal runtime)
  • Runtime dependencies: libvips42, libgomp1, ca-certificates
  • Includes: the Go binary, golang-migrate, templates, and migrations
  • Entrypoint: runs database migrations automatically, then starts the server
  • Data persistence: SQLite database stored in a Docker volume mounted at /data

Health Endpoints

The container exposes two health check endpoints:

Endpoint Purpose
GET /health/live Liveness probe (always returns 200)
GET /health/ready Readiness probe (checks database connectivity)

Local Docker Build

For testing the Docker image locally:

make docker-build

This builds an image tagged goartesory:local using the same Dockerfile.

Repository

This project is hosted on Forgejo at git.jesusmarin.dev/angel/goartesory. The Go module path is goartesory.jesusmarin.dev.