1use axum::http::{HeaderMap, header};
2
3use std::net::SocketAddr;
4use std::path::PathBuf;
5use std::time::Duration;
6
7use crate::model::Action;
8use crate::namespace::Namespace;
9
10#[derive(Debug, Clone)]
11pub struct Config {
12 pub bind: SocketAddr,
13 pub storage_root: PathBuf,
14 pub public_url: Option<String>,
15 pub action_lifetime: u32,
16 pub gc_grace: Duration,
17 pub staging_max_age: Duration,
18 pub max_object_size: Option<u64>,
19 pub repo_quota: Option<u64>,
20 pub compression: Option<i32>,
21 pub encryption_key_file: Option<PathBuf>,
25 pub storage: Storage,
26 pub auth: Auth,
27}
28
29#[derive(Debug, Clone)]
30pub enum Storage {
31 Local,
32 Bucket {
37 endpoint: String,
38 bucket: String,
39 region: String,
40 access_key: String,
41 secret_key: String,
42 path_style: bool,
43 presign: bool,
48 },
49}
50
51impl Storage {
52 fn from_env() -> Self {
53 if std::env::var("LFSX_STORAGE").as_deref() != Ok("s3") {
54 return Self::Local;
55 }
56
57 let required = |name: &str| {
58 std::env::var(name)
59 .ok()
60 .filter(|value| !value.is_empty())
61 .unwrap_or_else(|| panic!("LFSX_STORAGE=s3 needs {name}"))
62 };
63
64 Self::Bucket {
65 endpoint: required("LFSX_S3_ENDPOINT"),
66 bucket: required("LFSX_S3_BUCKET"),
67 region: std::env::var("LFSX_S3_REGION").unwrap_or_else(|_| "us-east-1".into()),
68 access_key: required("LFSX_S3_ACCESS_KEY"),
69 secret_key: required("LFSX_S3_SECRET_KEY"),
70 path_style: std::env::var("LFSX_S3_PATH_STYLE").as_deref() != Ok("false"),
71 presign: std::env::var("LFSX_S3_PRESIGN").as_deref() == Ok("true"),
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
77pub enum Auth {
78 Forge {
79 provider: Provider,
80 api_url: String,
81 cache_ttl: Duration,
82 rejection_ttl: Duration,
83 },
84 Disabled,
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
88pub enum Provider {
89 Github,
90 Gitlab,
91}
92
93impl Provider {
94 fn default_api_url(self) -> &'static str {
95 match self {
96 Self::Github => "https://api.github.com",
97 Self::Gitlab => "https://gitlab.com/api/v4",
98 }
99 }
100
101 fn api_url_variable(self) -> &'static str {
102 match self {
103 Self::Github => "LFSX_GITHUB_API_URL",
104 Self::Gitlab => "LFSX_GITLAB_API_URL",
105 }
106 }
107}
108
109const CACHE_TTL: Duration = Duration::from_secs(60);
110const REJECTION_TTL: Duration = Duration::from_secs(10);
111const GC_GRACE: Duration = Duration::from_secs(14 * 24 * 60 * 60);
112const STAGING_MAX_AGE: Duration = Duration::from_secs(24 * 60 * 60);
113
114impl Config {
115 pub fn from_env() -> Self {
116 let bind = std::env::var("LFSX_BIND")
117 .ok()
118 .and_then(|raw| raw.parse().ok())
119 .unwrap_or_else(|| SocketAddr::from(([0, 0, 0, 0], 8080)));
120
121 let storage_root = std::env::var("LFSX_STORAGE_ROOT")
122 .map(PathBuf::from)
123 .unwrap_or_else(|_| PathBuf::from("/var/lib/lfsx"));
124
125 let public_url = std::env::var("LFSX_PUBLIC_URL")
126 .ok()
127 .filter(|url| !url.is_empty())
128 .map(|url| url.trim_end_matches('/').to_owned());
129
130 Self {
131 bind,
132 storage_root,
133 public_url,
134 action_lifetime: 1800,
135 gc_grace: seconds("LFSX_GC_GRACE").unwrap_or(GC_GRACE),
136 staging_max_age: seconds("LFSX_STAGING_MAX_AGE").unwrap_or(STAGING_MAX_AGE),
137 max_object_size: bytes("LFSX_MAX_OBJECT_SIZE"),
138 repo_quota: bytes("LFSX_REPO_QUOTA"),
139 compression: compression(),
140 encryption_key_file: std::env::var("LFSX_ENCRYPTION_KEY_FILE")
141 .ok()
142 .filter(|path| !path.is_empty())
143 .map(PathBuf::from),
144 storage: Storage::from_env(),
145 auth: Auth::from_env(),
146 }
147 }
148
149 pub fn base_url(&self, headers: &HeaderMap) -> String {
150 if let Some(configured) = &self.public_url {
151 return configured.clone();
152 }
153
154 let scheme = headers
155 .get("x-forwarded-proto")
156 .and_then(|value| value.to_str().ok())
157 .and_then(|value| value.split(',').next())
158 .map(str::trim)
159 .filter(|scheme| !scheme.is_empty())
160 .unwrap_or("http");
161
162 let authority = headers
163 .get(header::HOST)
164 .and_then(|value| value.to_str().ok())
165 .map(str::trim)
166 .filter(|host| !host.is_empty())
167 .unwrap_or("localhost");
168
169 format!("{scheme}://{authority}")
170 }
171
172 pub fn object_url(&self, base: &str, ns: &Namespace, oid: &str) -> String {
173 format!("{base}/{ns}/objects/{oid}")
174 }
175
176 pub fn verify_url(&self, base: &str, ns: &Namespace) -> String {
177 format!("{base}/{ns}/objects/verify")
178 }
179
180 pub fn action(&self, href: String) -> Action {
181 Action {
182 href,
183 expires_in: self.action_lifetime,
184 }
185 }
186}
187
188impl Auth {
189 fn from_env() -> Self {
190 if std::env::var("LFSX_AUTH").as_deref() == Ok("disabled") {
191 tracing::warn!(
192 "LFSX_AUTH=disabled — every request is accepted, run this on a trusted network only"
193 );
194 return Self::Disabled;
195 }
196
197 let provider = match std::env::var("LFSX_AUTH").as_deref() {
198 Ok("gitlab") => Provider::Gitlab,
199 _ => Provider::Github,
200 };
201
202 let api_url = std::env::var(provider.api_url_variable())
203 .unwrap_or_else(|_| provider.default_api_url().to_owned())
204 .trim_end_matches('/')
205 .to_owned();
206
207 Self::Forge {
208 provider,
209 api_url,
210 cache_ttl: seconds("LFSX_AUTH_CACHE_TTL").unwrap_or(CACHE_TTL),
211 rejection_ttl: seconds("LFSX_AUTH_REJECTION_TTL").unwrap_or(REJECTION_TTL),
212 }
213 }
214}
215
216fn compression() -> Option<i32> {
225 match std::env::var("LFSX_COMPRESSION").ok()?.trim() {
226 "" | "none" | "off" => None,
227 "zstd" => Some(3),
228 other => match other
229 .strip_prefix("zstd:")
230 .and_then(|level| level.parse().ok())
231 {
232 Some(level @ 1..=19) => Some(level),
233 _ => {
234 tracing::warn!(
235 "LFSX_COMPRESSION={other} is not a codec this server knows — storing objects as they arrive"
236 );
237 None
238 }
239 },
240 }
241}
242
243fn bytes(variable: &str) -> Option<u64> {
244 let configured = std::env::var(variable).ok()?.trim().parse().ok()?;
245
246 if configured == 0 {
247 tracing::warn!("{variable}=0 would refuse every upload — ignoring it");
248 return None;
249 }
250
251 Some(configured)
252}
253
254fn seconds(variable: &str) -> Option<Duration> {
255 std::env::var(variable)
256 .ok()
257 .and_then(|raw| raw.parse().ok())
258 .map(Duration::from_secs)
259}