kptui 0.15.0

TUI password manager for KeePass vaults
use std::fs::OpenOptions;
use std::io::Write;
use std::path::Path;

use anyhow::Context;
use serde::Serialize;

use crate::db::Entry;

pub enum ExportFormat {
	Kdbx,
	Csv,
	Json,
}

pub fn detect_format(path: &Path) -> anyhow::Result<ExportFormat> {
	let ext = path.extension().and_then(|e| e.to_str()).map(|e| e.to_ascii_lowercase());

	match ext.as_deref() {
		Some("kdbx") => Ok(ExportFormat::Kdbx),
		Some("csv") => Ok(ExportFormat::Csv),
		Some("json") => Ok(ExportFormat::Json),
		_ => anyhow::bail!("unrecognized file extension, expected .kdbx, .csv, or .json"),
	}
}

pub fn export_csv(path: &Path, entries: &[Entry]) -> anyhow::Result<()> {
	let mut options = OpenOptions::new();
	options.write(true).create_new(true);

	#[cfg(unix)]
	{
		use std::os::unix::fs::OpenOptionsExt;
		options.mode(0o600);
	}

	let file = options.open(path).with_context(|| format!("couldn't create {}", path.display()))?;

	let mut writer = csv::WriterBuilder::new().from_writer(file);

	writer.write_record(["name", "username", "password", "url", "totp", "notes"])?;

	for entry in entries {
		writer.write_record([
			entry.name.as_str(),
			entry.user.as_str(),
			entry.password.as_str(),
			entry.url.as_str(),
			entry.totp.as_str(),
			entry.notes.as_str(),
		])?;
	}

	writer.flush().context("couldn't finish writing CSV")?;

	Ok(())
}

#[derive(Serialize)]
struct ExportEntry<'a> {
	name: &'a str,
	username: &'a str,
	password: &'a str,
	url: &'a str,
	totp: &'a str,
	notes: &'a str,
}

pub fn export_json(path: &Path, entries: &[Entry]) -> anyhow::Result<()> {
	let out: Vec<ExportEntry> = entries
		.iter()
		.map(|e| ExportEntry { name: &e.name, username: &e.user, password: &e.password, url: &e.url, totp: &e.totp, notes: &e.notes })
		.collect();

	let json = serde_json::to_string_pretty(&out).context("couldn't serialize entries")?;

	let mut options = OpenOptions::new();
	options.write(true).create_new(true);

	#[cfg(unix)]
	{
		use std::os::unix::fs::OpenOptionsExt;
		options.mode(0o600);
	}

	let mut file = options.open(path).with_context(|| format!("couldn't create {}", path.display()))?;

	file.write_all(json.as_bytes()).with_context(|| format!("couldn't write {}", path.display()))?;

	Ok(())
}