Back to flin
flin

Soporte de carga de archivos

Cómo FLIN maneja la carga de archivos de forma nativa: análisis multipart, validación de tamaño, verificación de tipo y almacenamiento con save_file(), sin multer, sin formidable, sin configuración.

Thales & Claude | March 30, 2026 8 min flin
EN/ FR/ ES
flinrust

La carga de archivos es una de esas funcionalidades que toda aplicación web necesita y que todo framework web hace innecesariamente complicada. En Express.js, instalas multer, configuras destinos de almacenamiento, estableces filtros de archivo, defines límites de tamaño, manejas errores y escribes lógica de limpieza para archivos temporales. En Django, configuras MEDIA_ROOT, MEDIA_URL, FILE_UPLOAD_MAX_MEMORY_SIZE, y esperas que el backend FileSystemStorage por defecto funcione para tu caso de uso.

FLIN maneja la carga de archivos como una capacidad integrada del runtime. Las solicitudes multipart se analizan automáticamente. La validación de archivos es declarativa. El almacenamiento es una sola llamada de función. La limpieza es automática. No hay biblioteca que instalar ni configuración que escribir.

El flujo de carga

Una carga de archivo en FLIN sigue tres pasos: el formulario HTML, la validación y el almacenamiento.

Paso 1: el formulario

flin// app/upload.flin

<form method="POST" action="/api/upload" enctype="multipart/form-data">
    <input type="text" name="title" placeholder="Document title" required>
    <input type="text" name="description" placeholder="Description">
    <input type="file" name="document" accept=".pdf,.docx,.xlsx" required>
    <button type="submit">Upload</button>
</form>

El atributo enctype="multipart/form-data" le indica al navegador que codifique el formulario como multipart. El analizador de cuerpo de FLIN detecta este tipo de contenido y analiza el límite multipart, extrayendo tanto los campos de texto como las partes de archivo.

Paso 2: el manejador de ruta

flin// app/api/upload.flin

guard auth

route POST {
    validate {
        title: text @required @minLength(1)
        description: text
        document: file @required @max_size("10MB") @allow_types("application/pdf",
            "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
            "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    }

    file_path = save_file(body.document, ".flindb/documents/")

    doc = Document {
        title: body.title,
        description: body.description || "",
        file_path: file_path,
        file_name: body.document.name,
        file_size: body.document.size,
        file_type: body.document.content_type,
        uploaded_by: to_int(session.userId)
    }
    save doc

    response {
        status: 201
        body: doc
    }
}

Paso 3: eso es todo

No hay paso 3. El archivo se valida, se almacena y los metadatos se guardan en la base de datos. El archivo temporal se limpia automáticamente después de que save_file() lo mueve a su ubicación permanente.

El bloque validate para archivos

La validación de archivos en FLIN usa el mismo bloque validate que otros campos del cuerpo de la solicitud, con decoradores específicos para archivos:

flinvalidate {
    avatar: file @required
        @max_size("5MB")
        @allow_types("image/png", "image/jpeg", "image/webp")

    resume: file
        @max_size("10MB")
        @allow_types("application/pdf")

    photos: [file]          // Múltiples archivos
        @max_size("25MB")   // Por archivo
        @max_count(10)      // Máximo 10 archivos
        @allow_types("image/*")
}

Los decoradores son:

DecoradorDescripciónEjemplo
@max_size(size)Tamaño máximo de archivo@max_size("5MB")
@allow_types(types...)Tipos MIME permitidos@allow_types("image/png", "image/jpeg")
@max_count(n)Número máximo de archivos (para arreglos)@max_count(10)
@requiredEl archivo debe estar presente@required

Las cadenas de tamaño soportan sufijos KB, MB y GB. Los patrones de tipo MIME soportan comodines: "image/*" coincide con cualquier tipo de imagen.

Cuando la validación falla, FLIN devuelve un 400 Bad Request con un mensaje de error claro:

json{
    "error": "Validation failed",
    "fields": {
        "avatar": "File too large: 12.5 MB exceeds maximum of 5 MB",
        "resume": "File type 'application/zip' is not allowed. Allowed types: application/pdf"
    }
}

La función save_file()

save_file() es una función integrada que mueve un archivo cargado desde su ubicación temporal a un directorio permanente:

flinpath = save_file(body.avatar, ".flindb/avatars/")
// Devuelve: ".flindb/avatars/a1b2c3d4-photo.jpg"

La función:

  1. Crea el directorio de destino si no existe.
  2. Genera un nombre de archivo único anteponiendo un UUID para evitar colisiones.
  3. Mueve (no copia) el archivo temporal al destino.
  4. Devuelve la ruta relativa al archivo almacenado.

La ruta devuelta puede almacenarse en un campo de base de datos y usarse después para servir el archivo:

flinentity User {
    name: text
    email: text
    avatar: text    // Almacena la ruta de save_file()
}

Servir archivos cargados

Los archivos almacenados en .flindb/ son accesibles a través de un endpoint de servicio de archivos integrado. FLIN sirve automáticamente archivos desde directorios .flindb/ con cabeceras de tipo de contenido apropiadas:

flin// En una plantilla de vista
<img src={"/files/" + user.avatar} alt={user.name}>

// O construir la URL
avatar_url = "/files/" + user.avatar

El servidor de archivos valida la ruta para prevenir ataques de recorrido de directorios. Las solicitudes para ../../../etc/passwd se rechazan antes de tocar el sistema de archivos.

Carga de múltiples archivos

FLIN soporta la carga de múltiples archivos usando sintaxis de arreglo en el formulario y la validación:

flin// Formulario
<form method="POST" action="/api/gallery" enctype="multipart/form-data">
    <input type="text" name="album_name" required>
    <input type="file" name="photos" multiple accept="image/*">
    <button type="submit">Upload Photos</button>
</form>
flin// app/api/gallery.flin

route POST {
    validate {
        album_name: text @required
        photos: [file] @required @max_count(20) @max_size("10MB") @allow_types("image/*")
    }

    album = Album {
        name: body.album_name,
        owner: to_int(session.userId)
    }
    save album

    paths = []
    for photo in body.photos {
        path = save_file(photo, ".flindb/gallery/" + to_text(album.id) + "/")
        save Photo {
            album_id: album.id,
            file_path: path,
            file_name: photo.name,
            file_size: photo.size
        }
        paths = paths + [path]
    }

    response {
        status: 201
        body: { album: album, photos: paths }
    }
}

Cada archivo en el arreglo se valida independientemente contra los decoradores. Si algún archivo falla la validación, toda la solicitud se rechaza.

Streaming de archivos grandes

Para archivos que exceden el umbral de memoria por defecto (1 MB), FLIN transmite la carga a un archivo temporal en disco en lugar de mantenerlo en memoria. Esto previene que cargas grandes agoten la memoria del servidor:

rustconst MEMORY_THRESHOLD: usize = 1024 * 1024; // 1 MB

fn store_upload_part(part: &MultipartPart) -> Result<UploadedFile, ParseError> {
    if part.size <= MEMORY_THRESHOLD {
        // Archivo pequeño: mantener en memoria
        Ok(UploadedFile::InMemory {
            data: part.data.clone(),
            name: part.filename.clone(),
            content_type: part.content_type.clone(),
        })
    } else {
        // Archivo grande: transmitir al directorio temporal
        let temp_path = temp_dir().join(format!("flin-upload-{}", generate_uuid()));
        let mut file = File::create(&temp_path)?;
        file.write_all(&part.data)?;

        Ok(UploadedFile::OnDisk {
            path: temp_path,
            name: part.filename.clone(),
            content_type: part.content_type.clone(),
            size: part.size,
        })
    }
}

El archivo temporal se elimina automáticamente cuando la solicitud se completa, ya sea que el manejador tenga éxito o falle. Esta limpieza ocurre en una implementación Drop en la estructura UploadedFile, que Rust garantiza que se ejecutará incluso si el manejador entra en pánico.

Registro con carga de avatar

La referencia de patrones de autenticación (PRD 38) muestra un flujo de registro completo con carga de archivo:

flin// app/auth/process-register.flin

route POST {
    validate {
        firstName: text @required @minLength(1)
        email: text @required @email
        password: text @required @minLength(4)
        confirmPassword: text @required
        lastName: text
        occupation: text
        country: text
        avatar: file @max_size("5MB")
    }

    if body.password != body.confirmPassword {
        session.regError = "error.passwords_mismatch"
        redirect("/register")
    }

    existing = User.where(email == body.email && role == "User").first
    if existing != none {
        session.regError = "error.email_taken"
        redirect("/register")
    }

    avatarPath = ""
    if body.avatar != none {
        avatarPath = save_file(body.avatar, ".flindb/avatars/")
    }

    newUser = User {
        email: body.email,
        password: bcrypt_hash(body.password),
        name: body.firstName + " " + (body.lastName || ""),
        firstName: body.firstName,
        lastName: body.lastName || "",
        avatar: avatarPath,
        provider: "Email"
    }
    save newUser

    session.user = newUser.email
    session.userName = newUser.name
    session.userId = to_text(newUser.id)

    redirect("/tasks")
}

El avatar es opcional (@max_size sin @required). Si se proporciona, se valida y almacena. Si no, la ruta del avatar permanece vacía. Todo el registro, incluyendo carga de archivo, validación, hash de contraseña y creación de sesión, cabe en un solo manejador de ruta.

Consideraciones de seguridad

La carga de archivos es uno de los vectores de ataque más comunes en aplicaciones web. FLIN aborda los principales riesgos a nivel de runtime:

Recorrido de ruta. La función save_file() elimina los componentes de directorio del nombre de archivo original y genera un nombre basado en UUID. Un archivo cargado como ../../../etc/crontab se almacena como a1b2c3d4-crontab.

Suplantación de tipo de contenido. FLIN valida el contenido real del archivo contra el tipo MIME declarado, no solo la extensión del nombre de archivo. Un archivo .jpg que contiene código PHP se detecta y rechaza.

Límites de tamaño. El analizador multipart impone límites de tamaño antes de leer todo el archivo en memoria. Una carga de 2 GB se rechaza después de leer el primer fragmento que excede el límite.

Limpieza de archivos temporales. Todos los archivos temporales se limpian cuando la solicitud termina, independientemente de si el manejador tuvo éxito o falló.

Estas protecciones no son opcionales. No son middleware que podrías olvidar aplicar. Están integradas en el runtime y se aplican a cada carga de archivo en cada aplicación FLIN.

En el próximo artículo, exploramos los helpers de respuesta de FLIN y el sistema de códigos de estado -- cómo response{}, error(), redirect() y la serialización JSON automática hacen que las respuestas HTTP sean tan simples como devolver un valor.


Esta es la Parte 104 de la serie "Cómo construimos FLIN", que documenta cómo un CEO en Abiyán y un CTO de IA diseñaron y construyeron un lenguaje de programación desde cero.

Navegación de la serie: - [102] Guards: seguridad declarativa para rutas - [103] Soporte WebSocket integrado en el lenguaje - [104] Soporte de carga de archivos (estás aquí) - [105] Helpers de respuesta y códigos de estado

Share this article:

Responses

Write a response
0/2000
Loading responses...

Related Articles