pinakes-core: add configurable rate limits and cors; add webhook dispatcher; bound job history
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ib0d34cd7878eb9e8d019497234a092466a6a6964
This commit is contained in:
parent
d5be5026a7
commit
672e11b592
4 changed files with 582 additions and 106 deletions
|
|
@ -91,6 +91,8 @@ pub struct Config {
|
|||
#[serde(default)]
|
||||
pub accounts: AccountsConfig,
|
||||
#[serde(default)]
|
||||
pub rate_limits: RateLimitConfig,
|
||||
#[serde(default)]
|
||||
pub jobs: JobsConfig,
|
||||
#[serde(default)]
|
||||
pub thumbnails: ThumbnailConfig,
|
||||
|
|
@ -129,25 +131,147 @@ pub struct ScheduledTaskConfig {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobsConfig {
|
||||
#[serde(default = "default_worker_count")]
|
||||
pub worker_count: usize,
|
||||
#[serde(default = "default_cache_ttl")]
|
||||
pub cache_ttl_secs: u64,
|
||||
pub struct RateLimitConfig {
|
||||
/// Global rate limit: requests per second (token replenish interval).
|
||||
/// Default: 1 (combined with `burst_size=100` gives ~100 req/sec)
|
||||
#[serde(default = "default_global_per_second")]
|
||||
pub global_per_second: u64,
|
||||
/// Global rate limit: burst size (max concurrent requests per IP)
|
||||
#[serde(default = "default_global_burst")]
|
||||
pub global_burst_size: u32,
|
||||
/// Login rate limit: seconds between token replenishment.
|
||||
/// Default: 12 (one token every 12s, combined with burst=5 gives ~5 req/min)
|
||||
#[serde(default = "default_login_per_second")]
|
||||
pub login_per_second: u64,
|
||||
/// Login rate limit: burst size
|
||||
#[serde(default = "default_login_burst")]
|
||||
pub login_burst_size: u32,
|
||||
/// Search rate limit: seconds between token replenishment.
|
||||
/// Default: 6 (one token every 6s, combined with burst=10 gives ~10 req/min)
|
||||
#[serde(default = "default_search_per_second")]
|
||||
pub search_per_second: u64,
|
||||
/// Search rate limit: burst size
|
||||
#[serde(default = "default_search_burst")]
|
||||
pub search_burst_size: u32,
|
||||
/// Streaming rate limit: seconds between token replenishment.
|
||||
/// Default: 60 (one per minute)
|
||||
#[serde(default = "default_stream_per_second")]
|
||||
pub stream_per_second: u64,
|
||||
/// Streaming rate limit: burst size (max concurrent streams)
|
||||
#[serde(default = "default_stream_burst")]
|
||||
pub stream_burst_size: u32,
|
||||
/// Share token rate limit: seconds between token replenishment.
|
||||
/// Default: 2
|
||||
#[serde(default = "default_share_per_second")]
|
||||
pub share_per_second: u64,
|
||||
/// Share token rate limit: burst size
|
||||
#[serde(default = "default_share_burst")]
|
||||
pub share_burst_size: u32,
|
||||
}
|
||||
|
||||
fn default_worker_count() -> usize {
|
||||
const fn default_global_per_second() -> u64 {
|
||||
1
|
||||
}
|
||||
const fn default_global_burst() -> u32 {
|
||||
100
|
||||
}
|
||||
const fn default_login_per_second() -> u64 {
|
||||
12
|
||||
}
|
||||
const fn default_login_burst() -> u32 {
|
||||
5
|
||||
}
|
||||
const fn default_search_per_second() -> u64 {
|
||||
6
|
||||
}
|
||||
const fn default_search_burst() -> u32 {
|
||||
10
|
||||
}
|
||||
const fn default_stream_per_second() -> u64 {
|
||||
60
|
||||
}
|
||||
const fn default_stream_burst() -> u32 {
|
||||
5
|
||||
}
|
||||
const fn default_share_per_second() -> u64 {
|
||||
2
|
||||
}
|
||||
fn default_cache_ttl() -> u64 {
|
||||
const fn default_share_burst() -> u32 {
|
||||
20
|
||||
}
|
||||
|
||||
impl Default for RateLimitConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
global_per_second: default_global_per_second(),
|
||||
global_burst_size: default_global_burst(),
|
||||
login_per_second: default_login_per_second(),
|
||||
login_burst_size: default_login_burst(),
|
||||
search_per_second: default_search_per_second(),
|
||||
search_burst_size: default_search_burst(),
|
||||
stream_per_second: default_stream_per_second(),
|
||||
stream_burst_size: default_stream_burst(),
|
||||
share_per_second: default_share_per_second(),
|
||||
share_burst_size: default_share_burst(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RateLimitConfig {
|
||||
/// Validate that all rate limit values are positive.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error string if any rate limit value is zero.
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
for (name, value) in [
|
||||
("global_per_second", self.global_per_second),
|
||||
("global_burst_size", u64::from(self.global_burst_size)),
|
||||
("login_per_second", self.login_per_second),
|
||||
("login_burst_size", u64::from(self.login_burst_size)),
|
||||
("search_per_second", self.search_per_second),
|
||||
("search_burst_size", u64::from(self.search_burst_size)),
|
||||
("stream_per_second", self.stream_per_second),
|
||||
("stream_burst_size", u64::from(self.stream_burst_size)),
|
||||
("share_per_second", self.share_per_second),
|
||||
("share_burst_size", u64::from(self.share_burst_size)),
|
||||
] {
|
||||
if value == 0 {
|
||||
return Err(format!("{name} must be > 0"));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct JobsConfig {
|
||||
#[serde(default = "default_worker_count")]
|
||||
pub worker_count: usize,
|
||||
#[serde(default = "default_cache_ttl")]
|
||||
pub cache_ttl_secs: u64,
|
||||
/// Maximum time a job is allowed to run before being cancelled (in seconds).
|
||||
/// Set to 0 to disable timeout. Default: 3600 (1 hour).
|
||||
#[serde(default = "default_job_timeout")]
|
||||
pub job_timeout_secs: u64,
|
||||
}
|
||||
|
||||
const fn default_worker_count() -> usize {
|
||||
2
|
||||
}
|
||||
const fn default_cache_ttl() -> u64 {
|
||||
60
|
||||
}
|
||||
const fn default_job_timeout() -> u64 {
|
||||
3600
|
||||
}
|
||||
|
||||
impl Default for JobsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
worker_count: default_worker_count(),
|
||||
cache_ttl_secs: default_cache_ttl(),
|
||||
worker_count: default_worker_count(),
|
||||
cache_ttl_secs: default_cache_ttl(),
|
||||
job_timeout_secs: default_job_timeout(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -164,13 +288,13 @@ pub struct ThumbnailConfig {
|
|||
pub video_seek_secs: u32,
|
||||
}
|
||||
|
||||
fn default_thumb_size() -> u32 {
|
||||
const fn default_thumb_size() -> u32 {
|
||||
320
|
||||
}
|
||||
fn default_thumb_quality() -> u8 {
|
||||
const fn default_thumb_quality() -> u8 {
|
||||
80
|
||||
}
|
||||
fn default_video_seek() -> u32 {
|
||||
const fn default_video_seek() -> u32 {
|
||||
2
|
||||
}
|
||||
|
||||
|
|
@ -217,13 +341,13 @@ fn default_theme() -> String {
|
|||
fn default_view() -> String {
|
||||
"library".to_string()
|
||||
}
|
||||
fn default_page_size() -> usize {
|
||||
48
|
||||
const fn default_page_size() -> usize {
|
||||
50
|
||||
}
|
||||
fn default_view_mode() -> String {
|
||||
"grid".to_string()
|
||||
}
|
||||
fn default_true() -> bool {
|
||||
const fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
|
@ -241,12 +365,29 @@ impl Default for UiConfig {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AccountsConfig {
|
||||
#[serde(default)]
|
||||
pub enabled: bool,
|
||||
pub enabled: bool,
|
||||
#[serde(default)]
|
||||
pub users: Vec<UserAccount>,
|
||||
pub users: Vec<UserAccount>,
|
||||
/// Session expiry in hours. Defaults to 24.
|
||||
#[serde(default = "default_session_expiry_hours")]
|
||||
pub session_expiry_hours: u64,
|
||||
}
|
||||
|
||||
const fn default_session_expiry_hours() -> u64 {
|
||||
24
|
||||
}
|
||||
|
||||
impl Default for AccountsConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
users: Vec::new(),
|
||||
session_expiry_hours: default_session_expiry_hours(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -269,15 +410,18 @@ pub enum UserRole {
|
|||
}
|
||||
|
||||
impl UserRole {
|
||||
pub fn can_read(self) -> bool {
|
||||
#[must_use]
|
||||
pub const fn can_read(self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn can_write(self) -> bool {
|
||||
#[must_use]
|
||||
pub const fn can_write(self) -> bool {
|
||||
matches!(self, Self::Admin | Self::Editor)
|
||||
}
|
||||
|
||||
pub fn can_admin(self) -> bool {
|
||||
#[must_use]
|
||||
pub const fn can_admin(self) -> bool {
|
||||
matches!(self, Self::Admin)
|
||||
}
|
||||
}
|
||||
|
|
@ -320,11 +464,11 @@ fn default_plugin_cache_dir() -> PathBuf {
|
|||
Config::default_data_dir().join("plugins").join("cache")
|
||||
}
|
||||
|
||||
fn default_max_concurrent_ops() -> usize {
|
||||
const fn default_max_concurrent_ops() -> usize {
|
||||
4
|
||||
}
|
||||
|
||||
fn default_plugin_timeout() -> u64 {
|
||||
const fn default_plugin_timeout() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
|
|
@ -359,11 +503,11 @@ pub struct TranscodingConfig {
|
|||
pub profiles: Vec<TranscodeProfile>,
|
||||
}
|
||||
|
||||
fn default_cache_ttl_hours() -> u64 {
|
||||
const fn default_cache_ttl_hours() -> u64 {
|
||||
48
|
||||
}
|
||||
|
||||
fn default_max_concurrent_transcodes() -> usize {
|
||||
const fn default_max_concurrent_transcodes() -> usize {
|
||||
2
|
||||
}
|
||||
|
||||
|
|
@ -444,7 +588,7 @@ pub struct CloudConfig {
|
|||
pub accounts: Vec<CloudAccount>,
|
||||
}
|
||||
|
||||
fn default_auto_sync_interval() -> u64 {
|
||||
const fn default_auto_sync_interval() -> u64 {
|
||||
60
|
||||
}
|
||||
|
||||
|
|
@ -493,7 +637,7 @@ pub struct AnalyticsConfig {
|
|||
pub retention_days: u64,
|
||||
}
|
||||
|
||||
fn default_retention_days() -> u64 {
|
||||
const fn default_retention_days() -> u64 {
|
||||
90
|
||||
}
|
||||
|
||||
|
|
@ -507,8 +651,9 @@ impl Default for AnalyticsConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// Feature toggles for photo processing (image analysis features).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PhotoConfig {
|
||||
pub struct PhotoFeatures {
|
||||
/// Generate perceptual hashes for image duplicate detection (CPU-intensive)
|
||||
#[serde(default = "default_true")]
|
||||
pub generate_perceptual_hash: bool,
|
||||
|
|
@ -520,6 +665,23 @@ pub struct PhotoConfig {
|
|||
/// Generate multi-resolution thumbnails (tiny, grid, preview)
|
||||
#[serde(default)]
|
||||
pub multi_resolution_thumbnails: bool,
|
||||
}
|
||||
|
||||
impl Default for PhotoFeatures {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
generate_perceptual_hash: true,
|
||||
auto_tag_from_exif: false,
|
||||
multi_resolution_thumbnails: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PhotoConfig {
|
||||
/// Feature toggles for photo processing
|
||||
#[serde(flatten)]
|
||||
pub features: PhotoFeatures,
|
||||
|
||||
/// Auto-detect photo events/albums based on time and location
|
||||
#[serde(default)]
|
||||
|
|
@ -538,28 +700,46 @@ pub struct PhotoConfig {
|
|||
pub event_max_distance_km: f64,
|
||||
}
|
||||
|
||||
fn default_min_event_photos() -> usize {
|
||||
impl PhotoConfig {
|
||||
/// Returns true if perceptual hashing is enabled.
|
||||
#[must_use]
|
||||
pub const fn generate_perceptual_hash(&self) -> bool {
|
||||
self.features.generate_perceptual_hash
|
||||
}
|
||||
|
||||
/// Returns true if auto-tagging from EXIF is enabled.
|
||||
#[must_use]
|
||||
pub const fn auto_tag_from_exif(&self) -> bool {
|
||||
self.features.auto_tag_from_exif
|
||||
}
|
||||
|
||||
/// Returns true if multi-resolution thumbnails are enabled.
|
||||
#[must_use]
|
||||
pub const fn multi_resolution_thumbnails(&self) -> bool {
|
||||
self.features.multi_resolution_thumbnails
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_min_event_photos() -> usize {
|
||||
5
|
||||
}
|
||||
|
||||
fn default_event_time_gap() -> i64 {
|
||||
const fn default_event_time_gap() -> i64 {
|
||||
2 * 60 * 60 // 2 hours
|
||||
}
|
||||
|
||||
fn default_event_distance() -> f64 {
|
||||
const fn default_event_distance() -> f64 {
|
||||
1.0 // 1 km
|
||||
}
|
||||
|
||||
impl Default for PhotoConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
generate_perceptual_hash: true,
|
||||
auto_tag_from_exif: false,
|
||||
multi_resolution_thumbnails: false,
|
||||
enable_event_detection: false,
|
||||
min_event_photos: default_min_event_photos(),
|
||||
event_time_gap_secs: default_event_time_gap(),
|
||||
event_max_distance_km: default_event_distance(),
|
||||
features: PhotoFeatures::default(),
|
||||
enable_event_detection: false,
|
||||
min_event_photos: default_min_event_photos(),
|
||||
event_time_gap_secs: default_event_time_gap(),
|
||||
event_max_distance_km: default_event_distance(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -590,7 +770,7 @@ fn default_managed_storage_dir() -> PathBuf {
|
|||
Config::default_data_dir().join("managed")
|
||||
}
|
||||
|
||||
fn default_max_upload_size() -> u64 {
|
||||
const fn default_max_upload_size() -> u64 {
|
||||
10 * 1024 * 1024 * 1024 // 10GB
|
||||
}
|
||||
|
||||
|
|
@ -647,23 +827,23 @@ pub struct SyncConfig {
|
|||
pub temp_upload_dir: PathBuf,
|
||||
}
|
||||
|
||||
fn default_max_sync_file_size() -> u64 {
|
||||
const fn default_max_sync_file_size() -> u64 {
|
||||
4096 // 4GB
|
||||
}
|
||||
|
||||
fn default_chunk_size() -> u64 {
|
||||
const fn default_chunk_size() -> u64 {
|
||||
4096 // 4MB
|
||||
}
|
||||
|
||||
fn default_upload_timeout() -> u64 {
|
||||
const fn default_upload_timeout() -> u64 {
|
||||
24 // 24 hours
|
||||
}
|
||||
|
||||
fn default_max_concurrent_uploads() -> usize {
|
||||
const fn default_max_concurrent_uploads() -> usize {
|
||||
3
|
||||
}
|
||||
|
||||
fn default_sync_log_retention() -> u64 {
|
||||
const fn default_sync_log_retention() -> u64 {
|
||||
90 // 90 days
|
||||
}
|
||||
|
||||
|
|
@ -686,26 +866,44 @@ impl Default for SyncConfig {
|
|||
}
|
||||
}
|
||||
|
||||
/// Core permission flags for the sharing subsystem.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SharingConfig {
|
||||
pub struct SharingPermissions {
|
||||
/// Enable sharing functionality
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub enabled: bool,
|
||||
/// Allow creating public share links
|
||||
#[serde(default = "default_true")]
|
||||
pub allow_public_links: bool,
|
||||
pub allow_public_links: bool,
|
||||
/// Allow users to reshare content shared with them
|
||||
#[serde(default = "default_true")]
|
||||
pub allow_reshare: bool,
|
||||
}
|
||||
|
||||
impl Default for SharingPermissions {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
allow_public_links: true,
|
||||
allow_reshare: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SharingConfig {
|
||||
/// Core permission flags for sharing
|
||||
#[serde(flatten)]
|
||||
pub permissions: SharingPermissions,
|
||||
/// Require password for public share links
|
||||
#[serde(default)]
|
||||
pub require_public_link_password: bool,
|
||||
/// Maximum expiry time for public links in hours (0 = unlimited)
|
||||
#[serde(default)]
|
||||
pub max_public_link_expiry_hours: u64,
|
||||
/// Allow users to reshare content shared with them
|
||||
#[serde(default = "default_true")]
|
||||
pub allow_reshare: bool,
|
||||
/// Enable share notifications
|
||||
#[serde(default = "default_true")]
|
||||
pub notifications_enabled: bool,
|
||||
/// Maximum expiry time for public links in hours (0 = unlimited)
|
||||
#[serde(default)]
|
||||
pub max_public_link_expiry_hours: u64,
|
||||
/// Notification retention in days
|
||||
#[serde(default = "default_notification_retention")]
|
||||
pub notification_retention_days: u64,
|
||||
|
|
@ -714,23 +912,41 @@ pub struct SharingConfig {
|
|||
pub activity_retention_days: u64,
|
||||
}
|
||||
|
||||
fn default_notification_retention() -> u64 {
|
||||
impl SharingConfig {
|
||||
/// Returns true if sharing is enabled.
|
||||
#[must_use]
|
||||
pub const fn enabled(&self) -> bool {
|
||||
self.permissions.enabled
|
||||
}
|
||||
|
||||
/// Returns true if public links are allowed.
|
||||
#[must_use]
|
||||
pub const fn allow_public_links(&self) -> bool {
|
||||
self.permissions.allow_public_links
|
||||
}
|
||||
|
||||
/// Returns true if resharing is allowed.
|
||||
#[must_use]
|
||||
pub const fn allow_reshare(&self) -> bool {
|
||||
self.permissions.allow_reshare
|
||||
}
|
||||
}
|
||||
|
||||
const fn default_notification_retention() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
fn default_activity_retention() -> u64 {
|
||||
const fn default_activity_retention() -> u64 {
|
||||
90
|
||||
}
|
||||
|
||||
impl Default for SharingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: true,
|
||||
allow_public_links: true,
|
||||
permissions: SharingPermissions::default(),
|
||||
require_public_link_password: false,
|
||||
max_public_link_expiry_hours: 0,
|
||||
allow_reshare: true,
|
||||
notifications_enabled: true,
|
||||
max_public_link_expiry_hours: 0,
|
||||
notification_retention_days: default_notification_retention(),
|
||||
activity_retention_days: default_activity_retention(),
|
||||
}
|
||||
|
|
@ -747,7 +963,7 @@ pub struct TrashConfig {
|
|||
pub auto_empty: bool,
|
||||
}
|
||||
|
||||
fn default_trash_retention_days() -> u64 {
|
||||
const fn default_trash_retention_days() -> u64 {
|
||||
30
|
||||
}
|
||||
|
||||
|
|
@ -776,7 +992,8 @@ pub enum StorageBackendType {
|
|||
}
|
||||
|
||||
impl StorageBackendType {
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
#[must_use]
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Sqlite => "sqlite",
|
||||
Self::Postgres => "postgres",
|
||||
|
|
@ -803,7 +1020,7 @@ pub struct PostgresConfig {
|
|||
pub username: String,
|
||||
pub password: String,
|
||||
pub max_connections: usize,
|
||||
/// Enable TLS for PostgreSQL connections
|
||||
/// Enable TLS for `PostgreSQL` connections
|
||||
#[serde(default)]
|
||||
pub tls_enabled: bool,
|
||||
/// Verify TLS certificates (default: true)
|
||||
|
|
@ -828,7 +1045,7 @@ pub struct ScanningConfig {
|
|||
pub import_concurrency: usize,
|
||||
}
|
||||
|
||||
fn default_import_concurrency() -> usize {
|
||||
const fn default_import_concurrency() -> usize {
|
||||
8
|
||||
}
|
||||
|
||||
|
|
@ -842,10 +1059,18 @@ pub struct ServerConfig {
|
|||
pub api_key: Option<String>,
|
||||
/// Explicitly disable authentication (INSECURE - use only for development).
|
||||
/// When true, all requests are allowed without authentication.
|
||||
/// This must be explicitly set to true; empty api_key alone is not
|
||||
/// This must be explicitly set to true; empty `api_key` alone is not
|
||||
/// sufficient.
|
||||
#[serde(default)]
|
||||
pub authentication_disabled: bool,
|
||||
/// Enable CORS (Cross-Origin Resource Sharing).
|
||||
/// When false, default localhost origins are used.
|
||||
#[serde(default)]
|
||||
pub cors_enabled: bool,
|
||||
/// Allowed CORS origins when `cors_enabled` is true.
|
||||
/// If empty and `cors_enabled` is true, defaults to localhost origins.
|
||||
#[serde(default)]
|
||||
pub cors_origins: Vec<String>,
|
||||
/// TLS/HTTPS configuration
|
||||
#[serde(default)]
|
||||
pub tls: TlsConfig,
|
||||
|
|
@ -863,7 +1088,7 @@ pub struct TlsConfig {
|
|||
/// Path to the TLS private key file (PEM format)
|
||||
#[serde(default)]
|
||||
pub key_path: Option<PathBuf>,
|
||||
/// Enable HTTP to HTTPS redirect (starts a second listener on http_port)
|
||||
/// Enable HTTP to HTTPS redirect (starts a second listener on `http_port`)
|
||||
#[serde(default)]
|
||||
pub redirect_http: bool,
|
||||
/// Port for HTTP redirect listener (default: 80)
|
||||
|
|
@ -877,12 +1102,12 @@ pub struct TlsConfig {
|
|||
pub hsts_max_age: u64,
|
||||
}
|
||||
|
||||
fn default_http_port() -> u16 {
|
||||
const fn default_http_port() -> u16 {
|
||||
80
|
||||
}
|
||||
|
||||
fn default_hsts_max_age() -> u64 {
|
||||
31536000 // 1 year in seconds
|
||||
const fn default_hsts_max_age() -> u64 {
|
||||
31_536_000 // 1 year in seconds
|
||||
}
|
||||
|
||||
impl Default for TlsConfig {
|
||||
|
|
@ -901,6 +1126,11 @@ impl Default for TlsConfig {
|
|||
|
||||
impl TlsConfig {
|
||||
/// Validate TLS configuration
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error string if TLS is enabled but required paths are missing
|
||||
/// or invalid.
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.enabled {
|
||||
if self.cert_path.is_none() {
|
||||
|
|
@ -928,6 +1158,13 @@ impl TlsConfig {
|
|||
}
|
||||
|
||||
impl Config {
|
||||
/// Load configuration from a TOML file, expanding environment variables in
|
||||
/// secret fields.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::error::PinakesError`] if the file cannot be read, parsed,
|
||||
/// or contains invalid environment variable references.
|
||||
pub fn from_file(path: &Path) -> crate::error::Result<Self> {
|
||||
let content = std::fs::read_to_string(path).map_err(|e| {
|
||||
crate::error::PinakesError::Config(format!(
|
||||
|
|
@ -942,7 +1179,7 @@ impl Config {
|
|||
}
|
||||
|
||||
/// Expand environment variables in secret fields.
|
||||
/// Supports ${VAR_NAME} and $VAR_NAME syntax.
|
||||
/// Supports ${`VAR_NAME`} and $`VAR_NAME` syntax.
|
||||
fn expand_env_vars(&mut self) -> crate::error::Result<()> {
|
||||
// Postgres password
|
||||
if let Some(ref mut postgres) = self.storage.postgres {
|
||||
|
|
@ -979,6 +1216,11 @@ impl Config {
|
|||
}
|
||||
|
||||
/// Try loading from file, falling back to defaults if the file doesn't exist.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::error::PinakesError`] if the file exists but cannot be
|
||||
/// read or parsed.
|
||||
pub fn load_or_default(path: &Path) -> crate::error::Result<Self> {
|
||||
if path.exists() {
|
||||
Self::from_file(path)
|
||||
|
|
@ -991,6 +1233,11 @@ impl Config {
|
|||
}
|
||||
|
||||
/// Save the current config to a TOML file.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::error::PinakesError`] if the file cannot be written or
|
||||
/// the config cannot be serialized.
|
||||
pub fn save_to_file(&self, path: &Path) -> crate::error::Result<()> {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)?;
|
||||
|
|
@ -1005,6 +1252,11 @@ impl Config {
|
|||
}
|
||||
|
||||
/// Ensure all directories needed by this config exist and are writable.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns [`crate::error::PinakesError`] if a required directory cannot be
|
||||
/// created or is read-only.
|
||||
pub fn ensure_dirs(&self) -> crate::error::Result<()> {
|
||||
if let Some(ref sqlite) = self.storage.sqlite
|
||||
&& let Some(parent) = sqlite.path.parent()
|
||||
|
|
@ -1026,20 +1278,29 @@ impl Config {
|
|||
}
|
||||
|
||||
/// Returns the default config file path following XDG conventions.
|
||||
#[must_use]
|
||||
pub fn default_config_path() -> PathBuf {
|
||||
if let Ok(xdg) = std::env::var("XDG_CONFIG_HOME") {
|
||||
PathBuf::from(xdg).join("pinakes").join("pinakes.toml")
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("pinakes")
|
||||
.join("pinakes.toml")
|
||||
} else {
|
||||
PathBuf::from("pinakes.toml")
|
||||
}
|
||||
std::env::var("XDG_CONFIG_HOME").map_or_else(
|
||||
|_| {
|
||||
std::env::var("HOME").map_or_else(
|
||||
|_| PathBuf::from("pinakes.toml"),
|
||||
|home| {
|
||||
PathBuf::from(home)
|
||||
.join(".config")
|
||||
.join("pinakes")
|
||||
.join("pinakes.toml")
|
||||
},
|
||||
)
|
||||
},
|
||||
|xdg| PathBuf::from(xdg).join("pinakes").join("pinakes.toml"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Validate configuration values for correctness.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error string if any configuration value is invalid.
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.server.port == 0 {
|
||||
return Err("server port cannot be 0".into());
|
||||
|
|
@ -1098,23 +1359,31 @@ impl Config {
|
|||
);
|
||||
}
|
||||
|
||||
// Validate rate limits
|
||||
self.rate_limits.validate()?;
|
||||
|
||||
// Validate TLS configuration
|
||||
self.server.tls.validate()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Returns the default data directory following XDG conventions.
|
||||
#[must_use]
|
||||
pub fn default_data_dir() -> PathBuf {
|
||||
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
|
||||
PathBuf::from(xdg).join("pinakes")
|
||||
} else if let Ok(home) = std::env::var("HOME") {
|
||||
PathBuf::from(home)
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("pinakes")
|
||||
} else {
|
||||
PathBuf::from("pinakes-data")
|
||||
}
|
||||
std::env::var("XDG_DATA_HOME").map_or_else(
|
||||
|_| {
|
||||
std::env::var("HOME").map_or_else(
|
||||
|_| PathBuf::from("pinakes-data"),
|
||||
|home| {
|
||||
PathBuf::from(home)
|
||||
.join(".local")
|
||||
.join("share")
|
||||
.join("pinakes")
|
||||
},
|
||||
)
|
||||
},
|
||||
|xdg| PathBuf::from(xdg).join("pinakes"),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1146,10 +1415,13 @@ impl Default for Config {
|
|||
port: 3000,
|
||||
api_key: None,
|
||||
authentication_disabled: false,
|
||||
cors_enabled: false,
|
||||
cors_origins: vec![],
|
||||
tls: TlsConfig::default(),
|
||||
},
|
||||
ui: UiConfig::default(),
|
||||
accounts: AccountsConfig::default(),
|
||||
rate_limits: RateLimitConfig::default(),
|
||||
jobs: JobsConfig::default(),
|
||||
thumbnails: ThumbnailConfig::default(),
|
||||
webhooks: vec![],
|
||||
|
|
@ -1228,11 +1500,14 @@ mod tests {
|
|||
vars: &'a std::collections::HashMap<&str, &str>,
|
||||
) -> impl Fn(&str) -> crate::error::Result<String> + 'a {
|
||||
move |name| {
|
||||
vars.get(name).map(|v| v.to_string()).ok_or_else(|| {
|
||||
crate::error::PinakesError::Config(format!(
|
||||
"environment variable not set: {name}"
|
||||
))
|
||||
})
|
||||
vars
|
||||
.get(name)
|
||||
.map(std::string::ToString::to_string)
|
||||
.ok_or_else(|| {
|
||||
crate::error::PinakesError::Config(format!(
|
||||
"environment variable not set: {name}"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue