rudolfs 0.3.8

A high-performance, caching Git LFS server with an AWS S3 back-end.
Documentation
// Copyright (c) 2021 Jason White
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
#![allow(unused)]
use std::fs::{self, File};
use std::io;
use std::net::IpAddr;
use std::net::Ipv4Addr;
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Mutex;

use duct::cmd;
use rand::Rng;

/// Bind test server to localhost port 0. We don't want this server to be
/// externally visible.
pub const SERVER_ADDR: SocketAddr =
    SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0);

/// A temporary git repository.
pub struct GitRepo {
    repo: tempfile::TempDir,
}

impl GitRepo {
    /// Initialize a temporary synthetic git repository. It is set up to be
    /// connected to our LFS server.
    pub fn init(lfs_server: SocketAddr) -> io::Result<Self> {
        let repo = tempfile::TempDir::new()?;
        let path = repo.path();

        cmd!("git", "init", ".").dir(path).run()?;

        git_lfs_install(path)?;

        cmd!("git", "remote", "add", "origin", "fake_remote")
            .dir(path)
            .run()?;
        cmd!(
            "git",
            "config",
            "lfs.url",
            format!("http://{}/api/test/test", lfs_server)
        )
        .dir(path)
        .run()?;
        cmd!("git", "config", "user.name", "Foo Bar")
            .dir(path)
            .run()?;
        cmd!("git", "config", "user.email", "foobar@example.com")
            .dir(path)
            .run()?;
        cmd!("git", "lfs", "track", "*.bin").dir(path).run()?;
        cmd!("git", "add", ".gitattributes").dir(path).run()?;
        cmd!("git", "commit", "-m", "Initial commit")
            .dir(path)
            .run()?;

        Ok(Self { repo })
    }

    pub fn clone_repo(&self) -> io::Result<Self> {
        let repo = tempfile::TempDir::new()?;
        let src_dir_str = self
            .repo
            .path()
            .to_str()
            .expect("could not convert src repo path to str");
        let dst_dir_str = repo
            .path()
            .to_str()
            .expect("could not convert src repo path to str");
        cmd!("git", "clone", src_dir_str, dst_dir_str).run()?;
        Ok(Self { repo })
    }

    /// Adds a random file with the given size and random number generator. The
    /// file is also staged with `git add`.
    pub fn add_random<R: Rng>(
        &self,
        path: &Path,
        size: usize,
        rng: &mut R,
    ) -> io::Result<()> {
        let mut file = File::create(self.repo.path().join(path))?;
        gen_file(&mut file, size, rng)?;
        cmd!("git", "add", path).dir(self.repo.path()).run()?;
        Ok(())
    }

    /// Commits the currently staged files.
    pub fn commit(&self, message: &str) -> io::Result<()> {
        cmd!("git", "commit", "-m", message)
            .dir(self.repo.path())
            .run()?;
        Ok(())
    }

    pub fn lfs_push(&self) -> io::Result<()> {
        cmd!("git", "lfs", "push", "origin", "master")
            .dir(self.repo.path())
            .run()?;
        Ok(())
    }

    pub fn lfs_pull(&self) -> io::Result<()> {
        cmd!("git", "lfs", "pull").dir(self.repo.path()).run()?;
        Ok(())
    }

    pub fn pull(&self) -> io::Result<()> {
        cmd!("git", "pull").dir(self.repo.path()).run()?;
        Ok(())
    }

    /// Deletes all cached LFS files in `.git/lfs/`. This will force a
    /// re-download from the server.
    pub fn clean_lfs(&self) -> io::Result<()> {
        fs::remove_dir_all(self.repo.path().join(".git/lfs"))
    }
}

fn gen_file<W, R>(
    writer: &mut W,
    mut size: usize,
    rng: &mut R,
) -> io::Result<()>
where
    W: io::Write,
    R: Rng,
{
    let mut buf = [0u8; 4096];

    while size > 0 {
        let to_write = buf.len().min(size);

        let buf = &mut buf[..to_write];
        rng.fill(buf);
        writer.write_all(buf)?;

        size -= to_write;
    }

    Ok(())
}

/// Sets the default subscriber for the current thread until the guard is
/// dropped.
///
/// NOTE: Use `cargo test -- --nocapture` to see server logs.
pub fn init_logger() -> tracing::subscriber::DefaultGuard {
    let subscriber = tracing_subscriber::fmt().with_test_writer().finish();
    tracing::subscriber::set_default(subscriber)
}

/// Runs `git lfs install`, but serializes it across unit tests. Tests are flaky
/// if this is not done because it tries to overwrite `~/.gitconfig` and races
/// with itself.
fn git_lfs_install(path: &Path) -> io::Result<()> {
    static LFS_INSTALL: Mutex<()> = Mutex::new(());

    let _guard = LFS_INSTALL.lock().unwrap();

    cmd!("git", "lfs", "install").dir(path).run()?;

    Ok(())
}