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 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}