8. Creating method upload files to the server

 

Método para Subir Archivos al Servidor

📚 Tema del Tutorial:

"Subida Segura de Archivos en PHP: Validación, Procesamiento y Almacenamiento"


🎯 Objetivo de Aprendizaje

Al final de este tutorial entenderás:

  • Cómo extraer información de archivos subidos

  • Cómo validar tipos y tamaños de archivo

  • Cómo generar nombres seguros para archivos

  • Cómo mover archivos temporales a ubicación permanente

  • Cómo manejar errores en la subida de archivos


📂 Parte 1: Entendiendo el Flujo de Subida de Archivos

El Proceso de Subida - Analogía del Correo:

text

📦 PAQUETE POSTAL (Archivo subido)

├── 🏷️ Nombre: audio.mp3 (nombre original)

├── 📏 Tamaño: 5MB (peso del paquete)

├── 🚚 Temporal: Oficina de correos (ubicación temporal)

├── ✅ Tipo: Audio/MP3 (qué contiene)

└── 📍 Destino: Tu casa (carpeta files/)

El Viaje de un Archivo:

text

👤 USUARIO selecciona archivo

    ↓

🌐 NAVEGADOR envía al servidor

    ↓

🖥️ PHP recibe en ubicación TEMPORAL

    ↓

🔍 PHP verifica (tipo, tamaño, seguridad)

    ↓

📁 PHP mueve a ubicación PERMANENTE

    ↓

✅ Archivo listo para usar en tu aplicación


🛠️ Parte 2: Extrayendo Información del Archivo

Variables Clave de $_FILES:

Cuando un usuario sube un archivo, PHP crea un array con esta estructura:

php

$_FILES['file'] = [

    'name'     => 'video.mp4',      // 🏷️ Nombre original

    'type'     => 'video/mp4',      // 📄 Tipo MIME

    'tmp_name' => '/tmp/php123',    // 📦 Ubicación temporal

    'error'    => 0,                // ✅ Código de error (0 = éxito)

    'size'     => 10485760          // 📏 Tamaño en bytes (10MB)

];

Extrayendo Cada Parte - Código Paso a Paso:

Paso 1: Ubicación temporal del archivo

php

$fileTmp = $file['tmp_name'];

// Ejemplo: '/tmp/phpX1Y2Z3'

// 📦 El paquete está en la oficina de correos temporal

Paso 2: Nombre original del archivo

php

$filename = basename($file['name']);

// basename('carpeta/video.mp4') = 'video.mp4'

// 🏷️ Solo el nombre, sin rutas de carpetas

Paso 3: Tamaño del archivo

php

$fileSize = $file['size'];

// Ejemplo: 10485760 (10MB en bytes)

// 📏 Cuánto pesa el archivo

Paso 4: Código de error

php

$errors = $file['error'];

// 0 = ✅ Éxito

// 1, 2, 3, 4, 6, 7, 8 = ❌ Diferentes tipos de errores

Paso 5: Tipo MIME del archivo

php

$mime = $file['type'];

// Ejemplo: 'video/mp4', 'audio/mpeg'

// 📄 Qué tipo de contenido es realmente


🔍 Parte 3: Obteniendo la Extensión del Archivo

¿Qué es una Extensión de Archivo?

La extensión son las letras después del punto:

text

🎵 audio.mp3 → .mp3

🎥 video.mp4 → .mp4

📸 foto.jpg  → .jpg

Código para Obtener la Extensión:

Método 1: Usando pathinfo() (Recomendado)

php

// Extrae información del path del archivo

$ext = pathinfo($filename, PATHINFO_EXTENSION);

// 'video.mp4' → 'mp4'

// 'audio.mpeg3' → 'mpeg3'

Método 2: Convertir a minúsculas

php

$ext = strtolower($ext);

// 'MP4' → 'mp4'

// 'JPG' → 'jpg'

// Importante: PHP en Linux diferencia mayúsculas/minúsculas

Visualización del proceso:

text

📁 Archivo: 'MiVideo.MP4'

    ↓ pathinfo()

🔧 Extensión: 'MP4'

    ↓ strtolower()

🔧 Extensión: 'mp4' ✅ Lista para comparar


📁 Parte 4: Encontrando la Ruta Correcta - Directorios en PHP

El Problema de las Rutas Relativas:

Cuando trabajas con archivos subidos, necesitas saber dónde estás y dónde quieres guardar.

Obteniendo el Directorio Raíz del Proyecto:

Código explicado:

php

$parentDirectory = dirname(dirname(dirname(__FILE__)));

Desglose visual:

text

__FILE__ = /home/usuario/proyecto/backend/classes/Whisper.php

    ↓ dirname() (1 vez)

/home/usuario/proyecto/backend/classes/

    ↓ dirname() (2 veces)

/home/usuario/proyecto/backend/

    ↓ dirname() (3 veces)

/home/usuario/proyecto/ ← ¡DIRECTORIO RAÍZ!

Analogía del edificio:

text

🏢 EDIFICIO (Proyecto completo)

├── 🏠 PISO 3: /home/usuario/proyecto/backend/classes/

├── 🏠 PISO 2: /home/usuario/proyecto/backend/

├── 🏠 PISO 1: /home/usuario/proyecto/ ← ¡Salida!

└── 📁 MALETEROS: /home/usuario/proyecto/files/

¿Por qué necesitamos esto?

Para poder guardar archivos en una carpeta específica:

text

/home/usuario/proyecto/files/video_abc123.mp4


🛡️ Parte 5: Validación de Seguridad - Lo Permitido vs Lo Prohibido

Creando la Lista de Tipos Permitidos:

Código del array de tipos permitidos:

php

$allowedMedia = [

    'video/mp4',      // 🎥 Videos MP4

    'video/mpeg',     // 🎥 Videos MPEG

    'audio/mpeg',     // 🎵 Audio MP3

    'audio/mpeg3',    // 🎵 Audio MPEG3

    'audio/wav'       // 🎵 Audio WAV

];

Visualización de tipos MIME comunes:

text

🎥 VIDEO:

├── MP4: video/mp4

├── AVI: video/x-msvideo

└── MOV: video/quicktime


🎵 AUDIO:

├── MP3: audio/mpeg

├── WAV: audio/wav

└── OGG: audio/ogg


🖼️ IMAGEN:

├── JPG: image/jpeg

├── PNG: image/png

└── GIF: image/gif

Validando el Tipo de Archivo:

Código de validación:

php

if(in_array($mime, $allowedMedia)){

    // ✅ Tipo permitido - Continuar

} else {

    // ❌ Tipo NO permitido - Mostrar error

    $this->error = "Formato de archivo inválido!";

}

¿Por qué validar por tipo MIME y no por extensión?

text

❌ Mala práctica: Validar solo por '.mp4'

   (Un virus.exe renombrado a virus.mp4 pasaría)


✅ Buena práctica: Validar por 'video/mp4'

   (PHP verifica el contenido real del archivo)


📏 Parte 6: Validando el Tamaño del Archivo

Entendiendo los Bytes - Conversiones:

text

1 Byte = 1 carácter

1 KB   = 1,024 Bytes

1 MB   = 1,024 KB = 1,048,576 Bytes

1 GB   = 1,024 MB = 1,073,741,824 Bytes

Límite de 20MB en Diferentes Unidades:

php

20 MB = 20 * 1024 * 1024 = 20,971,520 bytes

Código de validación de tamaño:

php

if($fileSize <= 20971520){  // 20MB en bytes

    // ✅ Tamaño aceptable

} else {

    // ❌ Archivo demasiado grande

    $this->error = "¡El archivo es muy grande!";

}

Mensajes de error útiles:

php

// Mostrar en MB para que el usuario entienda

$tamanoMB = round($fileSize / (1024 * 1024), 2);

$this->error = "El archivo tiene {$tamanoMB}MB. Máximo: 20MB";


🔐 Parte 7: Generando Nombres Seguros para Archivos

¿Por qué NO usar el nombre original?

Problemas con nombres originales:

text

❌ 'Mi Video.mp4' → Espacios causan problemas

❌ 'carnet.jpg' → Sobrescribe otros archivos

❌ '../../../virus.exe' → Inyección de rutas

❌ 'áéíóú.mp3' → Caracteres especiales

Solución: Nombres Aleatorios Únicos

Código para generar nombre seguro:

php

$folder = 'files/';

$file = $folder . md5(time() . mt_rand()) . '.' . $ext;

Desglose de la generación:

text

'files/' + md5('1678901234' + '54872') + '.mp4'

    ↓

'files/' + 'a1b2c3d4e5f6g7h8i9j0' + '.mp4'

    ↓

'files/a1b2c3d4e5f6g7h8i9j0.mp4' ✅

Explicación de las funciones:

  • time() = Segundos desde 1970 (siempre diferente)

  • mt_rand() = Número aleatorio (más diferencia)

  • md5() = Convierte a hash de 32 caracteres (seguro)

Ventajas de este método:

  1. ✅ Único (casi imposible repetir)

  2. ✅ Sin espacios ni caracteres especiales

  3. ✅ No revela información original

  4. ✅ Previene sobreescritura


🚚 Parte 8: Moviendo el Archivo a su Ubicación Final

La Diferencia Entre Temporal y Permanente:

text

📦 ARCHIVO TEMPORAL:

├── Ubicación: /tmp/phpX1Y2Z3

├── Duración: Hasta que termine el script

├── Propósito: Validación previa

└️️ Riesgo: Se borra automáticamente


🏠 ARCHIVO PERMANENTE:

├── Ubicación: /proyecto/files/abc123.mp4

├── Duración: Hasta que lo borres

├── Propósito: Uso en la aplicación

└️️ Seguridad: Tu responsabilidad

Función move_uploaded_file():

Código para mover el archivo:

php

move_uploaded_file($fileTmp, $parentDirectory . '/' . $file);

Parámetros explicados:

php

move_uploaded_file(

    $origen,      // 📦 '/tmp/phpX1Y2Z3' (archivo temporal)

    $destino      // 🏠 '/home/proyecto/files/abc123.mp4' (destino final)

);

Validación de seguridad automática:
La función move_uploaded_file() tiene seguridad integrada:

  • ✅ Verifica que el archivo fue subido via HTTP POST

  • ✅ Previene ataques de inyección de archivos

  • ✅ Es más seguro que copy() o rename()


🧩 Parte 9: Código Completo del Método upload()

Versión Completa con Comentarios:

php

public function upload($file){

    // 1️⃣ Extraer información del archivo

    $fileTmp  = $file['tmp_name'];      // Ubicación temporal

    $filename = basename($file['name']); // Nombre original

    $fileSize = $file['size'];          // Tamaño en bytes

    $errors   = $file['error'];         // Código de error

    $mime     = $file['type'];          // Tipo MIME

    

    // 2️⃣ Obtener extensión del archivo

    $ext = pathinfo($filename, PATHINFO_EXTENSION);

    $ext = strtolower($ext);

    

    // 3️⃣ Obtener directorio raíz del proyecto

    $parentDirectory = dirname(dirname(dirname(__FILE__)));

    

    // 4️⃣ Tipos de archivo permitidos

    $allowedMedia = [

        'video/mp4',

        'video/mpeg', 

        'audio/mpeg',

        'audio/mpeg3',

        'audio/wav'

    ];

    

    // 5️⃣ Validar tipo de archivo

    if(in_array($mime, $allowedMedia)){

        // 6️⃣ Validar tamaño (máximo 20MB)

        if($fileSize <= 20971520){

            // 7️⃣ Preparar carpeta destino

            $folder = 'files/';

            

            // 8️⃣ Generar nombre único seguro

            $file = $folder . md5(time() . mt_rand()) . '.' . $ext;

            

            // 9️⃣ Mover archivo a ubicación permanente

            move_uploaded_file($fileTmp, $parentDirectory . '/'.$file);

            

            // 🔟 Devolver ruta del archivo guardado

            return $file;

            

        } else {

            $this->error = "¡El archivo es muy grande!";

        }

    } else {

        $this->error = "Formato de archivo inválido!";

    }

}

Diagrama de Flujo del Método:

text

[📤 Archivo subido]

    ↓

[1️⃣ Extraer información]

    ↓

[2️⃣ Obtener extensión]

    ↓

[3️⃣ Encontrar directorio raíz]

    ↓

[4️⃣ ¿Tipo permitido?]

    │

    ├── NO → 🚨 Error: "Formato inválido"

    │

    └── SI → 5️⃣ ¿Tamaño ≤ 20MB?

               │

               ├── NO → 🚨 Error: "Muy grande"

               │

               └── SI → 6️⃣ Generar nombre único

                           ↓

                        7️⃣ Mover a carpeta files/

                           ↓

                        8️⃣ ✅ Devolver ruta guardada


🎯 Parte 10: Probando y Depurando el Método

Prueba Paso a Paso:

1. Crear carpeta files/ en el directorio raíz:

text

📁 proyecto/

├── 📁 backend/

├── 📁 frontend/

├── 📁 files/ ← ¡CREAR ESTA CARPETA!

└── 📄 index.php

2. Verificar permisos de la carpeta:

bash

# En Linux/Mac, dar permisos de escritura

chmod 755 files/

3. Probar con diferentes archivos:

  • ✅ video.mp4 (15MB)

  • ✅ audio.mp3 (5MB)

  • ❌ documento.pdf (formato no permitido)

  • ❌ video_grande.mkv (25MB - demasiado grande)

Posibles Errores y Soluciones:

Error 1: "Warning: move_uploaded_file(): failed to open stream"
Causa: La carpeta files/ no existe o no tiene permisos
Solución: Crear carpeta y dar permisos

Error 2: "File is large!" aunque el archivo es pequeño
Causa: Límite en php.ini (upload_max_filesize)
Solución: Verificar configuración PHP

Error 3: Archivo no se mueve pero no da error
Causa: move_uploaded_file() devuelve false silenciosamente
Solución: Verificar ruta completa de destino


📊 Parte 11: Ejercicio Práctico - Sistema de Subida de Imágenes

Clase para Subir Imágenes:

php

<?php

class ImageUploader {

    public $error;

    

    public function upload($image){

        // 1. Extraer información

        $tmpName = $image['tmp_name'];

        $originalName = basename($image['name']);

        $size = $image['size'];

        $type = $image['type'];

        

        // 2. Obtener extensión

        $ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));

        

        // 3. Tipos permitidos (solo imágenes)

        $allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];

        

        if(!in_array($type, $allowedTypes)){

            $this->error = "Solo se permiten imágenes JPG, PNG o GIF";

            return false;

        }

        

        // 4. Tamaño máximo: 5MB

        if($size > 5242880){

            $this->error = "La imagen no debe superar 5MB";

            return false;

        }

        

        // 5. Generar nombre único

        $newName = 'uploads/' . uniqid() . '.' . $ext;

        

        // 6. Mover archivo

        if(move_uploaded_file($tmpName, $newName)){

            return $newName; // ✅ Éxito

        } else {

            $this->error = "Error al subir la imagen";

            return false;

        }

    }

}

?>

Uso de la clase:

php

<?php

require 'ImageUploader.php';


$uploader = new ImageUploader();


if($_SERVER['REQUEST_METHOD'] == 'POST'){

    $resultado = $uploader->upload($_FILES['imagen']);

    

    if($resultado){

        echo "✅ Imagen subida: " . $resultado;

    } else {

        echo "❌ Error: " . $uploader->error;

    }

}

?>


🛡️ Parte 12: Consideraciones de Seguridad Avanzadas

Protecciones Adicionales Recomendadas:

1. Validar contenido real del archivo:

php

// Verificar que un MP3 sea realmente audio

$finfo = finfo_open(FILEINFO_MIME_TYPE);

$realType = finfo_file($finfo, $file['tmp_name']);

finfo_close($finfo);


if($realType != 'audio/mpeg'){

    $this->error = "El archivo no es un MP3 válido";

}

2. Escanear archivos con antivirus:

php

// En servidores Linux

exec("clamscan " . escapeshellarg($file['tmp_name']), $output, $return);

if($return != 0){

    $this->error = "Archivo potencialmente peligroso";

}

3. Limitar tipos de archivo por extensión también:

php

$allowedExtensions = ['mp4', 'mp3', 'wav', 'mpeg'];

if(!in_array($ext, $allowedExtensions)){

    $this->error = "Extensión no permitida";

}

4. Sanitizar nombres de archivo:

php

// Eliminar caracteres peligrosos

$safeName = preg_replace('/[^a-zA-Z0-9\.\-_]/', '', $originalName);


❓ Parte 13: Cuestionario de Evaluación

Pregunta 1:

¿Por qué es importante usar move_uploaded_file() en lugar de copy()?
a) Porque es más rápido
b) Porque tiene validaciones de seguridad integradas ✅
c) Porque usa menos memoria
d) Porque es más nuevo

Explicación: move_uploaded_file() verifica que el archivo fue subido via HTTP POST, previniendo ataques.

Pregunta 2:

¿Qué hace la función basename('carpeta/subcarpeta/archivo.mp4')?
a) Devuelve 'carpeta/subcarpeta/archivo.mp4'
b) Devuelve 'archivo.mp4' ✅
c) Devuelve 'carpeta'
d) Devuelve 'mp4'

Pregunta 3:

En este código, ¿por qué usamos md5(time() . mt_rand())?
a) Para encriptar el archivo
b) Para generar un nombre único y seguro ✅
c) Para comprimir el archivo
d) Para validar el tipo de archivo

Pregunta 4:

¿Cuál es el tamaño máximo permitido en nuestro método upload?
a) 10MB
b) 20MB ✅
c) 50MB
d) 100MB

Pregunta 5:

¿Qué tipo de archivos permite nuestra aplicación?
a) Cualquier tipo de archivo
b) Solo documentos PDF
c) Videos MP4/MPEG y audios MP3/WAV ✅
d) Solo imágenes JPG y PNG


Pregunta 6:

¿Por qué convertimos la extensión a minúsculas con strtolower()?
a) Para ahorrar espacio de almacenamiento
b) Porque PHP en Linux diferencia mayúsculas/minúsculas ✅
c) Para hacer más rápido la validación
d) Porque se ve mejor

Pregunta 7:

¿Qué significa este código?

php

$parentDirectory = dirname(dirname(dirname(__FILE__)));

a) Obtiene la carpeta actual
b) Sube 3 niveles en la estructura de directorios ✅
c) Crea 3 carpetas nuevas
d) Elimina 3 niveles de directorios

Respuesta correcta: b) Sube desde la ubicación actual hasta el directorio raíz del proyecto.


🏆 Resumen Final - Reglas para Subida Segura

Las 7 Reglas de Oro:

  1. 🔒 Valida siempre en el servidor - Nunca confíes en validación JavaScript

  2. 📏 Establece límites claros - Tamaño máximo definido

  3. 📋 Restringe tipos permitidos - Solo lo que tu aplicación necesita

  4. 🔑 Genera nombres únicos - Previene sobrescritura y ataques

  5. 📁 Usa move_uploaded_file() - Seguridad integrada

  6. 🚨 Maneja errores apropiadamente - Informa al usuario claramente

  7. 📝 Registra actividad - Para depuración y seguridad

Checklist de tu Método upload():

✅ Extrae información del array $_FILES
✅ Obtiene extensión con pathinfo()
✅ Encuentra directorio raíz con dirname()
✅ Valida tipo MIME contra lista permitida
✅ Verifica tamaño (≤ 20MB)
✅ Genera nombre único con md5(time() . mt_rand())
✅ Crea carpeta files/ en directorio raíz
✅ Mueve archivo con move_uploaded_file()
✅ Devuelve ruta del archivo guardado
✅ Maneja errores con $this->error


🚀 Próximos Pasos - Lo que Viene

Mejoras para Implementar:

  1. 🗂️ Subida múltiple de archivos

  2. 📊 Barra de progreso en tiempo real

  3. 🖼️ Generación de miniaturas para videos

  4. 📁 Organización por carpetas (por fecha/usuario)

  5. 🔍 Búsqueda y gestión de archivos subidos

  6. 🔄 Reintentos automáticos en fallos

Código para subida múltiple:

php

public function uploadMultiple($files){

    $resultados = [];

    

    foreach($files['tmp_name'] as $key => $tmpName){

        $archivo = [

            'name' => $files['name'][$key],

            'type' => $files['type'][$key],

            'tmp_name' => $tmpName,

            'error' => $files['error'][$key],

            'size' => $files['size'][$key]

        ];

        

        $resultados[] = $this->upload($archivo);

    }

    

    return $resultados;

}


📚 Recursos Adicionales

Configuraciones PHP importantes en php.ini:

ini

upload_max_filesize = 20M      ; Tamaño máximo por archivo

post_max_size = 21M            ; Tamaño máximo total POST

max_file_uploads = 20          ; Máximo de archivos simultáneos

file_uploads = On              ; Permitir subida de archivos

Para seguir aprendiendo:

  1. Subida con AJAX - Sin recargar página

  2. Compresión automática - Reducir tamaño de imágenes/videos

  3. Almacenamiento en la nube - AWS S3, Google Cloud Storage

  4. Streaming de video - Reproducción mientras se sube

  5. OCR de documentos - Extraer texto de PDFs e imágenes

¡Excelente trabajo! 🎉 Has creado un sistema seguro de subida de archivos. Esta es una de las funcionalidades más importantes (y peligrosas si se hace mal) en desarrollo web.


💡 Consejo final de seguridad:
Siempre trata los archivos subidos por usuarios como potencialmente peligrosos. Incluso con validación, un atacante creativo podría intentar explotar tu sistema. La regla de oro: desconfía, valida, sanitiza.


Comentarios

Entradas más populares de este blog

1-7. Transforma tu audio a texto

10. Haz que tu asistente hable

8. NUEVO - Solución si tu micrófono no está captando tu audio