- Go 80%
- HTML 16.8%
- Makefile 2.2%
- Dockerfile 0.7%
- Shell 0.3%
| .forgejo/workflows | ||
| cmd | ||
| internal | ||
| migrations | ||
| tests/integration | ||
| ui | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| comofunciona.md | ||
| docker-compose.yml | ||
| Dockerfile | ||
| entrypoint.sh | ||
| go.mod | ||
| go.sum | ||
| Makefile | ||
| README.md | ||
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
- Zero Coupling: Los modulos NO dependen de implementaciones concretas, solo de interfaces
- Interfaces minimas por modulo: Cada modulo define exactamente lo que necesita del exterior
- Template data autonoma: Cada modulo define su propia estructura de datos de template
- Errores compartidos centralizados: Errores comunes definidos en un paquete compartido
- Inyeccion de dependencias pura: Todos los componentes reciben sus dependencias via interfaces
- 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
imagesrecibe*storage.S3Clientcomo 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 comoErrDuplicateEmailyErrInvalidCredentialsson conceptos de dominio que deberian pertenecer a sus modulos respectivos, no a un paquete compartido. - Acoplamiento con
app.TemplateData: Cada modulo envuelveapp.TemplateDatay 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
SessionManagercon variaciones minimas en vez de usar la interfazapp.SessionManagerdel 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"desdeauth- NOimport "internal/modules/auth/contracts"para definir interfaces - SI- Adaptadores en
cmd/web/adapters.goconvierten 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 tienedomain.go,repository.gonicontracts/propio (usaauth/contracts/user_contract.gopara definir lo que necesita de users)dashboard: No tienedomain.go,repository.gonimodels.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:
- Verifica que los tests pasen
- Construye la imagen Docker con tags
v1.0.0ylatest - Hace push de la imagen al Forgejo Container Registry
- 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@v4ysetup-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@v3condriver: 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:packageyread:packagepara hacer push de imagenes al Container Registry. ElGITHUB_TOKENautomatico de Forgejo no tiene permisos de escritura sobre paquetes por defecto. Crear el PAT en Settings → Applications → Generate New Token y agregarlo como secretFORGEJO_TOKENen 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
- 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-Securitycuando-secure-cookiesesta 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 Gogithub.com/alexedwards/scs/v2- Gestion de sesionesgithub.com/justinas/alice- Middleware chaininggithub.com/go-playground/form/v4- Decodificacion de formulariosgithub.com/justinas/nosurf- Proteccion CSRFgithub.com/h2non/bimg- Procesamiento de imagenes (libvips)github.com/aws/aws-sdk-go-v2- Cliente S3golang.org/x/crypto- Bcrypt para passwordsgolang.org/x/time- Rate limitinggolang.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:
- Crear implementacion del repositorio que llame a una API/gRPC en lugar de acceder a la base de datos
- Cambiar la inicializacion en
main.gopara usar la nueva implementacion - No cambiar handlers ni rutas - la interfaz permanece igual
Licencia
MIT