En este post de programación de servidores con PHP te voy a enseñar a mantener sincronizado un archivo, de modo que si el archivo es modificado se puede subir la nueva versión, y luego descargar esa última versión en cualquier otro dispositivo.
Lo único que vamos a hacer con este script es mantener sincronizado un archivo con PHP cada vez que ejecutemos el programa cliente que sube o baja el archivo.
No es el próximo rsync, solo permite, a petición del usuario, subir o descargar un archivo usando PHP en el servidor y Golang en el cliente a partir de su fecha de modificación.
El programa es open source y con alguna modificación también va a permitir mantener un historial de versiones del archivo.
Recuerda que ya hice un gestor de archivos en la nube con PHP hace algún tiempo, solo que ahora te enseñaré a subir y bajar un archivo de manera automática.
Las funciones con PHP van a permitir autenticarnos devolviendo un JWT, subir el archivo, descargar el archivo y sincronizar el archivo.
He separado todo en scripts independientes que serán invocados desde el cliente.
Para el caso de la autenticación se utiliza un JSON Web token. Para ello consultamos el nombre de usuario y comparamos su contraseña, en caso de que ambas cosas coincidan se genera un nuevo JWT y se le devuelve al cliente.
<?php
$payload = json_decode(file_get_contents("php://input"));
if (!$payload) {
http_response_code(500);
exit(json_encode("No hay payload"));
}
$nombre = $payload->nombre;
$palabraSecreta = $payload->palabraSecreta;
include_once "funciones.php";
$posibleJWT = login($nombre, $palabraSecreta);
if (!$posibleJWT) {
http_response_code(401);
exit("usuario o contraseña incorrecta");
}
echo $posibleJWT;
A partir de aquí, todas las operaciones para revisar cómo sincronizar, subir o descargar un archivo van a necesitar el JWT.
Un punto importante para sincronizar un archivo con PHP es la opción para descargar un archivo a partir de su id, usuario y JWT.
El cliente es el encargado de revisar si debe subir o bajar el archivo; este script permite descargar el archivo sin importar su fecha de modificación.
<?php
$payload = json_decode(file_get_contents("php://input"));
if (!$payload) {
http_response_code(500);
echo json_encode("No hay payload");
exit;
}
include_once "funciones.php";
$jwt = $payload->jwt;
try {
$jwtDecodificado = decodificarToken($jwt);
} catch (Exception $e) {
http_response_code(401);
echo json_encode($e->getMessage());
exit();
}
$idUsuario = $jwtDecodificado->id_usuario;
$idArchivo = $payload->idArchivo;
$detalles = obtenerDetallesArchivo($idUsuario, $idArchivo);
if (!$detalles) {
http_response_code(404);
echo json_encode("No encontrado");
} else {
$rutaAbsoluta = DIRECTORIO_SUBIDAS . DIRECTORY_SEPARATOR . $detalles->nombre;
$nombreArchivo = $detalles->nombre; // El nombre que se le sugiere al usuario cuando guarda el archivo. Solo el nombre, NO la ruta absoluta
$tamanio = filesize($rutaAbsoluta);
$tamanioFragmento = 5 * (1024 * 1024); //5 MB
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"', $nombreArchivo));
// 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);
}
}
Aquí estoy usando el mismo método que utilicé en el gestor de archivos para descargar un archivo muy grande. Lo que hago es revisar si el token es correcto, obtener los detalles del archivo y en caso de que exista enviarlo como respuesta.
Ya vimos el script que hace login y permite descargar el archivo, veamos ahora cómo subir un archivo para guardarlo en la nube, ya sea porque es más nuevo o porque es la primera vez que se guarda.
<?php
include_once "funciones.php";
$jwt = $_POST["jwt"];
$ultimaModificacion = $_POST["ultimaModificacion"];
$idArchivo = $_POST["idArchivo"];
try {
$jwtDecodificado = decodificarToken($jwt);
} catch (Exception $e) {
http_response_code(401);
echo json_encode($e->getMessage());
exit();
}
$archivo = $_FILES["archivo"];
$ahora = date("Y-m-d H:i:s");
$nombreArchivo = $archivo["name"];
$extension = pathinfo($nombreArchivo, PATHINFO_EXTENSION);
$nuevoNombre = uniqid("", true) . "." . $extension;
$nuevaUbicacion = DIRECTORIO_SUBIDAS . DIRECTORY_SEPARATOR . $nuevoNombre;
crearDirectorioSubidasSiNoExiste();
move_uploaded_file($archivo["tmp_name"], $nuevaUbicacion);
$idUsuario = $jwtDecodificado->id_usuario;
# En hostings sin límite de espacio, tal vez no deberíamos borrar el archivo para tener varias versiones
borrarArchivo($idUsuario, $idArchivo);
guardarArchivo($idUsuario, $nuevoNombre, $idArchivo, $ultimaModificacion, $ahora);
En este caso puedes llevar un control de versiones del archivo (tener varias copias del mismo a lo largo del tiempo) para volver a una versión específica; para lograrlo simplemente no elimines el archivo cada vez que subas uno nuevo y ajusta lo necesario para explorar distintas versiones.
Aquí básicamente renombramos y guardamos el archivo, guardándolo tanto en el almacenamiento del sistema como en la base de datos SQLite3. Obviamente, en la base de datos solo guardamos el dueño del archivo, nombre del archivo, id del mismo, última modificación y la fecha de subida.
Ya te enseñé a subir y descargar un archivo, pero ahora nos hace falta un script en el servidor que nos brinde una pista sobre qué hacer con un archivo nuevo.
<?php
$payload = json_decode(file_get_contents("php://input"));
if (!$payload) {
http_response_code(500);
echo json_encode("No hay payload");
exit;
}
include_once "funciones.php";
$jwt = $payload->jwt;
try {
$jwtDecodificado = decodificarToken($jwt);
} catch (Exception $e) {
http_response_code(401);
echo json_encode($e->getMessage());
exit();
}
$idUsuario = $jwtDecodificado->id_usuario;
$idArchivo = $payload->idArchivo;
$detalles = obtenerDetallesArchivo($idUsuario, $idArchivo);
if (!$detalles) {
http_response_code(404);
echo json_encode("No encontrado");
} else {
echo json_encode($detalles, JSON_NUMERIC_CHECK);
}
Básicamente, si el archivo existe, se muestra su id, nombre, id de archivo, última modificación y fecha de subida. Es responsabilidad del cliente comparar la última fecha de modificación con la del archivo local y a partir de ello decidir si sube o descarga el archivo.
Ya tenemos todo el servidor funcionando, lo único que falta es programar el cliente para que utilice el servidor. Podemos usar cualquier lenguaje de programación y/o framework para consumir la API de PHP, y podemos hacer la interfaz con cualquier diseño.
Yo me he decidido por hacer el cliente con Golang, guardando los datos del archivo y el token en un archivo JSON para más tarde leer ese mismo archivo y hacer el proceso de sincronización más rápido.
Si has leído los fragmentos de código que he colocado anteriormente habrás notado que faltan funciones y el código del cliente. He colocado el código completo en GitHub, ahí puedes explorar el código del cliente como del servidor.
Por cierto, para que coloques esto en tu servidor. Primero:
composer install
para generar la carpeta vendor
y el autoload.php
Después, del lado del cliente:
go mod tidy
, luego go build
y finalmente ejecuta el archivo resultanteurlBase
en el main.go
, así como la CLAVE_JWT
en funciones.php
(que debe ser de 32 bytes)Con eso todo debería funcionar sin problemas. Siempre puedes hacer las pruebas necesarias en tu entorno local y cuando todo funcione subirlo a internet.
Hoy te voy a presentar un creador de credenciales que acabo de programar y que…
Ya te enseñé cómo convertir una aplicación web de Vue 3 en una PWA. Al…
En este artículo voy a documentar la arquitectura que yo utilizo al trabajar con WebAssembly…
En un artículo anterior te enseñé a crear un PWA. Al final, cualquier aplicación que…
Al usar Comlink para trabajar con los workers usando JavaScript me han aparecido algunos errores…
En este artículo te voy a enseñar cómo usar un "top level await" esperando a…
Esta web usa cookies.