Skip to main content

lfsx_server/
lib.rs

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::{S3Config, S3Store};
26use crate::storage::{LocalStore, Store};
27
28pub fn app(config: Config) -> Router {
29    // Refusing to start beats starting without it. A server that silently wrote
30    // plaintext because a Secret failed to mount is the one failure this feature
31    // must never have: nothing downstream would notice, and the objects written
32    // in the meantime are the ones the operator believed were covered.
33    let keys = config.encryption_key_file.as_deref().map(|path| {
34        std::sync::Arc::new(
35            crate::storage::crypt::Keyring::load(path)
36                .expect("the encryption key file is not usable"),
37        )
38    });
39
40    let local = LocalStore::new(config.storage_root.clone())
41        .with_max_object_size(config.max_object_size)
42        .with_compression(config.compression)
43        .with_encryption(keys);
44
45    // The two backends are chosen together and the lock policy is applied once,
46    // to both. Deciding it per arm is how `LFSX_LOCK_MAX_AGE` came to be silently
47    // ignored in bucket mode: the arms are far apart, only one of them had it,
48    // and nothing failed.
49    let (store, lock_backend) = match &config.storage {
50        crate::config::Storage::Local => (
51            Store::local(local),
52            LockStore::local(config.storage_root.clone()),
53        ),
54        crate::config::Storage::Bucket {
55            endpoint,
56            bucket,
57            region,
58            access_key,
59            secret_key,
60            path_style,
61            presign,
62        } => {
63            let bucket = S3Store::new(&S3Config {
64                endpoint: endpoint.clone(),
65                bucket: bucket.clone(),
66                region: region.clone(),
67                access_key: access_key.clone(),
68                secret_key: secret_key.clone(),
69                path_style: *path_style,
70                redirect: *presign,
71                lifetime: std::time::Duration::from_secs(config.action_lifetime.into()),
72            })
73            .expect("the bucket configuration is not usable");
74
75            tracing::warn!(
76                "objects and locks are stored in a bucket: collection, deduplication, compression                  and verification answer 501, and the lfsx_objects_stored and lfsx_store_bytes                  gauges are not measured — read capacity from the bucket itself"
77            );
78
79            if *presign {
80                tracing::warn!(
81                    "LFSX_S3_PRESIGN=true — downloads are redirected to the bucket, so                      lfsx_downloaded_bytes stops counting them and the bucket serves the ranges"
82                );
83            }
84
85            if config.encryption_key_file.is_some() {
86                tracing::warn!(
87                    "LFSX_ENCRYPTION_KEY_FILE is set and objects are stored in a bucket — the                      bucket holds them as they arrive, because an encrypted object is only                      readable through the local file the codec opens"
88                );
89            }
90
91            if config.compression.is_some() {
92                tracing::warn!(
93                    "LFSX_COMPRESSION is set and objects are stored in a bucket — the bucket                      holds them uncompressed, because a compressed object is only readable                      through the local file the codec opens"
94                );
95            }
96
97            // The locks go with the objects. Left on the volume they would make
98            // the bucket a half measure: capacity would be shared and the one
99            // piece of state a second replica must agree on would not be.
100            (
101                Store::bucket(bucket.clone(), local),
102                LockStore::bucket(bucket),
103            )
104        }
105    };
106    let locks = lock_backend.with_max_age(config.lock_max_age);
107    let authorizer = Authorizer::new(&config.auth);
108
109    routes::router(Arc::new(AppState {
110        store,
111        locks,
112        config,
113        authorizer,
114        metrics: Metrics::new(),
115    }))
116}