pub mod check;
pub mod collection;
pub mod export;
pub mod gc;
pub mod item;
pub mod queue;
pub mod store;
use std::{
io::{IsTerminal, stdin, stdout},
path::{Path, PathBuf},
time::{Duration, SystemTime},
};
use anyhow::{Result, anyhow, bail};
use clap::Args;
use io_pimdir::{PimdirBlobs, PimdirError, PimdirProducer, PimdirReader, PimdirStore};
use pimalaya_cli::{clap::parsers::path_parser, printer::Printer, prompt};
pub const PRODUCER: &str = "pimdir-cli";
#[derive(Debug, Args)]
pub struct StoreFlags {
#[arg(long, short = 's', global = true)]
#[arg(value_name = "PATH", value_parser = path_parser, default_value = ".")]
pub store: PathBuf,
#[arg(long, global = true, value_name = "NAME")]
pub source: Option<String>,
}
impl StoreFlags {
pub fn dir(&self) -> &Path {
&self.store
}
pub fn ensure_store(&self) -> Result<()> {
let db = self.store.join("pimdir.db");
if !db.is_file() {
bail!(
"no pimdir store at {}: {} not found",
self.store.display(),
db.display()
);
}
Ok(())
}
pub fn read(&self) -> Result<PimdirReader> {
self.ensure_store()?;
PimdirReader::open(&self.store).map_err(report)
}
pub fn owner(&self) -> Result<PimdirStore> {
self.ensure_store()?;
PimdirStore::open(&self.store).map_err(report)
}
pub fn owner_if_free(&self) -> Result<Option<PimdirStore>> {
self.ensure_store()?;
match PimdirStore::open(&self.store) {
Ok(store) => Ok(Some(store)),
Err(PimdirError::Owned(_)) => Ok(None),
Err(err) => Err(report(err)),
}
}
pub fn write_source(&self) -> Result<String> {
if let Some(source) = &self.source {
return Ok(source.clone());
}
let sources = self.read()?.distinct_sources().map_err(report)?;
match sources.len() {
1 => Ok(sources.into_iter().next().unwrap()),
0 => bail!("this store syncs no source yet: name the one to write as with --source"),
_ => bail!(
"this store syncs several sources ({}): pick the one to write as with --source",
sources.join(", ")
),
}
}
pub fn producer(&self) -> Result<PimdirProducer> {
self.ensure_store()?;
PimdirProducer::open(&self.store, PRODUCER).map_err(report)
}
pub fn blobs(&self) -> Result<PimdirBlobs> {
let store = PimdirReader::open(&self.store).map_err(report)?;
Ok(store.blobs())
}
}
pub fn report(err: PimdirError) -> anyhow::Error {
match err {
PimdirError::Owned(store) => {
anyhow!(
"another process owns the store at {} (a sync is running?); retry once it releases",
store.display()
)
}
PimdirError::Staging(store) => {
anyhow!(
"a producer is staging a body in the store at {} (a frontend is open?); \
retry once it is done",
store.display()
)
}
PimdirError::Busy => {
anyhow!(
"another writer holds the store lock (a sync is running?); retry once it releases"
)
}
err => anyhow!(err),
}
}
pub fn confirm(printer: &impl Printer, yes: bool, question: &str) -> Result<()> {
if yes {
return Ok(());
}
if printer.is_json() || !stdout().is_terminal() || !stdin().is_terminal() {
bail!("refusing to destroy data without a confirmation: pass --yes to proceed");
}
if !prompt::bool(question, false)? {
bail!("cancelled");
}
Ok(())
}
pub fn now() -> String {
humantime::format_rfc3339_millis(SystemTime::now()).to_string()
}
pub fn cutoff(age: Duration) -> String {
let cutoff = SystemTime::now()
.checked_sub(age)
.unwrap_or(SystemTime::UNIX_EPOCH);
humantime::format_rfc3339_millis(cutoff).to_string()
}
pub fn bytes(count: u64) -> String {
const UNITS: [&str; 5] = ["B", "KiB", "MiB", "GiB", "TiB"];
let mut value = count as f64;
let mut unit = 0;
while value >= 1024.0 && unit + 1 < UNITS.len() {
value /= 1024.0;
unit += 1;
}
if unit == 0 {
format!("{count} B")
} else {
format!("{value:.1} {}", UNITS[unit])
}
}
pub fn or_dash(value: Option<&str>) -> &str {
value.unwrap_or("-")
}