Una acción de Nemo para reducir el tamaño de los PDF

Primero guardamos este script que es el que se ocupa de reducir el PDF:

Lo descomprimimos (WordPress no deja adjuntar ficheros .sh) y lo guardamos por ejemplo en /home/koas/bin.

Y para la acción de Nemo creamos un fichero en /home/koas/.local/share/nemo/actions con este contenido:

[Nemo Action]
Active=true
Name=Shrink PDF
Exec=/home/koas/bin/shrinkpdf.sh %F %F.pdf 300
Selection=S
Extensions=pdf;
EscapeSpaces=true

Mover carpeta de datos de MySQL

Hay ya miles de entradas sobre cómo cambiar la carpeta de datos de MySQL de /var/lib/mysql a cualquier otra, pero me costó encontrar la solución al problema “Error 13 – Permission denied” que me daba al intentar arrancar MySQL después de cambiarle la carpeta de datos.

El problema es que la nueva ruta estaba en /media/koas/Ext4Bucket, un segundo disco duro que tengo instalado. Para que funcione hay que darle a /media/koas permisos 777.

Eliminando autenticación para phpMyAdmin

¡Atención! Esto solo hay que hacerlo en nuestro equipo de desarrollo, lógicamente es una locura quitar la autenticación al phpMyAdmin en un servidor accesible desde Internet, ahí no solo debemos tener autenticación sino proteger además la carpeta con una contraseña de acceso porque phpMyAdmin no tiene ninguna protección contra ataques de fuerza bruta (no entiendo por qué, no les costaría nada implementar algo como phpLoginBlacklist).

Pero en nuestro equipo es un rollo que cada cierto tiempo caduque la sesión y nos pida que nos identifiquemos otra vez, así que la solución es editar el fichero /etc/phpmyadmin/config.inc.php y en el bloque de datos del primer servidor añadir estas líneas:

// Esta línea ya existirá, cambiamos su valor a config
$cfg['Servers'][$i]['auth_type'] = 'config';

// Estas son las líneas que añadiremos
$cfg['Servers'][$i]['user'] = 'root';
$cfg['Servers'][$i]['password'] = 'pass';
$cfg['Servers'][$i]['AllowNoPassword'] = TRUE;
$cfg['Servers'][$i]['AllowRoot'] = TRUE;

Sublime Text: convertir espacios a tabs al guardar un fichero

No soy un maniático como Richard en Silicon Valley:

Aunque yo también prefiero usar tabulados

pero como llevo un tiempo usando Pug para el HTML en Vue he acabado harto del mensaje de error Invalid indentation, you can use tabs or spaces but not both.

Para solucionar esto crearemos una carpeta UnexpandTabsOnSave en la carpeta Packages de la configuración de Sublime Text y dentro un fichero UnexpandTabsOnSave.py con este contenido:

import sublime, sublime_plugin, os

class UnexpandTabsOnSave( sublime_plugin.EventListener ):
  # Run Sublime's 'unexpand_tabs' command when saving any file
  def on_pre_save( self, view ):
    view.window().run_command( 'unexpand_tabs' )

Así cada vez que guardemos un fichero convertirá los espacios a tabs, Pug no se quejará y el Richard que llevamos dentro estará satisfecho.

Server Sent Events en PHP

Estos días he estado muy atareado con el lanzamiento de PocaPoca, una plataforma de formación que empieza su negocio con varios cursos de electrónica.

La plataforma consta de tres partes: una página de presentación de la plataforma (web), la propia plataforma (front) y un entorno de gestión de la misma (back).

Las tres están desarrolladas con Vue. Para cada proyecto con Vue yo me abro siempre dos pestañas de terminal: uno para lanzar el entorno de desarrollo local (npm run serve) y otro para compilar y subir al servidor remoto el proyecto (npm run build). Así que para este proyecto (que son realmente tres) tenía seis pestañas abiertas en el terminal y era un lío tener que ir pasando de una a otra para compilar y subir ficheros. Incluso usando el interfaz web de Vue-cli serían tres pestañas. Y eso suponiendo que no necesite por algún motivo lanzar otro proyecto… Vamos, que había ocasiones en que aquello era el festival de la pestaña.

Una vez lanzada la página me puse a pensar cómo evitar que esto me vuelva a pasar en el futuro. El primer paso ha sido sustituir Gnome Terminal por Terminator, que permite partir una pantalla en varias bloques horizontales y verticales. Esto ya es un avance, porque me permitiría (siguiendo con el ejemplo de PocaPoca) tener solo tres pestañas, una para cada proyecto, o incluso una única pestaña con todas las ventanas juntas, porque en Terminator es rápido moverse entre los bloques de una misma pestaña.

Pero se me ha ocurrido algo mejor, y es aprovechar un iPad antiquísimo que tengo para que muestre todos los proyectos que tengo activos y me permita con solo pulsar un botón ejecutarlos o compilarlos. Esto es mucho más rápido incluso que cambiar a la ventana de Terminator y localizar el bloque y lanzar el comando.

Para no perder funcionalidad necesito poder ver la salida de los comandos, para ver si hay algún error. El entorno de ejecución (npm run serve) está continuamente mostrando información que tengo que poder ver, y esto es lo que nos lleva al asunto de la entrada de hoy: ¿cómo mostrar en una página web la salida de un comando que se está ejecutando de forma continua?

La respuesta son los Server Sent Events, es como un websocket pero en un solo sentido, del servidor al cliente. El navegador abre una conexión permanente con nuestro fichero PHP, que le va devolviendo datos según los vaya generando, y esto nos permite recibirlos en JS. Es la solución perfecta para lo que necesito, y en este vídeo se ve cómo desde el iPad puedo ver la salida de un ping según se va generando:

Copio aquí el código, cuando tenga un poco de tiempo quiero ponerlo un poco más bonito y subirlo a GitHub.

<?php
$mode = $_REQUEST['mode'] ?? '';
if ($mode == 'event')
{
  header('Content-Type: text/event-stream');
  header('X-Accel-Buffering: no');
  
  echo "event: message\n";
	
  $descriptorspec = [
    0 => ["pipe", "r"],   // stdin
    1 => ["pipe", "w"],   // stdout
    2 => ["pipe", "w"]];  // stderr
  
  $cmd = "ping 127.0.0.1";
  $process = proc_open($cmd, $descriptorspec, $pipes, realpath('./'), array());
  if (is_resource($process))
    while ($s = fgets($pipes[1]))
    {
      $s = nl2br($s);
      echo "data:$s\n\n";
      while (ob_get_level() > 0)
	ob_end_flush();
      flush();
    }
  
  die("data:#CLOSE\n\n");
}

?><!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width,initial-scale=1.0">
    <meta name="quality" content="Koas' Code Inside" />
  </head>
  <body>
  	<button onclick="run()">Run process</button>
  	<div id="result" style="white-space: pre"></div>

<script>
function run()
{
	document.getElementById("result").innerHTML = "";

	var source = new EventSource("dashboard.php?mode=event");
	source.onmessage = function(event) 
	{
		if (event.data == "#CLOSE")
			source.close();
		else document.getElementById("result").innerHTML += event.data;
	}
}
</script>

  </body>
</html>

Actualización 12-06-2020: usando esto en un proyecto real me he dado cuenta de que la petición que se hace al servidor al llamar al constructor de EventSource no envía cookies ni hay forma de añadir cabeceras ni nada, solamente se puede enviar información en la URL. Esto es importante tenerlo en cuenta, tendremos que buscarnos la vida para autentificar al usuario que hace la petición. Lo más sencillo es pedir al servidor un token temporal y enviarlo en la petición SSE.

AWS CLI requiere la ruta completa al ejecutable en cron

Al montar un entorno de producción lo primero que hago es preparar el sistema de copias de seguridad: creo una entrada en el cron que ejecuta un programa que vuelca la base de datos con una frecuencia mínima de una vez al día y que depende de la intensidad de uso del servidor.

Pero claro, esta copia de seguridad está en el mismo servidor, por lo que aunque resulta muy útil cuando se produce un error de tipo PICNIC no nos sirve de mucho si el problema es de hardware o perdemos el control del servidor. Necesitamos mantener una copia de esos ficheros en otra parte.

Para esto normalmente yo usaba rsync y copiaba los ficheros en algún otro servidor, pero decidí cambiar a S3 porque me fío más de sus servidores. Los pasos son estos:

  • Crear un usuario en AWS que tendrá acceso solo a un bucket y crear en ese bucket una carpeta backup donde se guardarán las copias de seguridad.
  • Instalar las utilidades de S3 AWS CLI y configurarlas con los datos de acceso de ese usuario de AWS.
  • Meter en el cron una entrada que sincronice nuestra carpeta de copias de seguridad con S3.

El comando para hacer la sincronización es:

aws s3 sync /home/backupDB/ s3://NOMBRE_BUCKET/backup --delete

El –delete indica que si un fichero no está en la carpeta local debemos borrarla de S3. Esto, unido a otra entrada en el cron como esta:

find /home/backupDB/ -mtime +60 -exec rm {} \;

nos permite limitar las copias de seguridad a los últimos dos meses. Dependiendo de los requisitos de este proyecto variamos este valor.

Bueno, pues si uno ejecuta el comando de sincronización verá que funciona estupendamente, lo meterá en el cron y seguirá con su vida tan tranquilo confiando en que todo funcionará bien…

…aunque por si acaso se pondrá una nota para dos días después para revisar que los ficheros se están copiando en S3. Eso hice yo, ¡y menos mal!

Porque dos días después cuando entré en S3 a mirar el bucket vi que solo tenía los ficheros de cuando hice la sincronización a mano desde la línea de comandos. En la carpeta local estaban los ficheros, pero no se habían copiado a S3.

Ejecuté de nuevo desde línea de comandos el trabajo tal y como estaba en el cron y funcionó perfectamente. Revisé que el trabajo estaba bien definido y esperé al día siguiente: los ficheros seguían sin copiarse en S3. Ejecuté otra vez el comando: los ficheros se subieron a S3. En este momento yo ya estaba pegándole bocaos a la mesa.

Rebuscando por Internet encontré un mensaje en Stack Overflow (¿dónde si no?) que daba la solución: hay que poner la ruta completa al ejecutable de aws. La entrada en el cron quedaría entonces así:

0 */2 * * * /usr/local/bin/aws s3 sync /home/backupDB/ s3://NOMBRE_BUCKET/backup --delete

En este caso yo lo tengo configurado para que haga la sincronización cada dos horas. Con este cambio al día siguiente por fin respiré tranquilo al ver que ya aparecían los nuevos ficheros.

Instalar WSL en Windows 10

WSL (Windows Subsystem for Linux) es una maravilla que nos permite tener un sistema operativo Linux dentro de nuestro Windows 10, sin necesidad de utilizar máquinas virtuales y completamente integrado.

Por completamente integrado me refiero a que los dos SO comparten la misma dirección IP y los mismos puertos, si yo arranco un nginx en WSL podré acceder a él mediante http://localhost en mi navegador de Windows.

Para instalar WSL en Windows 10 tenemos estas instrucciones que nos guían paso a paso; voy a dejar aquí un resumen.

Lo primero es habilitar WSL, para ello abrimos un PowerShell como administrador y escribimos

Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Windows-Subsystem-Linux

A continuación nos vamos a la Microsoft Store y buscamos el nombre de la distribución que queramos instalar, por ejemplo Ubuntu. Le damos a descargar y se descargará e instalará como si fuera cualquier programa.

Mientras se descarga debemos pararnos y reflexionar sobre lo que está pasando. Estamos descargando una distribución de Linux directamente de la tienda de aplicaciones oficial de Microsoft, que ha desarrollado un sistema para que ambos convivan en nuestro ordenador. ¿Cómo se os queda el cuerpo?

Una vez se haya descargado, nos vamos al menú de inicio y pulsamos en el icono de Ubuntu que nos habrá aparecido. Al hacerlo estará unos momentos haciendo brujería y magia negra terminando la instalación y luego nos pedirá un nombre de usuario y una contraseña. Este será nuestro usuario, que tendrá permisos para sudo.

Hecho esto, ya podemos instalar todo lo que queramos. Eso sí, si instalamos servicios como nginx o mysql no se arrancarán automáticamente al iniciar el ordenador o entrar en WSL, deberemos arrancarlos a mano (o crearnos un script que arranque todo lo necesario). Seguro que hay alguna forma de hacerlo pero no lo he buscado, no me parece mal arrancar esos servicios a mano.