1pub mod auth;
2pub mod config;
3pub mod dashboard;
4pub mod error;
5pub mod locks;
6pub mod metrics;
7pub mod model;
8pub mod namespace;
9pub mod page;
10pub mod range;
11pub mod routes;
12pub mod state;
13pub mod storage;
14pub mod tls;
15
16use std::sync::Arc;
17
18use axum::Router;
19
20use crate::auth::Authorizer;
21use crate::config::Config;
22use crate::locks::LockStore;
23use crate::metrics::Metrics;
24use crate::state::AppState;
25use crate::storage::s3::{Keyspace, S3Config, S3Store};
26use crate::storage::{LocalStore, Store};
27
28pub fn app(config: Config) -> Router {
29 let (store, locks) = backends(&config);
30 let authorizer = Authorizer::new(&config.auth);
31
32 routes::router(Arc::new(AppState {
33 store,
34 locks,
35 config,
36 authorizer,
37 metrics: Metrics::new(),
38 }))
39}
40
41pub async fn reclaim(config: &Config) {
46 let reclaimed = backends(config).0.reclaim(config.staging_max_age).await;
47
48 if reclaimed.files > 0 {
49 tracing::info!(
50 files = reclaimed.files,
51 bytes = reclaimed.bytes,
52 "reclaimed what interrupted uploads left behind"
53 );
54 }
55}
56
57fn backends(config: &Config) -> (Store, LockStore) {
58 if let crate::config::Auth::Forge {
62 anonymous_read: true,
63 ..
64 } = config.auth
65 {
66 tracing::info!(
67 "anonymous read is on: a request with no credentials is resolved against the forge, so objects in a repository the forge serves publicly can be read by anybody. Set LFSX_ANONYMOUS_READ=false to require a token whatever the repository's visibility"
68 );
69 }
70
71 let keys = config.encryption_key_file.as_deref().map(|path| {
76 std::sync::Arc::new(
77 crate::storage::crypt::Keyring::load(path)
78 .expect("the encryption key file is not usable"),
79 )
80 });
81
82 let local = LocalStore::new(config.storage_root.clone())
83 .with_max_object_size(config.max_object_size)
84 .with_compression(config.compression)
85 .with_encryption(keys);
86
87 let (store, lock_backend) = match &config.storage {
92 crate::config::Storage::Local => (
93 Store::local(local),
94 LockStore::local(config.storage_root.clone()),
95 ),
96 crate::config::Storage::Bucket {
97 endpoint,
98 bucket,
99 region,
100 access_key,
101 secret_key,
102 path_style,
103 presign,
104 } => {
105 let keys = Keyspace::new(&S3Config {
110 endpoint: endpoint.clone(),
111 bucket: bucket.clone(),
112 region: region.clone(),
113 access_key: access_key.clone(),
114 secret_key: secret_key.clone(),
115 path_style: *path_style,
116 lifetime: std::time::Duration::from_secs(config.action_lifetime.into()),
117 })
118 .expect("the bucket configuration is not usable");
119
120 tracing::warn!(
121 "objects and locks are stored in a bucket: collection, deduplication, rewriting and verification answer 501, and the lfsx_objects_stored and lfsx_store_bytes gauges are not measured — read capacity from the bucket itself"
122 );
123
124 if *presign {
125 tracing::warn!(
126 "LFSX_S3_PRESIGN=true — downloads are redirected to the bucket, so lfsx_downloaded_bytes stops counting them and the bucket serves the ranges"
127 );
128
129 if config.encryption_key_file.is_some() {
130 tracing::warn!(
131 "LFSX_ENCRYPTION_KEY_FILE is set, so uploads keep coming through this server rather than going straight to the bucket: an object a client writes itself would arrive unencrypted"
132 );
133 } else if config.compression.is_some() {
134 tracing::warn!(
135 "LFSX_COMPRESSION is set, and objects clients upload straight to the bucket arrive uncompressed — only what passes through this server is compressed"
136 );
137 }
138 }
139
140 (
144 Store::bucket(S3Store::new(keys.clone(), *presign), local),
145 LockStore::bucket(keys),
146 )
147 }
148 };
149 (store, lock_backend.with_max_age(config.lock_max_age))
150}