Skip to main content

oma_history/
lib.rs

1mod migrations;
2
3use std::{collections::HashMap, env::args, path::Path};
4
5use migrations::create_and_maybe_migration_from_oma_db_v2;
6use oma_pm_operation_type::{InstallOperation, OmaOperation, RemoveTag};
7use rusqlite::{Connection, Error, OpenFlags, Result};
8use serde::Deserialize;
9use spdlog::debug;
10use thiserror::Error;
11
12pub struct HistoryEntryInner {
13    pub install: Vec<InstallHistoryEntry>,
14    pub remove: Vec<RemoveHistoryEntry>,
15    pub disk_size: i64,
16    pub total_download_size: i64,
17    pub is_success: bool,
18}
19
20type HistoryResult<T> = Result<T, HistoryError>;
21
22#[derive(Debug, Error)]
23pub enum HistoryError {
24    #[error("Failed to create dir or file: {0}")]
25    FailedOperateDirOrFile(String, std::io::Error),
26    #[error("Failed to connect database")]
27    ConnectError(Error),
28    #[error("Failed to create transaction")]
29    CreateTransaction(Error),
30    #[error("Failed to execute sqlte stmt")]
31    ExecuteError(Error),
32    #[error("Failed to parser object")]
33    ParseDbError(Error),
34    #[error("History database is empty")]
35    HistoryEmpty,
36    #[error("Database no result by id: {0}")]
37    NoResult(i64),
38    #[error("Failed to get parent path: {0}")]
39    FailedParentPath(String),
40    #[error("Has no upgrade system log in this machine")]
41    NoUpgradeSystemLog,
42}
43
44pub const DATABASE_PATH: &str = "var/lib/oma/history.db";
45pub(crate) const INSERT_NEW_MAIN_TABLE: &str = r#"INSERT INTO "history_oma_1.14" (command, time, is_success, disk_size, total_download_size, install_count, remove_count, upgrade_count, downgrade_count, reinstall_count, is_fixbroken, is_undo)
46    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
47    RETURNING id;"#;
48pub(crate) const INSERT_INSTALL_TABLE: &str = r#"INSERT INTO "history_install_package_oma_1.14" (history_id, package_name, old_version, new_version, old_size, new_size, download_size, arch, operation)
49    VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"#;
50pub(crate) const INSERT_REMOVE_TABLE: &str = r#"INSERT INTO "history_remove_package_oma_1.14" (history_id, package_name, version, size, arch)
51    VALUES (?1, ?2, ?3, ?4, ?5)"#;
52pub(crate) const INSERT_REMOVE_DETAIL_TABLE: &str = r#"INSERT INTO "history_remove_package_detail_oma_1.14" (history_id, package_name, autoremove, purge, resolver)
53    VALUES (?1, ?2, ?3, ?4, ?5)"#;
54
55pub struct HistoryInfo<'a> {
56    pub summary: &'a OmaOperation,
57    pub start_time: i64,
58    pub success: bool,
59    pub is_fix_broken: bool,
60    pub is_undo: bool,
61    pub topics_enabled: Vec<String>,
62    pub topics_disabled: Vec<String>,
63}
64
65#[derive(Clone)]
66pub struct HistoryEntry {
67    pub id: i64,
68    pub time: i64,
69    pub command: String,
70    pub is_success: bool,
71    pub install_count: i64,
72    pub remove_count: i64,
73    pub upgrade_count: i64,
74    pub downgrade_count: i64,
75    pub reinstall_count: i64,
76    pub is_fixbroken: bool,
77    pub is_undo: bool,
78}
79
80#[derive(Deserialize)]
81pub struct InstallHistoryEntry {
82    #[serde(rename = "name")]
83    pub pkg_name: String,
84    pub old_version: Option<String>,
85    pub new_version: String,
86    pub old_size: Option<i64>,
87    pub new_size: i64,
88    pub download_size: i64,
89    pub arch: String,
90    #[serde(rename = "op")]
91    pub operation: InstallOperation,
92}
93
94pub struct RemoveHistoryEntryTmp {
95    pub pkg_name: String,
96    pub version: String,
97    pub size: i64,
98    pub arch: String,
99}
100
101#[derive(Deserialize)]
102pub struct RemoveHistoryEntry {
103    #[serde(rename = "name")]
104    pub pkg_name: String,
105    pub version: String,
106    pub size: i64,
107    pub arch: String,
108    #[serde(rename = "details")]
109    pub tags: Vec<RemoveTag>,
110}
111
112pub struct History {
113    connection: Connection,
114    dry_run: bool,
115}
116
117impl History {
118    pub fn new<P: AsRef<Path>>(
119        db_path: P,
120        create_history_database_if_not_exists: bool,
121        dry_run: bool,
122    ) -> HistoryResult<Self> {
123        if create_history_database_if_not_exists {
124            if let Some(parent) = db_path.as_ref().parent() {
125                std::fs::create_dir_all(parent).map_err(|e| {
126                    HistoryError::FailedOperateDirOrFile(parent.to_string_lossy().to_string(), e)
127                })?;
128            } else {
129                return Err(HistoryError::FailedParentPath(
130                    db_path.as_ref().to_string_lossy().to_string(),
131                ));
132            }
133        }
134
135        let conn = Connection::open_with_flags(
136            db_path,
137            if create_history_database_if_not_exists {
138                OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_CREATE
139            } else {
140                OpenFlags::SQLITE_OPEN_READ_WRITE
141            },
142        );
143
144        let mut conn = match conn {
145            Ok(conn) => conn,
146            Err(e) => match e {
147                Error::SqliteFailure(err, _) if [1, 14].contains(&err.extended_code) => {
148                    return Err(HistoryError::HistoryEmpty);
149                }
150                e => return Err(HistoryError::ConnectError(e)),
151            },
152        };
153
154        if create_history_database_if_not_exists {
155            create_and_maybe_migration_from_oma_db_v2(&mut conn)?;
156        }
157
158        Ok(Self {
159            connection: conn,
160            dry_run,
161        })
162    }
163
164    pub fn write(&mut self, entry: HistoryInfo<'_>) -> HistoryResult<i64> {
165        let HistoryInfo {
166            summary,
167            start_time,
168            success,
169            is_fix_broken,
170            is_undo,
171            topics_enabled,
172            topics_disabled,
173        } = entry;
174
175        if self.dry_run {
176            debug!("In dry-run mode, oma will not write history entries");
177            return Ok(-1);
178        }
179
180        let transaction = self
181            .connection
182            .transaction()
183            .map_err(HistoryError::CreateTransaction)?;
184
185        let command = args().collect::<Vec<_>>().join(" ");
186
187        let id: i64 = transaction
188            .query_row(
189                INSERT_NEW_MAIN_TABLE,
190                (
191                    if command.is_empty() {
192                        None
193                    } else {
194                        Some(command)
195                    },
196                    start_time,
197                    if success { 1 } else { 0 },
198                    summary.disk_size_delta,
199                    summary.total_download_size as i64,
200                    summary
201                        .install
202                        .iter()
203                        .filter(|x| x.op() == &InstallOperation::Install)
204                        .count() as i64,
205                    summary.remove.len() as i64,
206                    summary
207                        .install
208                        .iter()
209                        .filter(|x| x.op() == &InstallOperation::Upgrade)
210                        .count() as i64,
211                    summary
212                        .install
213                        .iter()
214                        .filter(|x| x.op() == &InstallOperation::Downgrade)
215                        .count() as i64,
216                    summary
217                        .install
218                        .iter()
219                        .filter(|x| x.op() == &InstallOperation::ReInstall)
220                        .count() as i64,
221                    if is_fix_broken { 1 } else { 0 },
222                    if is_undo { 1 } else { 0 },
223                ),
224                |row| row.get(0),
225            )
226            .map_err(HistoryError::ExecuteError)?;
227
228        for i in &summary.install {
229            let op: u8 = (*i.op()).into();
230            let op = op as i64;
231
232            transaction
233                .execute(
234                    INSERT_INSTALL_TABLE,
235                    (
236                        id,
237                        i.name(),
238                        i.old_version(),
239                        i.new_version(),
240                        i.old_size().map(|n| n as i64),
241                        i.new_size() as i64,
242                        i.download_size() as i64,
243                        i.arch(),
244                        op,
245                    ),
246                )
247                .map_err(HistoryError::ExecuteError)?;
248        }
249
250        for i in &summary.remove {
251            let Some(version) = i.version() else {
252                // 仅为删除配置文件时,版本为空,因此不记录
253                continue;
254            };
255
256            transaction
257                .execute(
258                    INSERT_REMOVE_TABLE,
259                    (id, i.name(), version, i.size() as i64, i.arch()),
260                )
261                .map_err(HistoryError::ExecuteError)?;
262
263            transaction
264                .execute(
265                    INSERT_REMOVE_DETAIL_TABLE,
266                    (
267                        id,
268                        i.name(),
269                        if i.details().contains(&RemoveTag::AutoRemove) {
270                            1
271                        } else {
272                            0
273                        },
274                        if i.details().contains(&RemoveTag::Purge) {
275                            1
276                        } else {
277                            0
278                        },
279                        if i.details().contains(&RemoveTag::Resolver) {
280                            1
281                        } else {
282                            0
283                        },
284                    ),
285                )
286                .map_err(HistoryError::ExecuteError)?;
287        }
288
289        if !topics_enabled.is_empty() || !topics_disabled.is_empty() {
290            for i in topics_enabled {
291                transaction
292                    .execute(
293                        r#"INSERT INTO "history_topic_oma_1.14" (history_id, topic_name, enable)
294                        VALUES (?1, ?2, ?3)"#,
295                        (id, i, 1),
296                    )
297                    .map_err(HistoryError::ExecuteError)?;
298            }
299
300            for i in topics_disabled {
301                transaction
302                    .execute(
303                        r#"INSERT INTO "history_topic_oma_1.14" (history_id, topic_name, enable)
304                        VALUES (?1, ?2, ?3)"#,
305                        (id, i, 0),
306                    )
307                    .map_err(HistoryError::ExecuteError)?;
308            }
309        }
310
311        transaction.commit().map_err(HistoryError::ExecuteError)?;
312
313        Ok(id)
314    }
315
316    pub fn edit_status(&mut self, id: i64, success: bool) -> HistoryResult<()> {
317        let transaction = self
318            .connection
319            .transaction()
320            .map_err(HistoryError::CreateTransaction)?;
321
322        transaction
323            .execute(
324                r#"UPDATE "history_oma_1.14" SET is_success = ?1 WHERE id = ?2"#,
325                [if success { 1 } else { 0 }, id],
326            )
327            .map_err(HistoryError::ExecuteError)?;
328
329        transaction.commit().map_err(HistoryError::ExecuteError)?;
330
331        Ok(())
332    }
333
334    pub fn list(&self) -> HistoryResult<Vec<HistoryEntry>> {
335        let mut res = vec![];
336        let stmt = self.connection.prepare(
337        r#"SELECT id, command, time, is_success, install_count, remove_count, upgrade_count, downgrade_count, reinstall_count, is_fixbroken, is_undo
338        FROM "history_oma_1.14"
339        ORDER BY id DESC"#,
340    );
341
342        let mut stmt = match stmt {
343            Ok(stmt) => stmt,
344            Err(e) => match e {
345                Error::SqliteFailure(err, _) if [1, 14].contains(&err.extended_code) => {
346                    return Err(HistoryError::HistoryEmpty);
347                }
348                e => return Err(HistoryError::ConnectError(e)),
349            },
350        };
351
352        let res_iter = stmt
353            .query_map([], |row| {
354                let id: i64 = row.get(0)?;
355                let command: String = row.get(1)?;
356                let time: i64 = row.get(2)?;
357                let is_success: i64 = row.get(3)?;
358                let install_count: i64 = row.get(4)?;
359                let remove_count: i64 = row.get(5)?;
360                let upgrade_count: i64 = row.get(6)?;
361                let downgrade_count: i64 = row.get(7)?;
362                let reinstall_count: i64 = row.get(8)?;
363                let is_fixbroken: i64 = row.get(9)?;
364                let is_undo: i64 = row.get(10)?;
365
366                Ok((
367                    id,
368                    command,
369                    time,
370                    is_success,
371                    install_count,
372                    remove_count,
373                    upgrade_count,
374                    downgrade_count,
375                    reinstall_count,
376                    is_fixbroken,
377                    is_undo,
378                ))
379            })
380            .map_err(HistoryError::ExecuteError)?;
381
382        for i in res_iter {
383            let (
384                id,
385                command,
386                time,
387                is_success,
388                install_count,
389                remove_count,
390                upgrade_count,
391                downgrade_count,
392                reinstall_count,
393                is_fixbroken,
394                is_undo,
395            ) = i.map_err(HistoryError::ParseDbError)?;
396
397            res.push(HistoryEntry {
398                id,
399                command,
400                time,
401                is_success: is_success == 1,
402                install_count,
403                remove_count,
404                upgrade_count,
405                downgrade_count,
406                reinstall_count,
407                is_fixbroken: is_fixbroken == 1,
408                is_undo: is_undo == 1,
409            });
410        }
411
412        if res.is_empty() {
413            return Err(HistoryError::HistoryEmpty);
414        }
415
416        Ok(res)
417    }
418
419    pub fn find_history_topics_status_by_id(
420        &self,
421        id: i64,
422    ) -> HistoryResult<(Vec<String>, Vec<String>)> {
423        let mut query_history_table = self
424            .connection
425            .prepare(
426                "SELECT topic_name, enable FROM \"history_topic_oma_1.14\" WHERE history_id = (?1)",
427            )
428            .map_err(HistoryError::ExecuteError)?;
429
430        let res_iter = query_history_table
431            .query_map([id], |row| {
432                let topic_name: String = row.get(0)?;
433                let enabled: i64 = row.get(1)?;
434                Ok((topic_name, enabled == 1))
435            })
436            .map_err(HistoryError::ExecuteError)?;
437
438        let (mut enabled_topics, mut disabled) = (vec![], vec![]);
439
440        for i in res_iter {
441            let (name, enabled) = i.map_err(HistoryError::ParseDbError)?;
442
443            if enabled {
444                enabled_topics.push(name);
445            } else {
446                disabled.push(name);
447            }
448        }
449
450        Ok((enabled_topics, disabled))
451    }
452
453    pub fn find_history_by_id(&self, id: i64) -> HistoryResult<HistoryEntryInner> {
454        let mut query_history_table = self.connection
455        .prepare("SELECT is_success, disk_size, total_download_size FROM \"history_oma_1.14\" WHERE id = (?1)")
456        .map_err(HistoryError::ExecuteError)?;
457
458        let mut res_iter = query_history_table
459            .query_map([id], |row| {
460                let is_success: i64 = row.get(0)?;
461                let disk_size: i64 = row.get(1)?;
462                let total_download_size: i64 = row.get(2)?;
463
464                Ok((is_success, disk_size, total_download_size))
465            })
466            .map_err(HistoryError::ExecuteError)?;
467
468        let mut query_install_table = self.connection.prepare(r#"SELECT package_name, old_version, new_version, old_size, new_size, download_size, arch, operation FROM "history_install_package_oma_1.14"
469    WHERE history_id = (?1)"#)
470    .map_err(HistoryError::ExecuteError)?;
471
472        let res_install_iter = query_install_table
473            .query_map([id], |row| {
474                let pkg_name: String = row.get(0)?;
475                let old_version: Option<String> = row.get(1)?;
476                let new_version: String = row.get(2)?;
477                let old_size: Option<i64> = row.get(3)?;
478                let new_size: i64 = row.get(4)?;
479                let download_size: i64 = row.get(5)?;
480                let arch: String = row.get(6)?;
481                let operation: i64 = row.get(7)?;
482                let operation = InstallOperation::from(operation as u8);
483
484                Ok(InstallHistoryEntry {
485                    pkg_name,
486                    old_version,
487                    new_version,
488                    old_size,
489                    new_size,
490                    download_size,
491                    arch,
492                    operation,
493                })
494            })
495            .map_err(HistoryError::ExecuteError)?;
496
497        let mut query_remove_table = self
498            .connection
499            .prepare(
500                r#"SELECT package_name, version, size, arch FROM "history_remove_package_oma_1.14"
501    WHERE history_id = (?1)"#,
502            )
503            .map_err(HistoryError::ExecuteError)?;
504
505        let res_remove_iter = query_remove_table
506            .query_map([id], |row| {
507                let pkg_name: String = row.get(0)?;
508                let version: String = row.get(1)?;
509                let size: i64 = row.get(2)?;
510                let arch: String = row.get(3)?;
511
512                Ok(RemoveHistoryEntryTmp {
513                    pkg_name,
514                    version,
515                    size,
516                    arch,
517                })
518            })
519            .map_err(HistoryError::ExecuteError)?;
520
521        let mut query_remove_details_table = self.connection
522        .prepare(
523            r#"SELECT package_name, autoremove, purge, resolver FROM "history_remove_package_detail_oma_1.14"
524    WHERE history_id = (?1)"#,
525        )
526        .map_err(HistoryError::ExecuteError)?;
527
528        let res_remove_details = query_remove_details_table
529            .query_map([id], |row| {
530                let mut remove_tag = vec![];
531                let package_name: String = row.get(0)?;
532                let autoremove: i64 = row.get(1)?;
533                let purge: i64 = row.get(2)?;
534                let resolver: i64 = row.get(3)?;
535
536                if autoremove == 1 {
537                    remove_tag.push(RemoveTag::AutoRemove);
538                }
539
540                if purge == 1 {
541                    remove_tag.push(RemoveTag::Purge);
542                }
543
544                if resolver == 1 {
545                    remove_tag.push(RemoveTag::Resolver);
546                }
547
548                Ok((package_name, remove_tag))
549            })
550            .map_err(HistoryError::ExecuteError)?
551            .collect::<Result<HashMap<_, _>>>()
552            .map_err(HistoryError::ParseDbError)?;
553
554        let mut res = None;
555
556        if let Some(i) = res_iter.next() {
557            let (is_success, disk_size, total_download_size) =
558                i.map_err(HistoryError::ParseDbError)?;
559
560            let is_success = is_success == 1;
561
562            let mut install = vec![];
563            let mut remove = vec![];
564
565            for i in res_install_iter {
566                let i = i.map_err(HistoryError::ParseDbError)?;
567
568                install.push(i);
569            }
570
571            for i in res_remove_iter {
572                let RemoveHistoryEntryTmp {
573                    pkg_name,
574                    version,
575                    size,
576                    arch,
577                } = i.map_err(HistoryError::ParseDbError)?;
578
579                let tags = res_remove_details.get(&pkg_name).unwrap().to_owned();
580
581                remove.push(RemoveHistoryEntry {
582                    pkg_name,
583                    version,
584                    size,
585                    arch,
586                    tags,
587                });
588            }
589
590            res = Some(HistoryEntryInner {
591                install,
592                remove,
593                disk_size,
594                total_download_size,
595                is_success,
596            })
597        }
598
599        res.ok_or_else(|| HistoryError::NoResult(id))
600    }
601
602    pub fn last_upgrade_timestamp(&self) -> HistoryResult<i64> {
603        let mut prepare = self
604            .connection
605            .prepare(
606                r#"SELECT command, time FROM "history_oma_1.14"
607    WHERE command LIKE ?"#,
608            )
609            .map_err(HistoryError::ExecuteError)?;
610
611        let query_str = "% upgrade%";
612        let res_iter = prepare
613            .query_map([query_str], |row| row.get(1))
614            .map_err(HistoryError::ExecuteError)?;
615
616        if let Some(Ok(n)) = res_iter.last() {
617            return Ok(n);
618        }
619
620        Err(HistoryError::NoUpgradeSystemLog)
621    }
622
623    pub fn query_like_install_and_remove_pkgname_item(
624        &self,
625        pkgname: &str,
626    ) -> HistoryResult<Vec<i64>> {
627        let query = format!("{pkgname}%");
628        let mut prepare = self.connection.prepare(
629            r#"SELECT history_id FROM "history_install_package_oma_1.14" where package_name LIKE ?1"#,
630        ).map_err(HistoryError::ExecuteError)?;
631
632        let mut res: Vec<_> = prepare
633            .query_map([&query], |row| row.get::<usize, i64>(0))
634            .map_err(HistoryError::ExecuteError)?
635            .collect::<Result<Vec<_>>>()
636            .map_err(HistoryError::ParseDbError)?;
637
638        let mut prepare = self.connection.prepare(
639            r#"SELECT history_id FROM "history_remove_package_oma_1.14" where package_name LIKE ?1"#,
640        ).map_err(HistoryError::ExecuteError)?;
641
642        let res_remove: Vec<_> = prepare
643            .query_map([&query], |row| row.get::<usize, i64>(0))
644            .map_err(HistoryError::ExecuteError)?
645            .collect::<Result<Vec<_>>>()
646            .map_err(HistoryError::ParseDbError)?;
647
648        res.extend(res_remove);
649
650        Ok(res)
651    }
652}