No description
  • Go 80%
  • HTML 16.8%
  • Makefile 2.2%
  • Dockerfile 0.7%
  • Shell 0.3%
Find a file
Jesus Marin 7cc1e8227f
All checks were successful
CI / test (push) Successful in 1m35s
CI / lint (push) Successful in 1m38s
CI / docker (push) Successful in 1m24s
first commit
2026-04-11 14:28:37 -04:00
.forgejo/workflows first commit 2026-04-11 14:28:37 -04:00
cmd first commit 2026-04-11 14:28:37 -04:00
internal first commit 2026-04-11 14:28:37 -04:00
migrations first commit 2026-04-11 14:28:37 -04:00
tests/integration first commit 2026-04-11 14:28:37 -04:00
ui first commit 2026-04-11 14:28:37 -04:00
.dockerignore first commit 2026-04-11 14:28:37 -04:00
.env.example first commit 2026-04-11 14:28:37 -04:00
.gitignore first commit 2026-04-11 14:28:37 -04:00
comofunciona.md first commit 2026-04-11 14:28:37 -04:00
docker-compose.yml first commit 2026-04-11 14:28:37 -04:00
Dockerfile first commit 2026-04-11 14:28:37 -04:00
entrypoint.sh first commit 2026-04-11 14:28:37 -04:00
go.mod first commit 2026-04-11 14:28:37 -04:00
go.sum first commit 2026-04-11 14:28:37 -04:00
Makefile first commit 2026-04-11 14:28:37 -04:00
README.md first commit 2026-04-11 14:28:37 -04:00

ImageStorage

Aplicacion web en Go para almacenar imagenes en S3, similar a ActiveStorage de Rails.

Este proyecto esta organizado como un monolito modular, donde cada modulo encapsula completamente su funcionalidad mediante interfaces, permitiendo autonomia total y facilitando la extraccion a microservicios.

Caracteristicas

  • Almacenamiento de imagenes en cualquier servicio S3-compatible (IDrive E2, BackBlaze, AWS S3, etc.)
  • Generacion automatica de variantes de imagenes (micro: 160x160, mini: 350x350)
  • Sistema de autenticacion de usuarios con sesiones en SQLite
  • Sistema de tags many-to-many para clasificar imagenes
  • URLs publicas para servir imagenes (compatible con CDN)
  • Dashboard para administrar imagenes y tags
  • Proteccion CSRF
  • Rate limiting en login y en endpoints de imagenes
  • Middlewares con alice
  • Soporte para cookies seguras (HTTPS)
  • Soporte para proxy inverso (X-Forwarded-For)
  • Autorizacion basada en roles (admin vs viewer)

Arquitectura Modular

Esta aplicacion implementa un monolito modular basado en interfaces, donde cada modulo es completamente autonomo y no tiene acoplamiento directo con otros modulos o con el core de la aplicacion.

Principios Clave

  1. Zero Coupling: Los modulos NO dependen de implementaciones concretas, solo de interfaces
  2. Interfaces minimas por modulo: Cada modulo define exactamente lo que necesita del exterior
  3. Template data autonoma: Cada modulo define su propia estructura de datos de template
  4. Errores compartidos centralizados: Errores comunes definidos en un paquete compartido
  5. Inyeccion de dependencias pura: Todos los componentes reciben sus dependencias via interfaces
  6. Extraccion a microservicios: Cualquier modulo puede convertirse en microservicio sin cambiar su codigo

Deudas Arquitectonicas

Aunque la arquitectura es fundamentalmente modular, existen las siguientes excepciones al patron:

  • S3Client concreto: El modulo images recibe *storage.S3Client como tipo concreto en vez de una interfaz (ObjectStorage). Esto impide testing con mocks y dificulta cambiar la implementacion de almacenamiento sin modificar el modulo.
  • Errores de dominio en shared/errors: Errores como ErrDuplicateEmail y ErrInvalidCredentials son conceptos de dominio que deberian pertenecer a sus modulos respectivos, no a un paquete compartido.
  • Acoplamiento con app.TemplateData: Cada modulo envuelve app.TemplateData y delega los mismos metodos (IsAuthenticated, IsAdmin, CSRFToken, etc.), repitiendo codigo. Es acoplamiento al core, pero razonable para un monolito.
  • SessionManager redefinido: Cada modulo define su propia interfaz SessionManager con variaciones minimas en vez de usar la interfaz app.SessionManager del core.

Arquitectura de Interfaces

┌─────────────────────────────────────────────────────────────┐
│                        cmd/web/                             │
│  ┌──────────────┐  ┌──────────────┐                         │
│  │  adapters.go │  │  main.go     │                         │
│  │  (dominio)   │  │  (wire-up)   │                         │
│  └──────────────┘  └──────────────┘                         │
└─────────────────────────────────────────────────────────────┘
                              │
        ┌─────────────────────┼─────────────────────┐
        ▼                     ▼                     ▼
┌──────────────┐      ┌──────────────┐      ┌──────────────┐
│  internal/   │      │  internal/   │      │  internal/   │
│  app/        │      │  shared/     │      │  modules/    │
│              │      │              │      │              │
│ app.go       │      │   errors/    │      │  ┌────────┐  │
│ interfaces.go│      │   storage/   │      │  │  auth  │  │
│ middleware.go│      │   session/   │      │  └────────┘  │
│ ratelimit.go │      │   validator/ │      │  ┌────────┐  │
│ templates.go │      └──────────────┘      │  │dashboard│ │
└──────────────┘                            │  └────────┘  │
                                            │  ┌────────┐  │
                                            │  │ images │  │
                                            │  └────────┘  │
                                            │  ┌────────┐  │
                                            │  │  tags  │  │
                                            │  └────────┘  │
                                            │  ┌────────┐  │
                                            │  │ users  │  │
                                            │  └────────┘  │
                                            └──────────────┘

Zero Coupling Entre Modulos

Regla fundamental: Los modulos NUNCA se importan entre si directamente.

  • import "internal/modules/users" desde auth - NO
  • import "internal/modules/auth/contracts" para definir interfaces - SI
  • Adaptadores en cmd/web/adapters.go convierten tipos entre modulos - SI

Estructura de un Modulo

Cada modulo sigue esta estructura, pero solo incluye los archivos que necesita:

internal/modules/[name]/
├── contracts/           # Interfaces que este modulo NECESITA de otros (solo si depende de otro modulo)
│   └── [name]_contract.go
├── domain.go           # Entidades del dominio (solo si el modulo tiene dominio propio)
├── repository.go        # Interfaces de repositorio (solo si el modulo tiene persistencia)
├── models.go            # Implementaciones de repositorios (solo si hay repository.go)
├── data.go             # Estructura de template data especifica
├── handlers.go          # HTTP handlers (dependen de interfaces, no implementaciones)
├── routes.go            # Registro de rutas
├── embed.go             # Embed de templates
└── templates/           # Templates propios del modulo

Ejemplos de modulos minimos:

  • auth: No tiene domain.go, repository.go ni contracts/ propio (usa auth/contracts/user_contract.go para definir lo que necesita de users)
  • dashboard: No tiene domain.go, repository.go ni models.go (es un agregador de solo lectura que usa contratos de otros modulos)

Core de Aplicacion Agnostico

El paquete internal/app/ es completamente agnostico de los modulos:

// app.go - NO contiene dependencias de modulos
type Application struct {
    Logger         *slog.Logger
    SessionManager *scs.SessionManager
    TemplateCache  map[string]*template.Template
    FormDecoder    *form.Decoder
    bufferPool     sync.Pool
    currentYear    int
    cdnDomain      string
    secureCookies  bool
}

Requisitos

  • Go 1.26+
  • libvips (para procesamiento de imagenes con bimg)
  • golang-migrate (para migraciones de base de datos)

Instalar libvips

Ubuntu/Debian:

sudo apt-get install libvips-dev

Instalar golang-migrate

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

Comandos Makefile

Target Descripcion
make build Compilar el binario principal
make build-admin Compilar el CLI de admin
make build-all Compilar todos los binarios
make run Compilar y ejecutar la aplicacion
make clean Eliminar binarios y bases de datos
make test Ejecutar todos los tests
make test-integration Ejecutar solo tests de integracion
make test-coverage Generar reporte de cobertura en HTML
make deps Descargar y ordenar dependencias
make db-migrate Aplicar todas las migraciones pendientes
make db-rollback Deshacer ultima migracion
make db-version Ver version actual de la base de datos
make db-force Forzar version (usar con precaucion)
make create-admin Crear un usuario administrador interactivamente
make dev Compilar y ejecutar con settings de desarrollo (puerto 4000)
make dev-cdn Compilar y ejecutar con dominio CDN configurado
make dev-s3 Compilar y ejecutar con S3 configurado (env vars)
make dev-secure Compilar y ejecutar con cookies seguras
make prod Compilar y ejecutar con settings de produccion (puerto 8080)
make prod-cdn Compilar y ejecutar con settings de produccion y CDN

Migraciones de Base de Datos

Este proyecto usa golang-migrate para manejar las migraciones de forma externa (similar a Rails).

Aplicar migraciones por primera vez

make db-migrate

Crear un usuario administrador

Despues de aplicar las migraciones, crea el usuario administrador con el comando CLI:

make create-admin

Se te pedira el nombre, email y password de forma interactiva. Alternativamente, puedes pasar los flags directamente:

./admin -dsn=./data.db -name="Admin" -email="admin@example.com"

Crear una nueva migracion

migrate create -ext sql -dir migrations -seq nombre_migracion

Testing

Ejecutar todos los tests

make test

Ejecutar tests de integracion

make test-integration

Ejecutar tests especificos

go test -v -run TestLogin ./tests/integration/...
go test -v -run TestImages ./tests/integration/...

Configuracion

Variables de Entorno / Flags

Flag Variable de Entorno Descripcion Default
-addr - Direccion del servidor HTTP :4000
-dsn - Ruta de la base de datos SQLite ./data.db
- S3_BUCKET Nombre del bucket S3 (requerido para S3) -
- S3_REGION Region S3 us-east-1
- S3_ENDPOINT Endpoint S3 (para S3-compatible) -
- S3_ACCESS_KEY Access Key ID (requerido para S3) -
- S3_SECRET_KEY Secret Access Key (requerido para S3) -
-cdn-domain CDN_DOMAIN Dominio CDN para URLs de imagenes -
-secure-cookies SECURE_COOKIES Habilitar cookies seguras (produccion) false
-trust-forwarded-headers TRUST_FORWARDED_HEADERS Confiar en X-Forwarded-For (detras de proxy) false

⚠️ Credenciales S3

Las credenciales S3 (bucket, access key, secret key) SOLO se configuran mediante variables de entorno. No se aceptan como flags de linea de comandos para evitar que sean visibles en el listado de procesos del sistema (ps aux, /proc/<pid>/cmdline).

# Configuracion S3 (variables de entorno - OBLIGATORIO en produccion)
export S3_BUCKET=my-bucket
export S3_REGION=us-east-1
export S3_ENDPOINT=https://s3.example.com
export S3_ACCESS_KEY=YOUR_ACCESS_KEY
export S3_SECRET_KEY=YOUR_SECRET_KEY

Cookies Seguras

Habilita -secure-cookies cuando la aplicacion este detras de un proxy HTTPS. Esto establece el flag Secure en las cookies de sesion y CSRF.

Proxy Inverso (Caddy, Nginx, etc.)

Cuando la aplicacion este detras de un proxy inverso que agrega X-Forwarded-For, habilita -trust-forwarded-headers para que el rate limiting use la IP real del cliente:

./imagestorage -addr=:4000 -dsn=./data.db -trust-forwarded-headers -secure-cookies

Para Caddy 2.x, esto es necesario ya que Caddy agrega automaticamente los headers de forwarding.

Modelo de Autorizacion

La aplicacion tiene dos niveles de acceso para usuarios autenticados:

Rol Ver contenido Crear/Editar/Eliminar
Admin Dashboard, imagenes, tags, usuarios Si (todo)
Viewer Dashboard, imagenes (solo lectura), tags (solo lectura) No
No autenticado Solo URLs de imagenes publicas (si las conoce) No
  • Los admins pueden crear/editar/eliminar imagenes, tags y usuarios
  • Los viewers (usuarios regulares) pueden ver el dashboard y listar imagenes y tags, pero no pueden modificar nada
  • Los usuarios no autenticados solo pueden acceder a las URLs de imagenes si las conocen (las URLs usan claves aleatorias de 128 bits)

Uso

Compilar

make build
# o
go build -o imagestorage ./cmd/web

Compilar el CLI de admin

make build-admin
# o
go build -o admin ./cmd/admin

Ejecutar en desarrollo

# Sin S3 (las imagenes no se almacenaran)
./imagestorage -addr=:4000 -dsn=./data.db

# Con S3 (variables de entorno)
export S3_BUCKET=my-bucket
export S3_REGION=us-east-1
export S3_ENDPOINT=http://localhost:9000
export S3_ACCESS_KEY=minioadmin
export S3_SECRET_KEY=minioadmin
./imagestorage -addr=:4000 -dsn=./data.db

# Con cookies seguras (detras de HTTPS)
./imagestorage -addr=:4000 -dsn=./data.db -secure-cookies

# Detras de proxy inverso
./imagestorage -addr=:4000 -dsn=./data.db -trust-forwarded-headers -secure-cookies

Usar con CDN

./imagestorage \
  -addr=:4000 \
  -cdn-domain=cdn.example.com \
  -secure-cookies

Configuracion de Caddy 2.x como proxy inverso

Caddy 2.x agrega automaticamente los headers X-Forwarded-For, X-Forwarded-Proto, y X-Forwarded-Host cuando usa reverse_proxy. Un Caddyfile minimo:

example.com {
    reverse_proxy localhost:4000
}

Caddy gestiona automaticamente los certificados TLS con Let's Encrypt.

Para ejecutar con proxy inverso:

./imagestorage -addr=:4000 -dsn=./data.db -trust-forwarded-headers -secure-cookies

CI/CD con Forgejo Actions

La aplicacion incluye workflows de Forgejo Actions para automatizar tests, construccion de imagenes Docker y releases.

Workflows

CI (.forgejo/workflows/ci.yml)

Se ejecuta en cada push a master y en pull requests:

Job Descripcion
test Instala Go 1.26 via setup-go, instala libvips, ejecuta go test ./... con CGO
lint Instala Go 1.26 via setup-go, instala libvips, ejecuta go vet ./...
docker Build y push de imagen Docker al Forgejo Container Registry (solo en push a master). Se ejecuta directamente en el host (runs-on: docker) usando Buildx con driver: docker

Los jobs test y lint se ejecutan en contenedor (runs-on: ubuntu-latest) con Go instalado via actions/setup-go@v5. El job docker se ejecuta directamente en la máquina host (runs-on: docker) donde Docker ya esta disponible, usando Buildx con driver docker para comunicarse con el daemon Docker del host.

Release (.forgejo/workflows/release.yml)

Se ejecuta al pushear tags v* (ej: v1.0.0):

Job Descripcion
test Instala Go 1.26 via setup-go, instala libvips, verifica que los tests pasan antes de release
release Build y push de Docker con tag de version + latest, crea Release en Forgejo. Se ejecuta directamente en el host (runs-on: docker) usando Buildx con driver: docker

Imagenes Docker

Las imagenes se almacenan en el Forgejo Container Registry integrado:

git.jesusmarin.dev/angel/goimagestorage:latest
git.jesusmarin.dev/angel/goimagestorage:sha-abcdef1234
git.jesusmarin.dev/angel/goimagestorage:v1.0.0

Para usar la imagen en produccion:

docker pull git.jesusmarin.dev/angel/goimagestorage:latest

O en docker-compose.yml:

services:
  app:
    image: git.jesusmarin.dev/angel/goimagestorage:latest
    # ... resto de la configuracion

Crear un Release

git tag v1.0.0
git push origin v1.0.0

Esto ejecuta el workflow de release que:

  1. Verifica que los tests pasen
  2. Construye la imagen Docker con tags v1.0.0 y latest
  3. Hace push de la imagen al Forgejo Container Registry
  4. Crea un Release en Forgejo asociado al tag

Configuracion necesaria en Forgejo

  • Runner: Necesita un Forgejo Runner con Docker disponible en el host. El runner debe tener Node.js instalado (necesario para ejecutar actions de JavaScript como checkout@v4 y setup-go@v5)
  • Labels del runner: El runner debe tener los siguientes labels en su config.yml:
    • ubuntu-latest:docker://data.forgejo.org/oci/node:22-trixie — para jobs de test/lint (ejecutados en contenedor con Node.js)
    • docker:host — para jobs de build de Docker (ejecutados directamente en la máquina host, donde Docker ya esta disponible)
  • Buildx con driver docker: Los jobs que construyen imagenes usan docker/setup-buildx-action@v3 con driver: docker, lo que significa que usan el daemon Docker del host directamente sin necesidad de Docker-in-Docker
  • Registry: El Container Registry debe estar habilitado (lo esta por defecto)
  • PAT (Personal Access Token): Se requiere un PAT con scope write:package y read:package para hacer push de imagenes al Container Registry. El GITHUB_TOKEN automatico de Forgejo no tiene permisos de escritura sobre paquetes por defecto. Crear el PAT en Settings → Applications → Generate New Token y agregarlo como secret FORGEJO_TOKEN en Settings → Actions → Secrets del repositorio

Despliegue con Docker

La aplicacion incluye configuracion para despliegue en produccion con Docker Compose, asumiendo que estara detras de un proxy inverso (Caddy 2.x) que gestiona TLS.

Requisitos

  • Docker
  • Docker Compose

Configuracion

  1. Copiar el archivo de variables de entorno y completar los valores de S3:
cp .env.example .env

Editar .env con las credenciales S3:

S3_BUCKET=mi-bucket
S3_REGION=us-east-1
S3_ENDPOINT=https://s3.example.com
S3_ACCESS_KEY=tu_access_key
S3_SECRET_KEY=tu_secret_key
CDN_DOMAIN=

Despliegue

# Construir e iniciar
docker compose up -d

# Ver logs
docker compose logs -f

# Detener
docker compose down

El contenedor ejecuta automaticamente las migraciones antes de iniciar la aplicacion. Los datos de SQLite se persisten en un volumen Docker (app-data).

Crear usuario administrador

docker compose exec app /app/admin -dsn=/data/data.db -name="Admin" -email="admin@example.com"

Comandos adicionales

# Reconstruir imagen tras cambios en codigo
docker compose build

# Ejecutar migraciones manualmente
docker compose exec app /app/migrate -path /app/migrations -database sqlite3:///data/data.db up

# Ver version de migraciones
docker compose exec app /app/migrate -path /app/migrations -database sqlite3:///data/data.db version

Configuracion de Caddy 2.x

Caddyfile de ejemplo apuntando al contenedor:

example.com {
    reverse_proxy localhost:8080
}

Las variables SECURE_COOKIES y TRUST_FORWARDED_HEADERS ya estan habilitadas en docker-compose.yml por defecto.

Rutas

Metodo Ruta Descripcion Auth Rol
GET / Pagina inicial No -
GET /dashboard Dashboard principal Si Cualquiera
GET /user/login Formulario de login No -
POST /user/login Procesar login No (rate-limited) -
POST /user/logout Logout Si Cualquiera
GET /images Listar imagenes Si Viewer+
GET /images/new Formulario nueva imagen Si Admin
POST /images Crear imagen Si Admin
GET /images/{id}/edit Editar imagen Si Admin
POST /images/{id}/edit Actualizar imagen Si Admin
POST /images/{id}/delete Eliminar imagen Si Admin
GET /tags Listar tags Si Viewer+
GET /tags/new Formulario nuevo tag Si Admin
POST /tags Crear tag Si Admin
GET /tags/{id}/edit Editar tag Si Admin
POST /tags/{id}/edit Actualizar tag Si Admin
POST /tags/{id}/delete Eliminar tag Si Admin
GET /users Listar usuarios Si Admin
GET /users/new Formulario nuevo usuario Si Admin
POST /users Crear usuario Si Admin
POST /users/{id}/delete Eliminar usuario Si Admin
GET /i/{key} Servir imagen original No (rate-limited) -
GET /i/{key}/{variant} Servir variante No (rate-limited) -

Modelo de Datos

users

  • id, name, email, hashed_password, is_admin, created_at

blobs (similar a active_storage_blobs)

  • id, key, filename, content_type, metadata, service_name, byte_size, checksum, created_at

images

  • id, name, description, blob_id, created_at

tags

  • id, name, created_at

image_tags (many-to-many)

  • image_id, tag_id

variant_records (similar a active_storage_variant_records)

  • id, blob_id, variant_key, key, created_at

Seguridad

  • Autorizacion por roles: Solo admins pueden crear/editar/eliminar recursos; viewers solo pueden ver
  • CSRF Protection: Todas las formas estan protegidas con tokens CSRF
  • Rate Limiting: Login (5 req/s por IP, burst 10), imagenes publicas (30 req/s por IP, burst 60)
  • Cookies Seguras: Soporte para cookies Secure y HttpOnly
  • Headers de seguridad: CSP, X-Frame-Options, X-Content-Type-Options, X-XSS-Protection, Permissions-Policy
  • HSTS: Header Strict-Transport-Security cuando -secure-cookies esta habilitado
  • Validacion de entrada: Longitud maxima en todos los campos de formulario
  • Sanitizacion de filenames: Los nombres de archivo se sanitizan contra inyeccion de headers y path traversal
  • Checksum SHA-256: Las imagenes usan SHA-256 para verificar integridad
  • Upload limitado: Maximo 10MB por archivo, solo tipos MIME permitidos
  • Admin routes: Las rutas de gestion requieren privilegio de admin
  • Credenciales: Las credenciales S3 se leen UNICAMENTE de variables de entorno
  • Validacion de S3 keys: Las URLs de imagenes validan formato de key (32 hex chars) y variant (micro|mini)
  • Proteccion de eliminacion: No se puede eliminar el propio usuario ni el ultimo admin
  • isAdmin revalidado: El estado de admin se consulta desde la BD en cada request, no solo en login
  • Session invalidation: Las sesiones se destruyen automaticamente si el usuario ya no existe en la BD
  • Timing attack protection: El login realiza un hash dummy cuando el usuario no existe, impidiendo enumeracion por timing
  • X-Forwarded-For seguro: Se usa la ultima IP del header (la del proxy de confianza), no la primera, previniendo spoofing
  • Cleanup de sesiones: Las sesiones expiradas se limpian automaticamente cada 10 minutos con graceful shutdown
  • Rate limiter con eviction: Los IPs del rate limiter se limpian automaticamente tras 10 minutos de inactividad
  • Proxy inverso: Soporte para X-Forwarded-For con flag -trust-forwarded-headers
  • Content-Length: Las respuestas de imagenes incluyen header Content-Length
  • Cache-Control moderado: Las imagenes publicas usan max-age=86400 (24h) en lugar de 1 ano

CDN

La aplicacion soporta servir imagenes a traves de un CDN usando el flag -cdn-domain.

Configuracion URLs generadas
Sin -cdn-domain /i/abc123, /i/abc123/micro
Con -cdn-domain=cdn.example.com https://cdn.example.com/i/abc123, https://cdn.example.com/i/abc123/micro

Dependencias

  • modernc.org/sqlite - Driver SQLite puro en Go
  • github.com/alexedwards/scs/v2 - Gestion de sesiones
  • github.com/justinas/alice - Middleware chaining
  • github.com/go-playground/form/v4 - Decodificacion de formularios
  • github.com/justinas/nosurf - Proteccion CSRF
  • github.com/h2non/bimg - Procesamiento de imagenes (libvips)
  • github.com/aws/aws-sdk-go-v2 - Cliente S3
  • golang.org/x/crypto - Bcrypt para passwords
  • golang.org/x/time - Rate limiting
  • golang.org/x/term - Lectura de password sin eco

Extraccion a Microservicios

Gracias a la arquitectura basada en interfaces modular, cualquier modulo puede convertirse en microservicio:

  1. Crear implementacion del repositorio que llame a una API/gRPC en lugar de acceder a la base de datos
  2. Cambiar la inicializacion en main.go para usar la nueva implementacion
  3. No cambiar handlers ni rutas - la interfaz permanece igual

Licencia

MIT