Sincronizar archivo con PHP

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.

Sincronizar archivo con PHP - Historial de versiones
Sincronizar archivo con PHP – Historial de versiones

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.

Programando el servidor

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.

Haciendo login

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.

Descargar archivo

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.

Subir archivo

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.

Sincronizar archivo

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.

Programando cliente

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.

Subiendo archivo más reciente
Subiendo archivo más reciente

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.

Poniendo todo junto

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:

  1. Debes contar con PHP y Apache, ya sea en local o en tu servidor.
  2. Necesitas tener la librería SQLite3, habilitada en la mayoría de proveedores de servidores
  3. Instala composer, navega (desde una terminal) a la carpeta del proyecto (en donde se encuentra composer.json) y ejecuta composer install para generar la carpeta vendor y el autoload.php
  4. Sube los archivos a tu servidor junto con las dependencias, ya sea que las instales en tu entorno local y las subas así, o que instales las dependencias directamente en el servidor

Después, del lado del cliente:

  1. Instala el compilador de golang
  2. Navega, desde una terminal, a la carpeta del cliente y ejecuta go mod tidy, luego go build y finalmente ejecuta el archivo resultante
  3. No olvides configurar la urlBase 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.

Encantado de ayudarte


Estoy disponible para trabajar en tu proyecto, modificar el programa del post o realizar tu tarea pendiente, no dudes en ponerte en contacto conmigo.

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.

Dejar un comentario