diff options
Diffstat (limited to 'src/get_routes.rs')
-rw-r--r-- | src/get_routes.rs | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/src/get_routes.rs b/src/get_routes.rs new file mode 100644 index 0000000..433e676 --- /dev/null +++ b/src/get_routes.rs @@ -0,0 +1,361 @@ +use chrono::NaiveDateTime; +use git2::FileMode; +use git2::ObjectType; +use git2::Oid; +use git2::Tree; +use git2::TreeEntry; +use hyper::header; +use hyper::http::response::Builder; +use hyper::StatusCode; +use serde::Deserialize; +use sputnik::html_escape; +use sputnik::request::SputnikParts; +use std::str::from_utf8; + +use crate::action_links; +use crate::controller::Controller; +use crate::diff::diff; +use crate::forms; +use crate::get_renderer; +use crate::ActionParam; +use crate::Context; +use crate::Error; +use crate::Page; +use crate::Response; + +#[derive(Deserialize)] +struct LogParam { + commit: Option<String>, +} + +pub(crate) fn view_blob<C: Controller>( + entr: TreeEntry, + params: ActionParam, + controller: &C, + ctx: Context, +) -> Result<Response, Error> { + let filename = ctx.path.file_name().unwrap().to_str().unwrap(); + + match params.action.as_ref() { + "view" => { + let mut page = Page { + title: filename.to_string(), + body: String::new(), + header: Some(action_links(¶ms.action, controller, &ctx)), + controller, + parts: &ctx.parts, + }; + + if let Some(access_info_html) = controller.access_info_html(&ctx) { + page.body.push_str(&access_info_html); + } + + if entr.filemode() == FileMode::Link.into() { + // TODO: indicate and link symbolic link + } + + let blob = ctx.repo.find_blob(entr.id()).unwrap(); + + if let Some(mime) = mime_guess::from_path(&ctx.path).first() { + if mime.type_() == "image" { + page.body + .push_str("<div class=img-container><img src=?action=raw></div>"); + return Ok(page.into()); + } + } + + match from_utf8(blob.content()) { + Ok(text) => { + if let Some(renderer) = get_renderer(&ctx.path) { + page.body.push_str(&renderer(text)); + } else { + page.body + .push_str(&format!("<pre>{}</pre>", html_escape(text))); + } + } + Err(_) => page.body.push_str("failed to decode file as UTF-8"), + } + + Ok(page.into()) + } + "edit" => { + if !controller.may_write_path(&ctx) { + return Err(Error::Unauthorized( + "you are not authorized to edit this file".into(), + ctx, + )); + } + let blob = ctx.repo.find_blob(entr.id()).unwrap(); + if let Ok(text) = from_utf8(blob.content()) { + return Ok(forms::edit_text_form( + &forms::EditForm { + text: text.to_string(), + oid: Some(entr.id().to_string()), + ..Default::default() + }, + None, + controller, + &ctx, + ) + .into()); + } else { + return Ok(forms::upload_form(true, controller, &ctx).into()); + } + } + "upload" => { + return Ok(forms::upload_form(true, controller, &ctx).into()); + } + "log" => { + let log_param: LogParam = ctx.parts.query().unwrap(); + + if let Some(commit) = log_param.commit { + let branch_commit = ctx.branch_head()?; + + let commit = ctx + .repo + .find_commit(Oid::from_str(&commit)?) + .map_err(|_| Error::NotFound("commit not found".into()))?; + + if branch_commit.id() != commit.id() + && !ctx + .repo + .graph_descendant_of(branch_commit.id(), commit.id())? + { + // disallow viewing commits from other branches you shouldn't have access to + return Err(Error::NotFound("commit not found".into())); + } + + let blob_id = if let Ok(entry) = commit.tree()?.get_path(&ctx.path) { + entry.id() + } else { + return Ok(Page { + title: format!("Commit for {}", filename), + body: "file removed".into(), + header: Some(action_links(¶ms.action, controller, &ctx)), + controller, + parts: &ctx.parts, + } + .into()); + }; + + // TODO: if UTF-8 decoding fails, link ?action=raw&rev= + // TODO: what if there are multiple parents? + let old_blob_id = commit + .parents() + .next() + .and_then(|p| p.tree().unwrap().get_path(&ctx.path).ok()) + .map(|e| e.id()); + if Some(blob_id) != old_blob_id { + let mut page = Page { + title: format!("Commit for {}", filename), + header: Some(action_links(¶ms.action, controller, &ctx)), + body: format!( + "<h1>{}</h1>{} committed on {}", + html_escape(commit.summary().unwrap_or_default()), + html_escape(commit.author().name().unwrap_or_default()), + NaiveDateTime::from_timestamp(commit.time().seconds(), 0) + .format("%b %d, %Y, %H:%M") + ), + controller, + parts: &ctx.parts, + }; + if let Some(old_blob_id) = old_blob_id { + page.body.push_str(&diff( + from_utf8(ctx.repo.find_blob(old_blob_id)?.content())?, + from_utf8(ctx.repo.find_blob(blob_id)?.content())?, + )) + } else { + page.body.push_str(&diff( + "", + from_utf8(&ctx.repo.find_blob(blob_id)?.content())?, + )); + } + return Ok(page.into()); + } else { + return Err(Error::NotFound("commit not found".into())); + } + } + + let mut page = Page { + title: format!("Log for {}", filename), + body: String::new(), + header: Some(action_links(¶ms.action, controller, &ctx)), + controller, + parts: &ctx.parts, + }; + + let mut walk = ctx.repo.revwalk()?; + let branch_head = ctx.branch_head()?; + walk.push(branch_head.id())?; + + let mut prev_commit = branch_head; + let mut prev_blobid = Some(prev_commit.tree()?.get_path(&ctx.path)?.id()); + + let mut commits = Vec::new(); + + // TODO: paginate + for oid in walk.flatten().skip(1) { + let commit = ctx.repo.find_commit(oid)?; + if let Ok(entr) = commit.tree()?.get_path(&ctx.path) { + let blobid = entr.id(); + if Some(blobid) != prev_blobid { + commits.push(prev_commit); + prev_blobid = Some(blobid); + } + prev_commit = commit; + } else { + if prev_blobid.is_some() { + commits.push(prev_commit); + } + prev_commit = commit; + prev_blobid = None; + } + } + if prev_commit.parent_count() == 0 && prev_commit.tree()?.get_path(&ctx.path).is_ok() { + // the very first commit of the branch + commits.push(prev_commit); + } + let mut prev_date = None; + for c in commits { + let date = NaiveDateTime::from_timestamp(c.time().seconds(), 0).date(); + if Some(date) != prev_date { + if prev_date != None { + page.body.push_str("</ul>"); + } + page.body + .push_str(&format!("{}<ul>", date.format("%b %d, %Y"))); + } + + page.body.push_str(&format!( + "<li><a href='?action=log&commit={}'>{}: {}</a></li>", + html_escape(c.id().to_string()), + html_escape(c.author().name().unwrap_or_default()), + html_escape(c.summary().unwrap_or_default()), + )); + prev_date = Some(date); + } + page.body.push_str("</ul>"); + Ok(page.into()) + } + "raw" => { + if let Some(etag) = ctx + .parts + .headers + .get(header::IF_NONE_MATCH) + .and_then(|v| v.to_str().ok()) + { + if etag.trim_matches('"') == entr.id().to_string() { + return Ok(Builder::new() + .status(StatusCode::NOT_MODIFIED) + .body("".into()) + .unwrap()); + } + } + + let blob = ctx.repo.find_blob(entr.id()).unwrap(); + let mut resp = Response::new(blob.content().to_owned().into()); + + resp.headers_mut() + .insert(header::ETAG, format!("\"{}\"", entr.id()).parse().unwrap()); + resp.headers_mut() + .insert(header::CACHE_CONTROL, "no-cache".parse().unwrap()); + + if let Some(mime) = mime_guess::from_path(&ctx.path).first() { + if mime.type_() == "text" { + // workaround for Firefox, which downloads non-plain text subtypes + // instead of displaying them (https://bugzilla.mozilla.org/1319262) + resp.headers_mut() + .insert(header::CONTENT_TYPE, "text/plain".parse().unwrap()); + } else { + resp.headers_mut() + .insert(header::CONTENT_TYPE, mime.to_string().parse().unwrap()); + } + } + Ok(resp) + } + "move" => { + if !controller.may_move_path(&ctx) { + return Err(Error::Unauthorized( + "you are not authorized to move this file".into(), + ctx, + )); + } + return forms::move_form( + filename, + &forms::MoveForm { + dest: ctx.path.to_str().unwrap().to_owned(), + msg: None, + }, + None, + controller, + &ctx, + ); + } + "remove" => { + if !controller.may_move_path(&ctx) { + return Err(Error::Unauthorized( + "you are not authorized to remove this file".into(), + ctx, + )); + } + let page = Page { + title: format!("Remove {}", filename), + controller, + parts: &ctx.parts, + header: Some(action_links(¶ms.action, controller, &ctx)), + body: "<form method=post autocomplete=off>\ + <label>Message <input name=msg autofocus></label>\ + <button>Remove</button></form>" + .into(), + }; + + Ok(page.into()) + } + _ => Err(Error::BadRequest("unknown action".into())), + } +} + +pub fn view_tree<C: Controller>( + tree: Result<Tree, git2::Error>, + controller: &C, + ctx: &Context, +) -> Result<Response, Error> { + let mut page = Page { + title: ctx.path.to_string_lossy().to_string(), + controller, + parts: &ctx.parts, + body: String::new(), + header: None, + }; + + page.body.push_str("<ul>"); + page.body + .push_str("<li><a href='..' title='go to parent directory'>../</a></li>"); + + if let Ok(tree) = &tree { + let mut entries: Vec<_> = tree.iter().collect(); + entries.sort_by_key(|a| a.kind().unwrap().raw()); + + for entry in entries { + // if the name isn't valid utf8 we skip the entry + if let Some(name) = entry.name() { + if entry.kind() == Some(ObjectType::Tree) { + page.body.push_str(&format!( + "<li><a href='{0}/'>{0}/</a></li>", + html_escape(name) + )); + } else { + page.body.push_str(&format!( + "<li><a href='{0}'>{0}</a></li>", + html_escape(name) + )); + } + } + } + } + page.body.push_str("</ul>"); + + controller.before_return_tree_page(&mut page, tree.ok(), ctx); + + Ok(page.into()) +} |