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.
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
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
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.
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:
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
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.
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.
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:
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:
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:
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.
Hola, me podrian ayudar?, al momento de poner el: npm run serve, me sale ESlint is not a constructor :C
Hola. Gracias por sus comentarios. Si tiene alguna consulta, solicitud de creación de un programa o solicitud de cambio de software estoy para servirle en https://parzibyte.me/#contacto
Saludos!
Hola, excelente video, quisiera preguntar si es posible subir carpetas y navegar en ellas.
¡Saludos!
Hola. Gracias por sus comentarios. Si tiene alguna consulta con gusto lo atiendo en https://parzibyte.me/#contacto
Saludos!
Al pasarlo a producción y en un servidor de ferozo solo puedo agregar un usuario y un archivo, al querer agregar un segundo archivo o usuario me salta el siguiente error, “Error subiendo archivo/usuario, unexpected end of json input” ¿Cual puede ser el error? Aclaro que el directorio se encuentra con los permisos adecuados en el servidor. Muchas gracias!
Si tiene alguna consulta puede hacérmela llegar en https://parzibyte.me#/contacto
buenas, cuando intento iniciar sesion me indica :
Failed to load resource: net::ERR_CONNECTION_REFUSED
Probablemente no ha iniciado el servidor
DIsculpe quiero implementar algo asi en mi negocio tengo una base de datos pequeña de mis clientes con facturas…pero quiero que cada cliente tenga un usurario y entre a descargar su facturas en pdf seria un proceso similar?
Yo creo que sí
Hola. estuve integrando el software en mi local pero me sale el siguiente error al intentar hacer login:
unexpected end of json input
Agradecería mucho me puedas indicar porque se produce este error.
Saludos.
Lo acabo de probar y funciona perfectamente. Le sugiero leer bien los pasos y aprender a encontrar los errores.
Saludos!
Como podría hacer para integrarlo a una pagina como esta https://imgur.com/a/DLaO2m5, actualmente los archivos se suben por ftp y se agrega el enlace desde el editor html, como podría integrar tu código para que una vez iniciada la sesión tenga la posibilidad de editar mis archivos directamente desde la pagina.
Hola. Para consultas personalizadas puede contactarme en https://parzibyte.me/#contacto
excelente información o usare local, cual es la contraseña del loginme lo podrías mandar al correo te lo agradeceria mucho.
Hola. Por favor lea el post, ahí está la contraseña.
Saludos!