Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
15 KiB
id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
| id | title | status | type | domain | scope | priority | depends | blocks | related | created | updated | tags |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0014 | File Upload & Storage | completado | feature | multi-app | media | 2026-05-17 | 2026-05-17 |
0014 — File Upload & Storage
Metadata
| Campo | Valor |
|---|---|
| ID | 0014 |
| Estado | pendiente |
| Prioridad | media |
| Tipo | feature |
Dependencias
- 0009 — HTTP Server Foundation: las funciones
upload_handleryfile_serveson handlers HTTP que dependen de los tipos y patrones definidos en 0009 (Route, Middleware, HTTPError, http_json_response, http_error_response, http_parse_body).
Objetivo
Crear funciones reutilizables en Go (dominio infra) para manejar subida de archivos, almacenamiento en disco, servido de archivos estaticos, y opcionalmente almacenamiento en S3. Cualquier app que necesite gestionar imagenes, documentos o media puede componer estas primitivas en vez de reimplementar el manejo de archivos desde cero.
Contexto
- No existen funciones de upload/storage en el registry. Cada app que necesita manejar archivos tiene que construir el handler de upload, la validacion, el almacenamiento y el servido desde cero.
- El patron es comun: apps con imagenes de perfil, documentos adjuntos, exports de datos, thumbnails, etc.
- Go stdlib tiene todo lo necesario para multipart parsing (
mime/multipart), manipulacion de imagenes (image,image/jpeg,image/png), y filesystem. Para S3 se necesita el SDK de AWS, pero como dependencia opcional. - Con estas funciones, una app nueva que necesite uploads solo hace: montar
upload_handlercomo ruta, configurarStorageConfig, y usarfile_servepara servir los archivos.
Arquitectura
functions/infra/
upload_handler.go -- NEW: HTTP handler multipart upload
upload_handler.md -- NEW
upload_parse.go -- NEW: parse multipart form, extraer archivos
upload_parse.md -- NEW
file_save_disk.go -- NEW: guardar archivo en disco con nombre unico
file_save_disk.md -- NEW
file_serve.go -- NEW: HTTP handler para servir archivos estaticos
file_serve.md -- NEW
file_delete.go -- NEW: eliminar archivo del disco
file_delete.md -- NEW
thumbnail_generate.go -- NEW: generar thumbnail de imagen
thumbnail_generate.md -- NEW
file_validate_type.go -- NEW: validar MIME type por magic bytes
file_validate_type.md -- NEW
file_unique_name.go -- NEW: generar nombre unico UUID + extension
file_unique_name.md -- NEW
s3_upload.go -- NEW: subir archivo a S3-compatible
s3_upload.md -- NEW
s3_download.go -- NEW: descargar archivo desde S3-compatible
s3_download.md -- NEW
s3_presign_url.go -- NEW: generar URL presignada S3
s3_presign_url.md -- NEW
types/infra/
uploaded_file.md -- NEW: metadata del tipo UploadedFile
storage_config.md -- NEW: metadata del tipo StorageConfig
s3_config.md -- NEW: metadata del tipo S3Config
Patron pure core / impure shell
- Pure:
file_validate_type(lee bytes en memoria, sin I/O),file_unique_name(genera string, determinista dado un UUID) - Impure: todo lo demas — interactuan con disco, red, HTTP requests/responses
Diseno
Tipos
// UploadedFile contiene la metadata de un archivo subido y almacenado.
type UploadedFile struct {
Filename string `json:"filename"` // nombre original del archivo
StoredName string `json:"stored_name"` // nombre en disco (UUID-based)
Size int64 `json:"size"` // tamano en bytes
ContentType string `json:"content_type"` // MIME type detectado
Path string `json:"path"` // ruta completa en disco
CreatedAt time.Time `json:"created_at"`
}
// StorageConfig configura el almacenamiento local de archivos.
type StorageConfig struct {
BaseDir string `json:"base_dir"` // directorio base para almacenar archivos
MaxFileSize int64 `json:"max_file_size"` // tamano maximo en bytes (ej: 10<<20 = 10MB)
AllowedTypes []string `json:"allowed_types"` // MIME types permitidos (ej: ["image/png", "image/jpeg", "application/pdf"])
}
// S3Config configura la conexion a almacenamiento S3-compatible.
type S3Config struct {
Endpoint string `json:"endpoint"` // URL del servidor (ej: "s3.amazonaws.com", "minio.local:9000")
Bucket string `json:"bucket"`
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
Region string `json:"region"`
UseSSL bool `json:"use_ssl"`
}
Funciones
| Funcion | Purity | Firma (simplificada) |
|---|---|---|
upload_handler |
impure | (cfg StorageConfig) http.HandlerFunc |
upload_parse |
impure | (r *http.Request, maxSize int64) ([]ParsedFile, error) |
file_save_disk |
impure | (baseDir string, filename string, data io.Reader) (UploadedFile, error) |
file_serve |
impure | (dir string, pathPrefix string, maxAge int) http.Handler |
file_delete |
impure | (path string) error |
thumbnail_generate |
impure | (srcPath string, dstPath string, maxWidth int, maxHeight int) error |
file_validate_type |
pure | (header []byte, allowedTypes []string) (string, bool) |
file_unique_name |
pure | (originalName string) string |
s3_upload |
impure | (cfg S3Config, key string, data io.Reader, contentType string) error |
s3_download |
impure | (cfg S3Config, key string, dst io.Writer) error |
s3_presign_url |
impure | (cfg S3Config, key string, expiry time.Duration) (string, error) |
Detalle de cada funcion
upload_handler — Handler HTTP completo para multipart upload. Recibe un StorageConfig, aplica limites de tamano, valida tipos MIME, guarda en disco con nombre unico, y responde con JSON del UploadedFile. Compone internamente upload_parse, file_validate_type, file_unique_name, y file_save_disk.
upload_parse — Parsea el request multipart, extrae uno o mas archivos con su metadata (nombre original, tamano, content type, contenido). Aplica http.MaxBytesReader para limitar tamano. Retorna un slice de structs intermedios con el contenido listo para guardar.
file_save_disk — Recibe un io.Reader con el contenido y lo escribe a disco en baseDir con un nombre unico generado por file_unique_name. Crea subdirectorios si no existen. Retorna el UploadedFile con la ruta final.
file_serve — Retorna un http.Handler que sirve archivos estaticos desde un directorio. Setea headers de cache (Cache-Control, ETag). Usa http.FileServer internamente pero stripea el prefijo de path y anade seguridad contra path traversal.
file_delete — Elimina un archivo del disco. Valida que el path es relativo al directorio de storage (no permite ../ traversal). Retorna error si el archivo no existe.
thumbnail_generate — Lee una imagen del disco, la redimensiona manteniendo aspect ratio al tamano maximo indicado, y la guarda en dstPath. Usa image, image/jpeg, image/png de stdlib. Soporta JPEG y PNG.
file_validate_type — Lee los primeros N bytes (magic bytes / file signature) de un []byte y determina el MIME type real del archivo. Compara contra la lista de tipos permitidos. No confia en el Content-Type del request — siempre verifica los bytes. Retorna el MIME type detectado y si esta en la lista permitida.
file_unique_name — Genera un nombre de archivo unico combinando un UUID v4 con la extension del archivo original. Ejemplo: a1b2c3d4-e5f6-7890-abcd-ef1234567890.png. Sanitiza la extension (solo alfanumericos).
s3_upload — Sube un archivo a un bucket S3-compatible. Acepta S3Config para conectar a AWS S3, MinIO, o cualquier implementacion compatible. Usa el AWS SDK v2.
s3_download — Descarga un archivo desde S3 a un io.Writer. Permite streaming directo a disco o a un HTTP response.
s3_presign_url — Genera una URL presignada para upload o download directo sin pasar por el servidor. Util para uploads grandes donde el cliente sube directamente a S3.
Tareas
Fase 1: Tipos
- 1.1 Crear tipo
UploadedFileenfunctions/infra/uploaded_file.gocon.mdentypes/infra/ - 1.2 Crear tipo
StorageConfigenfunctions/infra/storage_config.gocon.mdentypes/infra/ - 1.3 Crear tipo
S3Configenfunctions/infra/s3_config.gocon.mdentypes/infra/
Fase 2: Funciones puras (validacion y naming)
- 2.1
file_validate_type— detectar MIME type por magic bytes, comparar contra lista permitida. Tabla interna de signatures: JPEG (FF D8 FF), PNG (89 50 4E 47), GIF (47 49 46 38), PDF (25 50 44 46), WebP (52 49 46 46...57 45 42 50), ZIP (50 4B 03 04) - 2.2
file_unique_name— UUID v4 + extension sanitizada. Sin I/O, sin estado - 2.3 Tests unitarios para ambas funciones puras
Fase 3: Almacenamiento en disco
- 3.1
file_save_disk— escribir a disco con nombre unico, crear subdirectorios, retornar UploadedFile - 3.2
file_delete— eliminar archivo, validar path traversal - 3.3
file_serve— http.Handler con FileServer, cache headers, path traversal protection - 3.4
upload_parse— parsear multipart form, extraer archivos con metadata - 3.5
upload_handler— handler HTTP completo que compone parse + validate + save - 3.6
thumbnail_generate— resize con image stdlib, mantener aspect ratio - 3.7 Tests para funciones de disco con
os.MkdirTemp
Fase 4: S3-compatible storage
- 4.1
s3_upload— subir archivo a bucket con AWS SDK v2 - 4.2
s3_download— descargar archivo desde bucket - 4.3
s3_presign_url— generar URL presignada con expiracion configurable - 4.4 Tests con stub/mock de S3 (o
go build+go vetsi no hay MinIO local) - 4.5
fn indexy verificar que todas las funciones aparecen en registry.db - 4.6 Verificar
go vet -tags fts5
Ejemplo de uso
// Configurar storage
cfg := infra.StorageConfig{
BaseDir: "./uploads",
MaxFileSize: 10 << 20, // 10 MB
AllowedTypes: []string{"image/jpeg", "image/png", "application/pdf"},
}
// Montar rutas (usando funciones de 0009 HTTP Server Foundation)
routes := []infra.Route{
{Method: "POST", Path: "/api/upload", Handler: infra.UploadHandler(cfg)},
}
mux := infra.HttpRouter(routes)
// Servir archivos subidos
mux.Handle("/files/", infra.FileServe("./uploads", "/files/", 3600))
// Graceful shutdown
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()
infra.HttpServe(":8080", mux, ctx)
// Dentro de un handler custom (sin usar upload_handler):
func customUpload(w http.ResponseWriter, r *http.Request) {
files, err := infra.UploadParse(r, 10<<20)
if err != nil {
infra.HttpErrorResponse(w, infra.HTTPError{Status: 400, Code: "parse_error", Message: err.Error()})
return
}
for _, f := range files {
// Validar tipo real (no confiar en Content-Type del request)
mimeType, ok := infra.FileValidateType(f.Header, cfg.AllowedTypes)
if !ok {
infra.HttpErrorResponse(w, infra.HTTPError{Status: 415, Code: "invalid_type", Message: "tipo no permitido"})
return
}
// Guardar en disco
uploaded, err := infra.FileSaveDisk(cfg.BaseDir, f.Filename, f.Content)
if err != nil {
infra.HttpErrorResponse(w, infra.HTTPError{Status: 500, Code: "save_error", Message: err.Error()})
return
}
// Generar thumbnail si es imagen
if mimeType == "image/jpeg" || mimeType == "image/png" {
thumbPath := filepath.Join(cfg.BaseDir, "thumbs", uploaded.StoredName)
infra.ThumbnailGenerate(uploaded.Path, thumbPath, 200, 200)
}
}
infra.HttpJsonResponse(w, 200, map[string]string{"status": "ok"})
}
// S3 upload (opcional, para apps que necesiten storage remoto)
s3cfg := infra.S3Config{
Endpoint: "minio.local:9000",
Bucket: "uploads",
AccessKey: os.Getenv("S3_ACCESS_KEY"),
SecretKey: os.Getenv("S3_SECRET_KEY"),
Region: "us-east-1",
UseSSL: false,
}
f, _ := os.Open("./uploads/a1b2c3d4.png")
defer f.Close()
err := infra.S3Upload(s3cfg, "images/a1b2c3d4.png", f, "image/png")
// Generar URL presignada para download directo (1 hora)
url, err := infra.S3PresignUrl(s3cfg, "images/a1b2c3d4.png", time.Hour)
Decisiones de diseno
- Disco local primero, S3 opcional: la mayoria de apps del registry corren en un solo servidor. El almacenamiento en disco es suficiente y no requiere infraestructura adicional. S3 es para apps que escalan o necesitan storage distribuido.
- Validacion por magic bytes, no por Content-Type: el header Content-Type del request puede ser falso. Los primeros bytes del archivo son la fuente de verdad para determinar el tipo real.
- UUID para nombres en disco: evita colisiones de nombres y elimina problemas con caracteres especiales en nombres de archivo originales. Se preserva la extension para que el MIME type sea inferible por el filesystem.
- Solo stdlib para imagenes:
image/jpegyimage/pngde Go stdlib son suficientes para thumbnails basicos. Si se necesita soporte de mas formatos (WebP, AVIF), se puede extender despues sin romper la interfaz. - Sin base de datos de metadata: las funciones manejan archivos en disco/S3 pero no mantienen un indice de archivos subidos. Cada app decide como trackear sus uploads (puede usar operations.db, una tabla SQL, o simplemente el filesystem).
- S3Config como struct separado: permite que apps que no usan S3 no tengan que importar el AWS SDK. Las funciones S3 son independientes del resto.
Riesgos
- Path traversal: Un atacante podria intentar subir archivos con nombres como
../../etc/passwdo acceder a archivos fuera del directorio de storage. Mitigado:file_save_diskignora el nombre original y usa UUID,file_servevalida que el path resuelto esta dentro del directorio base,file_deleterechaza paths con... - File size DoS: Un cliente podria enviar archivos enormes para agotar disco o memoria. Mitigado:
upload_parseusahttp.MaxBytesReaderpara cortar la lectura al limite configurado,upload_handlerrechaza antes de leer siContent-Lengthexcede el maximo. - MIME type bypass: Un archivo puede tener magic bytes validos pero contenido malicioso despues.
file_validate_typesolo verifica los primeros bytes — no es un antivirus. Documentar que para apps con requisitos de seguridad altos se necesita escaneo adicional. - Dependencia AWS SDK para S3: Anade un arbol de dependencias significativo. Mitigado: las funciones S3 son opcionales y estan en archivos separados. Si una app no importa S3, no paga el costo.
- Thumbnails con stdlib limitados:
image/jpegyimage/pngno soportan formatos modernos (WebP, AVIF, HEIC). Para apps que necesiten mas formatos, habra que evaluar dependencias externas o delegar a herramientas de sistema (imagemagick).