Files
fn_registry/dev/issues/completed/0014-file-upload.md
T
2026-04-18 17:17:49 +02:00

15 KiB

0014 — File Upload & Storage

Metadata

Campo Valor
ID 0014
Estado pendiente
Prioridad media
Tipo feature

Dependencias

  • 0009 — HTTP Server Foundation: las funciones upload_handler y file_serve son 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_handler como ruta, configurar StorageConfig, y usar file_serve para 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 UploadedFile en functions/infra/uploaded_file.go con .md en types/infra/
  • 1.2 Crear tipo StorageConfig en functions/infra/storage_config.go con .md en types/infra/
  • 1.3 Crear tipo S3Config en functions/infra/s3_config.go con .md en types/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 vet si no hay MinIO local)
  • 4.5 fn index y 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/jpeg y image/png de 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/passwd o acceder a archivos fuera del directorio de storage. Mitigado: file_save_disk ignora el nombre original y usa UUID, file_serve valida que el path resuelto esta dentro del directorio base, file_delete rechaza paths con ...
  • File size DoS: Un cliente podria enviar archivos enormes para agotar disco o memoria. Mitigado: upload_parse usa http.MaxBytesReader para cortar la lectura al limite configurado, upload_handler rechaza antes de leer si Content-Length excede el maximo.
  • MIME type bypass: Un archivo puede tener magic bytes validos pero contenido malicioso despues. file_validate_type solo 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/jpeg y image/png no soportan formatos modernos (WebP, AVIF, HEIC). Para apps que necesiten mas formatos, habra que evaluar dependencias externas o delegar a herramientas de sistema (imagemagick).