Cómo prevenir la subida duplicada

Ultimamente vengo trabajando bastante con procesamiento de planillas Excel usando PHP.

Por lo general, el workflow del usuario es algo así como:

  • Trabajar con algún otro sistema (HomeBanking, Plataforma de trading, etc…)
  • Descargar información en formato Excel
  • Importar planilla descargada al sistema que yo desarrollé
  • Trabajar la información dentro del sistema

Uno de los errores comunes cuando una persona carga información a un sistema es el de la carga duplicada.

Este problema se agrava cuando los duplicados no siempre son errores .

Esta condición hace que no sea simple detectar y prevenir la importación duplicada.

El escenario sería algo como tomar el workflow original y modificarlo ligeramente:

  • Trabajar con algún otro sistema (HomeBanking, Plataforma de trading, etc…)
  • Descargar información en formato Excel
  • Importar planilla descargada al sistema que yo desarrollé
  • Salir por un café
  • Olvidar qué fue lo último que se hizo
  • Volver a importar planilla descargada al sistema que yo desarrollé
  • Trabajar la información dentro del sistema

Siendo que el sistema no puede automáticamente eliminar un registro sólo porque su contenido ya exista parece poco lo que puede hacerse…

Mi idea de solución se basa en intentar detectar archivos que ya han sido subidos al sistema de un modo eficiente.

Disclaimer: la solución que voy a descibir aquí es, hasta el momento, también teórica (más allá de alguna prueba de concepto).

Cómo detectar archivos ya subidos

Una primera idea es:

  1. Almacenar todos los archivos subidos
  2. Realizar una comparación binaria de los contenidos del nuevo archivo subido contra todos los anteriores

No me suena muy eficiente que digamos :p

Una idea algo más elaborada es la de usar un hash como base de comparación.

La idea es por cada archivo subido calcularlo y comparar contra los ya existentes.

Lo interesante de este método es que el cálculo es rápido y la comparación es casi trivial.

Un punto a tener en cuenta sin embargo es que, aunque muy poco probable, existe el riesgo de «falsos positivos».

Dos strings diferentes pueden tener el mismo hash, con lo cual el sistema podría dar por duplicado dos archivos que no tengan nada que ver.

Para evitar este problema dejaré la solución 1 como fallback para los casos en que encuentre hashes duplicados.

Implementación en PHP de detección de subidas duplicados

No te voy a dejar sólo con palabras, ¿cuál sería la gracia, cierto?

Vamos a ver algo de código:

Voy a almacenar todo el contenido de los archivos en una base de datos.

A efectos de esta prueba me basta con SQLite, pero podés usar la que te resulte más cómoda (o guardar los archivos en algún directorio si preferís).

Entonces, lo primero es crear la base usando este SQL:

CREATE TABLE "files" (
	`id`	INTEGER PRIMARY KEY AUTOINCREMENT,
	`contents`	BLOB,
	`hash`	TEXT
);
CREATE TABLE sqlite_sequence(name,seq);

Luego tendremos dos archivos, el que permite subir el archivo (HTML) y el que recibe el archivo (PHP):

upload.html

<form action="get_file.php" method="post" enctype="multipart/form-data">
    <p>Importar: <input type="file" name="fileToUpload"/></p>
    <p><input type="submit" value="Subir"/></p>
</form>

get_file.php

<?php
/**
* Created by PhpStorm.
* User: mauro
* Date: 1/9/19
* Time: 12:46 PM
*/

$tmp_name = $_FILES["fileToUpload"]['tmp_name'];
$hash = sha1_file($tmp_name);

echo 'Hash for uploaded file: "' . $hash . '"<br/>';

$new_file_contents = file_get_contents($tmp_name);

$db = new PDO('sqlite:uploded.sq3');

if ( $suspects = find_file_by_hash($hash) ) {
foreach ( $suspects as $suspect ) {
if ( $suspect == $new_file_contents) {
die ('Duplicate file');
}
}
}

if ( save_file_record( $new_file_contents, $hash ) ) {
echo 'File saved ok!';
} else {
echo 'Saving error :(';
}

/**
* @param $hash
* @return array
*/
function find_file_by_hash( string $hash ) : array
{
GLOBAL $db;

$sql = "SELECT * FROM files WHERE hash = '$hash'";

if ( $st = $db->query( $sql ) ) {

return array_map( function( $r ) {

return $r['contents'];
}, $st->fetchAll( PDO::FETCH_ASSOC ) );
} else {

return [];
}
}

/**
* @param string $contents
* @param string $hash
* @return int
*/
function save_file_record( string $contents, string $hash ) : bool
{
GLOBAL $db;

$sql = "INSERT INTO files ( contents, hash ) VALUES ( :contents, :hash )";

$st = $db->prepare( $sql );
$st->bindParam('contents', $contents, PDO::PARAM_LOB );
$st->bindParam('hash', $hash );

if ( !$st->execute() ) {
print_r( $st->errorInfo() );

return false;
}

return true;
}

Para hacer correr este código sólo hay que grabar los archivos en un mismo directorio e iniciar el servidor web que viene con PHP:

php -S localhost:8080

Y subir un archivo varias veces. La primera deberías ver el mensaje «File saved ok!» y la segunda «Duplicate file».

Listo! No te recomiendo implementar el código así como está, pero espero que haya servido para explicar la idea :).

Si preferís una versión en video, acá hay un link a YouTube.

mchojrin

Por mchojrin

Ayudo a desarrolladores PHP a acceder mercados y clientes más sofisticados y exigentes

¿Te quedó alguna duda? Publica aca tu pregunta

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.