use std::path::{Path, PathBuf};
use time::{Month, OffsetDateTime};
use crate::memo::MemoId;
pub const APP_SUPPORT_SUBDIR: &str = "com.oximemo.app";
pub const VAULT_DEFAULT_SUBDIR: &str = "vault";
pub const INDEX_SUBDIR: &str = "index";
pub const META_DB_NAME: &str = "meta.redb";
pub const META_LOCK_NAME: &str = "meta.redb.lock";
pub const SEARCH_SUBDIR: &str = "search";
pub const MEMOS_DIR: &str = "memos";
pub const TRASH_DIR: &str = ".trash";
pub const ASSETS_DIR: &str = "assets";
pub const BY_VAULT_SUBDIR: &str = "by-vault";
pub const CONFIG_NAME: &str = "config.toml";
#[derive(Debug, Clone)]
pub struct Paths {
pub vault: PathBuf,
pub index_dir: PathBuf,
}
impl Paths {
pub fn resolve(vault: Option<&Path>) -> Self {
let support = app_support_dir();
match vault {
None => {
let vault = support.join(VAULT_DEFAULT_SUBDIR);
let index_dir = support.join(INDEX_SUBDIR);
Self { vault, index_dir }
}
Some(v) => {
let index_dir = support
.join(INDEX_SUBDIR)
.join(BY_VAULT_SUBDIR)
.join(vault_namespace(v));
Self {
vault: v.to_path_buf(),
index_dir,
}
}
}
}
pub fn memos_root(&self) -> PathBuf {
self.vault.join(MEMOS_DIR)
}
pub fn trash_root(&self) -> PathBuf {
self.vault.join(TRASH_DIR)
}
pub fn assets_root(&self) -> PathBuf {
self.vault.join(ASSETS_DIR)
}
pub fn asset_path(&self, name: &str) -> PathBuf {
self.assets_root().join(name)
}
pub fn config_path(&self) -> PathBuf {
self.vault.join(CONFIG_NAME)
}
pub fn meta_db_path(&self) -> PathBuf {
self.index_dir.join(META_DB_NAME)
}
pub fn meta_lock_path(&self) -> PathBuf {
self.index_dir.join(META_LOCK_NAME)
}
pub fn index_fmt_marker_path(&self) -> PathBuf {
self.index_dir.join("index-fmt")
}
pub fn search_dir(&self) -> PathBuf {
self.index_dir.join(SEARCH_SUBDIR)
}
pub fn memo_path(&self, id: MemoId, created_at: OffsetDateTime) -> PathBuf {
let (year, month) = shard(created_at);
self.memos_root()
.join(year.to_string())
.join(month)
.join(format!("{}.md", id))
}
pub fn trash_path(&self, id: MemoId) -> PathBuf {
self.trash_root().join(format!("{}.md", id))
}
}
fn shard(t: OffsetDateTime) -> (i32, String) {
let month = match t.month() {
Month::January => "01",
Month::February => "02",
Month::March => "03",
Month::April => "04",
Month::May => "05",
Month::June => "06",
Month::July => "07",
Month::August => "08",
Month::September => "09",
Month::October => "10",
Month::November => "11",
Month::December => "12",
};
(t.year(), month.to_string())
}
pub fn app_support_dir() -> PathBuf {
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
PathBuf::from(home)
.join("Library")
.join("Application Support")
.join(APP_SUPPORT_SUBDIR)
}
fn vault_namespace(vault: &Path) -> String {
let abs = if vault.is_absolute() {
vault.to_path_buf()
} else {
std::env::current_dir().unwrap_or_default().join(vault)
};
let mut hasher = blake3::Hasher::new();
hasher.update(abs.to_string_lossy().as_bytes());
let hex = hasher.finalize().to_hex();
hex.as_str()[..16].to_string()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn custom_vault_index_lives_outside_vault() {
let p = Paths::resolve(Some(Path::new("/tmp/some-vault")));
assert!(
!p.index_dir.starts_with(&p.vault),
"index must not be inside the vault"
);
assert!(p.index_dir.starts_with(app_support_dir()));
}
#[test]
fn default_vault_uses_documented_index_layout() {
let p = Paths::resolve(None);
assert_eq!(p.index_dir, app_support_dir().join(INDEX_SUBDIR));
}
#[test]
fn distinct_custom_vaults_get_distinct_indexes() {
let a = Paths::resolve(Some(Path::new("/tmp/vault-a")));
let b = Paths::resolve(Some(Path::new("/tmp/vault-b")));
assert_ne!(a.index_dir, b.index_dir);
}
}