use crate::storage::UniqueProjectId;
use rusqlite::params;
use std::path::Path;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ProjectMetadataError {
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("Invalid project ID: {0}")]
InvalidId(String),
#[error("Project not found: {0}")]
NotFound(String),
}
pub type Result<T> = std::result::Result<T, ProjectMetadataError>;
#[derive(Debug, Clone)]
pub struct ProjectMetadata {
pub unique_project_id: UniqueProjectId,
pub base_name: String,
pub path_hash: String,
pub instance: u32,
pub canonical_path: String,
pub display_name: Option<String>,
pub is_clone: bool,
pub cloned_from: Option<String>,
}
impl ProjectMetadata {
pub fn new(project_path: &Path) -> Self {
let canonical_path = project_path
.canonicalize()
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|_| project_path.to_string_lossy().to_string());
let base_name = project_path
.file_name()
.map(|n| n.to_string_lossy().to_string())
.unwrap_or_else(|| "unknown".to_string());
let path_hash = blake3::hash(canonical_path.as_bytes()).to_hex().to_string();
let unique_project_id = UniqueProjectId::generate(project_path, &[]);
Self {
unique_project_id,
base_name,
path_hash,
instance: 0,
canonical_path,
display_name: None,
is_clone: false,
cloned_from: None,
}
}
pub fn load_existing_ids(
conn: &rusqlite::Connection,
base_name: &str,
) -> Result<Vec<UniqueProjectId>> {
let mut stmt =
conn.prepare("SELECT unique_project_id FROM project_metadata WHERE base_name = ?1")?;
let ids: Vec<UniqueProjectId> = stmt
.query_map(params![base_name], |row| {
let id_str: String = row.get(0)?;
Ok(UniqueProjectId::parse_id(&id_str))
})?
.filter_map(|r| r.ok().flatten())
.collect();
Ok(ids)
}
pub fn save(&self, conn: &rusqlite::Connection) -> Result<()> {
conn.execute(
r#"INSERT OR REPLACE INTO project_metadata
(unique_project_id, base_name, path_hash, instance, canonical_path, display_name, is_clone, cloned_from)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"#,
params![
self.unique_project_id.to_string(),
self.base_name,
self.path_hash,
self.instance,
self.canonical_path,
self.display_name,
self.is_clone,
self.cloned_from,
],
)?;
Ok(())
}
pub fn load(conn: &rusqlite::Connection, id: &UniqueProjectId) -> Result<Self> {
let mut stmt = conn.prepare(
r#"SELECT unique_project_id, base_name, path_hash, instance, canonical_path, display_name, is_clone, cloned_from
FROM project_metadata WHERE unique_project_id = ?1"#,
)?;
let result = stmt
.query_row(params![id.to_string()], |row| {
let id_str: String = row.get(0)?;
let unique_project_id = UniqueProjectId::parse_id(&id_str)
.ok_or_else(|| rusqlite::Error::InvalidQuery)?;
Ok(Self {
unique_project_id,
base_name: row.get(1)?,
path_hash: row.get(2)?,
instance: row.get(3)?,
canonical_path: row.get(4)?,
display_name: row.get(5)?,
is_clone: row.get(6)?,
cloned_from: row.get(7)?,
})
})
.map_err(ProjectMetadataError::from)?;
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_project_metadata_new() {
let dir = tempdir().unwrap();
let path = dir.path();
let metadata = ProjectMetadata::new(path);
assert!(!metadata.base_name.is_empty());
assert!(!metadata.canonical_path.is_empty());
assert!(!metadata.is_clone);
}
}