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