pakka 0.0.5

A cross platform meta package manager with auto snapshotting file system based transactions
Documentation
use chrono::Utc;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fs::{File, OpenOptions};
use std::io::{self, BufRead, BufReader, BufWriter, Write};
use std::sync::{Arc, Mutex};

#[derive(Serialize, Deserialize)]
pub enum EventType {
  Install,
  Uninstall,
}

#[derive(Serialize, Deserialize)]
pub struct Event {
  pub timestamp: String,
  pub event_type: EventType,
  pub package_name: String,
  pub package_manager: String,
}

impl Event {
  pub fn new(
    event_type: EventType,
    package_name: &str,
    package_manager: &str,
  ) -> Self {
    Event {
      timestamp: Utc::now().to_rfc3339(),
      event_type,
      package_name: package_name.to_string(),
      package_manager: package_manager.to_string(),
    }
  }
}

pub struct EventSourcingDatabase {
  log_file: String,
  file_lock: Mutex<()>,
}

impl EventSourcingDatabase {
  fn new(log_file: &str) -> Arc<Self> {
    Arc::new(Self { log_file: log_file.to_string(), file_lock: Mutex::new(()) })
  }

  pub fn instance() -> Arc<Self> {
    static INSTANCE: OnceCell<Arc<EventSourcingDatabase>> = OnceCell::new();
    INSTANCE
      .get_or_init(|| EventSourcingDatabase::new("event_log.jsonl"))
      .clone()
  }

  pub fn log_event(&self, event: &Event) {
    let _lock = self.file_lock.lock().unwrap();
    let event_json =
      serde_json::to_string(event).expect("Failed to serialize event");
    let mut file = OpenOptions::new()
      .append(true)
      .create(true)
      .open(&self.log_file)
      .expect("Failed to open log file");
    writeln!(file, "{}", event_json).expect("Failed to write to log file");
  }

  pub fn get_installed_packages(&self) -> io::Result<HashSet<String>> {
    let _lock = self.file_lock.lock().unwrap();
    let file = File::open(&self.log_file)?;
    let reader = BufReader::new(file);
    let mut packages = HashSet::new();

    for line in reader.lines() {
      let line = line?;
      let event: Event = serde_json::from_str(&line)?;
      match event.event_type {
        EventType::Install => {
          packages.insert(event.package_name.clone());
        }
        EventType::Uninstall => {
          packages.remove(&event.package_name);
        }
      }
    }
    Ok(packages)
  }

  pub fn export_installed_packages(
    &self,
    export_file: &str,
  ) -> Result<(), std::io::Error> {
    let log_file = "event_log.jsonl";
    let file = File::open(log_file)?;
    let reader = BufReader::new(file);

    let mut installed_packages: HashMap<String, String> = HashMap::new();

    for line in reader.lines() {
      let line = line?;
      let event: Event = serde_json::from_str(&line)?;
      match event.event_type {
        EventType::Install => {
          installed_packages
            .insert(event.package_name.clone(), event.package_manager.clone());
        }
        EventType::Uninstall => {
          installed_packages.remove(&event.package_name);
        }
      }
    }

    let export_file_path = export_file;
    let export_file = File::create(export_file_path)?;
    let mut writer = BufWriter::new(export_file);

    for (package_name, package_manager) in installed_packages.iter() {
      writeln!(writer, "{}\t{}", package_name, package_manager)?;
    }

    println!("Exported installed packages to {:?}", export_file_path);

    Ok(())
  }

  pub fn show_history(&self, date_filter: Option<String>) {
    let _lock = self.file_lock.lock().unwrap();
    let file =
      File::open(&self.log_file).expect("Failed to open event log file");
    let reader = BufReader::new(file);

    for line in reader.lines() {
      let line = line.expect("Failed to read line from event log");
      let event: Event =
        serde_json::from_str(&line).expect("Failed to parse event log line");

      if let Some(ref date) = date_filter {
        if !event.timestamp.starts_with(date) {
          continue;
        }
      }

      println!(
        "[{}] {} {} via {}",
        event.timestamp,
        match event.event_type {
          EventType::Install => "Installed",
          EventType::Uninstall => "Uninstalled",
        },
        event.package_name,
        event.package_manager
      );
    }
  }

  pub fn show_diff(&self, from_date: &str, to_date: &str) {
    let _lock = self.file_lock.lock().unwrap();
    let file =
      File::open(&self.log_file).expect("Failed to open event log file");
    let reader = BufReader::new(file);

    let from_time = chrono::NaiveDate::parse_from_str(from_date, "%Y-%m-%d")
      .expect("Invalid from date format")
      .and_hms_opt(0, 0, 0);
    let to_time = chrono::NaiveDate::parse_from_str(to_date, "%Y-%m-%d")
      .expect("Invalid to date format")
      .and_hms_opt(23, 59, 59);

    let mut packages = HashSet::new();
    let mut packages_at_from = HashSet::new();
    let mut packages_at_to = HashSet::new();

    for line in reader.lines() {
      let line = line.expect("Failed to read line from event log");
      let event: Event =
        serde_json::from_str(&line).expect("Failed to parse event log line");

      let event_time = chrono::DateTime::parse_from_rfc3339(&event.timestamp)
        .expect("Invalid timestamp format")
        .naive_local();

      if event_time <= from_time.unwrap() {
        match event.event_type {
          EventType::Install => {
            packages.insert(event.package_name.clone());
          }
          EventType::Uninstall => {
            packages.remove(&event.package_name);
          }
        }
        packages_at_from = packages.clone();
      }

      if event_time <= to_time.unwrap() {
        match event.event_type {
          EventType::Install => {
            packages.insert(event.package_name.clone());
          }
          EventType::Uninstall => {
            packages.remove(&event.package_name);
          }
        }
        packages_at_to = packages.clone();
      } else {
        break;
      }
    }

    let installed_in_interval = packages_at_to.difference(&packages_at_from);
    let uninstalled_in_interval = packages_at_from.difference(&packages_at_to);

    println!("Packages installed between {} and {}:", from_date, to_date);
    for pkg in installed_in_interval {
      println!("+ {}", pkg);
    }

    println!("Packages uninstalled between {} and {}:", from_date, to_date);
    for pkg in uninstalled_in_interval {
      println!("- {}", pkg);
    }
  }
}