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    Error(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
118#[derive(Debug, Clone, Deserialize)]
119#[serde(from = "RawRedisStatus")]
120pub struct RedisStatus {
121    pub url: String,
122    pub status: Health,
123}
124
125#[derive(Debug, Clone, Deserialize)]
126#[serde(from = "RawCeleryStatus")]
127pub struct CeleryStatus {
128    pub status: Health,
129    pub url: String,
130}
131
132#[derive(Debug, Clone, Deserialize)]
133#[serde(from = "RawIndexStatus")]
134pub struct IndexStatus {
135    pub status: Health,
136    pub last_modified: Option<chrono::DateTime<chrono::Utc>>,
137}
138
139#[derive(Debug, Clone, Deserialize)]
140#[serde(from = "RawClassifierStatus")]
141pub struct ClassifierStatus {
142    pub status: Health,
143    pub last_trained: Option<chrono::DateTime<chrono::Utc>>,
144}
145
146#[derive(Debug, Clone, Deserialize)]
147#[serde(from = "RawSanityCheckStatus")]
148pub struct SanityCheckStatus {
149    pub status: Health,
150    pub last_run: Option<chrono::DateTime<chrono::Utc>>,
151}
152
153#[derive(Deserialize)]
154pub struct RawDatabase {
155    #[serde(rename = "type")]
156    pub db_type: String,
157    pub url: String,
158    pub status: String,
159    pub error: Option<String>,
160    pub migration_status: MigrationStatus,
161}
162
163#[derive(Deserialize)]
164#[allow(clippy::struct_field_names)]
165struct RawRedisStatus {
166    redis_url: String,
167    redis_status: String,
168    redis_error: Option<String>,
169}
170
171#[derive(Deserialize)]
172#[allow(clippy::struct_field_names)]
173struct RawCeleryStatus {
174    celery_status: String,
175    celery_url: String,
176    celery_error: Option<String>,
177}
178
179#[derive(Deserialize)]
180#[allow(clippy::struct_field_names)]
181struct RawIndexStatus {
182    index_status: String,
183    index_last_modified: Option<chrono::DateTime<chrono::Utc>>,
184    index_error: Option<String>,
185}
186
187#[derive(Deserialize)]
188#[allow(clippy::struct_field_names)]
189struct RawClassifierStatus {
190    classifier_status: String,
191    classifier_last_trained: Option<chrono::DateTime<chrono::Utc>>,
192    classifier_error: Option<String>,
193}
194
195#[derive(Deserialize)]
196#[allow(clippy::struct_field_names)]
197struct RawSanityCheckStatus {
198    sanity_check_status: String,
199    sanity_check_last_run: Option<chrono::DateTime<chrono::Utc>>,
200    sanity_check_error: Option<String>,
201}
202
203impl From<RawDatabase> for Database {
204    fn from(raw: RawDatabase) -> Self {
205        Self {
206            db_type: raw.db_type,
207            url: raw.url,
208            status: merge_status_with_error(raw.status, raw.error),
209            migration_status: raw.migration_status,
210        }
211    }
212}
213
214impl From<RawRedisStatus> for RedisStatus {
215    fn from(raw: RawRedisStatus) -> Self {
216        Self {
217            url: raw.redis_url,
218            status: merge_status_with_error(raw.redis_status, raw.redis_error),
219        }
220    }
221}
222
223impl From<RawCeleryStatus> for CeleryStatus {
224    fn from(raw: RawCeleryStatus) -> Self {
225        Self {
226            status: merge_status_with_error(raw.celery_status, raw.celery_error),
227            url: raw.celery_url,
228        }
229    }
230}
231
232impl From<RawIndexStatus> for IndexStatus {
233    fn from(raw: RawIndexStatus) -> Self {
234        Self {
235            status: merge_status_with_error(raw.index_status, raw.index_error),
236            last_modified: raw.index_last_modified,
237        }
238    }
239}
240
241impl From<RawClassifierStatus> for ClassifierStatus {
242    fn from(raw: RawClassifierStatus) -> Self {
243        Self {
244            status: merge_status_with_error(raw.classifier_status, raw.classifier_error),
245            last_trained: raw.classifier_last_trained,
246        }
247    }
248}
249
250impl From<RawSanityCheckStatus> for SanityCheckStatus {
251    fn from(raw: RawSanityCheckStatus) -> Self {
252        Self {
253            status: merge_status_with_error(raw.sanity_check_status, raw.sanity_check_error),
254            last_run: raw.sanity_check_last_run,
255        }
256    }
257}
258
259impl std::fmt::Display for Health {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        match self {
262            Health::Ok => write!(f, "OK"),
263            Health::Error(err) => write!(f, "Error: {err}"),
264        }
265    }
266}
267
268fn merge_status_with_error(status: String, error: Option<String>) -> Health {
269    if let Some(error) = error {
270        Health::Error(error)
271    } else if status.to_lowercase() != "ok" {
272        Health::Error(status)
273    } else {
274        Health::Ok
275    }
276}
277
278impl ServerStatus {
279    /// Returns `Health::Ok` if all components report `Ok`.
280    /// Otherwise returns `Health::Error` with a combined message of all failures.
281    #[must_use]
282    pub fn overall(&self) -> Health {
283        let mut errors = Vec::new();
284
285        if let Health::Error(ref err) = self.database.status {
286            errors.push(format!("database: {err}"));
287        }
288        if let Health::Error(ref err) = self.tasks.redis.status {
289            errors.push(format!("task redis: {err}"));
290        }
291        if let Health::Error(ref err) = self.tasks.celery.status {
292            errors.push(format!("task celery: {err}"));
293        }
294        if let Health::Error(ref err) = self.tasks.index.status {
295            errors.push(format!("task index: {err}"));
296        }
297        if let Health::Error(ref err) = self.tasks.sanity_check.status {
298            errors.push(format!("task sanity_check: {err}"));
299        }
300
301        if errors.is_empty() {
302            Health::Ok
303        } else {
304            Health::Error(errors.join(", "))
305        }
306    }
307}