use std::fs;
use std::path::{Path, PathBuf};
use crate::error::{PwrError, Result};
use crate::metadata::ProjectMeta;
pub const PROJECT_FILE: &str = ".project.toml";
pub fn read_project_file(dir: &Path) -> Result<Option<ProjectMeta>> {
let file_path = dir.join(PROJECT_FILE);
if !file_path.exists() {
return Ok(None);
}
let contents = fs::read_to_string(&file_path)?;
let meta: ProjectMeta = toml::from_str(&contents).map_err(|e| PwrError::TomlParse {
path: file_path.to_string_lossy().to_string(),
source: e,
})?;
Ok(Some(meta))
}
pub fn write_project_file(dir: &Path, meta: &ProjectMeta) -> Result<()> {
fs::create_dir_all(dir)?;
let contents = toml::to_string_pretty(meta)?;
let file_path = dir.join(PROJECT_FILE);
let tmp_path = dir.join(format!(".project.toml.tmp"));
fs::write(&tmp_path, &contents)?;
fs::rename(&tmp_path, &file_path)?;
log::info!("Wrote project file: {}", file_path.display());
Ok(())
}
pub fn remove_project_file(dir: &Path) -> Result<()> {
let file_path = dir.join(PROJECT_FILE);
if file_path.exists() {
fs::remove_file(&file_path)?;
log::info!("Removed project file: {}", file_path.display());
}
Ok(())
}
pub fn is_archived_placeholder(dir: &Path) -> bool {
if !dir.is_dir() {
return false;
}
let project_file = dir.join(PROJECT_FILE);
if !project_file.exists() {
return false;
}
match read_project_file(dir) {
Ok(Some(meta)) => meta.is_archived(),
_ => false,
}
}
pub fn is_local_project(dir: &Path) -> bool {
if !dir.is_dir() {
return false;
}
match read_project_file(dir) {
Ok(Some(meta)) => meta.is_local(),
_ => false,
}
}
pub fn is_tracked(dir: &Path) -> bool {
dir.is_dir() && dir.join(PROJECT_FILE).exists()
}
pub fn find_projects(root: &Path) -> Result<Vec<(PathBuf, ProjectMeta)>> {
let mut projects = Vec::new();
if !root.is_dir() {
return Ok(projects);
}
for entry in fs::read_dir(root)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(meta) = read_project_file(&path)? {
projects.push((path, meta));
}
}
}
Ok(projects)
}
pub fn find_projects_recursive(root: &Path) -> Result<Vec<(PathBuf, ProjectMeta)>> {
let mut projects = Vec::new();
if !root.is_dir() {
return Ok(projects);
}
find_projects_recursive_inner(root, &mut projects)?;
Ok(projects)
}
fn find_projects_recursive_inner(
dir: &Path,
projects: &mut Vec<(PathBuf, ProjectMeta)>,
) -> Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
if let Some(meta) = read_project_file(&path)? {
projects.push((path.clone(), meta));
}
find_projects_recursive_inner(&path, projects)?;
}
}
Ok(())
}
pub fn dir_size(dir: &Path) -> Result<u64> {
let mut total = 0u64;
compute_dir_size(dir, &mut total)?;
Ok(total)
}
fn compute_dir_size(dir: &Path, total: &mut u64) -> Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.file_name().map_or(false, |n| n == PROJECT_FILE) {
continue;
}
if path.is_dir() {
compute_dir_size(&path, total)?;
} else if path.is_symlink() {
*total += fs::read_link(&path)
.map(|p| p.as_os_str().len() as u64)
.unwrap_or(0);
} else {
*total += entry.metadata()?.len();
}
}
Ok(())
}
pub fn file_count(dir: &Path) -> Result<u32> {
let mut count = 0u32;
count_files(dir, &mut count)?;
Ok(count)
}
fn count_files(dir: &Path, count: &mut u32) -> Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.file_name().map_or(false, |n| n == PROJECT_FILE) {
continue;
}
if path.is_dir() {
count_files(&path, count)?;
} else {
*count += 1;
}
}
Ok(())
}
pub fn remove_dir_contents_except_project(dir: &Path) -> Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
let file_name = path.file_name().and_then(|n| n.to_str());
if file_name == Some(PROJECT_FILE) {
continue;
}
if path.is_dir() {
fs::remove_dir_all(&path)?;
} else {
fs::remove_file(&path)?;
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
fn make_test_project(dir: &Path, name: &str) -> ProjectMeta {
ProjectMeta::new_local(
name.into(),
dir.to_string_lossy().to_string(),
format!("server:9742:/srv/pwr/projects/{}", name),
)
}
#[test]
fn test_read_write_round_trip() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("testproj");
let meta = make_test_project(&proj_dir, "testproj");
write_project_file(&proj_dir, &meta)?;
let read_back = read_project_file(&proj_dir)?;
assert!(read_back.is_some());
assert_eq!(read_back.unwrap().name, "testproj");
Ok(())
}
#[test]
fn test_atomic_write_no_partial_read() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("atomic");
let meta = make_test_project(&proj_dir, "atomic");
assert!(!proj_dir.join(PROJECT_FILE).exists());
write_project_file(&proj_dir, &meta)?;
assert!(proj_dir.join(PROJECT_FILE).exists());
assert!(!proj_dir.join(".project.toml.tmp").exists());
Ok(())
}
#[test]
fn test_remove_project_file() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("remove-test");
let meta = make_test_project(&proj_dir, "remove-test");
write_project_file(&proj_dir, &meta)?;
assert!(is_tracked(&proj_dir));
remove_project_file(&proj_dir)?;
assert!(!is_tracked(&proj_dir));
Ok(())
}
#[test]
fn test_is_archived_placeholder() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("archived-proj");
let mut meta = make_test_project(&proj_dir, "archived-proj");
meta.mark_archived(1024, 3, false);
write_project_file(&proj_dir, &meta)?;
assert!(is_archived_placeholder(&proj_dir));
assert!(!is_local_project(&proj_dir));
Ok(())
}
#[test]
fn test_is_local_project() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("local-proj");
let meta = make_test_project(&proj_dir, "local-proj");
write_project_file(&proj_dir, &meta)?;
assert!(is_local_project(&proj_dir));
assert!(!is_archived_placeholder(&proj_dir));
Ok(())
}
#[test]
fn test_is_tracked() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("tracked");
assert!(!is_tracked(&proj_dir));
let meta = make_test_project(&proj_dir, "tracked");
write_project_file(&proj_dir, &meta)?;
assert!(is_tracked(&proj_dir));
Ok(())
}
#[test]
fn test_dir_size() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("sized");
fs::create_dir_all(proj_dir.join("src"))?;
fs::write(proj_dir.join("src").join("main.rs"), b"fn main() {}")?;
fs::write(proj_dir.join("README.md"), b"# Hello")?;
let size = dir_size(&proj_dir)?;
assert!(size > 0);
assert_eq!(size, 19);
Ok(())
}
#[test]
fn test_file_count() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("counted");
fs::create_dir_all(proj_dir.join("subdir"))?;
fs::write(proj_dir.join("a.txt"), b"a")?;
fs::write(proj_dir.join("b.txt"), b"b")?;
fs::write(proj_dir.join("subdir").join("c.txt"), b"c")?;
let meta = make_test_project(&proj_dir, "counted");
write_project_file(&proj_dir, &meta)?;
assert_eq!(file_count(&proj_dir)?, 3);
Ok(())
}
#[test]
fn test_remove_dir_contents_except_project() -> Result<()> {
let tmp = TempDir::new()?;
let proj_dir = tmp.path().join("stripped");
fs::create_dir_all(proj_dir.join("src"))?;
fs::write(proj_dir.join("src").join("lib.rs"), b"pub fn foo() {}")?;
fs::write(proj_dir.join("Cargo.toml"), b"[package]")?;
let meta = make_test_project(&proj_dir, "stripped");
write_project_file(&proj_dir, &meta)?;
remove_dir_contents_except_project(&proj_dir)?;
let entries: Vec<_> = fs::read_dir(&proj_dir)?
.map(|e| e.unwrap().file_name().to_string_lossy().to_string())
.collect();
assert_eq!(entries, vec![PROJECT_FILE.to_string()]);
Ok(())
}
#[test]
fn test_find_projects() -> Result<()> {
let tmp = TempDir::new()?;
let root = tmp.path().join("projects");
fs::create_dir_all(&root)?;
for name in &["alpha", "beta"] {
let dir = root.join(name);
fs::create_dir(&dir)?;
let meta = make_test_project(&dir, name);
write_project_file(&dir, &meta)?;
}
fs::create_dir(root.join("random"))?;
let found = find_projects(&root)?;
assert_eq!(found.len(), 2);
let names: Vec<String> = found.iter().map(|(_, m)| m.name.clone()).collect();
assert!(names.contains(&"alpha".into()));
assert!(names.contains(&"beta".into()));
Ok(())
}
#[test]
fn test_find_projects_recursive() -> Result<()> {
let tmp = TempDir::new()?;
let root = tmp.path().join("nested");
fs::create_dir_all(root.join("outer").join("inner"))?;
let outer_meta = make_test_project(&root.join("outer"), "outer");
write_project_file(&root.join("outer"), &outer_meta)?;
let inner_meta = make_test_project(&root.join("outer").join("inner"), "inner");
write_project_file(&root.join("outer").join("inner"), &inner_meta)?;
let found = find_projects_recursive(&root)?;
assert_eq!(found.len(), 2);
Ok(())
}
}