initial
This commit is contained in:
commit
b4dfdb09c8
12 changed files with 1992 additions and 0 deletions
8
.editorconfig
Normal file
8
.editorconfig
Normal 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
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
/target
|
||||||
|
/repos
|
1661
Cargo.lock
generated
Normal file
1661
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
23
Cargo.toml
Normal file
23
Cargo.toml
Normal 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
3
config.toml
Normal 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
18
fedhub.sublime-project
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
{
|
||||||
|
"folders":
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings":
|
||||||
|
{
|
||||||
|
"LSP":
|
||||||
|
{
|
||||||
|
"rust-analyzer":
|
||||||
|
{
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
rust-toolchain
Normal file
1
rust-toolchain
Normal file
|
@ -0,0 +1 @@
|
||||||
|
nightly
|
9
src/config.rs
Normal file
9
src/config.rs
Normal 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
223
src/main.rs
Normal 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
14
templates/index.html
Normal 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
15
templates/repo_index.html
Normal 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
15
templates/repo_tree.html
Normal 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>
|
Loading…
Reference in a new issue