|
|
|
@@ -1,127 +1,625 @@
|
|
|
|
|
use serde::Deserialize;
|
|
|
|
|
use std::{env, fmt};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use std::env;
|
|
|
|
|
use std::path::PathBuf;
|
|
|
|
|
use toml::Value;
|
|
|
|
|
use tracing::{error, info};
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
#[serde(default)]
|
|
|
|
|
pub struct ConfigFile {
|
|
|
|
|
pub server: Server,
|
|
|
|
|
pub display: ConfDisplay,
|
|
|
|
|
pub analytics: Analytics,
|
|
|
|
|
pub filtering: Filtering,
|
|
|
|
|
pub sharing: Sharing,
|
|
|
|
|
pub ai: Ai,
|
|
|
|
|
pub scraping: Scraping,
|
|
|
|
|
pub processing: Processing,
|
|
|
|
|
pub migration: ConfMigration,
|
|
|
|
|
pub cli: Cli,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum DefaultView {
|
|
|
|
|
Compact,
|
|
|
|
|
Full,
|
|
|
|
|
Summary,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum Theme {
|
|
|
|
|
Light,
|
|
|
|
|
Dark,
|
|
|
|
|
Auto,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum DefaultSort {
|
|
|
|
|
#[serde(rename = "added_desc")]
|
|
|
|
|
AddedDesc,
|
|
|
|
|
#[serde(rename = "published_desc")]
|
|
|
|
|
PublishedDesc,
|
|
|
|
|
#[serde(rename = "title_asc")]
|
|
|
|
|
TitleAsc,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum DefaultFormat {
|
|
|
|
|
#[serde(rename = "text")]
|
|
|
|
|
Text,
|
|
|
|
|
#[serde(rename = "markdown")]
|
|
|
|
|
Markdown,
|
|
|
|
|
#[serde(rename = "json")]
|
|
|
|
|
JSON,
|
|
|
|
|
#[serde(rename = "html")]
|
|
|
|
|
HTML,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum AiProvider {
|
|
|
|
|
#[serde(rename = "ollama")]
|
|
|
|
|
Ollama,
|
|
|
|
|
#[serde(rename = "openai")]
|
|
|
|
|
OpenAi,
|
|
|
|
|
#[serde(rename = "anthropic")]
|
|
|
|
|
Anthropic,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
|
|
|
|
|
pub enum DefaultOutput {
|
|
|
|
|
#[serde(rename = "table")]
|
|
|
|
|
Table,
|
|
|
|
|
#[serde(rename = "json")]
|
|
|
|
|
JSON,
|
|
|
|
|
#[serde(rename = "csv")]
|
|
|
|
|
CSV,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct Server {
|
|
|
|
|
pub host: String,
|
|
|
|
|
pub port: u16,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct ConfDisplay {
|
|
|
|
|
pub default_view: DefaultView,
|
|
|
|
|
pub articles_per_page: u32,
|
|
|
|
|
pub show_reading_time: bool,
|
|
|
|
|
pub show_word_count: bool,
|
|
|
|
|
pub highlight_unread: bool,
|
|
|
|
|
pub theme: Theme,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct Analytics {
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
pub track_reading_time: bool,
|
|
|
|
|
pub track_scroll_position: bool,
|
|
|
|
|
pub retention_days: u32,
|
|
|
|
|
pub aggregate_older_data: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct Filtering {
|
|
|
|
|
pub enable_smart_suggestions: bool,
|
|
|
|
|
pub max_recent_filters: u32,
|
|
|
|
|
pub auto_save_filters: bool,
|
|
|
|
|
pub default_sort: DefaultSort,
|
|
|
|
|
pub enable_geographic_hierarchy: bool,
|
|
|
|
|
pub auto_migrate_country_filters: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct Sharing {
|
|
|
|
|
pub default_format: DefaultFormat,
|
|
|
|
|
pub include_summary: bool,
|
|
|
|
|
pub include_tags: bool,
|
|
|
|
|
pub include_source: bool,
|
|
|
|
|
pub copy_to_clipboard: bool,
|
|
|
|
|
pub templates: SharingTemplates,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct SharingTemplates {
|
|
|
|
|
pub text: SharingTemplate,
|
|
|
|
|
pub markdown: SharingTemplate,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct SharingTemplate {
|
|
|
|
|
pub format: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct Ai {
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
pub provider: AiProvider,
|
|
|
|
|
pub timeout_seconds: u32,
|
|
|
|
|
pub summary: AiSummary,
|
|
|
|
|
pub tagging: AiTagging,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct AiSummary {
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
pub temperature: f32,
|
|
|
|
|
pub max_tokens: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct AiTagging {
|
|
|
|
|
pub enabled: bool,
|
|
|
|
|
pub temperature: f32,
|
|
|
|
|
pub max_tokens: u32,
|
|
|
|
|
pub max_tags_per_article: u32,
|
|
|
|
|
pub min_confidence_threshold: f32,
|
|
|
|
|
pub enable_geographic_tagging: bool,
|
|
|
|
|
pub enable_category_tagging: bool,
|
|
|
|
|
pub geographic_hierarchy_levels: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct Scraping {
|
|
|
|
|
pub timeout_seconds: u32,
|
|
|
|
|
pub max_retries: u32,
|
|
|
|
|
pub max_content_length: u32,
|
|
|
|
|
pub respect_robots_txt: bool,
|
|
|
|
|
pub rate_limit_delay_ms: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct Processing {
|
|
|
|
|
pub batch_size: u32,
|
|
|
|
|
pub max_concurrent: u32,
|
|
|
|
|
pub retry_attempts: u32,
|
|
|
|
|
pub priority_manual: bool,
|
|
|
|
|
pub auto_mark_read_on_view: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct ConfMigration {
|
|
|
|
|
pub auto_convert_country_filters: bool,
|
|
|
|
|
pub preserve_legacy_data: bool,
|
|
|
|
|
pub migration_batch_size: u32,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
|
|
|
|
pub struct Cli {
|
|
|
|
|
pub default_output: DefaultOutput,
|
|
|
|
|
pub pager_command: String,
|
|
|
|
|
pub show_progress: bool,
|
|
|
|
|
pub auto_confirm_bulk: bool,
|
|
|
|
|
pub show_geographic_hierarchy: bool,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DefaultView {
|
|
|
|
|
pub fn display_name(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
DefaultView::Compact => "Compact",
|
|
|
|
|
DefaultView::Full => "Full Article",
|
|
|
|
|
DefaultView::Summary => "Summary",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn from_string(s: &str) -> Option<Self> {
|
|
|
|
|
match s.to_lowercase().as_str() {
|
|
|
|
|
"compact" => Some(DefaultView::Compact),
|
|
|
|
|
"full" => Some(DefaultView::Full),
|
|
|
|
|
"summary" => Some(DefaultView::Summary),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for DefaultView {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
DefaultView::Compact
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Theme {
|
|
|
|
|
pub fn display_name(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
Theme::Light => "Light Mode",
|
|
|
|
|
Theme::Dark => "Dark Mode",
|
|
|
|
|
Theme::Auto => "Auto (System)",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn is_dark_mode(&self, system_prefers_dark: bool) -> bool {
|
|
|
|
|
match self {
|
|
|
|
|
Theme::Light => false,
|
|
|
|
|
Theme::Dark => true,
|
|
|
|
|
Theme::Auto => system_prefers_dark,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Theme {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Theme::Auto
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DefaultSort {
|
|
|
|
|
pub fn display_name(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
DefaultSort::AddedDesc => "Recently Added",
|
|
|
|
|
DefaultSort::PublishedDesc => "Recently Published",
|
|
|
|
|
DefaultSort::TitleAsc => "Title A-Z",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn to_sql_order(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
DefaultSort::AddedDesc => "added_at DESC",
|
|
|
|
|
DefaultSort::PublishedDesc => "published_at DESC",
|
|
|
|
|
DefaultSort::TitleAsc => "title ASC",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for DefaultSort {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
DefaultSort::AddedDesc
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DefaultFormat {
|
|
|
|
|
pub fn display_name(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
DefaultFormat::Text => "Plain Text",
|
|
|
|
|
DefaultFormat::Markdown => "Markdown",
|
|
|
|
|
DefaultFormat::JSON => "JSON",
|
|
|
|
|
DefaultFormat::HTML => "HTML",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn mime_type(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
DefaultFormat::Text => "text/plain",
|
|
|
|
|
DefaultFormat::Markdown => "text/markdown",
|
|
|
|
|
DefaultFormat::JSON => "application/json",
|
|
|
|
|
DefaultFormat::HTML => "text/html",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for DefaultFormat {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
DefaultFormat::Text
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl AiProvider {
|
|
|
|
|
pub fn display_name(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
AiProvider::Ollama => "Ollama",
|
|
|
|
|
AiProvider::OpenAi => "OpenAI",
|
|
|
|
|
AiProvider::Anthropic => "Anthropic",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for AiProvider {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
AiProvider::Ollama
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl DefaultOutput {
|
|
|
|
|
pub fn display_name(&self) -> &str {
|
|
|
|
|
match self {
|
|
|
|
|
DefaultOutput::Table => "Table",
|
|
|
|
|
DefaultOutput::JSON => "JSON",
|
|
|
|
|
DefaultOutput::CSV => "CSV",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for DefaultOutput {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
DefaultOutput::Table
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Cli {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
default_output: DefaultOutput::Table,
|
|
|
|
|
pager_command: "less -R".to_string(),
|
|
|
|
|
show_progress: true,
|
|
|
|
|
auto_confirm_bulk: false,
|
|
|
|
|
show_geographic_hierarchy: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ConfMigration {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
auto_convert_country_filters: true,
|
|
|
|
|
preserve_legacy_data: true,
|
|
|
|
|
migration_batch_size: 100,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Processing {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
batch_size: 10,
|
|
|
|
|
max_concurrent: 5,
|
|
|
|
|
retry_attempts: 3,
|
|
|
|
|
priority_manual: true,
|
|
|
|
|
auto_mark_read_on_view: false,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Scraping {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
timeout_seconds: 30,
|
|
|
|
|
max_retries: 3,
|
|
|
|
|
max_content_length: 100_000,
|
|
|
|
|
respect_robots_txt: true,
|
|
|
|
|
rate_limit_delay_ms: 1000,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Ai {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
enabled: true,
|
|
|
|
|
provider: AiProvider::default(),
|
|
|
|
|
timeout_seconds: 120,
|
|
|
|
|
summary: AiSummary::default(),
|
|
|
|
|
tagging: AiTagging::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for AiSummary {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
enabled: true,
|
|
|
|
|
temperature: 0.1,
|
|
|
|
|
max_tokens: 1024,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for AiTagging {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
enabled: true,
|
|
|
|
|
temperature: 0.3,
|
|
|
|
|
max_tokens: 200,
|
|
|
|
|
max_tags_per_article: 10,
|
|
|
|
|
min_confidence_threshold: 0.7,
|
|
|
|
|
enable_geographic_tagging: true,
|
|
|
|
|
enable_category_tagging: true,
|
|
|
|
|
geographic_hierarchy_levels: 3,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Sharing {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
default_format: DefaultFormat::default(),
|
|
|
|
|
include_summary: true,
|
|
|
|
|
include_tags: true,
|
|
|
|
|
include_source: true,
|
|
|
|
|
copy_to_clipboard: true,
|
|
|
|
|
templates: SharingTemplates::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for SharingTemplates {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
text: SharingTemplate {
|
|
|
|
|
format: r#"📰 {title}
|
|
|
|
|
|
|
|
|
|
{summary}
|
|
|
|
|
|
|
|
|
|
🏷️ Tags: {tags}
|
|
|
|
|
🌍 Location: {geographic_tags}
|
|
|
|
|
🔗 Source: {url}
|
|
|
|
|
📅 Published: {published_at}
|
|
|
|
|
|
|
|
|
|
Shared via Owly News Summariser
|
|
|
|
|
"#
|
|
|
|
|
.to_string(),
|
|
|
|
|
},
|
|
|
|
|
markdown: SharingTemplate {
|
|
|
|
|
format: r#"# {title}
|
|
|
|
|
|
|
|
|
|
{summary}
|
|
|
|
|
|
|
|
|
|
**Tags:** {tags}
|
|
|
|
|
**Location:** {geographic_tags}
|
|
|
|
|
**Source:** [{url}]({url})
|
|
|
|
|
**Published:** {published_at}
|
|
|
|
|
|
|
|
|
|
---
|
|
|
|
|
*Shared via Owly News Summariser*
|
|
|
|
|
"#
|
|
|
|
|
.to_string(),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Filtering {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
enable_smart_suggestions: true,
|
|
|
|
|
max_recent_filters: 10,
|
|
|
|
|
auto_save_filters: true,
|
|
|
|
|
default_sort: DefaultSort::AddedDesc,
|
|
|
|
|
enable_geographic_hierarchy: true,
|
|
|
|
|
auto_migrate_country_filters: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Analytics {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
enabled: true,
|
|
|
|
|
track_reading_time: true,
|
|
|
|
|
track_scroll_position: true,
|
|
|
|
|
retention_days: 365,
|
|
|
|
|
aggregate_older_data: true,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for ConfDisplay {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
default_view: DefaultView::default(),
|
|
|
|
|
articles_per_page: 50,
|
|
|
|
|
show_reading_time: true,
|
|
|
|
|
show_word_count: false,
|
|
|
|
|
highlight_unread: true,
|
|
|
|
|
theme: Theme::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for Server {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
host: "127.0.0.1".to_string(),
|
|
|
|
|
port: 8090,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
impl Default for ConfigFile {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
server: Server::default(),
|
|
|
|
|
display: ConfDisplay::default(),
|
|
|
|
|
analytics: Analytics::default(),
|
|
|
|
|
filtering: Filtering::default(),
|
|
|
|
|
sharing: Sharing::default(),
|
|
|
|
|
ai: Ai::default(),
|
|
|
|
|
scraping: Scraping::default(),
|
|
|
|
|
processing: Processing::default(),
|
|
|
|
|
migration: ConfMigration::default(),
|
|
|
|
|
cli: Cli::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ConfigFile {
|
|
|
|
|
pub fn load_from_file(app_settings: &AppSettings) -> Result<Self, Box<dyn std::error::Error>> {
|
|
|
|
|
let config_path_str = &app_settings.config_path;
|
|
|
|
|
let config_path = PathBuf::from(config_path_str);
|
|
|
|
|
|
|
|
|
|
if !config_path.exists() {
|
|
|
|
|
let config_file = ConfigFile::default();
|
|
|
|
|
config_file.save_to_file(app_settings)?;
|
|
|
|
|
return Ok(config_file);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let contents = std::fs::read_to_string(config_path_str)?;
|
|
|
|
|
let config: ConfigFile = toml::from_str(&contents)?;
|
|
|
|
|
Ok(config)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn save_to_file(
|
|
|
|
|
&self,
|
|
|
|
|
app_settings: &AppSettings,
|
|
|
|
|
) -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
let contents = toml::to_string_pretty(self)?;
|
|
|
|
|
std::fs::write(&app_settings.config_path, contents)?;
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
|
pub struct AppSettings {
|
|
|
|
|
pub config_path: String,
|
|
|
|
|
pub db_path: String,
|
|
|
|
|
pub migration_path: String,
|
|
|
|
|
pub config: Config,
|
|
|
|
|
pub config: ConfigFile,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
|
pub struct Config {
|
|
|
|
|
pub server: Server,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
|
pub struct Server {
|
|
|
|
|
pub host: String,
|
|
|
|
|
pub port: u16,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize, Debug)]
|
|
|
|
|
struct ConfigFile {
|
|
|
|
|
server: Server,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub enum ConfigError {
|
|
|
|
|
InvalidPort,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl fmt::Display for ConfigError {
|
|
|
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
|
|
|
match self {
|
|
|
|
|
ConfigError::InvalidPort => write!(f, "Invalid port: port cannot be 0"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl std::error::Error for ConfigError {}
|
|
|
|
|
|
|
|
|
|
impl AppSettings {
|
|
|
|
|
pub fn validate(&self) -> Result<(), ConfigError> {
|
|
|
|
|
if self.config.server.port == 0 {
|
|
|
|
|
return Err(ConfigError::InvalidPort);
|
|
|
|
|
}
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn get_app_settings() -> Self {
|
|
|
|
|
let config_file = Self::load_config_file().unwrap_or_else(|| {
|
|
|
|
|
info!("Using default config values");
|
|
|
|
|
ConfigFile {
|
|
|
|
|
server: Server {
|
|
|
|
|
host: "127.0.0.1".to_string(),
|
|
|
|
|
port: 1337,
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
impl Default for AppSettings {
|
|
|
|
|
fn default() -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
config_path: Self::get_config_path(),
|
|
|
|
|
db_path: Self::get_db_path(),
|
|
|
|
|
migration_path: String::from("./migrations"),
|
|
|
|
|
config: Config {
|
|
|
|
|
server: config_file.server,
|
|
|
|
|
},
|
|
|
|
|
migration_path: Self::get_migration_path(),
|
|
|
|
|
config: ConfigFile::default(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn load_config_file() -> Option<ConfigFile> {
|
|
|
|
|
let config_path = Self::get_config_path();
|
|
|
|
|
let contents = std::fs::read_to_string(&config_path)
|
|
|
|
|
.map_err(|e| error!("Failed to read config file: {}", e))
|
|
|
|
|
.ok()?;
|
|
|
|
|
impl AppSettings {
|
|
|
|
|
const FALLBACK_DIR: &'static str = "/tmp";
|
|
|
|
|
const PROD_DB_PATH: &'static str = "/var/lib/owly-news/owlynews.sqlite3";
|
|
|
|
|
const PROD_MIGRATION_PATH: &'static str = "/usr/share/owly-news/migrations";
|
|
|
|
|
|
|
|
|
|
toml::from_str(&contents)
|
|
|
|
|
.map_err(|e| error!("Failed to parse TOML: {}", e))
|
|
|
|
|
.ok()
|
|
|
|
|
pub fn get_app_settings() -> Self {
|
|
|
|
|
AppSettings::default()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_base_path(env_var: &str, fallback: &str) -> String {
|
|
|
|
|
env::var(env_var).unwrap_or_else(|_| fallback.to_string())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_db_path() -> String {
|
|
|
|
|
if cfg!(debug_assertions) {
|
|
|
|
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
|
path.push("owlynews.sqlite3");
|
|
|
|
|
path.to_str()
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Failed to convert path to string"))
|
|
|
|
|
.ok()
|
|
|
|
|
.expect("Failed to convert path to string")
|
|
|
|
|
.to_string()
|
|
|
|
|
let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR);
|
|
|
|
|
format!("{}/owlynews.sqlite3", manifest_dir)
|
|
|
|
|
} else {
|
|
|
|
|
// Production: Use standard Linux applications data directory
|
|
|
|
|
"/var/lib/owly-news/owlynews.sqlite3".to_string()
|
|
|
|
|
Self::PROD_DB_PATH.to_string()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_config_path() -> String {
|
|
|
|
|
if cfg!(debug_assertions) {
|
|
|
|
|
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
|
|
|
path.push("config.toml");
|
|
|
|
|
path.to_str()
|
|
|
|
|
.ok_or_else(|| anyhow::anyhow!("Failed to convert path to string"))
|
|
|
|
|
.ok()
|
|
|
|
|
.expect("Failed to convert path to string")
|
|
|
|
|
.to_string()
|
|
|
|
|
let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR);
|
|
|
|
|
format!("{}/config.toml", manifest_dir)
|
|
|
|
|
} else {
|
|
|
|
|
// Production: Use standard Linux applications data directory
|
|
|
|
|
let home = env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
|
|
|
|
let home = Self::get_base_path("HOME", Self::FALLBACK_DIR);
|
|
|
|
|
format!("{}/.config/owly-news/config.toml", home)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn get_migration_path() -> String {
|
|
|
|
|
if cfg!(debug_assertions) {
|
|
|
|
|
let manifest_dir = Self::get_base_path("CARGO_MANIFEST_DIR", Self::FALLBACK_DIR);
|
|
|
|
|
format!("{}/migrations", manifest_dir)
|
|
|
|
|
} else {
|
|
|
|
|
Self::PROD_MIGRATION_PATH.to_string()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn database_url(&self) -> String {
|
|
|
|
|
format!("sqlite:{}", self.db_path)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn ensure_db_directory(&self) -> Result<(), std::io::Error> {
|
|
|
|
|
pub fn ensure_default_directory(&self) -> Result<(), std::io::Error> {
|
|
|
|
|
// Always create the database directory
|
|
|
|
|
if let Some(parent) = std::path::Path::new(&self.db_path).parent() {
|
|
|
|
|
std::fs::create_dir_all(parent)?;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// In production mode, also create the config directory
|
|
|
|
|
if !cfg!(debug_assertions) {
|
|
|
|
|
if let Some(parent) = std::path::Path::new(&self.config_path).parent() {
|
|
|
|
|
std::fs::create_dir_all(parent)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|