diff options
Diffstat (limited to 'src/post_routes.rs')
-rw-r--r-- | src/post_routes.rs | 354 |
1 files changed, 354 insertions, 0 deletions
diff --git a/src/post_routes.rs b/src/post_routes.rs new file mode 100644 index 0000000..0d4631c --- /dev/null +++ b/src/post_routes.rs @@ -0,0 +1,354 @@ +use git2::build::TreeUpdateBuilder; +use git2::FileMode; +use hyper::header; +use hyper::http::response::Builder; +use hyper::Body; +use hyper::StatusCode; +use multer::Multipart; +use serde::Deserialize; +use sputnik::hyper_body::SputnikBody; +use std::path::Path; +use std::str::from_utf8; + +use crate::build_url_path; +use crate::diff::diff; +use crate::forms::edit_text_form; +use crate::forms::move_form; +use crate::forms::EditForm; +use crate::forms::MoveForm; +use crate::get_renderer; +use crate::ActionParam; +use crate::Args; +use crate::Context; +use crate::Response; +use crate::{controller::Controller, Error}; + +pub(crate) async fn build_response<C: Controller>( + args: &Args, + params: &ActionParam, + controller: &C, + ctx: Context, + body: Body, +) -> Result<Response, Error> { + if let Some(ref enforced_origin) = args.origin { + if ctx + .parts + .headers + .get(header::ORIGIN) + .filter(|h| h.as_bytes() == enforced_origin.as_bytes()) + .is_none() + { + return Err(Error::BadRequest(format!( + "POST requests must be sent with the header Origin: {}", + enforced_origin + ))); + } + } + match params.action.as_ref() { + "edit" => return update_blob(body, controller, ctx).await, + "upload" => return upload_blob(body, controller, ctx).await, + "move" => return move_entry(body, controller, ctx).await, + "remove" => return remove_entry(body, controller, ctx).await, + "diff" => return diff_blob(body, controller, ctx).await, + "preview" => return preview_edit(body, controller, ctx).await, + _ => return Err(Error::BadRequest("unknown POST action".into())), + } +} + +fn commit_file_update<C: Controller>( + data: &[u8], + msg: Option<String>, + controller: &C, + ctx: &Context, +) -> Result<(), Error> { + let blob_id = ctx.repo.blob(data)?; + + let mut builder = TreeUpdateBuilder::new(); + + builder.upsert(ctx.path.to_str().unwrap(), blob_id, FileMode::Blob); + + let (parent_tree, parent_commits) = if let Ok(commit) = ctx.branch_head() { + let parent_tree = commit.tree()?; + if parent_tree.get_path(&ctx.path).ok().map(|e| e.id()) == Some(blob_id) { + // nothing changed, don't create an empty commit + return Err(Error::Redirect(ctx.parts.uri.path().to_string())); + } + (parent_tree, vec![commit]) + } else { + // the empty tree exists even in empty bare repositories + // we could also hard-code its hash here but magic strings are uncool + let empty_tree = ctx.repo.find_tree(ctx.repo.index()?.write_tree()?)?; + (empty_tree, vec![]) + }; + + let new_tree_id = builder.create_updated(&ctx.repo, &parent_tree)?; + + let signature = controller.signature(&ctx.repo, &ctx.parts)?; + ctx.commit( + &signature, + &msg.filter(|m| !m.trim().is_empty()).unwrap_or_else(|| { + format!( + "{} {}", + if parent_tree.get_path(&ctx.path).is_ok() { + "edit" + } else { + "create" + }, + ctx.path.to_str().unwrap() + ) + }), + &ctx.repo.find_tree(new_tree_id)?, + &parent_commits.iter().collect::<Vec<_>>()[..], + )?; + + Ok(()) +} + +async fn update_blob<C: Controller>( + body: Body, + controller: &C, + mut ctx: Context, +) -> Result<Response, Error> { + if !controller.may_write_path(&ctx) { + return Err(Error::Unauthorized( + "you are not authorized to edit this file".into(), + ctx, + )); + } + let mut data: EditForm = body.into_form().await?; + + if let Ok(commit) = ctx.branch_head() { + // edit conflict detection + let latest_oid = commit + .tree() + .unwrap() + .get_path(&ctx.path) + .ok() + .map(|e| e.id().to_string()); + + if data.oid != latest_oid { + data.oid = latest_oid; + return Ok(edit_text_form(&data, Some( + if data.oid.is_some() { + "this file has been edited in the meantime, if you save you will overwrite the changes made since" + } else { + "this file has been deleted in the meantime, if you save you will re-create it" + } + ), controller, &ctx).into()); + } + } + + // normalize newlines as per HTML spec + let text = data.text.replace("\r\n", "\n"); + if let Err(error) = controller.before_write(&text, &mut ctx) { + return Ok(edit_text_form(&data, Some(&error), controller, &ctx).into()); + } + + commit_file_update(text.as_bytes(), data.msg, controller, &ctx)?; + + controller.after_write(&mut ctx); + + return Ok(Builder::new() + .status(StatusCode::FOUND) + .header("location", ctx.parts.uri.path()) + .body("redirecting".into()) + .unwrap()); +} + +async fn upload_blob<C: Controller>( + body: Body, + controller: &C, + mut ctx: Context, +) -> Result<Response, Error> { + if !controller.may_write_path(&ctx) { + return Err(Error::Unauthorized( + "you are not authorized to edit this file".into(), + ctx, + )); + } + // Extract the `multipart/form-data` boundary from the headers. + let boundary = ctx + .parts + .headers + .get(header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .and_then(|ct| multer::parse_boundary(ct).ok()) + .ok_or_else(|| Error::BadRequest("expected multipart/form-data".into()))?; + + let mut multipart = Multipart::new(Box::new(body), boundary); + while let Some(field) = multipart + .next_field() + .await + .map_err(|_| Error::BadRequest("failed to parse multipart".into()))? + { + if field.name() == Some("file") { + // TODO: make commit message customizable + commit_file_update(&field.bytes().await.unwrap(), None, controller, &ctx)?; + + controller.after_write(&mut ctx); + + return Ok(Builder::new() + .status(StatusCode::FOUND) + .header("location", ctx.parts.uri.path()) + .body("redirecting".into()) + .unwrap()); + } + } + Err(Error::BadRequest( + "expected file upload named 'file'".into(), + )) +} + +async fn move_entry<C: Controller>( + body: Body, + controller: &C, + ctx: Context, +) -> Result<Response, Error> { + if !controller.may_move_path(&ctx) { + return Err(Error::Unauthorized( + "you are not authorized to move this file".into(), + ctx, + )); + } + let mut data: MoveForm = body.into_form().await?; + let filename = ctx.path.file_name().unwrap().to_str().unwrap(); + + if ctx.path == Path::new(&data.dest) { + return move_form( + filename, + &data, + Some("can not move entry to itself"), + controller, + &ctx, + ); + } + + let parent_commit = ctx.branch_head()?; + let parent_tree = parent_commit.tree().unwrap(); + if parent_tree.get_path(Path::new(&data.dest)).is_ok() { + return move_form( + filename, + &data, + Some("destination already exists"), + controller, + &ctx, + ); + } + + let mut builder = TreeUpdateBuilder::new(); + let entr = parent_tree.get_path(&ctx.path)?; + builder.remove(&ctx.path); + builder.upsert( + &data.dest, + entr.id(), + match entr.filemode() { + 0o000000 => FileMode::Unreadable, + 0o040000 => FileMode::Tree, + 0o100644 => FileMode::Blob, + 0o100755 => FileMode::BlobExecutable, + 0o120000 => FileMode::Link, + _ => { + panic!("unexpected mode") + } + }, + ); + + let new_tree_id = builder.create_updated(&ctx.repo, &parent_commit.tree()?)?; + + ctx.commit( + &controller.signature(&ctx.repo, &ctx.parts)?, + &data + .msg + .take() + .filter(|m| !m.trim().is_empty()) + .unwrap_or_else(|| format!("move {} to {}", ctx.path.to_str().unwrap(), data.dest)), + &ctx.repo.find_tree(new_tree_id)?, + &[&parent_commit], + )?; + + Ok(Builder::new() + .status(StatusCode::FOUND) + .header("location", build_url_path(&ctx.branch, &data.dest)) + .body("redirecting".into()) + .unwrap()) +} + +#[derive(Deserialize)] +struct RemoveForm { + msg: Option<String>, +} + +async fn remove_entry<C: Controller>( + body: Body, + controller: &C, + ctx: Context, +) -> Result<Response, Error> { + if !controller.may_move_path(&ctx) { + return Err(Error::Unauthorized( + "you are not authorized to remove this file".into(), + ctx, + )); + } + let data: RemoveForm = body.into_form().await?; + let mut builder = TreeUpdateBuilder::new(); + builder.remove(&ctx.path); + let parent_commit = ctx.branch_head()?; + let new_tree_id = builder.create_updated(&ctx.repo, &parent_commit.tree()?)?; + + ctx.commit( + &controller.signature(&ctx.repo, &ctx.parts)?, + &data + .msg + .filter(|m| !m.trim().is_empty()) + .unwrap_or_else(|| format!("remove {}", ctx.path.to_str().unwrap())), + &ctx.repo.find_tree(new_tree_id)?, + &[&parent_commit], + )?; + Ok(Builder::new() + .status(StatusCode::FOUND) + .header( + "location", + build_url_path(&ctx.branch, ctx.path.parent().unwrap().to_str().unwrap()), + ) + .body("redirecting".into()) + .unwrap()) +} + +async fn diff_blob<C: Controller>( + body: Body, + controller: &C, + ctx: Context, +) -> Result<Response, Error> { + if !controller.may_write_path(&ctx) { + return Err(Error::Unauthorized( + "you are not authorized to edit this file".into(), + ctx, + )); + } + + let form: EditForm = body.into_form().await?; + let new_text = form.text.replace("\r\n", "\n"); + + let entr = ctx.branch_head()?.tree().unwrap().get_path(&ctx.path)?; + + let blob = ctx.repo.find_blob(entr.id()).unwrap(); + let old_text = from_utf8(blob.content())?; + + let mut page = edit_text_form(&form, None, controller, &ctx); + page.body.push_str(&diff(old_text, &new_text)); + Ok(page.into()) +} + +async fn preview_edit<C: Controller>( + body: Body, + controller: &C, + ctx: Context, +) -> Result<Response, Error> { + let form: EditForm = body.into_form().await?; + let new_text = form.text.replace("\r\n", "\n"); + let mut page = edit_text_form(&form, None, controller, &ctx); + + page.body + .push_str(&(get_renderer(&ctx.path).unwrap()(&new_text))); + Ok(page.into()) +} |