Plataformas
Rust
Runlight funciona dentro de un servicio Rust sobre tokio, detrás de axum, hyper o cualquier servidor que acepte un servicio tower, con sus tablas en la propia base de datos de la aplicación a través de sqlx.
El crate de Rust es Runlight escrito de nuevo en Rust. Sirve el mismo panel y la misma API, y da a cada petición la misma respuesta que da la biblioteca de TypeScript. Sus cifras van en las mismas tablas, así que cualquiera de los dos puede leer una base de datos que haya escrito el otro. Necesita Rust 1.88 o posterior y funciona sobre tokio. El almacén viene de un segundo crate, runlight-sqlx, que necesita Rust 1.94 para sqlx 0.9.
Instalación
Añade ambos crates, con la base de datos que uses y el adaptador de framework que quieras.
toml
[dependencies]
runlight = { version = "0.1", features = ["axum"] }
runlight-sqlx = { version = "0.1", features = ["sqlite"] }La feature transport del crate runlight está activada por defecto y le da un cliente para los servicios de correo, las instalaciones conectadas, los iconos de los sitios y el Asistente de IA. La feature tower convierte las rutas en un servicio tower, y axum añade los routers de más abajo. Las features de runlight-sqlx son sqlite, postgres y mysql.
Crea la instancia
Crea un Runlight cuando arranque tu servicio y guárdalo en tu estado. Clonarlo es barato.
rust
use runlight::{Runlight, RunlightOptions, SiteOptions};
let store = runlight_sqlx::connect("sqlite:data/runlight.db").await?;
let mut options = RunlightOptions::new(store);
options.site = Some(SiteOptions {
name: Some("example.com".into()),
hostnames: Some(vec!["example.com".into()]),
timezone: Some("Europe/London".into()),
..SiteOptions::default()
});
let rl = Runlight::new(options)?;Las opciones tienen los mismos nombres que en Configuración, en snake case, así que sites, trust_proxy, rate_limit, mail y managed_sites funcionan como allí. Las tablas se crean en la primera petición.
Almacenes
runlight_sqlx::connect abre un pool propio a partir de una URL. Para usar el pool que tu aplicación ya tiene, pásaselo, y Runlight guarda sus tablas junto a las tuyas.
Las tablas de Runlight empiezan por rl_. sqlite_with_prefix, postgres_with_prefix, mysql_with_prefix y connect_with_prefix aceptan un prefijo para usar en su lugar, como runlight_sqlx::sqlite_with_prefix(pool, "wp_rl_")?, así que dos aplicaciones que comparten una base de datos mantienen cada una sus propias tablas. Un prefijo tiene como máximo 32 letras, dígitos y guiones bajos, y no empieza por un dígito. store.tables() lista las tablas del almacén por nombre, para una aplicación que quiera quitar Runlight.
SQLite
rust
let store = runlight_sqlx::sqlite(pool); // a sqlx::SqlitePoolEl almacén reserva una conexión del pool para sus sentencias, que van por turnos, en modo WAL con un busy timeout de cinco segundos. La feature sqlite compila SQLite desde el código fuente, así que tu build necesita un compilador de C.
MySQL y MariaDB
rust
let store = runlight_sqlx::mysql(pool); // a sqlx::MySqlPoolEste almacén funciona con MySQL 8.0.20 o posterior y MariaDB 11.4 o posterior, y el pool necesita al menos dos conexiones. La autenticación por defecto de MySQL 8 sobre una conexión sin TLS necesita la feature mysql-rsa de sqlx, así que actívala en tu propio Cargo.toml o conéctate por TLS.
Postgres
rust
let store = runlight_sqlx::postgres(pool); // a sqlx::PgPoolEl pool necesita al menos dos conexiones. Cada sentencia se ejecuta por separado, así que las escrituras de Runlight nunca se unen a una transacción que tu aplicación tenga abierta.
axum
Fusiona los routers con tu aplicación. El panel y la API responden en /runlight, y los enlaces cortos en /go/{slug}.
rust
use runlight::{RoutesOptions, TokenOption};
let routes = rl.routes(RoutesOptions {
token: TokenOption::Given(std::env::var("RUNLIGHT_TOKEN")?),
..RoutesOptions::default()
})?;
let app = axum::Router::new()
.merge(routes.router())
.merge(rl.link_router())
.layer(axum::middleware::from_fn_with_state(rl.clone(), runlight::adapters::axum_support::middleware));El middleware responde a una petición en un dominio de enlaces con su enlace corto antes de que la vean tus propias rutas, y registra los accesos de agentes de IA a tus páginas. Una petición para cualquier otro host sigue intacta hacia tu aplicación. Mantén la capa, ya que un router de axum solo ve las rutas que se le dieron y los enlaces cortos de un dominio de enlaces están en la raíz de su propio nombre. Sirve la aplicación con into_make_service_with_connect_info::<SocketAddr>() para que Runlight vea la dirección de la conexión cuando ninguna cabecera de proxy indique al visitante. Después añade el script a cada página, justo antes de </head>.
html
<script defer src="/runlight/s.js"></script>Después define RUNLIGHT_TOKEN como una cadena larga y aleatoria y abre una vez /runlight/?token= seguido de esa cadena para iniciar sesión, como describe Primeros pasos. RoutesOptions acepta los ajustes que acepta routes() en Configuración, en snake case, así que base_path, accounts y origin funcionan como allí.
link_router() responde a cada petición que axum enruta a /go/{slug}. Una petición en un dominio de enlaces va primero, así que ahí /go/<name> es un 404 como cualquier otra ruta que no sea uno de los enlaces de ese dominio. En tu propio host redirige cuando el enlace existe, y un nombre sin enlace recibe el 404 simple de Runlight, ya que axum ya ha emparejado la ruta y no puede pasar la petición al fallback de tu aplicación. rl.link_service() responde igual.
Otros servidores
Routes es un tower::Service para cualquier http::Request, así que hyper y cualquier otra cosa construida sobre tower puede servirlo tal cual. Cada petición que recibe en un dominio de enlaces se responde con el enlace corto, y el servicio de enlaces cortos de rl.link_service() hace lo mismo. Sin la feature tower, construye tú mismo un runlight::http::Request y llama a routes.handle(request).await, que responde con un runlight::http::Response.
La comprobación programada
La comprobación programada rota las sales diarias, envía los informes por email pendientes, aplica cuánto tiempo conserva cada sitio sus visitas y construye los resúmenes que mantienen rápidos los rangos largos. Llama a rl.check().await cada pocos minutos desde una task propia. Cuando nada en el servicio se ejecuta con un temporizador, define CRON_SECRET y haz que un programador de tareas llame a /runlight/api/check con él, como muestra Comprobación programada.
rust
let checker = rl.clone();
tokio::spawn(async move {
loop {
if let Err(error) = checker.check().await {
eprintln!("Runlight check failed: {error}");
}
tokio::time::sleep(std::time::Duration::from_secs(300)).await;
}
});Ubicación
Runlight lee el país, la región y la ciudad de las cabeceras que añaden Vercel, Cloudflare y Netlify. Detrás de cualquier otra cosa, dale una base de datos de ciudades de MaxMind o DB-IP a través de runlight::mmdb::Mmdb, que lee el archivo página a página según lo necesitan las consultas.
rust
options.geo = Some(std::sync::Arc::new(runlight::mmdb::Mmdb::open("data/dbip-city-lite.mmdb")?));En qué se diferencia de la biblioteca de TypeScript
El crate de Rust pasa las pruebas de conformidad que se exigen a la biblioteca de TypeScript, en SQLite, Postgres, MySQL y MariaDB. Las diferencias vienen de cómo se ejecuta Rust.
- El crate hace sus peticiones salientes, para los servicios de correo, las instalaciones conectadas, los iconos de los sitios y el Asistente de IA, a través de un
Fetcherque puedes reemplazar, así que los tests y las redes restringidas pueden pasar el suyo. - Lee las zonas horarias de la base de datos del sistema, así que una zona cuyas reglas cambiaron después de la copia de la biblioteca de TypeScript sigue las reglas más nuevas.
- No hay servidor independiente en Rust. Para tener un solo panel para varios sitios, ejecuta el servidor de Node o el servidor de otro lenguaje, y conecta a él el sitio de este servicio.