Software y sistemas

Software gestor de archivos en la nube con PHP y MySQL

En este post te mostraré un sistema que acabo de crear. Se trata de un software para el alojamiento de archivos en la nube usando PHP y MySQL. Es decir, algo como un Google Drive, Dropbox o Mega pero de forma básica.

Archivos en la nube con software escrito en PHP

Gracias a este software open source de archivos en la nube podemos montar nuestro propio disco en la nube en la red local o en internet, subir archivos y acceder a ellos desde cualquier dispositivo, pues este programa es responsivo.

También podremos compartir los archivos para su descarga, usando un hash único que podemos eliminar más tarde.

A través de este post te mostraré los módulos del programa, explicaré un poco la arquitectura y te enseñaré cómo descargarlo e instalarlo.

Arquitectura del sistema

Esqueleto de software para gestión de archivos en la nube con PHP y Vue

Esta aplicación web se divide en dos partes. Tenemos la parte del servidor que está con PHP y MySQL, y la parte del cliente con Buefy, que es una combinación de Vue y Bulma.

Los archivos del servidor, incluyendo dependencias y los archivos que se suben están en api. Aquí tenemos los archivos que sirven como puente, así como los archivos que gestionan toda la aplicación.

Para los archivos del lado del cliente tenemos la carpeta frontend en donde están todos los componentes de Vue, pues se usa la Vue CLI para compilar los archivos e iniciar el servidor de desarrollo.

Ya para pasar a producción simplemente compilamos la app del lado del cliente y copiamos todos sus archivos junto con la carpeta api al servidor en donde vamos a desplegar la app.

Al final tendremos una Single Page Application de Vue totalmente responsiva que va a consumir un servidor con PHP para la gestión de archivos.

Cada usuario tiene acceso solo a sus propios archivos, pero puede compartirlos para el público. Por cierto, los archivos se guardan en el sistema de archivos o disco duro, no en la base de datos (eso sería una ofensa).

El sistema también cuenta con un módulo para iniciar sesión así como la gestión de usuarios en donde se pueden crear, eliminar o cambiarles la contraseña.

Base de datos

Se puede usar MariaDB o MySQL como motor de base de datos. Sin importar cuál elijas, recuerda crear la base de datos que alojará los datos e importar las tablas:

CREATE TABLE IF NOT EXISTS usuarios
(
    id              BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    correo          VARCHAR(255)    NOT NULL UNIQUE,
    palabra_secreta VARCHAR(255)    NOT NULL,
    administrador   SMALLINT        NOT NULL
);

CREATE TABLE IF NOT EXISTS archivos
(
    id              BIGINT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT,
    nombre_original VARCHAR(1024)   NOT NULL,
    nombre_real     VARCHAR(36)     NOT NULL,
    fecha_creacion  VARCHAR(19)     NOT NULL,
    tamanio_bytes   BIGINT UNSIGNED NOT NULL,
    id_usuario      BIGINT UNSIGNED NOT NULL,
    FOREIGN KEY (id_usuario) REFERENCES usuarios (id) ON DELETE CASCADE ON UPDATE CASCADE
);

CREATE TABLE IF NOT EXISTS archivos_compartidos
(
    hash       VARCHAR(20)    NOT NULL PRIMARY KEY,
    id_archivo BIGINT UNSIGNED NOT NULL,
    FOREIGN KEY (id_archivo) REFERENCES archivos (id) ON DELETE CASCADE ON UPDATE CASCADE
);

/*
Usuario administrador por defecto
parzibyte@gmail.com
123
*/INSERT INTO `usuarios`
VALUES (1, 'parzibyte@gmail.com', '$2y$10$I6/Sw1DZCdLZXJqHq46YYeoCfYYnEMFBx193k/sxQerK2tmA3fB4u', 1);

Tenemos tres tablas. Una es de usuarios, otra es de archivos y la tercera es de los archivos que están compartidos. Además, tenemos un usuario administrador por defecto para poder acceder al sistema la primera vez.

La tabla de usuarios guarda los datos de los usuarios, que son el correo y la contraseña hasheada. También tiene una columna para saber si el usuario es administrador. Un administrador puede crear más usuarios.

La tabla de archivos guarda el nombre original, que es el nombre con el que el usuario sube el archivo, así como el nombre real que es un nombre único y aleatorio. También guarda la fecha de creación, el tamaño en bytes y el usuario al que le pertenece.

Finalmente la de los archivos compartidos permite saber si un archivo está compartido a través del id del mismo y un hash aleatorio seguro. De este modo podemos compartir archivos para que cualquier persona los descargue.

Login

Iniciar sesión en sistema de alojamiento de archivos con PHP

Para el login simplemente se solicita el correo y la contraseña del usuario que ya debe estar registrado. Si el correo no existe o la contraseña es inválida, se le indica al usuario. El formulario está validado.

Se requiere que se inicie sesión para que los archivos que se suban a este gestor estén relacionados de cierto modo al usuario.

Gestor de archivos en la nube con PHP

Comencemos viendo la parte para subir archivos. Los archivos se pueden seleccionar o arrastrar y soltar.

Subiendo archivos al sistema de drive en PHP

El sistema permite la subida de cualquier tipo de archivo, en cualquier cantidad. No hay límite en el peso más que por restricciones de PHP y el php.ini.

Todos los archivos subidos aparecen en Mis archivos que es lo que vendría a ser el Disco en la nube o espacio de almacenamiento del usuario:

Archivos en la nube con software escrito en PHP

Como puedes ver tenemos la lista de archivos del usuario. De nuevo te digo, los verdaderos archivos “físicos” están en el disco duro, aquí solo están sus detalles. El usuario puede descargar el archivo, eliminarlo o compartirlo.

Quiero que notes los iconos de los archivos, este icono depende de su extensión y no aparece por arte de magia. Está debidamente programado. Tenemos la columna con un icono:

<b-table-column searchable field="nombre_original" label="Nombre" sortable v-slot="props">
  <b-icon :icon="obtenerIcono(props.row.nombre_original)">
  </b-icon>
  {{ props.row.nombre_original }}
</b-table-column>

El icono se obtiene con la función obtenerIcono que está así:

obtenerIcono(nombreArchivo) {
  return Utiles.obtenerIconoSegunNombreArchivo(nombreArchivo);
},

Mismo que solo es un puente para la siguiente función:

const EXTENSIONES_ICONOS = {
    "txt": "file-document-outline",
    "pdf": "file-pdf-box",
    "sql": "database",
    "exe": "microsoft-windows",
    "jpg": "image",
    "png": "image",
    "go": "language-go",
    "py": "language-python",
    "c": "language-c",
    "cpp": "language-cpp",
    "cs": "language-csharp",
    "java": "language-java",
    "js": "language-javascript",
    "mkv": "movie",
    "mp4": "movie",
    "avi": "movie",
    "msi": "microsoft-windows",
    "zip": "zip-box",
    "rar": "zip-box",
};
const ICONO_POR_DEFECTO = "file";
const Utiles = {
    obtenerExtensionDeArchivo(nombreArchivo) {
        if (!nombreArchivo) {
            return "";
        }
        return nombreArchivo.substring(nombreArchivo.lastIndexOf(".") + 1);
    },
    obtenerIconoSegunNombreArchivo(nombreArchivo) {
        return EXTENSIONES_ICONOS[Utiles.obtenerExtensionDeArchivo(nombreArchivo)] || ICONO_POR_DEFECTO;
    }
};
export default Utiles;

Primero extraigo la extensión cortando la parte que está después del punto, luego la uso como clave para acceder al diccionario de iconos. Si la clave no existe, entonces se regresa el icono por defecto. Ah, estoy usando los iconos de Material Design.

Si quieres agregar más iconos simplemente agrega más entradas al diccionario, yo agregué las que creí necesarias.

Compartiendo archivo

Compartir archivo a través de enlace – Sistema de alojamiento de archivos con PHP

Se puede compartir un archivo con el público en general, es decir, cualquier usuario con acceso al link. Simplemente se verifica si el usuario es dueño del archivo y en caso de que sí, se genera un hash aleatorio seguro.

Este enlace puede ser compartido y cualquier persona puede descargar el archivo.  En cualquier momento se puede dejar de compartir y el link ya no será válido.

Archivo todavía no compartido

Por cierto, el diálogo que aparece para compartir el archivo es un ejemplo de cómo pasar propiedades a un componente en Vue.

Descarga de archivos

Se me hace importante resaltar este aspecto. Podríamos servir el archivo con Apache y evitarnos optimizaciones, pero debido a la gestión de permisos debemos hacerlo con PHP.

Anteriormente en mi blog ya te mostré que se puede forzar la descarga de un archivo con readfile, pero para archivos grandes es mejor hacerlo por fragmentos. Y en este caso el código que descarga un archivo público es:

<?php

use Parzibyte\CompartirArchivo;
use Parzibyte\Gestor;

if (!isset($_GET["hash"])) {
    exit("No hay hash");
}
include_once "vendor/autoload.php";
$hash = $_GET["hash"];
$detalles = CompartirArchivo::obtenerDetallesPublicosArchivoCompartido($hash);
if (!$detalles) {
    exit("El archivo solicitado no existe o se ha dejado de compartir");
}
Gestor::servirArchivoParaDescargar($detalles->id);

La magia ocurre en la función servirArchivoParaDescargar:

<?php
static function servirArchivoParaDescargar($idArchivo)
{
    $archivo = Gestor::obtenerUnoPorId($idArchivo);
    $rutaAbsoluta = Gestor::obtenerRutaAbsoluta($archivo->nombre_real);
    $tamanio = filesize($rutaAbsoluta);
    $tamanioFragmento = 5 * (1024 * 1024); //5 MB
    set_time_limit(300);
    header('Content-Type: application/octet-stream');
    header("Content-Transfer-Encoding: Binary");
    header("Pragma: no-cache");
    header('Content-Length: ' . $tamanio);
    header(sprintf('Content-disposition: attachment; filename="%s"', $archivo->nombre_original));
    // Servir el archivo en fragmentos, en caso de que el tamaño del mismo sea mayor que el tamaño del fragmento
    if ($tamanio > $tamanioFragmento) {
        $manejador = fopen($rutaAbsoluta, 'rb');

        // Mientras no lleguemos al final del archivo...
        while (!feof($manejador)) {
            // Imprime lo que regrese fread, y fread leerá N cantidad de bytes en donde N es el tamaño del fragmento
            print(@fread($manejador, $tamanioFragmento));

            ob_flush();
            flush();
        }
        // Cerrar el archivo
        fclose($manejador);
    } else {
        // Si el tamaño del archivo es menor que el del fragmento, podemos usar readfile sin problema
        readfile($rutaAbsoluta);
    }
}

Con esto podemos servir archivos de cualquier tamaño, o bueno, yo he probado descargando archivos de hasta 3.2 GB.

Protección de los archivos

A nivel de sistema operativo, los archivos están en el mismo directorio y no tienen ninguna protección (sería interesante encriptarlos). La protección se añade en primer lugar con Apache, denegando el acceso a esa carpeta.

En segundo lugar, PHP se encarga de servir el archivo solo si le pertenece al usuario.

Contenido del directorio en donde se guardan los archivos

Todos los archivos se guardan con un id aleatorio, además de que se les remueve la extensión pues al final ésta no importa (ya que el nombre original se encuentra en la base de datos).

Gestión de usuarios

Ya para terminar la explicación de este sistema que permite tener una nube en una red local o en internet usando PHP y MySQL te mostraré la parte de los usuarios:

Gestión de usuarios para nube con PHP y Vue

Lo que se puede hacer es cambiar la contraseña, darle permisos de administrador o eliminarlo. En este caso para cambiar la contraseña se solicita la contraseña actual y la nueva:

Cambiando contraseña de usuario

Instalación del sistema

Necesitas contar con composer y con npm, así como con una pila LAMP.

Descarga el código fuente y colócalo en tu carpeta pública del servidor Apache. Normalmente es C:\xampp\htdocs en Windows.

Una vez que lo tengas entra a api y ejecuta composer install para instalar todas las dependencias de PHP. Luego sal de ella, entra a frontend y ejecuta npm install para instalar las dependencias del lado del cliente.

En la carpeta api crea un archivo llamado env.php tomando como ejemplo el archivo env.ejemplo.php y configura las credenciales de la base de datos. No olvides importar las tablas junto con la creación del primer usuario.

También es importante configurar el archivo .env y .env.production dentro de frontend para que coincidan con la ruta en donde has montado el proyecto.

Dentro de frontend ejecuta npm run serve y visita localhost:8080 para visitar el servidor de desarrollo.

Otros puntos importantes son:

Configurar el directorio en donde estarán todos los archivos. Por defecto es en la carpeta subidas en el directorio de la api. Este directorio debe ser creado si no existe.

Proteger ese directorio con Apache o el servidor que se esté usando, así no se permiten las descargas sin autorización. Lo siguiente puede ser el ejemplo del .htaccess:

Allow from None
Order allow,deny

Recuerda ajustar las variables post_max_size, upload_max_filesize, memory_limit y max_file_uploads en el archivo php.ini según tus necesidades.

La variable memory_limit debería ser mayor que post_max_size y upload_max_filesize, aunque en el caso de las descargas he probado con archivos de hasta 3.2 GB y no hay problemas, teniendo 128M como valor en memory_limit.

Preparando para producción

Este sistema se puede montar en cualquier lugar que tenga Apache, PHP y MySQL. No es necesario contar con NPM ni nada de eso, pues éste último solo sirve para el desarrollo del lado del cliente.

Para llevar a producción hay que compilar todos los archivos y generar los css, js y html. Aquí las instrucciones:

Ejecutar npm run build y copiar al servidor de producción solo la carpeta api y todos los archivos generados dentro de frontend/dist.

Lee bien: debes copiar todos los archivos generados de manera que estos sean hermanos de la carpeta api, y no solo copiar la carpeta llamada dist. Solo como referencia, el directorio de producción en mi caso es gestor_archivos_php_prod y se ve así:

λ tree gestor_archivos_php_prod
Listado de rutas de carpetas
El número de serie del volumen es 
C:\XAMPP\HTDOCS\GESTOR_ARCHIVOS_PHP_PROD
├───api
│   ├───Parzibyte
│   ├───subidas
│   └───vendor
│       ├───bin
│       ├───brick
│       │   └───math
│       │       └───src
│       │           ├───Exception
│       │           └───Internal
│       │               └───Calculator
│       ├───composer
│       ├───ramsey
│       │   ├───collection
│       │   │   ├───bin
│       │   │   └───src
│       │   │       ├───Exception
│       │   │       ├───Map
│       │   │       └───Tool
│       │   └───uuid
│       │       └───src
│       │           ├───Builder
│       │           ├───Codec
│       │           ├───Converter
│       │           │   ├───Number
│       │           │   └───Time
│       │           ├───Exception
│       │           ├───Fields
│       │           ├───Generator
│       │           ├───Guid
│       │           ├───Lazy
│       │           ├───Math
│       │           ├───Nonstandard
│       │           ├───Provider
│       │           │   ├───Dce
│       │           │   ├───Node
│       │           │   └───Time
│       │           ├───Rfc4122
│       │           ├───Type
│       │           └───Validator
│       └───symfony
│           └───polyfill-ctype
├───css
├───fonts
└───js

Una vez que hayas copiado api, edita el archivo cors.php y establece la variable $produccion en true.

Conclusión

Este proyecto fue una mejora muy grande a un simple script para subir archivos con PHP. De hecho el código que se encarga de procesar los archivos es muy parecido.

Puedes montar este proyecto en un servidor de casa o en un hosting para tener tu propio disco en la nube. Yo lo he montado en una computadora un tanto antigua que corre sobre Linux. Y como lo dije, el sistema es totalmente responsivo:

Gestor de archivos en la nube con PHP – Captura en dispositivo móvil

El código fuente de este sistema es open source y gratuito para descargar, justo como mis otros proyectos. No puedo explicar cada apartado y línea, pues sería un post muy extenso. Por lo tanto dejaré el código completo en mi GitHub.

Te dejo otros enlaces para ver más sobre PHP y JavaScript.

Estoy aquí para ayudarte 🤝💻


Estoy aquí para ayudarte en todo lo que necesites. Si requieres alguna modificación en lo presentado en este post, deseas asistencia con tu tarea, proyecto o precisas desarrollar un software a medida, no dudes en contactarme. Estoy comprometido a brindarte el apoyo necesario para que logres tus objetivos. Mi correo es parzibyte(arroba)gmail.com, estoy como@parzibyte en Telegram o en mi página de contacto

No te pierdas ninguno de mis posts 🚀🔔

Suscríbete a mi canal de Telegram para recibir una notificación cuando escriba un nuevo tutorial de programación.
parzibyte

Programador freelancer listo para trabajar contigo. Aplicaciones web, móviles y de escritorio. PHP, Java, Go, Python, JavaScript, Kotlin y más :) https://parzibyte.me/blog/software-creado-por-parzibyte/

Ver comentarios

Entradas recientes

Desplegar PWA creada con Vue 3, Vite y SQLite3 en Apache

Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…

2 días hace

Arquitectura para wasm con Go, Vue 3, Pinia y Vite

En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…

2 días hace

Vue 3 y Vite: crear PWA (Progressive Web App)

En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…

2 días hace

Errores de Comlink y algunas soluciones

Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…

2 días hace

Esperar promesa para inicializar Store de Pinia con Vue 3

En este artículo te voy a enseñar cómo usar un "top level await" esperando a…

2 días hace

Solución: Apache – Server unable to read htaccess file

Ayer estaba editando unos archivos que son servidos con el servidor Apache y al visitarlos…

3 días hace

Esta web usa cookies.