This commit is contained in:
Michael Zhang 2020-04-20 00:21:26 -05:00
commit b4dfdb09c8
Signed by: michael
GPG key ID: BDA47A31A3C8EE6B
12 changed files with 1992 additions and 0 deletions

8
.editorconfig Normal file
View file

@ -0,0 +1,8 @@
[*.{rs,sql,tera}]
end_of_file = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
indent_style = space
indent_size = 4

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/repos

1661
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

23
Cargo.toml Normal file
View file

@ -0,0 +1,23 @@
[package]
name = "fedhub"
version = "0.1.0"
authors = ["Michael Zhang <iptq@protonmail.com>"]
edition = "2018"
[dependencies]
git2 = "0.13.0"
serde = "1.0.105"
serde_derive = "1.0.105"
serde_json = "1.0.48"
structopt = "0.3.12"
tera = "1.2.0"
toml = "0.5.6"
tokio = { version = "0.2.18", features = ["full"] }
anyhow = "1.0.28"
walkdir = "2.3.1"
lazy_static = "1.4.0"
parking_lot = "0.10.2"
hyper = "0.13.5"
redis = "0.15.1"
askama = "0.9.0"
futures = "0.3.4"

3
config.toml Normal file
View file

@ -0,0 +1,3 @@
addr = "127.0.0.1:3000"
repo_root = "./repos"
redis_url = "redis://127.0.0.1"

18
fedhub.sublime-project Normal file
View file

@ -0,0 +1,18 @@
{
"folders":
[
{
"path": "."
}
],
"settings":
{
"LSP":
{
"rust-analyzer":
{
"enabled": true
}
}
}
}

1
rust-toolchain Normal file
View file

@ -0,0 +1 @@
nightly

9
src/config.rs Normal file
View file

@ -0,0 +1,9 @@
use std::net::SocketAddr;
use std::path::PathBuf;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Config {
pub addr: SocketAddr,
pub repo_root: PathBuf,
pub redis_url: String,
}

223
src/main.rs Normal file
View file

@ -0,0 +1,223 @@
#![feature(async_closure)]
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;
mod config;
use std::convert::Infallible;
use std::net::SocketAddr;
use std::path::{Component as PathComponent, PathBuf};
use std::sync::Arc;
use anyhow::Result;
use futures::future::{self, TryFutureExt};
use git2::Repository;
use hyper::{
service::{make_service_fn, service_fn},
Body, Request, Response, Server, StatusCode,
};
use lazy_static::lazy_static;
use parking_lot::RwLock;
use redis::{Client as RedisClient, Cmd as RedisCmd};
use tera::{Context as TeraContext, Tera};
use tokio::{fs::File, io::AsyncReadExt};
use walkdir::WalkDir;
use crate::config::Config;
macro_rules! template {
($file:expr) => {
($file, include_str!(concat!("../templates/", $file)))
};
}
lazy_static! {
static ref TERA: Tera = {
let mut tera = Tera::default();
tera.add_raw_templates(vec![
template!("index.html"),
template!("repo_index.html"),
template!("repo_tree.html"),
])
.unwrap();
tera
};
}
#[derive(Clone)]
struct Fedhub {
config: Config,
state: Arc<RwLock<State>>,
}
#[derive(Clone)]
struct State {
cache: RedisClient,
}
impl Fedhub {
pub fn new(config: Config) -> Result<Self> {
let cache = RedisClient::open(config.redis_url.as_ref())?;
Ok(Fedhub {
config,
state: Arc::new(RwLock::new(State { cache })),
})
}
pub async fn get_dir_list(&self) -> Result<Vec<PathBuf>> {
let state = self.state.read();
let mut con = state.cache.get_connection()?;
if RedisCmd::exists("repos").query(&mut con)? {
let path_strs: Vec<String> = RedisCmd::smembers("repos").query(&mut con)?;
return Ok(path_strs.into_iter().map(|s| PathBuf::from(s)).collect());
} else {
let repo_root = self.config.repo_root.clone();
let mut dir_iter = WalkDir::new(&repo_root).into_iter();
let mut dirs = Vec::new();
loop {
let entry = match dir_iter.next() {
None => break,
Some(Err(err)) => panic!("error: {:?}", err),
Some(Ok(entry)) => entry,
};
let path = entry.path();
if let Ok(_) = Repository::open(path) {
let new_path = path.strip_prefix(&repo_root).unwrap();
dirs.push(new_path.to_path_buf());
dir_iter.skip_current_dir();
}
}
RedisCmd::sadd(
"repos",
dirs.iter()
.map(|path| path.to_str().unwrap().to_string())
.collect::<Vec<_>>(),
)
.execute(&mut con);
Ok(dirs)
}
}
pub async fn render_index(&self) -> Result<Response<Body>> {
let directories = self.get_dir_list().await?;
let mut ctx = TeraContext::new();
ctx.insert("repos", &directories);
Ok(Response::new(TERA.render("index.html", &ctx)?.into()))
}
pub async fn render_repo_index(
&self,
path: PathBuf,
repo: Repository,
) -> Result<Response<Body>> {
let mut ctx = TeraContext::new();
ctx.insert("name", &path);
let mut branches = Vec::new();
for branch in repo.branches(None)? {
let (branch, _) = branch?;
branches.push(branch.name()?.unwrap().to_string());
}
ctx.insert("branches", &branches);
return Ok(Response::new(TERA.render("repo_index.html", &ctx)?.into()));
}
pub async fn render_repo_tree(
&self,
path: PathBuf,
repo: Repository,
tree: String,
) -> Result<Response<Body>> {
let commit_ref = repo.resolve_reference_from_short_name(tree.as_ref())?;
let commit = commit_ref.peel_to_commit()?;
let tree = commit.tree()?;
let mut ctx = TeraContext::new();
ctx.insert("name", &path);
ctx.insert("ref", commit_ref.shorthand().unwrap());
let mut entries = Vec::new();
for entry in tree.iter() {
entries.push(json!({
"name": entry.name().unwrap(),
}));
}
ctx.insert("entries", &entries);
return Ok(Response::new(TERA.render("repo_tree.html", &ctx)?.into()));
}
pub async fn handle(self, req: Request<Body>) -> Result<Response<Body>> {
let repo_root = self.config.repo_root.clone();
let uri = req.uri();
let path = uri.path();
if path == "/" {
return self.render_index().await;
}
let repo_info = {
let mut repo_info = None;
for repo_dir in self.get_dir_list().await? {
let path = PathBuf::from(path.trim_start_matches("/"));
if path.starts_with(&repo_dir) {
let repo = Repository::open(repo_root.join(&repo_dir))?;
let remainder = PathBuf::from("/").join(path.strip_prefix(&repo_dir)?);
repo_info = Some((repo_dir, repo, remainder));
}
}
repo_info
};
if let Some((path, repo, remainder)) = repo_info {
if remainder == PathBuf::from("/") {
return self.render_repo_index(path, repo).await;
} else if remainder.starts_with("/tree") {
let tree = remainder.strip_prefix("/tree").unwrap().components().next();
if let Some(PathComponent::Normal(tree)) = tree {
return self
.render_repo_tree(path, repo, tree.to_str().unwrap().to_string())
.await;
}
}
}
Ok(Response::builder()
.status(StatusCode::NOT_FOUND)
.body("not found".into())
.unwrap())
}
}
#[tokio::main]
async fn main() -> Result<()> {
let mut config_file = File::open("config.toml").await?;
let mut config_str = String::new();
config_file.read_to_string(&mut config_str).await?;
let config = toml::from_str::<Config>(&config_str)?;
println!("Config: {:?}", config);
let fedhub = Fedhub::new(config)?;
let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
let make_svc = make_service_fn(move |_conn| {
let fedhub = fedhub.clone();
let main = move |req| {
let fedhub = fedhub.clone();
fedhub.handle(req).or_else(|err| {
eprintln!("Error: {:?}", err);
future::ok::<_, Infallible>(Response::new(Body::from(format!("Error: {:?}", err))))
})
};
future::ok::<_, Infallible>(service_fn(main))
});
let server = Server::bind(&addr).serve(make_svc);
if let Err(e) = server.await {
eprintln!("server error: {}", e);
}
unreachable!()
}

14
templates/index.html Normal file
View file

@ -0,0 +1,14 @@
<html>
<head>
</head>
<body>
<h1>Fedhub</h1>
<ul>
{% for repo in repos %}
<li><a href="/{{ repo }}">{{ repo }}</a></li>
{% endfor %}
</ul>
</body>
</html>

15
templates/repo_index.html Normal file
View file

@ -0,0 +1,15 @@
<html>
<head>
</head>
<body>
<h1>{{ name }}</h1>
<h3>branches:</h3>
<ul>
{% for branch in branches %}
{{ branch }}
{% endfor %}
</ul>
</body>
</html>

15
templates/repo_tree.html Normal file
View file

@ -0,0 +1,15 @@
<html>
<head>
</head>
<body>
<h1>{{ name }}: {{ ref }}</h1>
<h3>entries:</h3>
<ul>
{% for entry in entries %}
<li>{{ entry.name }}</li>
{% endfor %}
</ul>
</body>
</html>