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    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
41// Everything an interrupted upload left behind, wherever it left it: a staging
42// file on the volume, or bytes under an upload key nobody ever reported. Built
43// from the same construction the server uses, so a bucket deployment does not
44// end up sweeping only half of itself.
45pub 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    // Said out loud because it is on by default and it decides who can read the
59    // objects. An operator upgrading into it should see the line rather than
60    // discover the exposure.
61    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    // Refusing to start beats starting without it. A server that silently wrote
72    // plaintext because a Secret failed to mount is the one failure this feature
73    // must never have: nothing downstream would notice, and the objects written
74    // in the meantime are the ones the operator believed were covered.
75    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    // The two backends are chosen together and the lock policy is applied once,
88    // to both. Deciding it per arm is how `LFSX_LOCK_MAX_AGE` came to be silently
89    // ignored in bucket mode: the arms are far apart, only one of them had it,
90    // and nothing failed.
91    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 bucket = S3Store::new(&S3Config {
106                endpoint: endpoint.clone(),
107                bucket: bucket.clone(),
108                region: region.clone(),
109                access_key: access_key.clone(),
110                secret_key: secret_key.clone(),
111                path_style: *path_style,
112                redirect: *presign,
113                lifetime: std::time::Duration::from_secs(config.action_lifetime.into()),
114            })
115            .expect("the bucket configuration is not usable");
116
117            tracing::warn!(
118                "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"
119            );
120
121            if *presign {
122                tracing::warn!(
123                    "LFSX_S3_PRESIGN=true — downloads are redirected to the bucket, so                      lfsx_downloaded_bytes stops counting them and the bucket serves the ranges"
124                );
125
126                if config.encryption_key_file.is_some() {
127                    tracing::warn!(
128                        "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"
129                    );
130                } else if config.compression.is_some() {
131                    tracing::warn!(
132                        "LFSX_COMPRESSION is set, and objects clients upload straight to the                          bucket arrive uncompressed — only what passes through this server is                          compressed"
133                    );
134                }
135            }
136
137            // The locks go with the objects. Left on the volume they would make
138            // the bucket a half measure: capacity would be shared and the one
139            // piece of state a second replica must agree on would not be.
140            (
141                Store::bucket(bucket.clone(), local),
142                LockStore::bucket(bucket),
143            )
144        }
145    };
146    (store, lock_backend.with_max_age(config.lock_max_age))
147}