lb-rs 26.5.22

The rust library for interacting with your lockbook.
Documentation
use crate::LocalLb;
use crate::model::clock;
use crate::model::errors::LbResult;
use crate::service::lb_id::LbID;
use basic_human_duration::ChronoHumanDuration;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use time::Duration;

#[cfg(not(target_family = "wasm"))]
use std::path::Path;

#[cfg(not(target_family = "wasm"))]
use std::env;

#[cfg(not(target_family = "wasm"))]
use chrono::{Local, TimeZone};

#[cfg(not(target_family = "wasm"))]
use crate::get_code_version;

#[cfg(not(target_family = "wasm"))]
use tokio::fs::{self, OpenOptions};

#[cfg(not(target_family = "wasm"))]
use tokio::io::AsyncWriteExt;

#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct DebugInfo {
    pub lb_id: LbID,
    pub time: String,
    pub name: String,
    pub last_synced: String,
    pub lb_version: String,
    pub rust_triple: String,
    pub os_info: String,
    pub lb_dir: String,
    pub server_url: String,
    pub integrity: String,
    pub is_syncing: bool,
    pub status: String,
    pub panics: Vec<String>,
}

pub trait DebugInfoDisplay {
    fn to_string(&self) -> String;
}

impl DebugInfoDisplay for LbResult<DebugInfo> {
    fn to_string(&self) -> String {
        match self {
            Ok(debug_info) => serde_json::to_string_pretty(debug_info).unwrap_or_default(),
            Err(err) => format!("Error retrieving debug info: {:?}", err),
        }
    }
}

impl LocalLb {
    async fn human_last_synced(&self) -> String {
        let tx = self.ro_tx().await;
        let db = tx.db();

        let last_synced = *db.last_synced.get().unwrap_or(&0);

        if last_synced != 0 {
            Duration::milliseconds(clock::get_time().0 - last_synced)
                .format_human()
                .to_string()
        } else {
            "never".to_string()
        }
    }

    async fn lb_id(&self) -> LbResult<LbID> {
        let mut tx = self.begin_tx().await;
        let db = tx.db();

        let lb_id = if let Some(id) = db.id.get().copied() {
            id
        } else {
            let new_id = LbID::generate();
            db.id.insert(new_id)?;
            new_id
        };

        tx.end();

        Ok(lb_id)
    }

    fn now(&self) -> String {
        let now = chrono::Local::now();
        now.format("%Y-%m-%d %H:%M:%S %Z").to_string()
    }

    #[cfg(not(target_family = "wasm"))]
    async fn collect_panics(&self, populate_content: bool) -> LbResult<Vec<PanicInfo>> {
        let mut panics = vec![];
        let mut iter = iter_panic_files(&self.config.writeable_path).await?;
        while let Some(entry) = iter.next().await? {
            let content = if populate_content { entry.content().await? } else { String::new() };
            panics.push(PanicInfo { time: entry.time, file_path: entry.file_path, content });
        }
        panics.sort_by(|a, b| b.time.cmp(&a.time));

        Ok(panics)
    }

    /// returns true if we have crashed within the last 5 seconds
    #[cfg(not(target_family = "wasm"))]
    pub async fn recent_panic(&self) -> LbResult<bool> {
        let panics = self.collect_panics(false).await?;
        for panic in panics {
            let timestamp_local_time = Local
                .from_local_datetime(&panic.time)
                .single()
                .unwrap_or_default();

            let seconds_ago = (Local::now() - timestamp_local_time).abs().num_seconds();

            if seconds_ago <= 5 {
                return Ok(true);
            }
        }

        Ok(false)
    }

    #[instrument(level = "debug", skip(self), err(Debug))]
    #[cfg(not(target_family = "wasm"))]
    pub async fn write_panic_to_file(&self, error_header: String, bt: String) -> LbResult<String> {
        let file_name = generate_panic_filename(&self.config.writeable_path);
        let content = generate_panic_content(&error_header, &bt);

        let mut file = OpenOptions::new()
            .create(true)
            .append(true)
            .open(&file_name)
            .await?;

        file.write_all(content.as_bytes()).await?;

        Ok(file_name)
    }

    #[instrument(level = "debug", skip(self), err(Debug))]
    #[cfg(not(target_family = "wasm"))]
    pub async fn debug_info(&self, os_info: String, check_docs: bool) -> LbResult<DebugInfo> {
        let account = self.get_account()?;

        let arch = env::consts::ARCH;
        let os = env::consts::OS;
        let family = env::consts::FAMILY;

        let (integrity, last_synced, panics, lb_id) = tokio::join!(
            self.test_repo_integrity(check_docs),
            self.human_last_synced(),
            self.collect_panics(true),
            self.lb_id()
        );

        let panics = panics?.into_iter().map(|panic| panic.content).collect();

        let mut status = self.status().await;
        status.space_used = None;
        let status = format!("{status:?}");
        let is_syncing = self.syncer.try_lock().is_ok();

        Ok(DebugInfo {
            time: self.now(),
            name: account.username.clone(),
            lb_version: get_code_version().into(),
            lb_id: lb_id?,
            rust_triple: format!("{arch}.{family}.{os}"),
            server_url: account.api_url.clone(),
            integrity: format!(
                "{} {integrity:?}",
                if check_docs { "" } else { "doc checks skipped" }
            ),
            lb_dir: self.config.writeable_path.clone(),
            last_synced,
            os_info,
            status,
            is_syncing,
            panics,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PanicInfo {
    pub time: NaiveDateTime,
    pub file_path: PathBuf,
    pub content: String,
}

#[cfg(not(target_family = "wasm"))]
pub struct PanicFiles {
    entries: tokio::fs::ReadDir,
    base_path: PathBuf,
}

#[cfg(not(target_family = "wasm"))]
pub struct PanicFile {
    pub time: NaiveDateTime,
    pub file_path: PathBuf,
}

#[cfg(not(target_family = "wasm"))]
impl PanicFiles {
    pub async fn next(&mut self) -> LbResult<Option<PanicFile>> {
        const PREFIX: &str = "panic---";
        const SUFFIX: &str = ".log";
        const TIMESTAMP_FORMAT: &str = "%Y-%m-%d---%H-%M-%S";

        while let Some(entry) = self.entries.next_entry().await? {
            let file_name = entry.file_name().into_string().unwrap_or_default();
            if !file_name.starts_with(PREFIX) || !file_name.ends_with(SUFFIX) {
                continue;
            }
            let timestamp_str = &file_name[PREFIX.len()..file_name.len() - SUFFIX.len()];
            let Ok(time) = NaiveDateTime::parse_from_str(timestamp_str, TIMESTAMP_FORMAT) else {
                continue;
            };
            let file_path = self.base_path.join(file_name);
            return Ok(Some(PanicFile { time, file_path }));
        }
        Ok(None)
    }
}

#[cfg(not(target_family = "wasm"))]
impl PanicFile {
    pub async fn content(&self) -> LbResult<String> {
        let contents = fs::read_to_string(&self.file_path).await?;
        Ok(format!("time: {}: contents: {}", self.time, contents))
    }
}

#[cfg(not(target_family = "wasm"))]
pub(crate) async fn iter_panic_files(path: &str) -> LbResult<PanicFiles> {
    let base_path = Path::new(path).to_path_buf();
    let entries = fs::read_dir(&base_path).await?;
    Ok(PanicFiles { entries, base_path })
}

/// Millisecond UTC timestamp of the most recent panic file on disk, if any.
#[cfg(not(target_family = "wasm"))]
pub(crate) async fn latest_panic_time(path: &str) -> LbResult<Option<i64>> {
    let mut iter = iter_panic_files(path).await?;
    let mut max: Option<i64> = None;
    while let Some(entry) = iter.next().await? {
        let new_ts = entry.time.and_utc().timestamp_millis();
        match &mut max {
            Some(ts) => {
                if new_ts > *ts {
                    *ts = new_ts;
                }
            }
            None => max = Some(new_ts),
        }
    }
    Ok(max)
}

pub fn generate_panic_filename(path: &str) -> String {
    let timestamp = chrono::Local::now().format("%Y-%m-%d---%H-%M-%S");
    format!("{path}/panic---{timestamp}.log")
}

pub fn generate_panic_content(panic_info: &str, bt: &str) -> String {
    format!("INFO: {panic_info}\nBT: {bt}")
}