diff --git a/git/src/lib.rs b/git/src/lib.rs index 13c350c..5de0d2e 100644 --- a/git/src/lib.rs +++ b/git/src/lib.rs @@ -1,11 +1,15 @@ -extern crate git2; +pub extern crate git2; use git2::{Error, Repository}; -use std::path::Path; +use std::{collections::HashMap, fs, path::Path}; pub trait GitOps { - fn cat_file(&self, repo_path: &Path, reference: &str, filename: &str) - -> Result, Error>; + fn cat_file( + &self, + repo: &Repository, + reference: &str, + filename: &str, + ) -> Result, Error>; } pub struct LibGitOps; @@ -15,12 +19,10 @@ impl GitOps for LibGitOps { /// point to and return it as a String. fn cat_file( &self, - repo_path: &Path, + repo: &Repository, reference: &str, filename: &str, ) -> Result, Error> { - let repo = Repository::open(repo_path)?; - let reference = repo.find_reference(reference)?; let tree = reference.peel_to_tree()?; let path = std::path::Path::new(filename); @@ -30,12 +32,35 @@ impl GitOps for LibGitOps { } } +pub fn load_repos(root_path: &Path) -> HashMap { + fs::read_dir(root_path) + .expect("Failed to read repos directory") + .filter_map(|entry| { + entry.ok().and_then(|e| { + let path = e.path(); + if path.is_dir() { + let local_path = path.clone(); + let repo_name = local_path + .file_stem() + .and_then(|name| name.to_os_string().into_string().ok()); + + repo_name.and_then(|name| { + Repository::open(path).ok().and_then(|repo| Some((name, repo))) + }) + } else { + None + } + }) + }) + .collect() +} + #[cfg(test)] mod tests { extern crate tempdir; - use super::{LibGitOps, GitOps}; + use super::{GitOps, LibGitOps}; use git2::Repository; use std::fs; @@ -43,7 +68,7 @@ mod tests { use std::path::Path; fn git_cat_file( - repo_path: &Path, + repo_path: &Repository, reference: &str, filename: &str, ) -> Result, git2::Error> { @@ -51,7 +76,7 @@ mod tests { gh.cat_file(repo_path, reference, filename) } - fn git_cat_file_err(repo_path: &Path, reference: &str, filename: &str) -> git2::Error { + fn git_cat_file_err(repo_path: &Repository, reference: &str, filename: &str) -> git2::Error { git_cat_file(repo_path, reference, filename).expect_err("should be an error") } @@ -96,7 +121,7 @@ mod tests { pub fn with_repo(file_contents: &str, file: &str, callback: F) where - F: Fn(&Path), + F: Fn(&Repository), { let dir = tempdir::TempDir::new("testgitrepo").expect("can't create tmp dir"); @@ -124,7 +149,7 @@ mod tests { }) .expect("can't do first commit"); - callback(repo.path()); + callback(&repo); dir.close().expect("couldn't close the dir"); } diff --git a/handlers/Cargo.toml b/handlers/Cargo.toml index 48bd4ec..7081bd4 100644 --- a/handlers/Cargo.toml +++ b/handlers/Cargo.toml @@ -7,10 +7,7 @@ license = "MIT" [dependencies] git = { path = "../git" } -actix-web = "^0.7.18" -git2 = "^0.8.0" -serde = "^1.0.89" -serde_derive = "^1.0.89" +actix = "^0.7.9" # When building for musl (ie. a static binary), we opt into the "vendored" # feature flag of openssl-sys which compiles libopenssl statically for us. diff --git a/handlers/src/lib.rs b/handlers/src/lib.rs index c506da2..a07ab46 100644 --- a/handlers/src/lib.rs +++ b/handlers/src/lib.rs @@ -1,100 +1,70 @@ -#[macro_use] -extern crate serde_derive; +use actix::dev::{MessageResponse, ResponseChannel}; +use actix::{Actor, Context, Handler, Message}; +use git::{git2::Repository, GitOps, LibGitOps}; +use std::collections::HashMap; -use actix_web::{dev::Handler, Binary, FromRequest, HttpRequest, Path, Query}; -use git::{LibGitOps, GitOps}; -use std::path::PathBuf; - -#[derive(Deserialize)] -pub struct PathParams { - repo: String, +pub struct CatFile { + pub repo_key: String, + pub reference: String, + pub filename: String, } -#[derive(Deserialize)] -pub struct QueryParams { - reference: String, - file: String, +impl CatFile { + pub fn new(repo_key: String, reference: String, filename: String) -> CatFile { + CatFile { + repo_key, + filename, + reference, + } + } } -pub struct RepoHandler { - pub repo_root: String, - git_ops: Box, +impl Message for CatFile { + type Result = CatFileResponse; } -impl Handler for RepoHandler { - type Result = Binary; - - fn handle(&self, req: &HttpRequest) -> Self::Result { - //TODO https://actix.rs/docs/errors/ - let path_params = Path::::extract(req).expect("Wrong path params"); - let query_params = Query::::extract(req).expect("Wront query params"); - let repo_path: PathBuf = [&self.repo_root, &path_params.repo].iter().collect(); - let reference = format!("refs/{}", query_params.reference); - //TODO return proper content type depending on the content of the blob - self.git_ops - .cat_file(&repo_path, &reference, &query_params.file) - .map(Binary::from) - .expect("Can't cat file") - } -} +pub struct CatFileResponse(pub Result, String>); -impl RepoHandler { - pub fn new(repo_root: String) -> RepoHandler { - RepoHandler { - repo_root, - git_ops: Box::new(LibGitOps {}), +impl MessageResponse for CatFileResponse +where + A: Actor, + M: Message, +{ + fn handle>(self, _: &mut A::Context, tx: Option) { + if let Some(tx) = tx { + tx.send(self); } } } -#[cfg(test)] -mod tests { - - use super::RepoHandler; - use actix_web::http::StatusCode; - use actix_web::test; - use actix_web::{Binary, Body}; - use git::GitOps; - use std::path; - - struct TestGitOps { - res: Vec, - } +pub struct GitRepos { + repos: HashMap, + ops: Box, +} - impl GitOps for TestGitOps { - fn cat_file( - &self, - _repo_path: &path::Path, - _reference: &str, - _filename: &str, - ) -> Result, git2::Error> { - Ok(self.res.to_owned()) - } - } +impl Actor for GitRepos { + type Context = Context; +} - fn bin_ref(body: &Body) -> &Binary { - match *body { - Body::Binary(ref bin) => bin, - _ => panic!(), +impl GitRepos { + pub fn new(repos: HashMap) -> GitRepos { + GitRepos { + repos, + ops: Box::new(LibGitOps {}), } } +} - #[test] - fn it_returns_the_content_of_the_file_by_cat_file() { - let rp = RepoHandler { - repo_root: "not-used".to_string(), - git_ops: Box::new(TestGitOps { - res: b"hello".to_vec(), - }), - }; +impl Handler for GitRepos { + type Result = CatFileResponse; - let resp = test::TestRequest::with_header("content-type", "application/json") - .param("repo", "client-config.git") - .uri("/repo/?reference=the-reference&file=the-file") - .run(&rp) - .expect("can't run test request"); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(bin_ref(resp.body()), &Binary::from_slice(b"hello")); + fn handle(&mut self, task: CatFile, _: &mut Self::Context) -> Self::Result { + CatFileResponse(match self.repos.get(&task.repo_key) { + Some(repo) => self + .ops + .cat_file(repo, &task.reference, &task.filename) + .map_err(|x| x.to_string()), + None => Err(format!("No repo found with name {}", &task.repo_key)), + }) } - } diff --git a/server/Cargo.toml b/server/Cargo.toml index 28e9044..3afe4c7 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,11 +7,13 @@ edition = "2018" license = "MIT" [dependencies] +git = { path = "../git" } handlers = { path = "../handlers" } actix-web = "^0.7.18" clap = "^2.32.0" -git2 = "^0.8.0" -listenfd = "^0.3.3" +futures = "^0.1.26" +serde = "^1.0.89" +serde_derive = "^1.0.89" # When building for musl (ie. a static binary), we opt into the "vendored" # feature flag of openssl-sys which compiles libopenssl statically for us. diff --git a/server/src/main.rs b/server/src/main.rs index 91af4de..61549d4 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,56 +1,91 @@ #[macro_use] extern crate clap; +#[macro_use] +extern crate serde_derive; extern crate actix_web; -extern crate listenfd; -use actix_web::{server, App}; -use listenfd::ListenFd; -use handlers::RepoHandler; +use actix_web::actix::{Actor, Addr, System}; +use actix_web::{http, middleware, server, App, Binary, FromRequest, HttpRequest, Responder}; +use futures::future::Future; +use handlers::{CatFile, GitRepos}; +use std::path::Path; const DEFAULT_PORT: &str = "7791"; const DEFAULT_HOST: &str = "localhost"; -const DEFAULT_REPO_PATH: &str = "./"; - -#[derive(Debug)] -enum AppError { - GitError(git2::Error), - ObjectNotFound(String), -} - -impl From for AppError { - fn from(error: git2::Error) -> Self { - match error.code() { - git2::ErrorCode::NotFound => AppError::ObjectNotFound(error.to_string()), - _ => AppError::GitError(error), - } - } -} +const DEFAULT_REPO_ROOT: &str = "./"; +const DEFAULT_REFERENCE: &str = "heads/master"; fn main() { let args = parse_args().get_matches(); let host = args.value_of("host").unwrap_or(DEFAULT_HOST); let port = args.value_of("port").unwrap_or(DEFAULT_PORT); + let repo_root = Path::new(args.value_of("repo-root").unwrap_or(DEFAULT_REPO_ROOT)); - create_server(host, port).run(); + run_server(host, port, repo_root); } -fn create_server(host: &str, port: &str) -> server::HttpServer, fn() -> App<()>> { +#[derive(Deserialize)] +pub struct PathParams { + pub repo: String, +} - let mut listenfd = ListenFd::from_env(); - let server: server::HttpServer, fn() -> App<()>> = server::new(|| { - App::new() - .resource("/repo/{repo}", |r| r.h(RepoHandler::new(DEFAULT_REPO_PATH.to_string()))) - }); +#[derive(Deserialize)] +pub struct QueryParams { + pub reference: Option, + pub file: String, +} - match listenfd.take_tcp_listener(0).expect("can't take tcp listener") { - Some(l) => server.listen(l), - None => { - let address = format!("{}:{}", host, port); - println!("Listening to {}", address); - server.bind(address).expect("can't bind into address") - } - } +pub struct AppState { + pub git_repos: Addr, +} + +fn run_server(host: &str, port: &str, repo_root: &Path) { + let _sys = System::new("gitkv-server"); + + let addr = GitRepos::new(git::load_repos(&repo_root)).start(); + + let listen_address = format!("{}:{}", host, port); + + server::new(move || { + App::with_state(AppState { + git_repos: addr.clone(), + }) + .middleware(middleware::Logger::default()) + .resource("/repo/{repo}", |r| r.method(http::Method::GET).f(get_repo)) + }) + .bind(listen_address) + .expect("can't bind into address") + .run(); +} + +fn get_repo(req: &HttpRequest) -> impl Responder { + //TODO https://actix.rs/docs/errors/ + let path_params = actix_web::Path::::extract(req).expect("Wrong path params"); + let query_params = actix_web::Query::::extract(req).expect("Wrong query params"); + let repo_key = path_params.repo.clone(); + let filename = query_params.file.clone(); + let reference = format!( + "refs/{}", + query_params + .reference + .as_ref() + .map(String::as_str) + .unwrap_or(DEFAULT_REFERENCE) + ); + let gr: &Addr = &req.state().git_repos; + //TODO return proper content type depending on the content of the blob + gr.send(CatFile { + repo_key, + filename, + reference, + }) + .map(|x| { + x.0.map(Binary::from) + .map_err(|e| actix_web::error::InternalError::new(e, http::StatusCode::NOT_FOUND)) + }) + //TODO don't wait and return the future itself + .wait() } fn parse_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -76,4 +111,13 @@ fn parse_args<'a, 'b>() -> clap::App<'a, 'b> { .default_value(DEFAULT_HOST) .help("host to listen to"), ) + .arg( + clap::Arg::with_name("repo-root") + .short("r") + .long("repo-root") + .takes_value(true) + .value_name("PATH") + .default_value(DEFAULT_REPO_ROOT) + .help("path where the different repositories are located"), + ) }