Skip to main content

ferro_storage/
config.rs

1//! Configuration for the storage system.
2
3use crate::facade::DiskConfig;
4#[cfg(feature = "s3")]
5use crate::facade::DiskDriver;
6use std::collections::HashMap;
7use std::env;
8
9/// Configuration for the storage system.
10#[derive(Debug, Clone)]
11pub struct StorageConfig {
12    /// Default disk name.
13    pub default: String,
14    /// Disk configurations.
15    pub disks: HashMap<String, DiskConfig>,
16}
17
18impl Default for StorageConfig {
19    fn default() -> Self {
20        let mut disks = HashMap::new();
21        disks.insert("local".to_string(), DiskConfig::local("./storage"));
22
23        Self {
24            default: "local".to_string(),
25            disks,
26        }
27    }
28}
29
30impl StorageConfig {
31    /// Create a new storage config with a default disk.
32    pub fn new(default: impl Into<String>) -> Self {
33        Self {
34            default: default.into(),
35            disks: HashMap::new(),
36        }
37    }
38
39    /// Create configuration from environment variables.
40    ///
41    /// Reads the following environment variables:
42    /// - `FILESYSTEM_DISK`: Default disk name (default: "local")
43    /// - `FILESYSTEM_LOCAL_ROOT`: Root path for local disk (default: "./storage")
44    /// - `FILESYSTEM_LOCAL_URL`: Public URL for local files
45    /// - `FILESYSTEM_PUBLIC_ROOT`: Root path for public disk (default: "./storage/public")
46    /// - `FILESYSTEM_PUBLIC_URL`: Public URL for public files (default: "/storage")
47    ///
48    /// With `s3` feature:
49    /// - `AWS_ACCESS_KEY_ID`: S3 access key
50    /// - `AWS_SECRET_ACCESS_KEY`: S3 secret key
51    /// - `AWS_DEFAULT_REGION`: S3 region (default: "us-east-1")
52    /// - `AWS_BUCKET`: S3 bucket name
53    /// - `AWS_PUBLIC_URL`: Public base URL for generated file URLs (overrides `AWS_URL` for this purpose)
54    /// - `AWS_URL`: S3 API endpoint; also used as public URL base if `AWS_PUBLIC_URL` is not set
55    ///
56    /// # Example
57    ///
58    /// ```rust,ignore
59    /// use ferro_storage::{StorageConfig, Storage};
60    ///
61    /// let config = StorageConfig::from_env();
62    /// let storage = Storage::with_storage_config(config);
63    /// ```
64    pub fn from_env() -> Self {
65        let default = env::var("FILESYSTEM_DISK").unwrap_or_else(|_| "local".to_string());
66        let mut disks = HashMap::new();
67
68        // Local disk
69        let local_root =
70            env::var("FILESYSTEM_LOCAL_ROOT").unwrap_or_else(|_| "./storage".to_string());
71        let mut local_config = DiskConfig::local(&local_root);
72        if let Ok(url) = env::var("FILESYSTEM_LOCAL_URL") {
73            local_config = local_config.with_url(url);
74        }
75        disks.insert("local".to_string(), local_config);
76
77        // Public disk (for publicly accessible files)
78        let public_root =
79            env::var("FILESYSTEM_PUBLIC_ROOT").unwrap_or_else(|_| "./storage/public".to_string());
80        let public_url =
81            env::var("FILESYSTEM_PUBLIC_URL").unwrap_or_else(|_| "/storage".to_string());
82        let public_config = DiskConfig::local(&public_root).with_url(public_url);
83        disks.insert("public".to_string(), public_config);
84
85        // S3 disk (if configured)
86        #[cfg(feature = "s3")]
87        if let Ok(bucket) = env::var("AWS_BUCKET") {
88            let region = env::var("AWS_DEFAULT_REGION").unwrap_or_else(|_| "us-east-1".to_string());
89            let mut s3_config = DiskConfig {
90                driver: DiskDriver::S3,
91                root: None,
92                url: None,
93                bucket: Some(bucket.clone()),
94                region: Some(region),
95            };
96            // Resolve public file URL base (used by Storage::url() to build asset URLs).
97            // Priority: AWS_PUBLIC_URL → computed from AWS_URL+bucket → AWS_URL bare.
98            // Auto-compute handles providers like DigitalOcean Spaces and Cloudflare R2
99            // where the public URL is {bucket}.{endpoint_host} but the API endpoint is
100            // just {endpoint_host}.
101            let public_url = if let Ok(explicit) = env::var("AWS_PUBLIC_URL") {
102                Some(explicit)
103            } else if let Ok(api_url) = env::var("AWS_URL") {
104                let host = api_url
105                    .trim_start_matches("https://")
106                    .trim_start_matches("http://");
107                let scheme = if api_url.starts_with("https://") {
108                    "https"
109                } else {
110                    "http"
111                };
112                Some(format!("{scheme}://{bucket}.{host}"))
113            } else {
114                None
115            };
116            s3_config.url = public_url;
117            disks.insert("s3".to_string(), s3_config);
118        }
119
120        Self { default, disks }
121    }
122
123    /// Add a disk configuration.
124    pub fn disk(mut self, name: impl Into<String>, config: DiskConfig) -> Self {
125        self.disks.insert(name.into(), config);
126        self
127    }
128
129    /// Set the default disk.
130    pub fn default_disk(mut self, name: impl Into<String>) -> Self {
131        self.default = name.into();
132        self
133    }
134
135    /// Get the default disk name.
136    pub fn get_default(&self) -> &str {
137        &self.default
138    }
139
140    /// Get a disk configuration by name.
141    pub fn get_disk(&self, name: &str) -> Option<&DiskConfig> {
142        self.disks.get(name)
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn test_storage_config_defaults() {
152        let config = StorageConfig::default();
153        assert_eq!(config.default, "local");
154        assert!(config.disks.contains_key("local"));
155    }
156
157    #[test]
158    fn test_storage_config_builder() {
159        let config = StorageConfig::new("s3")
160            .disk("local", DiskConfig::local("./storage"))
161            .disk("public", DiskConfig::local("./public").with_url("/files"));
162
163        assert_eq!(config.default, "s3");
164        assert!(config.disks.contains_key("local"));
165        assert!(config.disks.contains_key("public"));
166    }
167
168    #[test]
169    fn test_storage_config_from_env() {
170        // Test with default env (no env vars set)
171        let config = StorageConfig::from_env();
172        assert_eq!(config.default, "local");
173        assert!(config.disks.contains_key("local"));
174        assert!(config.disks.contains_key("public"));
175    }
176}