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/// Storage information for the server.
74#[derive(Debug, Clone, Deserialize)]
75pub struct Storage {
76    /// Total storage in bytes.
77    pub total: u64,
78
79    /// Available storage in bytes.
80    pub available: u64,
81}
82
83/// Database information.
84#[derive(Debug, Clone, Deserialize)]
85#[serde(from = "RawDatabase")]
86pub struct Database {
87    /// Type of the database.
88    pub db_type: String,
89
90    /// URL of the database.
91    pub url: String,
92
93    /// Health status of the database.
94    pub status: Health,
95
96    /// Migration status of the database.
97    pub migration_status: MigrationStatus,
98}
99
100/// Migration status of the database.
101#[derive(Debug, Clone, Deserialize)]
102pub struct MigrationStatus {
103    /// The latest applied migration.
104    pub latest_migration: Option<String>,
105
106    /// Unapplied migrations.
107    pub unapplied_migrations: Vec<String>,
108}
109
110/// Health status of a component.
111#[derive(Debug, Clone, Deserialize)]
112pub enum Health {
113    /// The component is healthy.
114    #[serde(rename = "OK")]
115    Ok,
116
117    /// The component is not healthy, with an error message.
118    #[serde(untagged)]
119    NotOk(String),
120}
121
122/// Task status information.
123#[derive(Debug, Clone, Deserialize)]
124pub struct StatusTask {
125    #[serde(flatten)]
126    pub redis: RedisStatus,
127
128    #[serde(flatten)]
129    pub celery: CeleryStatus,
130
131    #[serde(flatten)]
132    pub index: IndexStatus,
133
134    #[serde(flatten)]
135    pub sanity_check: SanityCheckStatus,
136
137    #[serde(flatten)]
138    pub classifier: ClassifierStatus,
139}
140
141/// Redis status information.
142#[derive(Debug, Clone, Deserialize)]
143#[serde(from = "RawRedisStatus")]
144pub struct RedisStatus {
145    /// URL of the Redis server.
146    pub url: String,
147
148    /// Health status of Redis.
149    pub status: Health,
150}
151
152/// Celery status information.
153#[derive(Debug, Clone, Deserialize)]
154#[serde(from = "RawCeleryStatus")]
155pub struct CeleryStatus {
156    /// Health status of Celery.
157    pub status: Health,
158
159    /// URL of the Celery broker.
160    pub url: String,
161}
162
163/// Index status information.
164#[derive(Debug, Clone, Deserialize)]
165#[serde(from = "RawIndexStatus")]
166pub struct IndexStatus {
167    /// Health status of the index.
168    pub status: Health,
169
170    /// When the index was last modified.
171    pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
172}
173
174/// Classifier status information.
175#[derive(Debug, Clone, Deserialize)]
176#[serde(from = "RawClassifierStatus")]
177pub struct ClassifierStatus {
178    /// Health status of the classifier.
179    pub status: Health,
180
181    /// When the classifier was last trained.
182    pub last_trained: Option<chrono::DateTime<chrono::Utc>>,
183}
184
185/// Sanity check status information.
186#[derive(Debug, Clone, Deserialize)]
187#[serde(from = "RawSanityCheckStatus")]
188pub struct SanityCheckStatus {
189    /// Health status of the sanity check.
190    pub status: Health,
191
192    /// When the sanity check was last run.
193    pub last_run: Option<chrono::DateTime<chrono::Utc>>,
194}
195
196#[derive(Deserialize)]
197pub struct RawDatabase {
198    #[serde(rename = "type")]
199    pub db_type: String,
200    pub url: String,
201    pub status: String,
202    pub error: Option<String>,
203    pub migration_status: MigrationStatus,
204}
205
206#[derive(Deserialize)]
207#[allow(clippy::struct_field_names)]
208struct RawRedisStatus {
209    redis_url: String,
210    redis_status: String,
211    redis_error: Option<String>,
212}
213
214#[derive(Deserialize)]
215#[allow(clippy::struct_field_names)]
216struct RawCeleryStatus {
217    celery_status: String,
218    celery_url: String,
219    celery_error: Option<String>,
220}
221
222#[derive(Deserialize)]
223#[allow(clippy::struct_field_names)]
224struct RawIndexStatus {
225    index_status: String,
226    index_last_modified: Option<chrono::DateTime<chrono::Utc>>,
227    index_error: Option<String>,
228}
229
230#[derive(Deserialize)]
231#[allow(clippy::struct_field_names)]
232struct RawClassifierStatus {
233    classifier_status: String,
234    classifier_last_trained: Option<chrono::DateTime<chrono::Utc>>,
235    classifier_error: Option<String>,
236}
237
238#[derive(Deserialize)]
239#[allow(clippy::struct_field_names)]
240struct RawSanityCheckStatus {
241    sanity_check_status: String,
242    sanity_check_last_run: Option<chrono::DateTime<chrono::Utc>>,
243    sanity_check_error: Option<String>,
244}
245
246impl From<RawDatabase> for Database {
247    fn from(raw: RawDatabase) -> Self {
248        Self {
249            db_type: raw.db_type,
250            url: raw.url,
251            status: merge_status_with_error(&raw.status, raw.error),
252            migration_status: raw.migration_status,
253        }
254    }
255}
256
257impl From<RawRedisStatus> for RedisStatus {
258    fn from(raw: RawRedisStatus) -> Self {
259        Self {
260            url: raw.redis_url,
261            status: merge_status_with_error(&raw.redis_status, raw.redis_error),
262        }
263    }
264}
265
266impl From<RawCeleryStatus> for CeleryStatus {
267    fn from(raw: RawCeleryStatus) -> Self {
268        Self {
269            status: merge_status_with_error(&raw.celery_status, raw.celery_error),
270            url: raw.celery_url,
271        }
272    }
273}
274
275impl From<RawIndexStatus> for IndexStatus {
276    fn from(raw: RawIndexStatus) -> Self {
277        Self {
278            status: merge_status_with_error(&raw.index_status, raw.index_error),
279            last_modified: raw.index_last_modified,
280        }
281    }
282}
283
284impl From<RawClassifierStatus> for ClassifierStatus {
285    fn from(raw: RawClassifierStatus) -> Self {
286        Self {
287            status: merge_status_with_error(&raw.classifier_status, raw.classifier_error),
288            last_trained: raw.classifier_last_trained,
289        }
290    }
291}
292
293impl From<RawSanityCheckStatus> for SanityCheckStatus {
294    fn from(raw: RawSanityCheckStatus) -> Self {
295        Self {
296            status: merge_status_with_error(&raw.sanity_check_status, raw.sanity_check_error),
297            last_run: raw.sanity_check_last_run,
298        }
299    }
300}
301
302impl std::fmt::Display for Health {
303    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
304        match self {
305            Health::Ok => write!(f, "OK"),
306            Health::NotOk(err) => write!(f, "Error: {err}"),
307        }
308    }
309}
310
311fn merge_status_with_error(status: &str, error: Option<String>) -> Health {
312    if status.to_lowercase() == "ok" && error.is_none() {
313        Health::Ok
314    } else {
315        Health::NotOk(format!(
316            "{status}: {error}",
317            error = error.unwrap_or_default()
318        ))
319    }
320}
321
322impl ServerStatus {
323    /// Returns `Health::Ok` if all components report `Ok`.
324    /// Otherwise returns `Health::NotOk` with a combined message.
325    #[must_use]
326    pub fn overall(&self) -> Health {
327        let components = [
328            ("database.status", &self.database.status),
329            ("redis", &self.tasks.redis.status),
330            ("celery", &self.tasks.celery.status),
331            ("index", &self.tasks.index.status),
332            ("sanity_check", &self.tasks.sanity_check.status),
333            ("classifier", &self.tasks.classifier.status),
334        ];
335
336        let errors: Vec<_> = components
337            .iter()
338            .filter_map(|(name, health)| match health {
339                Health::NotOk(err) => Some(format!("{name}: {err}")),
340                Health::Ok => None,
341            })
342            .collect();
343
344        if errors.is_empty() {
345            Health::Ok
346        } else {
347            Health::NotOk(errors.join(", "))
348        }
349    }
350}