lfsx-server 0.28.0

A fast, lightweight, secure Git LFS server
Documentation
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::Duration;

use axum::body::Bytes;
use futures_util::StreamExt;
use futures_util::stream;

use super::*;

const OID: &str = "5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03";
const CHUNK: usize = 1024;

fn namespace() -> Namespace {
    Namespace::new("FerrLabs", "Blastlands").unwrap()
}

fn body(
    chunks: usize,
) -> (
    impl Stream<Item = Result<Bytes, std::io::Error>> + Unpin,
    Arc<AtomicUsize>,
) {
    let pulled = Arc::new(AtomicUsize::new(0));
    let counted = pulled.clone();

    let stream = stream::iter(0..chunks).map(move |_| {
        counted.fetch_add(1, Ordering::Relaxed);
        Ok(Bytes::from(vec![0u8; CHUNK]))
    });

    (stream, pulled)
}

fn staging_files(root: &std::path::Path) -> Vec<PathBuf> {
    let mut found = Vec::new();
    let mut directories = vec![root.to_path_buf()];

    while let Some(directory) = directories.pop() {
        let Ok(entries) = std::fs::read_dir(&directory) else {
            continue;
        };

        for entry in entries.flatten() {
            let path = entry.path();
            if path.is_dir() {
                directories.push(path);
            } else if path
                .extension()
                .is_some_and(|extension| extension == "part")
            {
                found.push(path);
            }
        }
    }

    found
}

#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn an_upload_survives_a_collection_emptying_its_fanout() {
    let root = tempfile::tempdir().unwrap();
    let store = Arc::new(LocalStore::new(root.path()));
    let ns = namespace();

    let collecting = {
        let store = store.clone();
        let ns = ns.clone();
        tokio::spawn(async move {
            for _ in 0..200 {
                let _ = store
                    .sweep(
                        &ns,
                        &std::collections::HashSet::new(),
                        Duration::ZERO,
                        false,
                    )
                    .await;
                tokio::task::yield_now().await;
            }
        })
    };

    for asset in 0..100u32 {
        let payload = format!("an asset worth pushing {asset}").into_bytes();
        let oid = hex::encode(Sha256::digest(&payload));

        let written = store
            .write(
                &ns,
                &oid,
                None,
                None,
                stream::iter([Ok::<_, std::io::Error>(Bytes::from(payload))]),
            )
            .await;

        assert!(
            written.is_ok(),
            "scheduled collection removes a fanout directory once it has emptied it, and an \
             upload that arrives in that window did nothing wrong: {written:?}"
        );
    }

    collecting.await.unwrap();
}

#[tokio::test]
async fn a_body_that_outgrows_the_limit_is_cut_off_rather_than_written_to_completion() {
    let root = tempfile::tempdir().unwrap();
    let store = LocalStore::new(root.path()).with_max_object_size(Some(4 * CHUNK as u64));
    let (body, pulled) = body(4096);

    let refused = store.write(&namespace(), OID, None, None, body).await;

    assert!(matches!(refused, Err(Error::TooLarge { limit: 4096 })));
    assert_eq!(
        pulled.load(Ordering::Relaxed),
        5,
        "the transfer has to stop at the chunk that crosses the line — reading to the end to \
         find out how big it was is the outage this limit exists to prevent"
    );
    assert!(
        staging_files(root.path()).is_empty(),
        "a refused transfer leaves nothing behind to reclaim later"
    );
}

#[tokio::test]
async fn a_declared_size_over_the_limit_never_touches_the_disk() {
    let root = tempfile::tempdir().unwrap();
    let store = LocalStore::new(root.path()).with_max_object_size(Some(1024));
    let (body, pulled) = body(1);

    let refused = store.write(&namespace(), OID, Some(4096), None, body).await;

    assert!(matches!(refused, Err(Error::TooLarge { limit: 1024 })));
    assert_eq!(
        pulled.load(Ordering::Relaxed),
        0,
        "the size is known before the body is read, so nothing is read"
    );
    assert!(!root.path().join("FerrLabs").exists());
}

#[tokio::test]
async fn an_object_at_the_limit_is_accepted() {
    let root = tempfile::tempdir().unwrap();
    let payload = vec![0u8; CHUNK];
    let oid = hex::encode(Sha256::digest(&payload));
    let store = LocalStore::new(root.path()).with_max_object_size(Some(CHUNK as u64));

    let written = store
        .write(
            &namespace(),
            &oid,
            Some(CHUNK as u64),
            None,
            stream::iter([Ok::<_, std::io::Error>(Bytes::from(payload))]),
        )
        .await;

    assert_eq!(
        written.unwrap(),
        CHUNK as u64,
        "the limit is a ceiling, not a value to stay under"
    );
}

#[tokio::test]
async fn without_a_limit_a_large_object_still_goes_through() {
    let root = tempfile::tempdir().unwrap();
    let payload = vec![0u8; 64 * CHUNK];
    let oid = hex::encode(Sha256::digest(&payload));
    let store = LocalStore::new(root.path());

    let written = store
        .write(
            &namespace(),
            &oid,
            Some(payload.len() as u64),
            None,
            stream::iter([Ok::<_, std::io::Error>(Bytes::from(payload))]),
        )
        .await;

    assert_eq!(written.unwrap(), 64 * CHUNK as u64);
}