aboutsummaryrefslogtreecommitdiff
path: root/src/post_routes.rs
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2021-06-23 14:12:02 +0200
committerMartin Fischer <martin@push-f.com>2021-06-23 14:36:17 +0200
commit3ed32ef268b54965c97b13efdf24a1539c4ec1c1 (patch)
treea0f80b097339693763a0bce2896c083c1000c6cc /src/post_routes.rs
parent086e0e30e47f796a12809036dfc787490904a8ae (diff)
refactor: split up main.rs into multiple modules
Diffstat (limited to 'src/post_routes.rs')
-rw-r--r--src/post_routes.rs354
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())
+}