ecr_server/state.rs
1use crate::auth::TokenStore;
2use crate::events::EventBus;
3use ecr_core::revision::Revision;
4use ecr_store::NotmuchStore;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7use std::time::SystemTime;
8use tokio::sync::RwLock;
9
10#[derive(Clone)]
11pub struct AppState {
12 pub store: Arc<NotmuchStore>,
13 pub tokens: Arc<RwLock<TokenStore>>,
14 pub events: EventBus,
15 pub read_only: bool,
16 /// Where the tokens came from, so they can be re-read. `None` when they
17 /// were handed over directly, which is every test but the one below.
18 token_path: Option<PathBuf>,
19 /// What the token file looked like when it was last read.
20 token_seen: Arc<RwLock<Option<Stamp>>>,
21 /// The revision this server's own last tag write left behind. See
22 /// `own_write`.
23 written: Arc<RwLock<Option<Revision>>>,
24}
25
26impl AppState {
27 pub fn new(store: Arc<NotmuchStore>, tokens: TokenStore, read_only: bool) -> Self {
28 Self {
29 store,
30 tokens: Arc::new(RwLock::new(tokens)),
31 events: EventBus::new(),
32 read_only,
33 token_path: None,
34 token_seen: Arc::new(RwLock::new(None)),
35 written: Arc::new(RwLock::new(None)),
36 }
37 }
38
39 /// Names the file the tokens were read from, so a token issued while this
40 /// server is running is one it will accept.
41 pub fn with_token_file(mut self, path: PathBuf) -> Self {
42 self.token_seen = Arc::new(RwLock::new(changed_at(&path)));
43 self.token_path = Some(path);
44 self
45 }
46
47 /// Re-reads the token store when the file has moved under it.
48 ///
49 /// `ecr token new` is a *different process*: it writes the file and exits,
50 /// and a server holding the copy it read at startup goes on refusing the
51 /// token that command just printed. The client reports *the server refused
52 /// that token* about a token that is perfectly valid, and nothing on either
53 /// side connects the two — the fix is to restart a server nobody has any
54 /// reason to suspect.
55 ///
56 /// The mtime is what bounds the work: an ordinary request pays one `stat`
57 /// and the file is read only when it has actually changed.
58 ///
59 /// A read that fails is kept rather than adopted. `TokenStore::save`
60 /// truncates before it writes, so a request landing in that window sees a
61 /// partial file — and taking a parse error for an empty store would turn
62 /// authentication off on a running server at the exact moment someone is
63 /// issuing a token.
64 pub async fn refresh_tokens(&self) {
65 let Some(path) = &self.token_path else { return };
66
67 let found = changed_at(path);
68 if *self.token_seen.read().await == found {
69 return;
70 }
71
72 match TokenStore::load(path) {
73 Ok(loaded) => {
74 *self.tokens.write().await = loaded;
75 *self.token_seen.write().await = found;
76 }
77 Err(error) => tracing::warn!(
78 path = %path.display(),
79 %error,
80 "could not re-read the token store; keeping the tokens already loaded"
81 ),
82 }
83 }
84
85 pub async fn requires_auth(&self) -> bool {
86 !self.tokens.read().await.is_empty()
87 }
88
89 /// Remembers what a tag write left the database at. notmuch synchronises
90 /// maildir flags, so dropping `unread` renames the file — which the
91 /// delivery watcher sees, and would otherwise announce as new mail.
92 pub async fn note_own_write(&self, revision: &Revision) {
93 *self.written.write().await = Some(revision.clone());
94 }
95
96 /// Whether the database still stands exactly where this server's own last
97 /// write left it, meaning nothing has been delivered since.
98 pub async fn own_write(&self, observed: &Revision) -> bool {
99 self.written.read().await.as_ref() == Some(observed)
100 }
101}
102
103/// The length as well as the mtime, because mtime granularity is a property of
104/// the filesystem rather than of Linux: a second write inside the same tick is
105/// invisible where that tick is a whole second, and `ecr token new` twice in a
106/// row is an ordinary thing to do.
107type Stamp = (SystemTime, u64);
108
109/// `None` for a file that is not there, which is a state the token store has:
110/// it does not exist until the first token is issued, and deleting it is how a
111/// server is put back to serving everyone.
112fn changed_at(path: &Path) -> Option<Stamp> {
113 let meta = std::fs::metadata(path).ok()?;
114 Some((meta.modified().ok()?, meta.len()))
115}