Skip to main content

paperless_api/
util.rs

1//! Utility types.
2
3use serde::Deserialize;
4
5#[derive(Debug, Clone, Deserialize)]
6pub struct Statistics {
7    /// Total number of documents.
8    pub documents_total: u32,
9
10    /// Number of documents in the inbox.
11    pub documents_inbox: u32,
12
13    /// Tag used for documents in the inbox.
14    pub inbox_tag: u32,
15
16    /// Tags used for documents in the inbox.
17    pub inbox_tags: Vec<u32>,
18
19    /// Counts of document file types.
20    pub document_file_type_counts: Vec<DocumentFileTypeCount>,
21
22    /// Total number of characters in all documents.
23    pub character_count: u64,
24
25    /// Total number of tags.
26    pub tag_count: u32,
27
28    /// Total number of correspondents.
29    pub correspondent_count: u32,
30
31    /// Total number of document types.
32    pub document_type_count: u32,
33
34    /// Total number of storage paths.
35    pub storage_path_count: u32,
36
37    /// Current ASN.
38    pub current_asn: u32,
39}
40
41#[derive(Debug, Clone, Deserialize)]
42pub struct DocumentFileTypeCount {
43    /// MIME type of the document type.
44    pub mime_type: String,
45
46    /// Number of documents of this type.
47    #[serde(rename = "mime_type_count")]
48    pub count: u32,
49}
50
51#[derive(Debug, Clone, Deserialize)]
52pub struct ServerStatus {
53    /// The version of Paperless-ngx.
54    #[serde(rename = "pngx_version")]
55    pub version: String,
56
57    /// Operating system of the server.
58    pub server_os: String,
59
60    /// Type of installation (e.g. docker).
61    pub install_type: String,
62
63    /// Storage information.
64    pub storage: Storage,
65
66    /// Database information.
67    pub database: Database,
68
69    /// Task status information.
70    pub tasks: StatusTask,
71}
72
73#[derive(Debug, Clone, Deserialize)]
74pub struct Storage {
75    pub total: u64,
76    pub available: u64,
77}
78
79#[derive(Debug, Clone, Deserialize)]
80#[serde(from = "RawDatabase")]
81pub struct Database {
82    pub db_type: String,
83    pub url: String,
84    pub status: Health,
85    pub migration_status: MigrationStatus,
86}
87
88#[derive(Debug, Clone, Deserialize)]
89pub struct MigrationStatus {
90    pub latest_migration: String,
91    pub unapplied_migrations: Vec<String>,
92}
93
94#[derive(Debug, Clone, Deserialize)]
95pub enum Health {
96    #[serde(rename = "OK")]
97    Ok,
98
99    #[serde(untagged)]
100    NotOk(String),
101}
102
103#[derive(Debug, Clone, Deserialize)]
104pub struct StatusTask {
105    #[serde(flatten)]
106    pub redis: RedisStatus,
107
108    #[serde(flatten)]
109    pub celery: CeleryStatus,
110
111    #[serde(flatten)]
112    pub index: IndexStatus,
113
114    #[serde(flatten)]
115    pub sanity_check: SanityCheckStatus,
116
117    #[serde(flatten)]
118    pub classifier: ClassifierStatus,
119}
120
121#[derive(Debug, Clone, Deserialize)]
122#[serde(from = "RawRedisStatus")]
123pub struct RedisStatus {
124    pub url: String,
125    pub status: Health,
126}
127
128#[derive(Debug, Clone, Deserialize)]
129#[serde(from = "RawCeleryStatus")]
130pub struct CeleryStatus {
131    pub status: Health,
132    pub url: String,
133}
134
135#[derive(Debug, Clone, Deserialize)]
136#[serde(from = "RawIndexStatus")]
137pub struct IndexStatus {
138    pub status: Health,
139    pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
140}
141
142#[derive(Debug, Clone, Deserialize)]
143#[serde(from = "RawClassifierStatus")]
144pub struct ClassifierStatus {
145    pub status: Health,
146    pub last_trained: Option<chrono::DateTime<chrono::Utc>>,
147}
148
149#[derive(Debug, Clone, Deserialize)]
150#[serde(from = "RawSanityCheckStatus")]
151pub struct SanityCheckStatus {
152    pub status: Health,
153    pub last_run: Option<chrono::DateTime<chrono::Utc>>,
154}
155
156#[derive(Deserialize)]
157pub struct RawDatabase {
158    #[serde(rename = "type")]
159    pub db_type: String,
160    pub url: String,
161    pub status: String,
162    pub error: Option<String>,
163    pub migration_status: MigrationStatus,
164}
165
166#[derive(Deserialize)]
167#[allow(clippy::struct_field_names)]
168struct RawRedisStatus {
169    redis_url: String,
170    redis_status: String,
171    redis_error: Option<String>,
172}
173
174#[derive(Deserialize)]
175#[allow(clippy::struct_field_names)]
176struct RawCeleryStatus {
177    celery_status: String,
178    celery_url: String,
179    celery_error: Option<String>,
180}
181
182#[derive(Deserialize)]
183#[allow(clippy::struct_field_names)]
184struct RawIndexStatus {
185    index_status: String,
186    index_last_modified: Option<chrono::DateTime<chrono::Utc>>,
187    index_error: Option<String>,
188}
189
190#[derive(Deserialize)]
191#[allow(clippy::struct_field_names)]
192struct RawClassifierStatus {
193    classifier_status: String,
194    classifier_last_trained: Option<chrono::DateTime<chrono::Utc>>,
195    classifier_error: Option<String>,
196}
197
198#[derive(Deserialize)]
199#[allow(clippy::struct_field_names)]
200struct RawSanityCheckStatus {
201    sanity_check_status: String,
202    sanity_check_last_run: Option<chrono::DateTime<chrono::Utc>>,
203    sanity_check_error: Option<String>,
204}
205
206impl From<RawDatabase> for Database {
207    fn from(raw: RawDatabase) -> Self {
208        Self {
209            db_type: raw.db_type,
210            url: raw.url,
211            status: merge_status_with_error(&raw.status, raw.error),
212            migration_status: raw.migration_status,
213        }
214    }
215}
216
217impl From<RawRedisStatus> for RedisStatus {
218    fn from(raw: RawRedisStatus) -> Self {
219        Self {
220            url: raw.redis_url,
221            status: merge_status_with_error(&raw.redis_status, raw.redis_error),
222        }
223    }
224}
225
226impl From<RawCeleryStatus> for CeleryStatus {
227    fn from(raw: RawCeleryStatus) -> Self {
228        Self {
229            status: merge_status_with_error(&raw.celery_status, raw.celery_error),
230            url: raw.celery_url,
231        }
232    }
233}
234
235impl From<RawIndexStatus> for IndexStatus {
236    fn from(raw: RawIndexStatus) -> Self {
237        Self {
238            status: merge_status_with_error(&raw.index_status, raw.index_error),
239            last_modified: raw.index_last_modified,
240        }
241    }
242}
243
244impl From<RawClassifierStatus> for ClassifierStatus {
245    fn from(raw: RawClassifierStatus) -> Self {
246        Self {
247            status: merge_status_with_error(&raw.classifier_status, raw.classifier_error),
248            last_trained: raw.classifier_last_trained,
249        }
250    }
251}
252
253impl From<RawSanityCheckStatus> for SanityCheckStatus {
254    fn from(raw: RawSanityCheckStatus) -> Self {
255        Self {
256            status: merge_status_with_error(&raw.sanity_check_status, raw.sanity_check_error),
257            last_run: raw.sanity_check_last_run,
258        }
259    }
260}
261
262impl std::fmt::Display for Health {
263    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264        match self {
265            Health::Ok => write!(f, "OK"),
266            Health::NotOk(err) => write!(f, "Error: {err}"),
267        }
268    }
269}
270
271fn merge_status_with_error(status: &str, error: Option<String>) -> Health {
272    if status.to_lowercase() == "ok" && error.is_none() {
273        Health::Ok
274    } else {
275        Health::NotOk(format!(
276            "{status}: {error}",
277            error = error.unwrap_or_default()
278        ))
279    }
280}
281
282impl ServerStatus {
283    /// Returns `Health::Ok` if all components report `Ok`.
284    /// Otherwise returns `Health::NotOk` with a combined message.
285    #[must_use]
286    pub fn overall(&self) -> Health {
287        let components = [
288            ("database.status", &self.database.status),
289            ("redis", &self.tasks.redis.status),
290            ("celery", &self.tasks.celery.status),
291            ("index", &self.tasks.index.status),
292            ("sanity_check", &self.tasks.sanity_check.status),
293            ("classifier", &self.tasks.classifier.status),
294        ];
295
296        let errors: Vec<_> = components
297            .iter()
298            .filter_map(|(name, health)| match health {
299                Health::NotOk(err) => Some(format!("{name}: {err}")),
300                Health::Ok => None,
301            })
302            .collect();
303
304        if errors.is_empty() {
305            Health::Ok
306        } else {
307            Health::NotOk(errors.join(", "))
308        }
309    }
310}