diff options
Diffstat (limited to 'pkgs/by-name/ba/back/src/web')
-rw-r--r-- | pkgs/by-name/ba/back/src/web/generate/mod.rs | 225 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/web/mod.rs | 127 | ||||
-rw-r--r-- | pkgs/by-name/ba/back/src/web/responses.rs | 50 |
3 files changed, 0 insertions, 402 deletions
diff --git a/pkgs/by-name/ba/back/src/web/generate/mod.rs b/pkgs/by-name/ba/back/src/web/generate/mod.rs deleted file mode 100644 index ae783a3..0000000 --- a/pkgs/by-name/ba/back/src/web/generate/mod.rs +++ /dev/null @@ -1,225 +0,0 @@ -use std::{fs, path::Path}; - -use gix::hash::Prefix; -use log::info; -use rinja::Template; -use url::Url; - -use crate::{ - config::BackConfig, - error, - git_bug::{ - dag::issues_from_repository, - issue::{CollapsedIssue, Status}, - }, -}; - -#[derive(Template)] -#[template(path = "./issues.html")] -struct IssuesTemplate { - wanted_status: Status, - counter_status: Status, - issues: Vec<CollapsedIssue>, - - /// The path to the repository - repo_path: String, - - /// The URL to `back`'s source code - source_code_repository_url: Url, -} -pub fn issues( - config: &BackConfig, - wanted_status: Status, - counter_status: Status, - repo_path: &Path, -) -> error::Result<String> { - let repository = config - .repositories()? - .get(repo_path)? - .open(&config.scan_path)?; - - let mut issue_list = issues_from_repository(&repository.to_thread_local())? - .into_iter() - .map(|issue| issue.collapse()) - .filter(|issue| issue.status == wanted_status) - .collect::<Vec<CollapsedIssue>>(); - - // Sort by date descending. - // SAFETY: - // The time stamp is only used for sorting, so a malicious attacker could only affect the issue - // sorting. - issue_list.sort_by_key(|issue| unsafe { issue.timestamp.to_unsafe() }); - issue_list.reverse(); - - Ok(IssuesTemplate { - wanted_status, - counter_status, - source_code_repository_url: config.source_code_repository_url.clone(), - issues: issue_list, - repo_path: repo_path.display().to_string(), - } - .render() - .expect("This should always work")) -} - -use crate::git_bug::format::HtmlString; -#[derive(Template)] -#[template(path = "./issue.html")] -struct IssueTemplate { - issue: CollapsedIssue, - - /// The path to the repository - repo_path: String, - - /// The URL to `back`'s source code - source_code_repository_url: Url, -} -pub fn issue(config: &BackConfig, repo_path: &Path, prefix: Prefix) -> error::Result<String> { - let repository = config - .repositories()? - .get(repo_path)? - .open(&config.scan_path)? - .to_thread_local(); - - let maybe_issue = issues_from_repository(&repository)? - .into_iter() - .map(|val| val.collapse()) - .find(|issue| issue.id.to_string().starts_with(&prefix.to_string())); - - match maybe_issue { - Some(issue) => Ok(IssueTemplate { - issue, - repo_path: repo_path.display().to_string(), - source_code_repository_url: config.source_code_repository_url.clone(), - } - .render() - .expect("This should always work")), - None => Err(error::Error::IssuesPrefixMissing { prefix }), - } -} - -#[derive(Template)] -#[template(path = "./repos.html")] -struct ReposTemplate { - repos: Vec<RepoValue>, - - /// The URL to `back`'s source code - source_code_repository_url: Url, -} -struct RepoValue { - description: String, - owner: String, - path: String, -} -pub fn repos(config: &BackConfig) -> error::Result<String> { - let repos: Vec<RepoValue> = config - .repositories()? - .iter() - .filter_map(|raw_repo| match raw_repo.open(&config.scan_path) { - Ok(repo) => { - let repo = repo.to_thread_local(); - let git_config = repo.config_snapshot(); - - let path = raw_repo.path().to_string_lossy().to_string(); - - let owner = git_config - .string("cgit.owner") - .map(|v| v.to_string()) - .unwrap_or("<No owner>".to_owned()); - - let description = fs::read_to_string(repo.git_dir().join("description")) - .unwrap_or("<No description>".to_owned()); - - Some(RepoValue { - description, - owner, - path, - }) - } - Err(err) => { - info!( - "Repo '{}' could not be opened: '{err}'", - raw_repo.path().display() - ); - None - } - }) - .collect(); - - Ok(ReposTemplate { - repos, - source_code_repository_url: config.source_code_repository_url.clone(), - } - .render() - .expect("this should work")) -} - -pub fn feed(config: &BackConfig, repo_path: &Path) -> error::Result<String> { - use rss::{ChannelBuilder, Item, ItemBuilder}; - - let repository = config - .repositories()? - .get(repo_path)? - .open(&config.scan_path)? - .to_thread_local(); - - let issues: Vec<CollapsedIssue> = issues_from_repository(&repository)? - .into_iter() - .map(|issue| issue.collapse()) - .collect(); - - // Collect all Items as rss items - let mut items: Vec<Item> = issues - .iter() - .map(|issue| { - ItemBuilder::default() - .title(issue.title.to_string()) - .author(issue.author.to_string()) - .description(issue.message.to_string()) - .pub_date(issue.timestamp.to_string()) - .link(format!( - "/{}/{}/issue/{}", - repo_path.display(), - &config.root_url, - issue.id - )) - .build() - }) - .collect(); - - // Append all comments after converting them to rss items - items.extend( - issues - .iter() - .filter(|issue| !issue.comments.is_empty()) - .flat_map(|issue| { - issue - .comments - .iter() - .map(|comment| { - ItemBuilder::default() - .title(issue.title.to_string()) - .author(comment.author.to_string()) - .description(comment.message.to_string()) - .pub_date(comment.timestamp.to_string()) - .link(format!( - "/{}/{}/issue/{}", - repo_path.display(), - &config.root_url, - issue.id - )) - .build() - }) - .collect::<Vec<Item>>() - }) - .collect::<Vec<Item>>(), - ); - - let channel = ChannelBuilder::default() - .title("Issues") - .link(config.root_url.to_string()) - .description(format!("The rss feed for issues on {}.", &config.root_url)) - .items(items) - .build(); - Ok(channel.to_string()) -} diff --git a/pkgs/by-name/ba/back/src/web/mod.rs b/pkgs/by-name/ba/back/src/web/mod.rs deleted file mode 100644 index cc087ab..0000000 --- a/pkgs/by-name/ba/back/src/web/mod.rs +++ /dev/null @@ -1,127 +0,0 @@ -use bytes::Bytes; -use http_body_util::combinators::BoxBody; -use hyper::{server::conn::http1, service::service_fn, Method, Request, Response, StatusCode}; -use hyper_util::rt::TokioIo; -use log::{error, info}; -use responses::{html_response, html_response_status, html_response_status_content_type}; -use tokio::net::TcpListener; - -use std::{convert::Infallible, net::SocketAddr, path::PathBuf, sync::Arc}; - -use crate::{config::BackConfig, error, git_bug::issue::Status}; - -mod generate; -mod responses; - -async fn match_uri( - config: Arc<BackConfig>, - req: Request<hyper::body::Incoming>, -) -> Result<Response<BoxBody<Bytes, Infallible>>, hyper::Error> { - if req.method() != Method::GET { - return Ok(html_response_status( - "Only get requests are supported", - StatusCode::NOT_ACCEPTABLE, - )); - } - - let output = || -> Result<Response<BoxBody<Bytes, Infallible>>, error::Error> { - match req.uri().path().trim_end_matches("/") { - "" => Ok(html_response(generate::repos(&config)?)), - - "/style.css" => Ok(responses::html_response_status_content_type( - include_str!("../../assets/style.css"), - StatusCode::OK, - "text/css", - )), - - path if path.ends_with("/issues/open") => { - let repo_path = PathBuf::from( - path.strip_suffix("/issues/open") - .expect("This suffix exists") - .strip_prefix("/") - .expect("This also exists"), - ); - - let issues = generate::issues(&config, Status::Open, Status::Closed, &repo_path)?; - Ok(html_response(issues)) - } - path if path.ends_with("/issues/closed") => { - let repo_path = PathBuf::from( - path.strip_suffix("/issues/closed") - .expect("This suffix exists") - .strip_prefix("/") - .expect("This also exists"), - ); - - let issues = generate::issues(&config, Status::Closed, Status::Open, &repo_path)?; - Ok(html_response(issues)) - } - path if path.ends_with("/issues/feed") => { - let repo_path = PathBuf::from( - path.strip_suffix("/issues/feed") - .expect("This suffix exists") - .strip_prefix("/") - .expect("This also exists"), - ); - - let feed = generate::feed(&config, &repo_path)?; - Ok(html_response_status_content_type( - feed, - StatusCode::OK, - "text/xml", - )) - } - - path if path.contains("/issue/") => { - let (repo_path, prefix) = { - let split: Vec<&str> = path.split("/issue/").collect(); - - let prefix = - gix::hash::Prefix::from_hex(split[1]).map_err(error::Error::from)?; - - let repo_path = - PathBuf::from(split[0].strip_prefix("/").expect("This prefix exists")); - - (repo_path, prefix) - }; - Ok(html_response(generate::issue(&config, &repo_path, prefix)?)) - } - - other => Ok(responses::html_response_status_content_type( - format!("'{}' not found", other), - StatusCode::NOT_FOUND, - "text/plain", - )), - } - }; - match output() { - Ok(response) => Ok(response), - Err(err) => Ok(err.into_response()), - } -} - -pub async fn main(config: Arc<BackConfig>) -> Result<(), error::Error> { - let addr: SocketAddr = ([127, 0, 0, 1], 8000).into(); - - let listener = TcpListener::bind(addr) - .await - .map_err(|err| error::Error::TcpBind { addr, err })?; - info!("Listening on http://{}", addr); - loop { - let (stream, _) = listener - .accept() - .await - .map_err(|err| error::Error::TcpAccept { err })?; - let io = TokioIo::new(stream); - - let local_config = Arc::clone(&config); - - let service = service_fn(move |req| match_uri(Arc::clone(&local_config), req)); - - tokio::task::spawn(async move { - if let Err(err) = http1::Builder::new().serve_connection(io, service).await { - error!("Error serving connection: {:?}", err); - } - }); - } -} diff --git a/pkgs/by-name/ba/back/src/web/responses.rs b/pkgs/by-name/ba/back/src/web/responses.rs deleted file mode 100644 index e50f8c2..0000000 --- a/pkgs/by-name/ba/back/src/web/responses.rs +++ /dev/null @@ -1,50 +0,0 @@ -use std::convert::Infallible; - -use bytes::Bytes; -use http::{Response, StatusCode, Version}; -use http_body_util::{combinators::BoxBody, BodyExt, Full}; - -use crate::{error, git_bug::format::HtmlString}; - -pub(super) fn html_response<T: Into<Bytes>>(html_text: T) -> Response<BoxBody<Bytes, Infallible>> { - html_response_status(html_text, StatusCode::OK) -} - -pub(super) fn html_response_status<T: Into<Bytes>>( - html_text: T, - status: StatusCode, -) -> Response<BoxBody<Bytes, Infallible>> { - html_response_status_content_type(html_text, status, "text/html") -} - -pub(super) fn html_response_status_content_type<T: Into<Bytes>>( - html_text: T, - status: StatusCode, - content_type: &str, -) -> Response<BoxBody<Bytes, Infallible>> { - Response::builder() - .status(status) - .version(Version::HTTP_2) - .header("Content-Type", format!("{}; charset=utf-8", content_type)) - .header("x-content-type-options", "nosniff") - .header("x-frame-options", "SAMEORIGIN") - .body(full(html_text)) - .expect("This will always build") -} - -fn full<T: Into<Bytes>>(chunk: T) -> BoxBody<Bytes, Infallible> { - Full::new(chunk.into()).boxed() -} - -// FIXME: Not all errors should return `INTERNAL_SERVER_ERROR`. <2025-03-08> -impl error::Error { - pub fn into_response(self) -> Response<BoxBody<Bytes, Infallible>> { - html_response_status( - format!( - "<h1> Internal server error. </h1> <pre>Error: {}</pre>", - HtmlString::from(self.to_string()) - ), - StatusCode::INTERNAL_SERVER_ERROR, - ) - } -} |