1- use std:: str:: FromStr ;
1+ use std:: {
2+ str:: FromStr ,
3+ sync:: { Arc , RwLock } ,
4+ } ;
25
36use axum:: { extract:: FromRef , middleware} ;
47use axum_extra:: extract:: cookie;
8+ use eyre:: Context ;
9+ use notify:: Watcher ;
510use sqlx:: PgPool ;
611use tracing:: info;
712
813use crate :: {
914 common:: config:: RunDatabaseMigrations ,
15+ common:: error:: AppError ,
1016 config:: AppConfig ,
1117 email:: { MailTransport , Mailer } ,
1218} ;
@@ -29,6 +35,7 @@ pub struct AppState {
2935 private_cookie_key : cookie:: Key ,
3036 mailer : Mailer ,
3137 config : AppConfig ,
38+ geodb : Arc < RwLock < Arc < maxminddb:: Reader < Vec < u8 > > > > > ,
3239}
3340
3441pub trait DbConnLike < ' a > :
@@ -77,6 +84,61 @@ pub async fn pool_conn(
7784 }
7885}
7986
87+ async fn load_geodb (
88+ geodb_path : & std:: path:: Path ,
89+ ) -> Result < Arc < maxminddb:: Reader < Vec < u8 > > > , AppError > {
90+ let geodb_raw = tokio:: fs:: read ( geodb_path)
91+ . await
92+ . wrap_err ( "Could not load geolocation database from disk" ) ?;
93+
94+ Ok ( Arc :: new (
95+ maxminddb:: Reader :: from_source ( geodb_raw) . wrap_err ( "Invalid geolocation database" ) ?,
96+ ) )
97+ }
98+
99+ async fn manage_geodb (
100+ geodb_path : impl AsRef < std:: path:: Path > + Send + ' static ,
101+ ) -> Result < Arc < RwLock < Arc < maxminddb:: Reader < Vec < u8 > > > > > , AppError > {
102+ let ( change_sender, mut change_receiver) = tokio:: sync:: mpsc:: unbounded_channel :: < ( ) > ( ) ;
103+ // Use a poll watcher here as INotify can be unreliable in many ways and I don't want to deal with that.
104+ let mut watcher = notify:: poll:: PollWatcher :: new (
105+ move |event : notify:: Result < notify:: Event > | {
106+ if event. is_ok ( ) {
107+ let _ = change_sender. send ( ( ) ) ;
108+ }
109+ } ,
110+ notify:: Config :: default ( )
111+ . with_poll_interval ( std:: time:: Duration :: from_secs ( 60 ) )
112+ . with_compare_contents ( true ) ,
113+ )
114+ . wrap_err ( "Could not setup watcher for changes in geolocation database" ) ?;
115+
116+ watcher
117+ . watch ( geodb_path. as_ref ( ) , notify:: RecursiveMode :: NonRecursive )
118+ . wrap_err ( "Could not watch geolocation database for changes" ) ?;
119+
120+ let geodb = Arc :: new ( RwLock :: new ( load_geodb ( geodb_path. as_ref ( ) ) . await ?) ) ;
121+ let geodb_cloned = geodb. clone ( ) ;
122+
123+ tokio:: spawn ( async move {
124+ // keep the watcher alive
125+ let _w = watcher;
126+ loop {
127+ change_receiver. recv ( ) . await ;
128+ match load_geodb ( geodb_path. as_ref ( ) ) . await {
129+ Ok ( new_geodb) => {
130+ * geodb. write ( ) . unwrap ( ) = new_geodb;
131+ }
132+ Err ( e) => {
133+ tracing:: error!( "Could not refresh geolocation database: {e}" ) ;
134+ }
135+ }
136+ }
137+ } ) ;
138+
139+ Ok ( geodb_cloned)
140+ }
141+
80142#[ tokio:: main]
81143async fn main ( ) {
82144 let config = AppConfig :: from_env ( ) . expect ( "Failed to load configuration" ) ;
@@ -122,6 +184,10 @@ async fn main() {
122184
123185 let serve_dir_service = tower_http:: services:: ServeDir :: new ( & config. assets_path ) ;
124186
187+ let geodb = manage_geodb ( config. geolocation_db . clone ( ) )
188+ . await
189+ . expect ( "Unable to initialize geolocation database" ) ;
190+
125191 // construct the application state
126192 let state = AppState {
127193 db,
@@ -130,6 +196,7 @@ async fn main() {
130196 private_cookie_key,
131197 mailer,
132198 config,
199+ geodb,
133200 } ;
134201
135202 // setup routes
0 commit comments