1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
|
//! The `pilcrow` command-line interface.
//!
//! This module supports running `pilcrow` as a freestanding program, via the
//! [`Args`] struct.
use std::{future, io};
use axum::{
Router,
http::header,
middleware,
response::{IntoResponse, Response},
};
use clap::{CommandFactory, Parser};
use sqlx::sqlite::SqlitePool;
use tokio::net;
use crate::{
app::App,
boot, channel, clock, db, event, expire, invite, message,
setup::{self, middleware::setup_required},
ui, user,
};
/// Command-line entry point for running the `pilcrow` server.
///
/// This is intended to be used as a Clap [Parser], to capture command-line
/// arguments for the `pilcrow` server:
///
/// ```no_run
/// # use pilcrow::cli::Error;
/// #
/// # #[tokio::main]
/// # async fn main() -> Result<(), Error> {
/// use clap::Parser;
/// use pilcrow::cli::Args;
///
/// let args = Args::parse();
/// args.run().await?;
/// # Ok(())
/// # }
/// ```
#[derive(Parser)]
#[command(
version,
about = "Run the `pilcrow` server.",
long_about = r#"Run the `pilcrow` server.
The database at `--database-url` will be created, or upgraded, automatically."#
)]
pub struct Args {
/// The network address `pilcrow` should listen on
#[arg(short, long, env, default_value = "localhost")]
address: String,
/// The network port `pilcrow` should listen on
#[arg(short, long, env, default_value_t = 64209)]
port: u16,
/// Sqlite URL or path for the `pilcrow` database
#[arg(short, long, env, default_value = "sqlite://pilcrow.db")]
database_url: String,
/// Sqlite URL or path for a backup of the `pilcrow` database during
/// upgrades
#[arg(short = 'D', long, env, default_value = "sqlite://pilcrow.db.backup")]
backup_database_url: String,
}
impl Args {
/// Runs the `pilcrow` server, using the parsed configuation in `self`.
///
/// This will perform the following tasks:
///
/// * Migrate the `pilcrow` database (at `--database-url`).
/// * Start an HTTP server (on the interface and port controlled by
/// `--address` and `--port`).
/// * Print a status message.
/// * Wait for that server to shut down.
///
/// # Errors
///
/// Will return `Err` if the server is unable to start, or terminates
/// prematurely. The specific [`Error`] variant will expose the cause
/// of the failure.
pub async fn run(self) -> Result<(), Error> {
let pool = self.pool().await?;
let app = App::from(pool);
let app = routers(&app)
.route_layer(middleware::from_fn(clock::middleware))
.route_layer(middleware::map_response(Self::server_info()))
.with_state(app);
let listener = self.listener().await?;
let started_msg = started_msg(&listener)?;
let serve = axum::serve(listener, app);
println!("{started_msg}");
serve.await?;
Ok(())
}
async fn listener(&self) -> io::Result<net::TcpListener> {
let listen_addr = self.listen_addr();
let listener = tokio::net::TcpListener::bind(listen_addr).await?;
Ok(listener)
}
fn listen_addr(&self) -> impl net::ToSocketAddrs + '_ {
(self.address.as_str(), self.port)
}
async fn pool(&self) -> Result<SqlitePool, db::Error> {
db::prepare(&self.database_url, &self.backup_database_url).await
}
fn server_info() -> impl Clone + Fn(Response) -> future::Ready<Response> {
let command = Self::command();
let name = command.get_name();
let version = command.get_version().unwrap_or("unknown version");
let version = format!("{name}/{version}");
move |resp| {
let response = ([(header::SERVER, &version)], resp).into_response();
future::ready(response)
}
}
}
fn routers(app: &App) -> Router<App> {
[
[
// API endpoints that require setup to function
boot::router(),
channel::router(),
event::router(),
invite::router(),
user::router(),
message::router(),
]
.into_iter()
.fold(Router::default(), Router::merge)
// Run expiry whenever someone accesses the API. This was previously a blanket middleware
// affecting the whole service, but loading the client makes a several requests before the
// client can completely load, each of which was triggering expiry. There is absolutely no
// upside to re-checking expiry tens of times back-to-back like that; the API is accessed
// more regularly and with less of a traffic rush.
//
// This should, probably, be moved to a background job at some point.
.route_layer(middleware::from_fn_with_state(
app.clone(),
expire::middleware,
))
.route_layer(middleware::from_fn_with_state(app.clone(), setup_required)),
// API endpoints that handle setup
setup::router(),
// The UI (handles setup state itself)
ui::router(app),
]
.into_iter()
.fold(Router::default(), Router::merge)
}
fn started_msg(listener: &net::TcpListener) -> io::Result<String> {
let local_addr = listener.local_addr()?;
Ok(format!("listening on http://{local_addr}/"))
}
/// Errors that can be raised by [`Args::run`].
#[derive(Debug, thiserror::Error)]
#[error(transparent)]
pub enum Error {
/// Failure due to `io::Error`. See [`io::Error`].
Io(#[from] io::Error),
/// Failure due to a database initialization error. See [`db::Error`].
Database(#[from] db::Error),
}
|