1use std::fs;
2use std::path::{Path, PathBuf};
3
4use directories::ProjectDirs;
5use serde::{Deserialize, Serialize};
6
7use crate::errors::{Result, SiteforgeError};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(default)]
11pub struct Config {
17 pub archive_root: PathBuf,
19 pub archive_profile: ArchiveOutputProfile,
21 pub default_concurrency: usize,
23 pub default_delay_ms: u64,
25 pub max_depth: usize,
27 pub max_pages: usize,
29 pub max_page_size_bytes: u64,
31 pub max_asset_size_bytes: u64,
33 pub max_total_archive_size_bytes: u64,
35 pub include_url_patterns: Vec<String>,
37 pub exclude_url_patterns: Vec<String>,
39 pub ocr_enabled: bool,
41 pub ocr_backend_command: String,
43 pub user_agent: String,
45 pub timeout_secs: u64,
47 pub retry_count: usize,
49 pub ai_pack_token_budget: usize,
51 pub render_js: bool,
53 pub render_js_auto: bool,
55 pub browser_command: Option<String>,
57 pub browser_profile: Option<PathBuf>,
59 pub render_wait_ms: u64,
61}
62
63#[derive(Debug, Default, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
64#[serde(rename_all = "kebab-case")]
65pub enum ArchiveOutputProfile {
67 #[default]
69 Full,
70 Agent,
72 Lean,
74}
75
76impl ArchiveOutputProfile {
77 pub fn stores_raw_pages(self) -> bool {
79 matches!(self, Self::Full | Self::Agent)
80 }
81
82 pub fn stores_rendered_pages(self) -> bool {
84 matches!(self, Self::Full | Self::Agent)
85 }
86
87 pub fn downloads_assets(self) -> bool {
89 matches!(self, Self::Full | Self::Agent)
90 }
91
92 pub fn stores_markdown(self) -> bool {
94 matches!(self, Self::Full)
95 }
96
97 pub fn stores_basic_markdown(self) -> bool {
99 true
100 }
101
102 pub fn stores_json(self) -> bool {
104 true
105 }
106
107 pub fn stores_text(self) -> bool {
109 matches!(self, Self::Full)
110 }
111
112 pub fn stores_chunks(self) -> bool {
114 true
115 }
116}
117
118impl Default for Config {
119 fn default() -> Self {
120 Self {
121 archive_root: PathBuf::from("archives"),
122 archive_profile: ArchiveOutputProfile::Full,
123 default_concurrency: 4,
124 default_delay_ms: 250,
125 max_depth: 4,
126 max_pages: 500,
127 max_page_size_bytes: 50 * 1024 * 1024,
128 max_asset_size_bytes: 25 * 1024 * 1024,
129 max_total_archive_size_bytes: 5 * 1024 * 1024 * 1024,
130 include_url_patterns: Vec::new(),
131 exclude_url_patterns: vec![
132 "**/login**".to_string(),
133 "**/logout**".to_string(),
134 "**/signup**".to_string(),
135 "**/cart**".to_string(),
136 ],
137 ocr_enabled: false,
138 ocr_backend_command: "tesseract {input} stdout".to_string(),
139 user_agent: "siteforge/0.1 (+https://github.com/Tknott95/siteforge)".to_string(),
140 timeout_secs: 30,
141 retry_count: 2,
142 ai_pack_token_budget: 120_000,
143 render_js: false,
144 render_js_auto: false,
145 browser_command: None,
146 browser_profile: None,
147 render_wait_ms: 5_000,
148 }
149 }
150}
151
152impl Config {
153 pub fn load(path: Option<&Path>) -> Result<Self> {
158 let path = match path {
159 Some(path) => path.to_path_buf(),
160 None => Self::default_path()?,
161 };
162
163 if !path.exists() {
164 return Ok(Self::default());
165 }
166
167 let raw = fs::read_to_string(&path)?;
168 let mut config: Config = serde_yaml::from_str(&raw)?;
169 if config.default_concurrency == 0 {
170 config.default_concurrency = 1;
171 }
172 Ok(config)
173 }
174
175 pub fn default_path() -> Result<PathBuf> {
177 let dirs = ProjectDirs::from("com", "siteforge", "siteforge")
178 .ok_or(SiteforgeError::ConfigDirUnavailable)?;
179 Ok(dirs.config_dir().join("config.yaml"))
180 }
181
182 pub fn ensure_default_file() -> Result<PathBuf> {
184 let path = Self::default_path()?;
185 if !path.exists() {
186 if let Some(parent) = path.parent() {
187 fs::create_dir_all(parent)?;
188 }
189 fs::write(&path, serde_yaml::to_string(&Self::default())?)?;
190 }
191 Ok(path)
192 }
193
194 pub fn resolved_archive_root(&self) -> Result<PathBuf> {
196 if self.archive_root.is_absolute() {
197 Ok(self.archive_root.clone())
198 } else {
199 Ok(std::env::current_dir()?.join(&self.archive_root))
200 }
201 }
202}