fluffer 4.0.2

๐ŸฆŠ Fluffer is a fun and experimental gemini server framework.
Documentation
use crate::{async_trait, GemBytes};
use std::{fs::File, io::Read};

/// ๐Ÿฐ A general-purpose implementation of GemBytes.
pub enum Fluff {
	/// (20,51,50): Send file at `./static/{path}`.
	///
	/// The file's mimetype is guessed using the [`mime_guess`] crate.
	File(String),

	/// (10)
	Input(String),

	/// (11)
	InputSensitive(String),

	/// (51)
	NotFound(String),

	// (30)
	RedirectTemporary(String),

	// (31)
	RedirectPermanent(String),

	/// (30): Redirect to `path` with a list of query strings.
	RedirectQueries {
		path: String,
		queries: Vec<(String, String)>,
	},

	/// (30): Same as [`Fluff::RedirectQueries`], except with a single query string.
	RedirectQuery {
		path: String,
		query: String,
	},

	// (40): Temporary failure.
	FailureTemporary(String),

	// (50): Permanent failure.
	FailurePermanent(String),

	/// (30): Redirect to `..`
	GoUp,

	/// (20): A gemtext document with a language parameter.
	Lang {
		lang: String,
		body: String,
	},

	/// (20): Text as a specific mimetype.
	Document {
		mime: String,
		body: String,
	},

	/// (20): Gemtext.
	Text(String),

	/// (20): Wait 10 seconds, and send a test response.
	///
	/// Only useful for debugging and trolling people.
	DebugWait,
}

#[async_trait]
impl GemBytes for Fluff {
	async fn gem_bytes(self) -> Vec<u8> {
		match self {
			Fluff::File(path) => {
				// Sanitize path
				let path = format!("static/{}", sanitize_filename::sanitize(path));

				// Open file
				let Ok(mut file) = File::open(&path) else {
					return "51 File not found.\r\n".into();
				};

				// Guess mimetype
				let mimetype = match mime_guess::from_path(path).first() {
					Some(m) => m,
					None => return "50 File mimetype could not be guessed.\r\n".into(),
				};

				// Write file bytes
				let mut v: Vec<u8> = Vec::new();
				match file.read_to_end(&mut v) {
					Ok(_) => {
						let mut v2 = format!("20 {mimetype}\r\n").into_bytes();
						v2.append(&mut v);
						v2
					}
					Err(e) => {
						trace!("File read error: {e}");
						"50 File read error.\r\n".into()
					}
				}
			}
			Fluff::NotFound(s) => format!("51 {s}\r\n").into(),
			Fluff::Input(s) => format!("10 {s}\r\n").into(),
			Fluff::InputSensitive(s) => format!("11 {s}\r\n").into(),
			Fluff::RedirectTemporary(path) => format!("30 {path}\r\n").into(),
			Fluff::RedirectPermanent(path) => format!("31 {path}\r\n").into(),
			Fluff::RedirectQueries { path, queries } => {
				// Using dummy address that always works, because we only care about the query
				if let Ok(url) = url::Url::parse_with_params("https://example.com", queries) {
					format!(
						"30 {path}?{}\r\n",
						url.query().unwrap_or("").replace('+', "%20")
					)
				} else {
					format!("30 {path}\r\n")
				}
				.into()
			}
			Fluff::RedirectQuery { path, query } => {
				format!("30 {path}?{}\r\n", urlencoding::encode(&query)).into()
			}
			Fluff::FailureTemporary(s) => format!("40 {s}\r\n").into(),
			Fluff::FailurePermanent(s) => format!("50 {s}\r\n").into(),
			Fluff::GoUp => "30 ..\r\n".into(),
			Fluff::Lang { lang, body } => format!("20 text/gemini; lang={lang}\r\n{body}").into(),
			Fluff::Document { mime, body } => format!("20 {mime}\r\n{body}").into(),
			Fluff::Text(s) => format!("20 text/gemini\r\n{s}").into(),
			Fluff::DebugWait => {
				std::thread::sleep(std::time::Duration::from_secs(10));
				"20 text/gemini\r\n๐Ÿงต Waited 10 seconds!\n".into()
			}
		}
	}
}