#![deny(warnings)] use std::convert::Infallible; use std::fs; use core::hash::{Hash, Hasher}; use std::collections::hash_map::DefaultHasher; use std::sync::Arc; use std::process::{Command, Stdio}; use hyper::service::{make_service_fn, service_fn}; use hyper::{body, Body, Method, Request, Response, Server, StatusCode}; use clap::{Arg, ArgMatches, App}; struct Opts { port: u16, file: String, commit: bool, } fn no_changes(file: &str) -> bool { let output = Command::new("git") .args(&["status", "--porcelain", file]) .output() .expect("Failed to execute git"); output.stdout.is_empty() } fn commit(file: &str) { if no_changes(file) { return; } let commit = Command::new("git") .args(&[ "commit", file, "-m", "Commit after PUT", "--author", "DAV Daemon <dav@example.org>", ]) .stdout(Stdio::null()) .status() .expect("Failed to execute git"); if !commit.success() { panic!("Failed to commit: {}", file) } } fn check_if_tracked(file: &str) { let check = Command::new("git") .args(&["ls-files", "--error-unmatch", file]) .stdout(Stdio::null()) .status() .expect("Failed to execute git"); if !check.success() { panic!("File not tracked in git: {}", file) } } fn etag(bytes: &[u8]) -> String { let mut hasher = DefaultHasher::new(); bytes.hash(&mut hasher); hasher.finish().to_string() } fn favicon() -> Response<Body> { let file = "favicon.ico"; match fs::read(file) { Ok(bytes) => Response::builder() .status(StatusCode::OK) .header("etag", etag(&bytes)) .body(Body::from(bytes)) .unwrap(), Err(_) => Response::builder() .status(StatusCode::NOT_FOUND) .body(Body::empty()) .unwrap() } } fn get(opts: &Opts) -> Response<Body> { let bytes = fs::read(&opts.file).expect("the file is gone"); Response::builder() .status(StatusCode::OK) .header("content-type", "text/html") .header("etag", etag(&bytes)) .body(Body::from(bytes)) .unwrap() } fn head(opts: &Opts) -> Response<Body> { let mut result = get(opts); *result.body_mut() = Body::empty(); *result.status_mut() = StatusCode::NO_CONTENT; result } fn save(opts: &Opts, bytes: &[u8]) { fs::write(&opts.file, bytes).expect("cannot write to the file"); if opts.commit { commit(&opts.file) } } async fn put(opts: &Opts, req: Request<Body>) -> Response<Body> { let (head, body) = req.into_parts(); let client_etag = head.headers.get("if-match").unwrap().to_str().unwrap(); let our_etag = etag(&fs::read(&opts.file).unwrap()); if client_etag != our_etag { Response::builder() .status(StatusCode::PRECONDITION_FAILED) .body(Body::empty()) .unwrap() } else { let bytes = body::to_bytes(body).await.expect("put body"); save(opts, &bytes); Response::builder() .status(StatusCode::NO_CONTENT) .body(Body::empty()) .unwrap() } } fn bad_request() -> Response<Body> { Response::builder() .status(StatusCode::BAD_REQUEST) .header("content-type", "text/plain") .body(Body::from("bad request\n")) .unwrap() } fn options() -> Response<Body> { Response::builder() .status(StatusCode::OK) .header("dav", "1") .body(Body::empty()) .unwrap() } async fn handle(opts: Arc<Opts>, req: Request<Body>) -> Result<Response<Body>, Infallible> { Ok(match (req.method(), req.uri().path()) { (&Method::GET, "/") => { get(&opts) } (&Method::GET, "/favicon.ico") => { favicon() } (&Method::HEAD, "/") => { head(&opts) } (&Method::OPTIONS, "/") => { options() } (&Method::PUT, "/") => { put(&opts, req).await } _ => { bad_request() } }) } fn commit_opt(file: &str, matches: &ArgMatches) -> bool { if matches.is_present("git") { check_if_tracked(file); true } else { false } } fn opts() -> Opts { let matches = App::new("dav") .arg(Arg::with_name("port") .help("TCP port to listen on") .short("p") .default_value("53502")) .arg(Arg::with_name("git") .help("Periodically commit changes to git") .takes_value(false) .short("g")) .arg(Arg::with_name("file") .help("File to serve") .short("f") .default_value("tiddlywiki.html")) .get_matches(); let port = matches.value_of("port").unwrap().parse().unwrap(); let file = matches.value_of("file").unwrap().to_owned(); if fs::metadata(&file).is_err() { panic!("File doesn't exist: {}", &file); } let commit = commit_opt(&file, &matches); Opts{ file, port, commit } } #[tokio::main] pub async fn main() { let opts = Arc::new(opts()); let addr = ([127, 0, 0, 1], opts.port).into(); let make_svc = make_service_fn(|_conn| { let opts = opts.clone(); async { Ok::<_, Infallible>(service_fn(move |req| { handle(opts.clone(), req) })) } }); let server = Server::bind(&addr).serve(make_svc); println!("Listening on http://{}", addr); if let Err(e) = server.await { eprintln!("server error: {}", e); } }