Las horas de trabajo detrás de una función simple

Desde que inicié este sitio web en abril de 2021, sabía que los usuarios cargarían archivos PDF protegidos con contraseña. Pensé que sería demasiado molesto hacer el trabajo para admitir estos archivos PDF, así que lo pospuse. Recientemente, un usuario me pidió que admitiera archivos PDF protegidos con contraseña, así que lo hice.

Es una función simple, y funciona bastante bien. Pasé todo el día de ayer trabajando en ella y lo puse en producción a las 2:32 a. m. de esta mañana. A partir de la pequeña cantidad de datos que recopilé en los últimos días, parece que alrededor del 5% de los usuarios suben archivos PDF con contraseñas. Anteriormente, rechazaba la carga sin siquiera mostrar un mensaje de error. Muchos de esos usuarios probablemente se fueron y asumieron que el sitio web no funcionaba.

Me sorprendió y me molestó la cantidad de tiempo que me tomó implementar esta característica, así que pensé que sería bueno compartirlo con ustedes para que puedan apreciar el trabajo que he hecho por ustedes. 1

Sábado 11:46 PM - Adición de campos a la tabla file_mapping

La tabla file_mapping almacena información sobre cada PDF que carga un usuario. Agregué dos campos contraseña y requires_password. La idea es almacenar la contraseña en la base de datos junto con otros metadatos del documento.

Domingo 11:10 AM - Cuando la API recibe un PDF protegido

Cambié la API para que cuando cree un registro file_mapping con requires_password como verdadero y el tipo de pdf como UNKNOWN. Anteriormente, pdf_type podía ser TEXT_BASED o IMAGE_BASED. Dado que no podemos leer el archivo sin una contraseña, no podemos analizarlo para determinar si es TEXT_BASED o IMAGE_BASED.

Hacemos esto porque ejecutamos diferentes rutas de código para archivos PDF de TEXTO e IMAGEN.

Domingo 1:14 PM - Escribí SQL para actualizar los registros del mapeo de archivos

UPDATE file_mapping SET password = ? WHERE uuid = ?;

Sunday 2:55 PM - Cambió el SQL y creó una API para establecer una contraseña en un documento

UPDATE file_mapping SET password = ? pdf_type = ? page_count = ? WHERE uuid = ?;

Mientras programaba la API, me di cuenta de que me gustaría cambiar más campos en file_mapping después de que un usuario estableciera con éxito una contraseña para un documento. Una vez que pueda leer el documento, puede clasificar su pdf_type y contar el número de páginas que tiene.

val fileMappings = repository.getFileMappings(body.passwords.map { it.uuid })
val updates = mutableMapOf<String, UpdateFileMapping>()

val results = fileMappings.map { mapping ->
    mapping.validateOwnership(userId, ipAddress)
    val password = body.passwords.first { it.uuid == mapping.uuid}.password
    val file = File(mapping.filename)
    val result = uploadAction.analysePdf(file, password, userId, ipAddress, mapping.uuid, mapping.originalFilename)

    if (result.state != UploadResponse.State.REQUIRES_PASSWORD) {
        updates[mapping.uuid] = UpdateFileMapping(password, result.pdfType.toString(), result.numberOfPages)
    }

    result
}

// Establecer las contraseñas
repository.updateFileMappings(updates)

call.respondText(contentType = ContentType.Application.Json) {
    stringify(results)
}

La API acepta identificadores de documentos y contraseñas. A continuación, intenta abrir el documento con la contraseña proporcionada. Escribe las contraseñas correctas en la base de datos. Responde indicando qué documentos se abrieron correctamente con las contraseñas proporcionadas.

Domingo 3:21 PM - se cambia el código de apertura del documento para usar una contraseña cuando esté disponible

val document = if (password == null) Loader.loadPDF(statement) else Loader.loadPDF(statement, password)

Si tenemos una contraseña, se usa. En realidad, esto puede ser más simple. Loader.loadPDF se complace en recibir un valor nulo para la contraseña.

Domingo 11:57 PM - Cambiando todas las llamadas de Loader.loadPDF para proporcionar una contraseña

Bastante fácil de hacer, pero también escribí un montón de casos de prueba para verificar que los PDF protegidos puedan pasar por todas las rutas de conversión. Hay una gran hueco de tiempo porque fui a la casa de mi padre y tomé una siesta, cené y luego vi los Premios de Cine de Hong Kong.

Lunes 12:30 AM - Se corrigió un error de eliminación de carpetas en DEV

Periódicamente ejecuto un trabajo de limpieza para eliminar datos de usuario como archivos PDF, imágenes renderizadas y resultados de reconocimiento óptico de caracteres.

for (directory in directories) {
    for (file in directory.walk()) {
        // delete file
    }
}

Si el directorio está vacío, el código anterior eliminará el directorio. Esto no sucede con los servidores de producción porque los directorios nunca están vacíos ya que las personas usan constantemente la aplicación. ¡Sin embargo, sucede en DEV! Arreglé este error con el siguiente código.

fun start(): AccountingProApp {
    tempFileDirectories.forEach {
        if (!it.exists()) {
            it.mkdirs()
        }
    }
    startServerWithRetries()
    cleanup.scheduleExecution()
    imagePdfWorker.scheduleExecution()
    return this
}

Al iniciar la aplicación, si el directorio no existe, el código lo creará. Mientras escribo esta publicación, me doy cuenta de que esta solución no es excelente. Las carpetas vacías aún se pueden eliminar y, para recuperarlas, necesito reiniciar el servidor. El servidor de desarrollo se reinicia bastante porque se vuelve a desplegar cada vez que hago commit de nuevo código. Creo que una mejor solución sería no eliminar carpetas en el trabajo de limpieza.

Lunes 2:24 AM - Interfaz de usuario para permitir establecer una contraseña para un documento

<Input onChange={onChange} value={value} placeholder="Password"/>
<Button
  onClick={onClick}
  css={{ marginLeft: '0.25em' }}
  size="compact"
  emphasis="primary">
  Submit
</Button>

Proporciona un elemento de entrada cuando un PDF cargado esté protegido. Llama a la API cuando se presiona el botón Submit. Concilia la respuesta de la API con el estado local en el navegador. Todo fue muy complicado, especialmente porque no soy muy bueno escribiendo código front-end.

No se ve tan bien, los cuadros de entrada son demasiado grandes. No me gusta escribir CSS, y la funcionalidad funcionó, así que me detuve ahí.

Lunes 2:32 AM - Se corrigió un error de lógica en la API

Me di cuenta de que los archivos PDF basados en texto se estaban procesando con OCR. Eso no es correcto. Así que arreglé el error. Puse el código en producción y me fui a dormir.

Lunes 11:13 AM: toques finales de la interfaz de usuario

Entro en mi oficina, reviso mi tablero de Grafana y veo que alguien usó con éxito la función de configuración de contraseña. ¡Agradable!

Agrego indicadores de carga y hago que el elemento de entrada sea más pequeño.

<Input
  size="compact"
  onChange={onChange}
  value={value}
  placeholder="Password"
  disabled={disabled}
/>
<Button
  onClick={onClick}
  css={{ marginLeft: '0.25em' }}
  size="compact"
  emphasis="primary"
  disabled={disabled}
  decoratorLeft={decoratorLeft}
>
  Submit
</Button>

Lunes 11:38 AM - Se corrigió otro error de la API

// Antes
val state = if (it.uuid in inProgressUuids) UploadResponse.State.PROCESSING else UploadResponse.State.READY

// Después
val state = when {
    it.uuid in inProgressUuids -> UploadResponse.State.PROCESSING
    it.pdfType == PDFType.UNKNOWN -> UploadResponse.State.REQUIRES_PASSWORD
    else -> UploadResponse.State.READY
}

El código antiguo asumía solo dos estados, LISTO y PROCESANDO. Esta nueva función agregó el estado REQUIRES_PASSWORD.

Conclusión

Aquí hay una demostración de la función una vez más. Es difícil creer que una función tan simple requiriera dormir dos veces y ver los Premios de Cine de Hong Kong.

¿Valió la pena todo este trabajo? Sólo el tiempo lo dirá. Si está leyendo esto en 2032, comuníquese conmigo, haré un análisis para averiguar cuánto dinero generó esta función. “Sume los ingresos de los usuarios cuyo primer archivo cargado fue protegido”. Si el año es 2033, no voy a hacer el análisis.


  1. tu dinero ↩︎

Join The Mailing List