git-gemini-forge 0.6.2

A simple Gemini server that serves a read-only view of public repositories from a Git forge.
use super::templates::PercentEncoded;
use crate::config::*;
use crate::network;
use crate::network::net::Net;
use crate::network::responses::Contents;
use windmark::response::Response;
// use data_encoding::BASE64;

/// Answers with Gemtext that consists of the contents of the file
/// or directory at the given path.
pub fn handler(
	cfg: &Config,
	net: &Net,
	username: PercentEncoded,
	repo_name: PercentEncoded,
	branch_name: PercentEncoded,
	item_path: PercentEncoded,
) -> Result<Response, network::error::Error> {
	let repo = &cfg.forge_api().get_repo(net, &username, &repo_name)?;
	let kind = cfg.forge_kind.to_string();

	// Figure out what we're looking at
	let contents =
		cfg.forge_api()
			.get_contents(net, &username, &repo_name, &branch_name, &item_path)?;

	match contents {
		Contents::Multiple(contents) => {
			// Transform contents into Gemtext internal page links
			let contents_list: Vec<String> = contents
				.iter()
				.map(|f| super::templates::file_link(&repo, Some(&branch_name), &f))
				.collect();

			// Format contents into a Gemtext list
			let gemtext_tree_list: String = contents_list
				.iter()
				.map(|f| format!("{f}\n"))
				.collect::<Vec<String>>()
				.concat();

			let segmented = item_path.uri_component_retaining_sep();
			let is_at_branch = segmented.len() <= 1;
			let mut segments: Vec<&str> = segmented.split("/").collect();
			segments.pop();
			let parent_segmented = segments.join("/");
			let back = if is_at_branch {
				String::new()
			} else if segments.is_empty() {
				format!(
					"=> /{}/{}/src/branch/{} ⮢ ..\n",
					username.uri_component(),
					repo_name.uri_component(),
					branch_name.uri_component()
				)
			} else {
				format!(
					"=> /{}/{}/src/branch/{}/{} ⮢ ..\n",
					username.uri_component(),
					repo_name.uri_component(),
					branch_name.uri_component(),
					parent_segmented
				)
			};

			let contents_http_url = {
				let mut repo_http_url = repo.html_url.clone();
				if !repo_http_url.path().ends_with("/") {
					// Make sure path ends with a slash so the join works correctly
					repo_http_url.set_path(&format!("{}/", repo_http_url.path()));
				}
				repo_http_url
					.join(&format!(
						"src/branch/{}/{}",
						branch_name.uri_component(),
						segmented
					))
					// Use repo URL if we can't parse a path one
					// TODO: We can do better...
					.unwrap_or_else(|_| repo.html_url.clone())
			};
			let contents_http = format!("\n\n=> {contents_http_url} View on {kind}");

			let gemtext = format!(
				"=> /{}/{} Back to {}/{}

## Tree for branch {}
/{}
{back}{gemtext_tree_list}{contents_http}",
				username.uri_component(),
				repo_name.uri_component(),
				username.display(),
				repo_name.display(),
				branch_name.display(),
				item_path.display(),
			);
			Ok(Response::success(gemtext))
		}
		Contents::Single(_file) => {
			Ok(Response::temporary_failure(
				"This route is not implemented yet",
			))

			// TODO: Show file metadata
			// TODO: Render Gemtext (fix internal links to be relative to this repository)
			// TODO: Query param for raw file (in the case of Gemtext and other formats we might later decide to render)

			/*
			let path = file.path();

			let size = match file {
				Item::Dir { size, .. } => format!("{}", size.display().si()),
				Item::File { size, .. } => format!("{}", size.display().si()),
				Item::Submodule { .. } | Item::Symlink { .. } => String::new(),
			};

			let content: &str = match &file {
				Item::File {
					content, encoding, ..
				} => match encoding {
					None => &format!("size: {size}"),
					Some(encoding) => match content {
						None => &format!("size: {size}"),
						Some(content) => &decode(content, encoding), // TODO: Send this as text/plain (or text/gemini if path ends in .gmi)
					},
				},
				_ => &format!("size: {size}"),
			};

			if path.ends_with(".gmi") {
				Ok(Response::success(content))
			} else {
				Ok(Response::binary_success(content, "text/plain"))
			}
			*/
		}
	}
}

///// Decodes the given data.
// fn decode(data: &String, encoding: &FileEncoding) -> String {
// 	match encoding {
// 		FileEncoding::Base64 => {
// 			let input = data.as_bytes();
// 			let result = BASE64.decode(input)?;
// 			String::from_utf8(result)?
// 		}
// 	}
// }