aboutsummaryrefslogtreecommitdiff
path: root/src/controller.rs
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2021-06-23 22:46:31 +0200
committerMartin Fischer <martin@push-f.com>2021-06-23 22:50:55 +0200
commit43b4b8693890a85f24eb358bc5545232ebf8e796 (patch)
treec83bbcacae18b034037399c197e1976346c40eb9 /src/controller.rs
parentca074febae4cd56ad5443c110a15662fa110dd81 (diff)
make single-user mode operate on HEAD branch
Diffstat (limited to 'src/controller.rs')
-rw-r--r--src/controller.rs108
1 files changed, 95 insertions, 13 deletions
diff --git a/src/controller.rs b/src/controller.rs
index 1f66b44..5dc2a47 100644
--- a/src/controller.rs
+++ b/src/controller.rs
@@ -3,10 +3,10 @@ use std::path::Path;
use std::str::from_utf8;
use std::sync::RwLock;
-use git2::ObjectType;
use git2::Repository;
use git2::Signature;
use git2::Tree;
+use git2::{BranchType, ObjectType};
use hyper::http::request::Parts;
use serde::Deserialize;
use sputnik::html_escape;
@@ -19,8 +19,13 @@ use crate::Page;
use crate::Response;
pub trait Controller {
- /// Allows the controller to abort if the request is invalid.
- fn before_route(&self, parts: &Parts) -> Result<(), Error>;
+ fn parse_url_path<'a>(&'a self, url_path: &'a str) -> Result<(Branch, &'a str), Error>;
+
+ /// Builds a URL path from a given Git branch and file path.
+ fn build_url_path<'a>(&self, branch: &Branch, path: &'a str) -> String;
+
+ /// Allows the controller to intercept the request handling.
+ fn before_route(&self, parts: &Parts, repo: &Repository) -> Option<Result<Response, Error>>;
/// Returns some HTML info to display for the current page.
fn user_info_html(&self, parts: &Parts) -> Option<String> {
@@ -48,7 +53,7 @@ pub trait Controller {
None
}
- fn before_return_tree_page(&self, page: &mut Page, tree: Option<Tree>, context: &Context) {}
+ fn before_return_tree_page(&self, page: &mut Page, tree: Option<Tree>, context: &Context);
/// Executed before writing a file. Return an error to abort the writing process.
fn before_write(&self, text: &str, context: &mut Context) -> Result<(), String> {
@@ -64,21 +69,29 @@ pub trait Controller {
}
}
-pub struct SoloController;
+pub struct SoloController(pub Branch);
const USERNAME_HEADER: &str = "Username";
impl Controller for SoloController {
- fn before_route(&self, parts: &Parts) -> Result<(), Error> {
+ fn parse_url_path<'a>(&'a self, url_path: &'a str) -> Result<(Branch, &'a str), Error> {
+ Ok((self.0.clone(), url_path))
+ }
+
+ fn build_url_path<'a>(&self, branch: &Branch, path: &'a str) -> String {
+ path.to_owned()
+ }
+
+ fn before_route(&self, parts: &Parts, repo: &Repository) -> Option<Result<Response, Error>> {
if parts.headers.contains_key(USERNAME_HEADER) {
- return Err(Error::BadRequest(format!(
+ return Some(Err(Error::BadRequest(format!(
"unexpected header {} (only \
allowed in multi-user mode), aborting to prevent accidental \
information leakage",
USERNAME_HEADER
- )));
+ ))));
}
- Ok(())
+ None
}
fn signature(&self, repo: &Repository, parts: &Parts) -> Result<Signature, Error> {
@@ -97,6 +110,12 @@ impl Controller for SoloController {
true
}
+ fn before_return_tree_page(&self, page: &mut Page, tree: Option<Tree>, context: &Context) {
+ if tree.map(|t| t.len()).unwrap_or_default() == 0 {
+ page.body.push_str("<p>create files by editing the URL, e.g. <a href='/hello-world.md'>/hello-world.md</a></p>");
+ }
+ }
+
fn before_write(&self, text: &str, ctx: &mut Context) -> Result<(), String> {
if let Some(ext) = ctx.path.extension().and_then(|e| e.to_str()) {
validate_formats(text, ext)?;
@@ -250,17 +269,80 @@ fn validate_formats(text: &str, extension: &str) -> Result<(), String> {
Ok(())
}
+fn multi_user_startpage(
+ controller: &MultiUserController,
+ parts: &Parts,
+ repo: &Repository,
+) -> Result<Response, Error> {
+ // TODO: add domain name to title?
+ let mut page = Page {
+ title: "GitPad".into(),
+ controller,
+ parts: &parts,
+ body: String::new(),
+ header: None,
+ };
+
+ let branches: Vec<_> = repo.branches(Some(BranchType::Local))?.collect();
+
+ page.body.push_str("This GitPad instance has ");
+
+ if branches.is_empty() {
+ page.body.push_str("no branches yet.");
+ } else {
+ page.body.push_str("the following branches:");
+ page.body.push_str("<ul>");
+ for (branch, _) in repo.branches(Some(BranchType::Local))?.flatten() {
+ page.body.push_str(&format!(
+ "<li><a href='~{0}/'>~{0}</a></li>",
+ html_escape(branch.name()?.unwrap())
+ ));
+ }
+ page.body.push_str("</ul>");
+ }
+
+ Ok(page.into())
+}
+
impl Controller for MultiUserController {
- fn before_route(&self, parts: &Parts) -> Result<(), Error> {
+ fn parse_url_path<'a>(&'a self, url_path: &'a str) -> Result<(Branch, &'a str), Error> {
+ let mut iter = url_path.splitn(3, '/');
+ iter.next();
+ let rev = iter.next().unwrap();
+ if !rev.starts_with('~') {
+ return Err(Error::NotFound(
+ "branch name must be prefixed a tilde (~)".into(),
+ ));
+ }
+ let rev = &rev[1..];
+ if rev.trim().is_empty() {
+ return Err(Error::NotFound("invalid branch name".into()));
+ }
+ let rev = Branch(rev.to_owned());
+ let unsanitized_path = match iter.next() {
+ Some(value) => value,
+ None => return Err(Error::MissingTrailingSlash(url_path.to_string())),
+ };
+ Ok((rev, unsanitized_path))
+ }
+
+ fn build_url_path<'a>(&self, branch: &Branch, path: &'a str) -> String {
+ format!("/~{}/{}", branch.0, path)
+ }
+
+ fn before_route(&self, parts: &Parts, repo: &Repository) -> Option<Result<Response, Error>> {
if !parts.headers.contains_key(USERNAME_HEADER) {
- return Err(Error::BadRequest(format!(
+ return Some(Err(Error::BadRequest(format!(
"expected header {} because of multi-user mode \
(this shouldn't be happening because a reverse-proxy \
should be used to set this header)",
USERNAME_HEADER
- )));
+ ))));
}
- Ok(())
+ if parts.uri.path() == "/" {
+ return Some(multi_user_startpage(&self, parts, repo));
+ }
+ None
}
fn user_info_html(&self, parts: &Parts) -> Option<String> {