1use crate::errors::AppError;
2use crate::i18n::validacao;
3use directories::ProjectDirs;
4use std::path::{Path, PathBuf};
5
6#[derive(Debug)]
7pub struct AppPaths {
8 pub db: PathBuf,
9 pub models: PathBuf,
10}
11
12impl AppPaths {
13 pub fn resolve(db_override: Option<&str>) -> Result<Self, AppError> {
14 let proj = ProjectDirs::from("", "", "sqlite-graphrag").ok_or_else(|| {
15 AppError::Io(std::io::Error::other(
16 "não foi possível determinar o diretório home",
17 ))
18 })?;
19
20 let cache_root = if let Some(override_dir) = std::env::var_os("SQLITE_GRAPHRAG_CACHE_DIR") {
21 PathBuf::from(override_dir)
22 } else {
23 proj.cache_dir().to_path_buf()
24 };
25
26 let db = if let Some(p) = db_override {
27 validate_path(p)?;
28 PathBuf::from(p)
29 } else if let Ok(env_path) = std::env::var("SQLITE_GRAPHRAG_DB_PATH") {
30 validate_path(&env_path)?;
31 PathBuf::from(env_path)
32 } else {
33 std::env::current_dir()
34 .map_err(AppError::Io)?
35 .join("graphrag.sqlite")
36 };
37
38 Ok(Self {
39 db,
40 models: cache_root.join("models"),
41 })
42 }
43
44 pub fn ensure_dirs(&self) -> Result<(), AppError> {
45 for dir in [parent_or_err(&self.db)?, self.models.as_path()] {
46 std::fs::create_dir_all(dir)?;
47 }
48 Ok(())
49 }
50}
51
52fn validate_path(p: &str) -> Result<(), AppError> {
53 if p.contains("..") {
54 return Err(AppError::Validation(validacao::path_traversal(p)));
55 }
56 Ok(())
57}
58
59pub(crate) fn parent_or_err(path: &Path) -> Result<&Path, AppError> {
60 path.parent().ok_or_else(|| {
61 AppError::Validation(format!(
62 "caminho '{}' não possui componente pai válido",
63 path.display()
64 ))
65 })
66}
67
68#[cfg(test)]
69mod testes {
70 use super::*;
71
72 #[test]
73 fn parent_or_err_aceita_path_normal() {
74 let p = PathBuf::from("/home/usuario/db.sqlite");
75 let pai = parent_or_err(&p).expect("parent valido");
76 assert_eq!(pai, Path::new("/home/usuario"));
77 }
78
79 #[test]
80 fn parent_or_err_aceita_path_relativo() {
81 let p = PathBuf::from("subpasta/arquivo.sqlite");
82 let pai = parent_or_err(&p).expect("parent relativo");
83 assert_eq!(pai, Path::new("subpasta"));
84 }
85
86 #[test]
87 fn parent_or_err_rejeita_raiz_unix() {
88 let p = PathBuf::from("/");
89 let resultado = parent_or_err(&p);
90 assert!(matches!(resultado, Err(AppError::Validation(_))));
91 }
92
93 #[test]
94 fn parent_or_err_rejeita_path_vazio() {
95 let p = PathBuf::from("");
96 let resultado = parent_or_err(&p);
97 assert!(matches!(resultado, Err(AppError::Validation(_))));
98 }
99}