Skip to main content

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}