mirror of
https://github.com/narrrl/narl-workspace.git
synced 2026-11-04 17:07:00 +01:00
Terminal-themed markdown diary
Single-user diary served as one Rust binary with the Svelte frontend embedded. - axum 0.8 + SQLite (sqlx), migrations run at startup - stateless HMAC session cookie; credentials from .env, compared in constant time - FTS5 full-text search, user input rewritten into quoted prefix terms - media uploads linked to entries by scanning bodies for /api/media/<id>, which scopes share-token media access and entry deletion - unguessable share links (192-bit token) serving a public read-only page - vim keybindings via CodeMirror plus a modal browse layer and : command line; disabled on touch devices
This commit is contained in:
commit
4002a35305
41 files changed
+6195
No files matched your search
+87
@@ -0,0 +1,87 @@
|
||||
mod auth;
|
||||
mod config;
|
||||
mod error;
|
||||
mod routes;
|
||||
mod state;
|
||||
mod static_files;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use axum::Router;
|
||||
use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode, SqlitePoolOptions};
|
||||
use tower_http::{compression::CompressionLayer, trace::TraceLayer};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
|
||||
|
||||
use crate::{config::Config, state::AppState};
|
||||
|
||||
/// Seconds since the Unix epoch. Every timestamp in the database uses this.
|
||||
pub fn now() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system clock is after 1970")
|
||||
.as_secs() as i64
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
dotenvy::dotenv().ok();
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(EnvFilter::try_from_env("DIARY_LOG").unwrap_or_else(|_| EnvFilter::new("info")))
|
||||
.with(tracing_subscriber::fmt::layer())
|
||||
.init();
|
||||
|
||||
let config = Config::from_env()?;
|
||||
tokio::fs::create_dir_all(config.uploads_dir())
|
||||
.await
|
||||
.with_context(|| format!("could not create {}", config.uploads_dir().display()))?;
|
||||
|
||||
let db = SqlitePoolOptions::new()
|
||||
.max_connections(8)
|
||||
.connect_with(
|
||||
SqliteConnectOptions::new()
|
||||
.filename(config.data_dir.join("diary.db"))
|
||||
.create_if_missing(true)
|
||||
.journal_mode(SqliteJournalMode::Wal)
|
||||
.foreign_keys(true),
|
||||
)
|
||||
.await
|
||||
.context("could not open the diary database")?;
|
||||
|
||||
sqlx::migrate!("./migrations")
|
||||
.run(&db)
|
||||
.await
|
||||
.context("could not apply database migrations")?;
|
||||
|
||||
let bind = config.bind;
|
||||
let max_upload = config.max_upload_bytes;
|
||||
let state = AppState {
|
||||
db,
|
||||
config: Arc::new(config),
|
||||
};
|
||||
|
||||
let app = Router::new()
|
||||
.nest("/api", routes::api_router())
|
||||
.fallback(static_files::handler)
|
||||
.layer(axum::extract::DefaultBodyLimit::max(max_upload + 1024 * 1024))
|
||||
.layer(CompressionLayer::new())
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(bind)
|
||||
.await
|
||||
.with_context(|| format!("could not bind {bind}"))?;
|
||||
|
||||
tracing::info!("narl-diary listening on http://{bind}");
|
||||
|
||||
axum::serve(listener, app)
|
||||
.with_graceful_shutdown(shutdown_signal())
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn shutdown_signal() {
|
||||
let _ = tokio::signal::ctrl_c().await;
|
||||
tracing::info!("shutting down");
|
||||
}
|
||||
Reference in new issue
Block a user