Cómo sincronicé mi catálogo de impresión 3D con PrintStash y Spoolman


Homelab · Self-hosting
Docker PrintStash Spoolman Python ZFS Authentik 3D Printing Sysadmin

Entre productos que vendo, encargos de clientes y piezas para la impresora, tenía más de 90 archivos .3mf y .stl repartidos en carpetas de OneDrive sin ningún catálogo real. Encontré PrintStash, un gestor de archivos self-hosted para impresión 3D, y en vez de subir todo a mano decidí escribir un script: Python puro, sin dependencias externas, que recorre mis carpetas locales, calcula el hash SHA-256 de cada archivo y sube solo lo que cambió a la API de PrintStash. Cada carpeta de primer nivel se mapea a su propia colección, así que la estructura de mis carpetas locales queda reflejada directo en PrintStash:

02_PRODUCTOS/Llaveros/LORK_Keychain.3mf → Productos/Llaveros
03_ENCARGOS/Funko/Mapu/Funko_Mapu_Tripo.3mf → Encargos/Funko/Mapu

El primer obstáculo fue simple de resolver pero raro de diagnosticar: la API devolvía 403 con “error code: 1010”. No era la API — era Cloudflare, que está delante de mi instancia, rechazando el user-agent por defecto de urllib (Python-urllib/3.13) por parecer tráfico automatizado. Un header de navegador normal lo resolvió. El segundo error fue más tonto todavía: el .env con las credenciales seguía con los valores de la plantilla, sin completar. Tardé varios minutos en darme cuenta de que nunca había pegado la clave real.

Con la sincronización funcionando, al subir el catálogo completo (unos 95 archivos, varios de más de 15MB) la VM de Docker se quedó sin memoria y se cayó por completo, incluyendo el túnel de Cloudflare, que devolvió el error 1033 — túnel completamente inalcanzable, no solo un 502. La VM tenía 4GB de RAM y nada de swap, y el proceso de PrintStash que calcula geometría y genera miniaturas para cada modelo tiene picos de memoria más altos de lo que esperaba. Sin margen, un solo archivo pesado bastaba para tumbar todo.

Agregar swap terminó siendo más complicado de lo normal. Usé fallocate para crear el archivo, pero swapon lo rechazó con el error “appears to have holes”. Probé con dd en su lugar: mismo error, pero la escritura fue sospechosamente rápida — 5.9 GB/s para 4GB, algo imposible en un disco real. Esa velocidad fue la pista: el filesystem raíz del servidor es ZFS, que detecta bloques en cero y no los escribe de verdad, así que un archivo de swap normal no iba a funcionar ahí. La solución correcta en ZFS es un volumen dedicado:

zfs create -V 8G -b $(getconf PAGESIZE) rpool/swap
mkswap -f /dev/zvol/rpool/swap
swapon /dev/zvol/rpool/swap

Con swap real disponible, los mismos picos de memoria dejaron de tumbar la VM entera — en el peor caso, un contenedor se reiniciaba solo. Pasar de “todo el servidor caído” a “un contenedor se recupera solo” fue un cambio grande con una sola modificación.

Un archivo .3mf específico, un set de clips para bolsas, seguía fallando incluso con el swap puesto. Lo abrí como el ZIP que es (un .3mf es XML dentro de un ZIP) y encontré siete archivos de objeto vacíos y sin usar, residuos de una edición anterior en Bambu Studio. Escribí un script para eliminarlos sin tocar la geometría real, pero no era ese el problema: el archivo limpio falló igual. La causa real era una malla de 282 mil triángulos instanciada ocho veces en la misma plancha — el renderizador de miniaturas de PrintStash no reutiliza geometría entre instancias, así que termina procesando el equivalente a 2.25 millones de triángulos de una sola vez. Terminé dejando ese archivo fuera de la sincronización con una lista de exclusión en vez de seguir investigando.

Con PrintStash funcionando, agregué Spoolman para el inventario de filamento — se integra directo con PrintStash a través de su API. El despliegue fue simple, pero Spoolman no tiene ningún sistema de login propio, y exponerlo público sin protección no era una opción. Ya tenía Authentik corriendo como proveedor de identidad para otro servicio, así que en vez de escribir autenticación desde cero puse Spoolman detrás de un Proxy Provider en modo forward-auth: nginx le delega a Authentik la decisión de si un usuario está autenticado antes de dejar pasar cualquier solicitud.

Aproveché para conectar PrintStash también a Authentik por OIDC estándar, y ahí encontré un error real del proyecto: comparaba el issuer del token quitándole la barra final, pero Authentik siempre manda el issuer con barra final en el token real. El login por SSO fallaba siempre con oidc_invalid_id_token, sin importar qué tan bien estuviera configurado todo lo demás. Corregí el archivo Python directo dentro del contenedor (montado como volumen, para que el cambio no se pierda con futuras actualizaciones) y abrí un issue en el repositorio para que se corrija en la versión oficial.

Al final, ninguno de estos problemas fue especialmente complejo por separado. Fue una mañana de errores comunes, uno detrás de otro, hasta que dejaron de aparecer.

© 2026 Sebastian Choperena Solano — Construido con Astrofy