Compare commits
20
Commits
a2ac39db43
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b4cdb95ef
|
||
|
|
014d258a45
|
||
|
|
f3e765b578
|
||
|
|
b76c650874
|
||
|
|
f173393f19
|
||
|
|
ca7691ee18
|
||
|
|
faef0b0c86
|
||
|
|
4fe0f35c9e
|
||
|
|
079cfc7a64
|
||
|
|
b11134c754
|
||
|
|
5940e203b9
|
||
|
|
ec19975538
|
||
|
|
713edfb4cf
|
||
|
|
4d33e4fe43
|
||
|
|
5d0b1ab208
|
||
|
|
58d922b062
|
||
|
|
08330633fb
|
||
|
|
bfe0f07f4b
|
||
|
|
ac86e4a13b
|
||
|
|
1f8eed80c7
|
+13
-1
@@ -1,4 +1,16 @@
|
|||||||
/* martian theme */
|
/*
|
||||||
|
* Copyright (c) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||||
|
* SPDX-License-Identifier: FSFAP
|
||||||
|
*
|
||||||
|
* This file is part of Mintee.
|
||||||
|
*
|
||||||
|
* Copying and distribution of this file, with or without modification, are
|
||||||
|
* permitted in any medium without royalty provided the copyright notice and
|
||||||
|
* this notice are preserved. This file is offered as-is, without any
|
||||||
|
* warranty.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* arctica theme */
|
||||||
:root {
|
:root {
|
||||||
--glass: url("frost.webp"), #24293370;
|
--glass: url("frost.webp"), #24293370;
|
||||||
--background: #242933;
|
--background: #242933;
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||||
|
* SPDX-License-Identifier: FSFAP
|
||||||
|
*
|
||||||
|
* This file is part of Mintee.
|
||||||
|
*
|
||||||
|
* Copying and distribution of this file, with or without modification, are
|
||||||
|
* permitted in any medium without royalty provided the copyright notice and
|
||||||
|
* this notice are preserved. This file is offered as-is, without any
|
||||||
|
* warranty.
|
||||||
|
*/
|
||||||
|
|
||||||
/* martian theme */
|
/* martian theme */
|
||||||
:root {
|
:root {
|
||||||
--glass: url("frost.webp"), hsla(0, 20%, 7%, 0.5);
|
--glass: url("frost.webp"), hsla(0, 20%, 7%, 0.5);
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||||
|
* SPDX-License-Identifier: FSFAP
|
||||||
|
*
|
||||||
|
* This file is part of Mintee.
|
||||||
|
*
|
||||||
|
* Copying and distribution of this file, with or without modification, are
|
||||||
|
* permitted in any medium without royalty provided the copyright notice and
|
||||||
|
* this notice are preserved. This file is offered as-is, without any
|
||||||
|
* warranty.
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
/* mint theme */
|
/* mint theme */
|
||||||
:root {
|
:root {
|
||||||
--glass: url("frost.webp"), #33333377;
|
--glass: url("frost.webp"), #33333377;
|
||||||
|
|||||||
+19
-2
@@ -1,5 +1,21 @@
|
|||||||
/* mint theme */
|
/*
|
||||||
:root {
|
* Copyright (c) 2022, 2025 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||||
|
* Copyright (c) 2026 Emma Tebibyte <sashakoshka@tebibyte.media>
|
||||||
|
* SPDX-License-Identifier: FSFAP
|
||||||
|
*
|
||||||
|
* This file is part of Mintee.
|
||||||
|
*
|
||||||
|
* Copying and distribution of this file, with or without modification, are
|
||||||
|
* permitted in any medium without royalty provided the copyright notice and
|
||||||
|
* this notice are preserved. This file is offered as-is, without any
|
||||||
|
* warranty.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@import url("/res/mint-dark.css") (prefers-color-scheme: dark);
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
/* mint theme */
|
||||||
|
:root {
|
||||||
--glass: #EEEEEE77;
|
--glass: #EEEEEE77;
|
||||||
--background: #EEE;
|
--background: #EEE;
|
||||||
--control: #FFF4;
|
--control: #FFF4;
|
||||||
@@ -12,4 +28,5 @@
|
|||||||
--subtler-accent: #209D6E12;
|
--subtler-accent: #209D6E12;
|
||||||
--shadow: 0 4px 32px #03281A12;
|
--shadow: 0 4px 32px #03281A12;
|
||||||
--icon-filter: hue-rotate(157deg) saturate(0.80) brightness(1.3);
|
--icon-filter: hue-rotate(157deg) saturate(0.80) brightness(1.3);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This file is part of Mintee.
|
||||||
|
*
|
||||||
|
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
scrollbar-color: var(--accent) transparent;
|
scrollbar-color: var(--accent) transparent;
|
||||||
|
|||||||
@@ -28,7 +28,10 @@
|
|||||||
<meta content="width=device-width, initial-scale=1" name="viewport">
|
<meta content="width=device-width, initial-scale=1" name="viewport">
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<link rel="stylesheet" href="res/style.css">
|
<link rel="stylesheet" href="res/style.css">
|
||||||
<link rel="stylesheet" href="res/mint.css">
|
<link rel="stylesheet" href="res/mint.css" title="Mint">
|
||||||
|
<link rel="alternate stylesheet" href="res/mint-dark.css" title="Mint Dark">
|
||||||
|
<link rel="alternate stylesheet" href="res/arctica.css" title="Arctica">
|
||||||
|
<link rel="alternate stylesheet" href="res/martian.css" title="Martian">
|
||||||
<script>
|
<script>
|
||||||
/*
|
/*
|
||||||
@licstart The following is the entire license notice for the
|
@licstart The following is the entire license notice for the
|
||||||
@@ -1,5 +1,26 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{#
|
||||||
|
Copyright (c) 2022 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||||
|
Copyright (c) 2023,2025 Emma Tebibyte <emma@tebibyte.media>
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This file is part of Mintee.
|
||||||
|
|
||||||
|
Mintee is free software: you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU Affero General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||||
|
#}
|
||||||
|
|
||||||
{% block page %}code{% endblock page %}
|
{% block page %}code{% endblock page %}
|
||||||
|
|
||||||
{% block breadcrumbs %}
|
{% block breadcrumbs %}
|
||||||
@@ -1,5 +1,26 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{#
|
||||||
|
Copyright (c) 2025 Sasha Koshka <sashakoshka@tebibyte.media>
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
This file is part of Mintee.
|
||||||
|
|
||||||
|
Mintee is free software: you can redistribute it and/or modify it under the
|
||||||
|
terms of the GNU Affero General Public License as published by the Free
|
||||||
|
Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
any later version.
|
||||||
|
|
||||||
|
Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||||
|
#}
|
||||||
|
|
||||||
|
|
||||||
{% block page %}tickets{% endblock page %}
|
{% block page %}tickets{% endblock page %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[toolchain]
|
|
||||||
channel = "nightly"
|
|
||||||
+65
-39
@@ -21,9 +21,8 @@
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
|
|
||||||
use gix::{
|
use gix::{
|
||||||
Repository,
|
Object,
|
||||||
Tree,
|
Tree,
|
||||||
object::tree::EntryKind,
|
|
||||||
revision::walk::Sorting,
|
revision::walk::Sorting,
|
||||||
};
|
};
|
||||||
use gix_traverse::commit::simple::CommitTimeOrder;
|
use gix_traverse::commit::simple::CommitTimeOrder;
|
||||||
@@ -31,71 +30,102 @@ use serde::Serialize;
|
|||||||
use tera::Context;
|
use tera::Context;
|
||||||
use time_format::format_iso8601_utc;
|
use time_format::format_iso8601_utc;
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Default, Serialize)]
|
||||||
|
/// Directory Entry inside git
|
||||||
struct Entry {
|
struct Entry {
|
||||||
|
/// Kind of this entry; file or directory
|
||||||
class: String,
|
class: String,
|
||||||
|
/// Latest commit for this entry
|
||||||
last_commit: String,
|
last_commit: String,
|
||||||
|
/// Message of the latest commit for this entry
|
||||||
last_commit_message: String,
|
last_commit_message: String,
|
||||||
|
/// Time of the latest commit for this entry
|
||||||
last_commit_time: String,
|
last_commit_time: String,
|
||||||
|
/// Path to this entry
|
||||||
path: String,
|
path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// return a vector of entries from any given git Tree
|
||||||
fn get_entries(tree: Tree) -> Result<Vec<Entry>, Box<dyn Error>> {
|
fn get_entries(tree: Tree) -> Result<Vec<Entry>, Box<dyn Error>> {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
let order = Sorting::ByCommitTime(CommitTimeOrder::NewestFirst);
|
let order = Sorting::ByCommitTime(CommitTimeOrder::NewestFirst);
|
||||||
|
|
||||||
for e in tree.iter() {
|
for e in tree.iter() {
|
||||||
let entry = e.unwrap();
|
let entry = e?;
|
||||||
let path = entry.filename().to_string();
|
let mut entry_t = Entry::default();
|
||||||
let mut last_commit = String::new();
|
entry_t.path = entry.filename().to_string();
|
||||||
let mut last_commit_message = String::new();
|
|
||||||
let mut last_commit_time = String::new();
|
|
||||||
|
|
||||||
let class = match entry.kind() {
|
use gix::object::tree::EntryKind::*;
|
||||||
EntryKind::Tree | EntryKind::Commit => "directory",
|
entry_t.class = match entry.kind() {
|
||||||
|
Tree | Commit => "directory",
|
||||||
_ => "file",
|
_ => "file",
|
||||||
}.to_owned();
|
}.to_owned();
|
||||||
|
|
||||||
|
/* TODO: rewrite to not assume we are at the root of the repository, to
|
||||||
|
* compare entries in each commit to determine the last commit to touch
|
||||||
|
* the file, and to get the last commit to touch any file in a subtree
|
||||||
|
* and use that as the commit for that subtree */
|
||||||
for i in tree.repo.head_commit()?.ancestors().sorting(order).all()? {
|
for i in tree.repo.head_commit()?.ancestors().sorting(order).all()? {
|
||||||
let commit = i?.id().object()?.peel_to_commit()?;
|
let commit = i?.id().object()?.peel_to_commit()?;
|
||||||
let path_slice: &[u8] = path.as_ref();
|
let path_slice: &[u8] = entry_t.path.as_ref();
|
||||||
|
|
||||||
if commit.tree()?.lookup_entry([path_slice])?.is_some() {
|
if commit.tree()?.lookup_entry([path_slice])?.is_some() {
|
||||||
last_commit = commit.short_id()?.to_string();
|
entry_t.last_commit = commit.id.to_string();
|
||||||
last_commit_message = commit.message_raw()?.to_string();
|
entry_t.last_commit_message = commit
|
||||||
last_commit_time = format_iso8601_utc(commit.time()?.seconds)?;
|
.message_raw()?
|
||||||
|
.to_string();
|
||||||
|
entry_t.last_commit_time = format_iso8601_utc(
|
||||||
|
commit.time()?.seconds
|
||||||
|
)?;
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entries.push(Entry {
|
entries.push(entry_t);
|
||||||
class,
|
|
||||||
last_commit,
|
|
||||||
last_commit_message,
|
|
||||||
last_commit_time,
|
|
||||||
path,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(entries)
|
Ok(entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn repo_to_context(r: Repository) -> Result<Context, Box<dyn Error>> {
|
/// Converts a git type to a Tera Context
|
||||||
let branches = r.clone().branch_names()
|
pub trait ToContext {
|
||||||
|
type Error;
|
||||||
|
fn to_context(&self) -> Result<Context, Self::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Implementation of git Object to Tera Context
|
||||||
|
impl ToContext for Object<'_> {
|
||||||
|
type Error = Box<dyn Error>;
|
||||||
|
|
||||||
|
fn to_context(&self) -> Result<Context, Self::Error> {
|
||||||
|
let mut ctx = Context::new();
|
||||||
|
let current_branch = self.repo
|
||||||
|
.head()?
|
||||||
|
.referent_name()
|
||||||
|
.unwrap()
|
||||||
|
.shorten()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
use gix::object::Kind::*;
|
||||||
|
match self.kind {
|
||||||
|
Blob => todo!("need sasha to make a file page"),
|
||||||
|
Commit | Tag => todo!("need sasha to make a commit page"),
|
||||||
|
Tree => {
|
||||||
|
let tree = self.clone().peel_to_tree()?;
|
||||||
|
let entries = get_entries(tree)?;
|
||||||
|
let repo = self.repo;
|
||||||
|
let dir = repo.common_dir();
|
||||||
|
let name = dir.file_name().unwrap().to_str().unwrap();
|
||||||
|
let branches = repo.clone().branch_names()
|
||||||
.iter()
|
.iter()
|
||||||
.map(|x| x.to_owned().to_owned())
|
.map(|x| x.to_owned().to_owned())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
/* @ is HEAD */
|
/*
|
||||||
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
|
|
||||||
|
|
||||||
/* replace with configurable branch name when we have a database */
|
|
||||||
let current_branch = r.head()?.referent_name().unwrap().shorten().to_string();
|
|
||||||
|
|
||||||
let entries = get_entries(tree)?;
|
|
||||||
|
|
||||||
let mut readme_content = String::new();
|
let mut readme_content = String::new();
|
||||||
|
|
||||||
/*
|
|
||||||
/* TODO: determine what file to use as README better */
|
/* TODO: determine what file to use as README better */
|
||||||
let readme_candidates = entries.iter().filter(|e| {
|
let readme_candidates = entries.iter().filter(|e| {
|
||||||
match e.path.as_str().to_uppercase().as_str() {
|
match e.path.as_str().to_uppercase().as_str() {
|
||||||
@@ -113,12 +143,6 @@ pub fn repo_to_context(r: Repository) -> Result<Context, Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let dir = r.common_dir();
|
|
||||||
let name = dir.file_name().unwrap().to_str().unwrap();
|
|
||||||
|
|
||||||
|
|
||||||
let mut ctx = Context::new();
|
|
||||||
|
|
||||||
/* stubs til we have a real database */
|
/* stubs til we have a real database */
|
||||||
ctx.insert("user", "anon");
|
ctx.insert("user", "anon");
|
||||||
ctx.insert("site", "TiB.");
|
ctx.insert("site", "TiB.");
|
||||||
@@ -134,6 +158,8 @@ pub fn repo_to_context(r: Repository) -> Result<Context, Box<dyn Error>> {
|
|||||||
ctx.insert("branch", ¤t_branch);
|
ctx.insert("branch", ¤t_branch);
|
||||||
ctx.insert("repo", name);
|
ctx.insert("repo", name);
|
||||||
ctx.insert("entries", &entries);
|
ctx.insert("entries", &entries);
|
||||||
|
},
|
||||||
|
};
|
||||||
Ok(ctx)
|
Ok(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,47 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2026 Emma Tebibyte <emma@tebibyte.media>
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
*
|
||||||
|
* This file is part of Mintee.
|
||||||
|
*
|
||||||
|
* Mintee is free software: you can redistribute it and/or modify it under the
|
||||||
|
* terms of the GNU Affero General Public License as published by the Free
|
||||||
|
* Software Foundation, either version 3 of the License, or (at your option)
|
||||||
|
* any later version.
|
||||||
|
*
|
||||||
|
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||||
|
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
||||||
|
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
||||||
|
* details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||||
|
*/
|
||||||
|
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod render;
|
pub mod render;
|
||||||
|
|
||||||
|
const MINTEE_DIR: &str = "/var/mintee";
|
||||||
|
const MINTEE_ASSETS: &str = "/usr/local/share/mintee";
|
||||||
|
|
||||||
|
pub mod srv {
|
||||||
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fs::File,
|
||||||
|
io::{ BufReader, BufWriter, Write, copy },
|
||||||
|
path::Path,
|
||||||
|
};
|
||||||
|
use crate::backend::MINTEE_ASSETS;
|
||||||
|
|
||||||
|
pub fn get_res<T, W>(path: T, dest: &mut W) -> Result<(), Box<dyn Error>>
|
||||||
|
where T: AsRef<Path>, W: Write {
|
||||||
|
let path_absolute = Path::new(MINTEE_ASSETS).join("res").join(path);
|
||||||
|
|
||||||
|
let mut reader = BufReader::new(File::open(path_absolute)?);
|
||||||
|
let mut writer = BufWriter::new(dest);
|
||||||
|
|
||||||
|
copy(&mut reader, &mut writer)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+151
-23
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Emma Tebibyte <emma@tebibyte.media>
|
* Copyright (c) 2025–2026 Emma Tebibyte <emma@tebibyte.media>
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* This file is part of Mintee.
|
* This file is part of Mintee.
|
||||||
@@ -21,32 +21,77 @@
|
|||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
error::Error,
|
||||||
fmt::{ self, Display, Formatter },
|
fmt::{ self, Display, Formatter },
|
||||||
path::Path,
|
io::Write,
|
||||||
|
path::PathBuf,
|
||||||
};
|
};
|
||||||
|
|
||||||
use tera::{ Context, Tera };
|
use gix::open;
|
||||||
|
use tera::Tera;
|
||||||
|
|
||||||
#[non_exhaustive]
|
use crate::backend::{
|
||||||
pub enum PageKind {
|
MINTEE_DIR,
|
||||||
Code,
|
MINTEE_ASSETS,
|
||||||
Dashboard,
|
git::ToContext,
|
||||||
Tickets,
|
};
|
||||||
User,
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
/// A revision for which a repository page may be rendered
|
||||||
|
pub enum Revision {
|
||||||
|
Branch(String),
|
||||||
|
Commit(String),
|
||||||
|
Tag(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PageKind {
|
#[derive(Debug, Clone)]
|
||||||
pub fn render_page(&self, ctx: Context) -> Result<String, Box<dyn Error>> {
|
/// A type representing the git repository requested from the frontend
|
||||||
let page_dir = self.to_string();
|
pub struct Repo {
|
||||||
let template = String::from_utf8(Path::new(&page_dir)
|
/// The user or project this repository is associated with
|
||||||
.to_path_buf()
|
entity: String,
|
||||||
.as_mut_os_string()
|
/// The name of this repository (equivalent to its path under its entity)
|
||||||
.as_encoded_bytes()
|
name: String,
|
||||||
.to_vec()
|
/// The subtree of the repository the page should be rendered for
|
||||||
)?;
|
path: Option<String>,
|
||||||
|
/// The revision name that should be checked out when rendering pages
|
||||||
|
revision: Option<Revision>,
|
||||||
|
}
|
||||||
|
|
||||||
let tera = Tera::new(&page_dir)?;
|
impl Repo {
|
||||||
Ok(tera.render(&template, &ctx)?)
|
/// Instanatiate a new Repo
|
||||||
|
pub fn new(
|
||||||
|
entity: String,
|
||||||
|
name: String,
|
||||||
|
revision: Option<Revision>,
|
||||||
|
path: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
entity,
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
revision,
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
/// Type of page
|
||||||
|
pub enum PageKind {
|
||||||
|
/// Main homepage of the site
|
||||||
|
Dashboard,
|
||||||
|
/// Project page
|
||||||
|
Project,
|
||||||
|
/// Repository page
|
||||||
|
Repo(Repo),
|
||||||
|
/// Settings page for a user; Should not be used if there is no
|
||||||
|
/// authenticated user
|
||||||
|
Settings,
|
||||||
|
/// Tickets page for a repository
|
||||||
|
Tickets(Repo),
|
||||||
|
/// User page
|
||||||
|
User,
|
||||||
|
/* TODO: silt exports a generic frontend non-success status trait */
|
||||||
|
/// Page does not exist or is not well-formed
|
||||||
|
Invalid,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for PageKind {
|
impl Display for PageKind {
|
||||||
@@ -54,12 +99,95 @@ impl Display for PageKind {
|
|||||||
use PageKind::*;
|
use PageKind::*;
|
||||||
|
|
||||||
let path = match self {
|
let path = match self {
|
||||||
Code => "repo/code.html",
|
|
||||||
Dashboard => "dashboard.html",
|
Dashboard => "dashboard.html",
|
||||||
Tickets => "repo/tickets.html",
|
Project => "project.html",
|
||||||
|
Repo(_) => "repo/repo.html",
|
||||||
|
Settings => "user/settings.html",
|
||||||
|
Tickets(_) => "repo/tickets.html",
|
||||||
User => "user.html",
|
User => "user.html",
|
||||||
|
Invalid => todo!("generate error pages based on error type"),
|
||||||
};
|
};
|
||||||
|
|
||||||
write!(f, "./assets/templates/{}", path)
|
write!(f, "{}", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Page {
|
||||||
|
/// Type of page
|
||||||
|
kind: PageKind,
|
||||||
|
/// Currently-authenticated user for whose perspective pages will be
|
||||||
|
/// rendered
|
||||||
|
user: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Page {
|
||||||
|
/// Instantiate a new page
|
||||||
|
pub fn new(
|
||||||
|
kind: PageKind,
|
||||||
|
user: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
kind,
|
||||||
|
user,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render a page to a Write-able stream
|
||||||
|
pub fn render<W: Write>(
|
||||||
|
&self, dest: &mut W
|
||||||
|
) -> Result<(), Box<dyn Error>> {
|
||||||
|
let assets = format!("{}/templates/**/*", MINTEE_ASSETS);
|
||||||
|
let tera = Tera::new(&assets)?;
|
||||||
|
let mut page_template = self.kind.to_string();
|
||||||
|
|
||||||
|
use PageKind::*;
|
||||||
|
let ctx = match self.kind.clone() {
|
||||||
|
Dashboard | Project | Settings | Tickets(_) | User | Invalid => {
|
||||||
|
todo!()
|
||||||
|
},
|
||||||
|
Repo(r) => {
|
||||||
|
|
||||||
|
let mut repo_path = PathBuf::from(MINTEE_DIR.to_string());
|
||||||
|
repo_path.push(r.entity);
|
||||||
|
repo_path.push(r.name);
|
||||||
|
let repo = open(repo_path)?;
|
||||||
|
/* TODO: handle specifying a commit, tag, or branch */
|
||||||
|
let head_tree = repo.head_tree()?;
|
||||||
|
let object;
|
||||||
|
|
||||||
|
if let Some(dir) = r.path {
|
||||||
|
let path = PathBuf::from(dir);
|
||||||
|
|
||||||
|
object = head_tree
|
||||||
|
.lookup_entry(path.iter().map(|c| {
|
||||||
|
/* unwrap() is okay because self.path is guaranteed
|
||||||
|
* to be valid UTF-8 */
|
||||||
|
String::from_utf8(
|
||||||
|
c.to_os_string().into_encoded_bytes()
|
||||||
|
).unwrap()
|
||||||
|
}))?
|
||||||
|
/* replace this expect() with proper error handling */
|
||||||
|
.expect("attempted to render nonexistent page")
|
||||||
|
.to_owned()
|
||||||
|
.id()
|
||||||
|
.object()?;
|
||||||
|
|
||||||
|
use gix::object::Kind::*;
|
||||||
|
page_template = match object.kind {
|
||||||
|
Blob => "repo/file.html",
|
||||||
|
Commit | Tag => "repo/commit.html",
|
||||||
|
Tree => "repo/repo.html",
|
||||||
|
}.to_string();
|
||||||
|
} else { object = head_tree.id().object()?; }
|
||||||
|
|
||||||
|
object.to_context()?
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(dest.write_all(
|
||||||
|
tera.render(&page_template, &ctx)?.as_bytes()
|
||||||
|
)?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
use std::{ env::current_dir, error::Error };
|
|
||||||
|
|
||||||
use gix::{ traverse::tree::Recorder, prelude::* };
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
let r = gix::open(current_dir()?)?;
|
|
||||||
|
|
||||||
let mut rec = Recorder::default();
|
|
||||||
|
|
||||||
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
|
|
||||||
|
|
||||||
tree.traverse().breadthfirst(&mut rec)?;
|
|
||||||
|
|
||||||
for e in rec.records.iter() {
|
|
||||||
println!("{}", e.filepath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
for e in tree.iter() {
|
|
||||||
println!("{}", e?.filename())
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Emma Tebibyte <emma@tebibyte.media>
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* This file is part of Mintee.
|
|
||||||
*
|
|
||||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
|
||||||
* terms of the GNU Affero General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
|
||||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
* details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
error::Error,
|
|
||||||
fmt::{ self, Display, Formatter },
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
use tera::{ Context, Tera };
|
|
||||||
|
|
||||||
#[non_exhaustive]
|
|
||||||
pub enum PageKind {
|
|
||||||
Code,
|
|
||||||
Dashboard,
|
|
||||||
Tickets,
|
|
||||||
User,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PageKind {
|
|
||||||
pub fn render_page(&self, ctx: Context) -> Result<String, Box<dyn Error>> {
|
|
||||||
let page_dir = self.to_string();
|
|
||||||
let template = String::from_utf8(Path::new(&page_dir)
|
|
||||||
.to_path_buf()
|
|
||||||
.as_mut_os_string()
|
|
||||||
.as_encoded_bytes()
|
|
||||||
.to_vec()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let tera = Tera::new(&page_dir)?;
|
|
||||||
Ok(tera.render(&template, &ctx)?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for PageKind {
|
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
|
|
||||||
use PageKind::*;
|
|
||||||
|
|
||||||
let path = match self {
|
|
||||||
Code => "repo/code.html",
|
|
||||||
Dashboard => "dashboard.html",
|
|
||||||
Tickets => "repo/tickets.html",
|
|
||||||
User => "user.html",
|
|
||||||
};
|
|
||||||
|
|
||||||
write!(f, "./assets/templates/{}", path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025–2026 Emma Tebibyte <emma@tebibyte.media>
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
*
|
|
||||||
* This file is part of Mintee.
|
|
||||||
*
|
|
||||||
* Mintee is free software: you can redistribute it and/or modify it under the
|
|
||||||
* terms of the GNU Affero General Public License as published by the Free
|
|
||||||
* Software Foundation, either version 3 of the License, or (at your option)
|
|
||||||
* any later version.
|
|
||||||
*
|
|
||||||
* Mintee is distributed in the hope that it will be useful, but WITHOUT ANY
|
|
||||||
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
||||||
* details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
|
||||||
*/
|
|
||||||
|
|
||||||
use std::{
|
|
||||||
env::{ args, current_dir },
|
|
||||||
error::Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
use gix::open;
|
|
||||||
use tera::{ Context, Tera };
|
|
||||||
|
|
||||||
mod git;
|
|
||||||
|
|
||||||
use git::repo_to_context;
|
|
||||||
|
|
||||||
/*
|
|
||||||
impl From<GitRepo> for Context {
|
|
||||||
fn from(repo: GitRepo) -> Self {
|
|
||||||
let mut ctx = Context::new();
|
|
||||||
|
|
||||||
let directory = format!("{}/{}", repo.owner, repo.name);
|
|
||||||
|
|
||||||
let main_branch = repo.branches
|
|
||||||
.iter()
|
|
||||||
.find(|b| b.name == "main")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let latest_commit = &main_branch.commits[0];
|
|
||||||
let hash = latest_commit.hash.clone();
|
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
|
|
||||||
for e in latest_commit.entries.iter() {
|
|
||||||
let entry = SampleEntry {
|
|
||||||
class: e.kind.to_string(),
|
|
||||||
last_commit: hash.clone(),
|
|
||||||
last_commit_message: latest_commit.message.clone().unwrap(),
|
|
||||||
last_commit_time: latest_commit.time,
|
|
||||||
path: e.path.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stubs til we have a real database */
|
|
||||||
ctx.insert("user", "anon");
|
|
||||||
ctx.insert("site", "TiB.");
|
|
||||||
ctx.insert("notif_count", "");
|
|
||||||
ctx.insert("ticket_count", "(47)");
|
|
||||||
|
|
||||||
ctx.insert("readme_content", "this is a readme");
|
|
||||||
|
|
||||||
ctx.insert("owner", &repo.owner);
|
|
||||||
ctx.insert("branch", &main_branch.name);
|
|
||||||
ctx.insert("repo", &repo.name);
|
|
||||||
ctx.insert("directory", &directory);
|
|
||||||
ctx.insert("entries", &entries);
|
|
||||||
|
|
||||||
ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
if let Some(templates) = args().collect::<Vec<_>>().get(1) {
|
|
||||||
let tera = Tera::new(templates.as_str())?;
|
|
||||||
let ctx = repo_to_context(open(current_dir()?)?)?;
|
|
||||||
|
|
||||||
println!("{}", tera.render("code.html", &ctx)?);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
} else {
|
|
||||||
eprintln!("Usage: {} template_glob", args().collect::<Vec<_>>()[0]);
|
|
||||||
std::process::exit(64);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
use std::{ env::current_dir, error::Error };
|
|
||||||
|
|
||||||
use gix::{ traverse::tree::Recorder, prelude::* };
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
|
||||||
let r = gix::open(current_dir()?)?;
|
|
||||||
|
|
||||||
let mut rec = Recorder::default();
|
|
||||||
|
|
||||||
let tree = r.rev_parse_single("@")?.object()?.peel_to_tree()?;
|
|
||||||
|
|
||||||
tree.traverse().breadthfirst(&mut rec)?;
|
|
||||||
|
|
||||||
for e in rec.records.iter() {
|
|
||||||
println!("{}", e.filepath)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
for e in tree.iter() {
|
|
||||||
println!("{}", e?.filename())
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
+283
-241
@@ -1,5 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025, 2026 silt <silt@tebibyte.media>
|
* Copyright (c) 2025, 2026 silt <silt@tebibyte.media>
|
||||||
|
* Copyright (c) 2026 Emma Tebibyte <emma@tebibyte.media>
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* This file is part of Mintee.
|
* This file is part of Mintee.
|
||||||
@@ -20,18 +21,31 @@
|
|||||||
|
|
||||||
use httparse::{self};
|
use httparse::{self};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath, Utf8UnixPathBuf};
|
|
||||||
use std::{
|
use std::{
|
||||||
error::Error, fmt, io::{self, BufRead, BufReader, Read}, net::{Incoming, SocketAddr, TcpListener, TcpStream, ToSocketAddrs}, ops::Deref, pin::Pin, process::exit, str::FromStr, time::Duration
|
error::Error,
|
||||||
|
fmt::{self},
|
||||||
|
io::{BufRead, BufReader, Read},
|
||||||
|
net::TcpStream,
|
||||||
|
ops::Deref,
|
||||||
|
str::FromStr,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use typed_path::{Utf8Component, Utf8UnixComponent, Utf8UnixPath, Utf8UnixPathBuf};
|
||||||
|
|
||||||
use mintee::util::yapper::{yap, eyap};
|
use mintee::{
|
||||||
|
backend::{
|
||||||
|
self,
|
||||||
|
render::{ Page, PageKind Repo, Revision },
|
||||||
|
srv::get_res,
|
||||||
|
},
|
||||||
|
util::yapper::{eyap, yap},
|
||||||
|
};
|
||||||
|
|
||||||
pub use super::manager::{Frontend, FrontendImpl};
|
pub use super::manager::{Frontend, FrontendImpl};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum HttpMethod {
|
pub enum HttpMethod<'a> {
|
||||||
GET,
|
GET,
|
||||||
POST,
|
POST,
|
||||||
HEAD,
|
HEAD,
|
||||||
@@ -41,11 +55,11 @@ pub enum HttpMethod {
|
|||||||
OPTIONS,
|
OPTIONS,
|
||||||
TRACE,
|
TRACE,
|
||||||
PATCH,
|
PATCH,
|
||||||
Unknown,
|
Other(&'a str),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<&str> for HttpMethod {
|
impl<'a> From<&'a str> for HttpMethod<'a> {
|
||||||
fn from(val: &str) -> Self {
|
fn from(val: &'a str) -> Self {
|
||||||
use HttpMethod::*;
|
use HttpMethod::*;
|
||||||
match val {
|
match val {
|
||||||
"GET" => GET,
|
"GET" => GET,
|
||||||
@@ -57,13 +71,13 @@ impl From<&str> for HttpMethod {
|
|||||||
"OPTIONS" => OPTIONS,
|
"OPTIONS" => OPTIONS,
|
||||||
"TRACE" => TRACE,
|
"TRACE" => TRACE,
|
||||||
"PATCH" => PATCH,
|
"PATCH" => PATCH,
|
||||||
_ => Unknown,
|
other => Other(other),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<HttpMethod> for &'static str {
|
impl<'a> From<HttpMethod<'a>> for &'a str {
|
||||||
fn from(val: HttpMethod) -> Self {
|
fn from(val: HttpMethod<'a>) -> Self {
|
||||||
use HttpMethod::*;
|
use HttpMethod::*;
|
||||||
match val {
|
match val {
|
||||||
GET => "GET",
|
GET => "GET",
|
||||||
@@ -75,15 +89,15 @@ impl From<HttpMethod> for &'static str {
|
|||||||
OPTIONS => "OPTIONS",
|
OPTIONS => "OPTIONS",
|
||||||
TRACE => "TRACE",
|
TRACE => "TRACE",
|
||||||
PATCH => "PATCH",
|
PATCH => "PATCH",
|
||||||
Unknown => "?",
|
Other(other) => other,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<String> for HttpMethod {
|
impl<'a> From<&'a String> for HttpMethod<'a> {
|
||||||
fn from(val: String) -> Self {
|
fn from(val: &'a String) -> Self {
|
||||||
use HttpMethod::*;
|
use HttpMethod::*;
|
||||||
match val.as_str() {
|
match &**val {
|
||||||
"GET" => GET,
|
"GET" => GET,
|
||||||
"POST" => POST,
|
"POST" => POST,
|
||||||
"HEAD" => HEAD,
|
"HEAD" => HEAD,
|
||||||
@@ -93,53 +107,43 @@ impl From<String> for HttpMethod {
|
|||||||
"OPTIONS" => OPTIONS,
|
"OPTIONS" => OPTIONS,
|
||||||
"TRACE" => TRACE,
|
"TRACE" => TRACE,
|
||||||
"PATCH" => PATCH,
|
"PATCH" => PATCH,
|
||||||
_ => Unknown,
|
_ => Other(val),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<HttpMethod> for String {
|
impl<'a> From<HttpMethod<'a>> for String {
|
||||||
fn from(val: HttpMethod) -> Self {
|
fn from(val: HttpMethod) -> Self {
|
||||||
use HttpMethod::*;
|
use HttpMethod::*;
|
||||||
match val {
|
match val {
|
||||||
GET => "GET".to_string(),
|
GET => "GET".to_owned(),
|
||||||
POST => "POST".to_string(),
|
POST => "POST".to_owned(),
|
||||||
HEAD => "HEAD".to_string(),
|
HEAD => "HEAD".to_owned(),
|
||||||
PUT => "PUT".to_string(),
|
PUT => "PUT".to_owned(),
|
||||||
DELETE => "DELETE".to_string(),
|
DELETE => "DELETE".to_owned(),
|
||||||
CONNECT => "CONNECT".to_string(),
|
CONNECT => "CONNECT".to_owned(),
|
||||||
OPTIONS => "OPTIONS".to_string(),
|
OPTIONS => "OPTIONS".to_owned(),
|
||||||
TRACE => "TRACE".to_string(),
|
TRACE => "TRACE".to_owned(),
|
||||||
PATCH => "PATCH".to_string(),
|
PATCH => "PATCH".to_owned(),
|
||||||
Unknown => "?".to_string(),
|
Other(other) => other.to_owned(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum ResponseStatus {
|
pub enum ResponseStatus<'a> {
|
||||||
Okay,
|
Okay,
|
||||||
Created,
|
Created,
|
||||||
MovedPermanently {
|
MovedPermanently { location: String },
|
||||||
location: String,
|
SeeOther { location: String },
|
||||||
},
|
TemporaryRedirect { location: String },
|
||||||
SeeOther {
|
PermanentRedirect { location: String },
|
||||||
location: String,
|
|
||||||
},
|
|
||||||
TemporaryRedirect {
|
|
||||||
location: String,
|
|
||||||
},
|
|
||||||
PermanentRedirect {
|
|
||||||
location: String,
|
|
||||||
},
|
|
||||||
BadRequest,
|
BadRequest,
|
||||||
Unauthorized,
|
Unauthorized,
|
||||||
Forbidden,
|
Forbidden,
|
||||||
NotFound,
|
NotFound,
|
||||||
MethodNotAllowed {
|
MethodNotAllowed { allow: Vec<HttpMethod<'a>> },
|
||||||
allow: Vec<HttpMethod>,
|
|
||||||
},
|
|
||||||
UriTooLong,
|
UriTooLong,
|
||||||
ImATeapot,
|
ImATeapot,
|
||||||
InternalServerError,
|
InternalServerError,
|
||||||
@@ -147,7 +151,7 @@ pub enum ResponseStatus {
|
|||||||
HttpVersionNotSupported,
|
HttpVersionNotSupported,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResponseStatus {
|
impl ResponseStatus<'_> {
|
||||||
fn as_code(&self) -> usize {
|
fn as_code(&self) -> usize {
|
||||||
use ResponseStatus::*;
|
use ResponseStatus::*;
|
||||||
match self {
|
match self {
|
||||||
@@ -191,223 +195,110 @@ impl ResponseStatus {
|
|||||||
HttpVersionNotSupported => "HTTP Version Not Supported",
|
HttpVersionNotSupported => "HTTP Version Not Supported",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
fn to_headers(&self) -> Vec<(&'static str, String)> {
|
||||||
pub struct HttpError {
|
use ResponseStatus::*;
|
||||||
kind: ResponseStatus,
|
match self {
|
||||||
}
|
MovedPermanently { location } => vec![("location", location.clone())],
|
||||||
|
SeeOther { location } => vec![("location", location.clone())],
|
||||||
impl HttpError {
|
TemporaryRedirect { location } => vec![("location", location.clone())],
|
||||||
pub fn new(kind: ResponseStatus) -> Self {
|
PermanentRedirect { location } => vec![("location", location.clone())],
|
||||||
Self { kind }
|
MethodNotAllowed { allow } => vec![(
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for HttpError {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"HTTP/1.1 {} {}",
|
|
||||||
self.kind.as_code(),
|
|
||||||
self.kind.as_description()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error for HttpError {}
|
|
||||||
|
|
||||||
impl From<HttpError> for io::Error {
|
|
||||||
fn from(val: HttpError) -> Self {
|
|
||||||
io::Error::other(val.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<httparse::Error> for HttpError {
|
|
||||||
fn from(_: httparse::Error) -> Self {
|
|
||||||
HttpError::new(ResponseStatus::BadRequest)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Response<'a> {
|
|
||||||
pub status: ResponseStatus,
|
|
||||||
pub headers: Vec<(&'a str, String)>,
|
|
||||||
pub body: Option<&'a [u8]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<Response<'a>> for Vec<u8> {
|
|
||||||
fn from(val: Response<'a>) -> Self {
|
|
||||||
[
|
|
||||||
"HTTP/1.1 ".as_bytes(),
|
|
||||||
val.status.as_code().to_string().as_bytes(),
|
|
||||||
b" ",
|
|
||||||
val.status.as_description().as_bytes(),
|
|
||||||
b"\r\n",
|
|
||||||
&val.headers.into_iter().fold(
|
|
||||||
Default::default(),
|
|
||||||
|mut acc: Vec<u8>, e: (&str, String)| -> Vec<u8> {
|
|
||||||
acc.append(&mut [e.0.as_bytes(), b": ", e.1.as_bytes(), b"\r\n"].concat());
|
|
||||||
acc
|
|
||||||
}
|
|
||||||
),
|
|
||||||
b"\r\n",
|
|
||||||
val.body.unwrap_or_default(),
|
|
||||||
].concat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<HttpError> for Response<'a> {
|
|
||||||
fn from(err: HttpError) -> Self {
|
|
||||||
let status = err.kind.clone();
|
|
||||||
let headers = match err.kind {
|
|
||||||
ResponseStatus::MovedPermanently { location }
|
|
||||||
| ResponseStatus::SeeOther { location }
|
|
||||||
| ResponseStatus::TemporaryRedirect { location }
|
|
||||||
| ResponseStatus::PermanentRedirect { location } => vec![("location", location)],
|
|
||||||
ResponseStatus::MethodNotAllowed { allow } => vec![(
|
|
||||||
"allow",
|
"allow",
|
||||||
allow.iter().map(|x| Into::<String>::into(*x)).join(", ")
|
allow.iter().map(|x| Into::<String>::into(*x)).join(", "),
|
||||||
)],
|
)],
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
|
||||||
Response {
|
|
||||||
status,
|
|
||||||
headers,
|
|
||||||
body: None
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for ResponseStatus<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "{} {}", self.as_code(), self.as_description())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Response<'a> {
|
||||||
|
pub status: Option<ResponseStatus<'a>>,
|
||||||
|
pub headers: Vec<(&'a str, String)>,
|
||||||
|
pub body: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Response<'_> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
status: None,
|
||||||
|
headers: Default::default(),
|
||||||
|
body: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&Response<'a>> for Vec<u8> {
|
||||||
|
fn from(val: &Response<'a>) -> Self {
|
||||||
|
let status = val
|
||||||
|
.status
|
||||||
|
.as_ref()
|
||||||
|
.unwrap_or(&ResponseStatus::InternalServerError);
|
||||||
|
let headers: &Vec<(&'a str, String)> = val.headers.as_ref();
|
||||||
|
[
|
||||||
|
"HTTP/1.1 ".as_bytes(),
|
||||||
|
status.as_code().to_string().as_bytes(),
|
||||||
|
b" ",
|
||||||
|
status.as_description().as_bytes(),
|
||||||
|
b"\r\n",
|
||||||
|
&headers.iter().chain(&status.to_headers()).fold(
|
||||||
|
Default::default(),
|
||||||
|
|mut acc: Vec<u8>, e: &(&str, String)| -> Vec<u8> {
|
||||||
|
acc.append(&mut [e.0.as_bytes(), b": ", e.1.as_bytes(), b"\r\n"].concat());
|
||||||
|
acc
|
||||||
|
},
|
||||||
|
),
|
||||||
|
b"\r\n",
|
||||||
|
&val.body,
|
||||||
|
]
|
||||||
|
.concat()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct FeConfig {
|
pub struct FeConfig {
|
||||||
bind_address: SocketAddr,
|
|
||||||
read_timeout: Duration,
|
read_timeout: Duration,
|
||||||
write_timeout: Duration,
|
write_timeout: Duration,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FeConfig {
|
impl FeConfig {
|
||||||
pub fn init<A: ToSocketAddrs>(
|
pub fn init(read_timeout: Duration, write_timeout: Duration) -> Result<Self, Box<dyn Error>> {
|
||||||
bind_address: A,
|
|
||||||
read_timeout: Duration,
|
|
||||||
write_timeout: Duration,
|
|
||||||
) -> Result<Self, Box<dyn Error>> {
|
|
||||||
Ok(FeConfig {
|
Ok(FeConfig {
|
||||||
bind_address: bind_address.to_socket_addrs()?.collect::<Vec<_>>()[0],
|
// bind_address: bind_address.to_socket_addrs()?.collect::<Vec<_>>()[0],
|
||||||
read_timeout,
|
read_timeout,
|
||||||
write_timeout,
|
write_timeout,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FeStorage {
|
#[derive(Debug, Clone)]
|
||||||
listener: TcpListener,
|
struct Request<'a> {
|
||||||
// TODO: tera template store
|
method: HttpMethod<'a>,
|
||||||
}
|
|
||||||
|
|
||||||
impl Frontend<FeStorage, FeConfig> {
|
|
||||||
fn router(
|
|
||||||
method: HttpMethod,
|
|
||||||
path: Utf8UnixPathBuf,
|
path: Utf8UnixPathBuf,
|
||||||
params: Option<Vec<(&str, &str)>>,
|
headers: Vec<(&'a str, &'a str)>,
|
||||||
headers: &[httparse::Header],
|
params: Option<Vec<(&'a str, &'a str)>>,
|
||||||
) -> Result<<Frontend<FeStorage, FeConfig> as FrontendImpl<TcpStream>>::Response, Box<dyn Error>> {
|
|
||||||
use HttpMethod::*;
|
|
||||||
use ResponseStatus::*;
|
|
||||||
// unwrapping is safe here because the resource path it came from is a valid UTF-8 &str
|
|
||||||
match (method, path.components().map(|c| c.as_str()).collect::<Vec<&str>>().deref(), params, headers) {
|
|
||||||
(method, ["/", "index.html"], _, _) => {
|
|
||||||
if matches!(method, GET) {
|
|
||||||
Ok(Response {
|
|
||||||
status: ResponseStatus::Okay,
|
|
||||||
headers: vec![("x-test1", "test1".to_string()), ("x-test2", "test2".to_string())],
|
|
||||||
body: Some(b"totally cool and swag homepage"),
|
|
||||||
}.into())
|
|
||||||
} else {
|
|
||||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(method, ["/", "login"], _, _) => {
|
|
||||||
if matches!(method, GET | POST) {
|
|
||||||
todo!()
|
|
||||||
} else {
|
|
||||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET, POST] })))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// oh how i long for inline const patterns
|
|
||||||
(method, ["/", user], _, _) if let Some(user) = user.strip_prefix('~') => {
|
|
||||||
if matches!(method, GET) {
|
|
||||||
todo!()
|
|
||||||
} else {
|
|
||||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(method, ["/", user, repo], _, _) if let Some(user) = user.strip_prefix('~') => {
|
|
||||||
if matches!(method, GET) {
|
|
||||||
todo!()
|
|
||||||
} else {
|
|
||||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(method, ["/", project], _, _) if let Some(project) = project.strip_prefix('+') => {
|
|
||||||
if matches!(method, GET) {
|
|
||||||
todo!()
|
|
||||||
} else {
|
|
||||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(method, ["/", project, repo], _, _) if let Some(project) = project.strip_prefix('+') => {
|
|
||||||
if matches!(method, GET) {
|
|
||||||
todo!()
|
|
||||||
} else {
|
|
||||||
Err(Box::new(HttpError::new(MethodNotAllowed { allow: vec![GET] })))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => Err(Box::new(HttpError::new(ResponseStatus::NotFound))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Iterator for Frontend<FeStorage, FeConfig> {
|
trait ReqResp<'a> {
|
||||||
type Item = Incoming<'static>;
|
fn new(buf: &'a [u8]) -> (Option<Request<'a>>, Response<'a>);
|
||||||
|
fn route(&mut self) -> &Self;
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
|
||||||
todo!()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FrontendImpl<TcpStream> for Frontend<FeStorage, FeConfig> {
|
impl<'a> ReqResp<'a> for (Request<'a>, Response<'_>) {
|
||||||
type FeConfig = FeConfig;
|
fn new(buf: &'a [u8]) -> (Option<Request<'a>>, Response<'a>) {
|
||||||
type Request = TcpStream;
|
|
||||||
type Response = Vec<u8>;
|
|
||||||
|
|
||||||
fn init(config: FeConfig) -> Self {
|
|
||||||
// TODO: load tera templates into FeStorage
|
|
||||||
Frontend {
|
|
||||||
storage: self::FeStorage {
|
|
||||||
listener: TcpListener::bind(config.bind_address).unwrap_or_else(|e| {
|
|
||||||
eyap!(&e);
|
|
||||||
exit(1)
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
config: config,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_request(&self, subj: Self::Request) -> Result<Self::Response, Box<dyn Error>> {
|
|
||||||
subj.set_read_timeout(Some(self.config.read_timeout))
|
|
||||||
.and_then(|_| subj.set_write_timeout(Some(self.config.write_timeout)))?;
|
|
||||||
|
|
||||||
let stream_read = BufReader::new(subj);
|
|
||||||
let mut headers = [httparse::EMPTY_HEADER; 32];
|
let mut headers = [httparse::EMPTY_HEADER; 32];
|
||||||
let mut req = httparse::Request::new(&mut headers);
|
let mut req = httparse::Request::new(&mut headers);
|
||||||
let buf: &mut Vec<u8> = &mut vec![];
|
let mut resp = Response::default();
|
||||||
|
|
||||||
// TODO: validate more of the request before sending to the router
|
|
||||||
stream_read.take(8192).read_until(b'\n', buf)?;
|
|
||||||
let res = req.parse(buf);
|
let res = req.parse(buf);
|
||||||
|
|
||||||
Ok(match (res, req) {
|
match (res, req) {
|
||||||
|
|
||||||
// Presumably well-formed enough to get sent off to the route handler
|
// Presumably well-formed enough to get sent off to the route handler
|
||||||
(
|
(
|
||||||
Ok(httparse::Status::Partial),
|
Ok(httparse::Status::Partial),
|
||||||
@@ -434,32 +325,183 @@ impl FrontendImpl<TcpStream> for Frontend<FeStorage, FeConfig> {
|
|||||||
);
|
);
|
||||||
if path.is_absolute() {
|
if path.is_absolute() {
|
||||||
// context-valid lexical normalization without da feature
|
// context-valid lexical normalization without da feature
|
||||||
let path = Utf8UnixPathBuf::from_iter(path.components().try_fold(Vec::<Utf8UnixComponent>::new(), |mut acc, item| -> Result<Vec<Utf8UnixComponent<'_>>, Box<dyn Error>> {
|
let path = Utf8UnixPathBuf::from_iter(path.components().fold(
|
||||||
|
Vec::<Utf8UnixComponent>::new(),
|
||||||
|
|mut acc, item| -> Vec<Utf8UnixComponent<'_>> {
|
||||||
match item {
|
match item {
|
||||||
Utf8UnixComponent::CurDir => Ok(acc),
|
Utf8UnixComponent::CurDir => acc,
|
||||||
Utf8UnixComponent::RootDir => {acc.push(item); Ok(acc)},
|
Utf8UnixComponent::RootDir => { acc.push(item); acc }
|
||||||
Utf8UnixComponent::Normal(_) => {acc.push(item); Ok(acc)},
|
Utf8UnixComponent::Normal(_) => { acc.push(item); acc }
|
||||||
Utf8UnixComponent::ParentDir => {acc.pop_if(|c| c != &Utf8UnixComponent::RootDir); Ok(acc)},
|
Utf8UnixComponent::ParentDir => { acc.pop_if(|c| c != &Utf8UnixComponent::RootDir); acc }
|
||||||
}
|
}
|
||||||
})?);
|
},
|
||||||
|
));
|
||||||
|
|
||||||
Self::router(method.into(), path, params, headers)
|
let headers = headers
|
||||||
|
.iter()
|
||||||
|
.filter_map(|h| str::from_utf8(h.value).map(|v| (h.name, v)).ok())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(
|
||||||
|
Some(Request {
|
||||||
|
method: method.into(),
|
||||||
|
path,
|
||||||
|
headers,
|
||||||
|
params,
|
||||||
|
}),
|
||||||
|
resp,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
Err(Box::new(HttpError::new(ResponseStatus::BadRequest)) as Box<dyn Error>)
|
resp.status = Some(ResponseStatus::BadRequest);
|
||||||
|
(None, resp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Malformed request lines and HTTP/1.1 requests without a Host header
|
// Malformed request lines and HTTP/1.1 requests without a Host header
|
||||||
(Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => {
|
(Ok(httparse::Status::Partial), _) | (Ok(httparse::Status::Complete(_)), _) => {
|
||||||
Err(Box::new(HttpError::new(ResponseStatus::BadRequest)) as Box<dyn Error>)
|
resp.status = Some(ResponseStatus::BadRequest);
|
||||||
|
(None, resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal parsing error; obvious bad request
|
// Fatal parsing error; obvious bad request
|
||||||
(Err(e), _) => Err(Box::new(e) as Box<dyn Error>),
|
(Err(e), _) => {
|
||||||
}?)
|
eyap!(e);
|
||||||
|
resp.status = Some(ResponseStatus::BadRequest);
|
||||||
|
(None, resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_error(&mut self, res: Result<Self::Response, Box<dyn Error>>) -> Vec<u8> {
|
fn route(&mut self) -> &Self {
|
||||||
|
let (request, response) = self;
|
||||||
|
use HttpMethod::*;
|
||||||
|
use ResponseStatus::*;
|
||||||
|
match (
|
||||||
|
request.method,
|
||||||
|
request.path.components().map(|c| c.as_str()).collect::<Vec<&str>>().deref(),
|
||||||
|
&request.params,
|
||||||
|
&request.headers,
|
||||||
|
) {
|
||||||
|
(method, ["/", "index.html"], _, _) => {
|
||||||
|
if matches!(method, GET) {
|
||||||
|
response.status = Some(ResponseStatus::Okay);
|
||||||
|
response.headers = vec![
|
||||||
|
("x-test1", "test1".to_string()),
|
||||||
|
("x-test2", "test2".to_string()),
|
||||||
|
];
|
||||||
|
response.body = b"totally cool and swag homepage".to_vec();
|
||||||
|
self
|
||||||
|
} else {
|
||||||
|
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(method, ["/", "login"], _, _) => {
|
||||||
|
if matches!(method, GET | POST) {
|
||||||
todo!()
|
todo!()
|
||||||
|
} else {
|
||||||
|
response.status = Some(MethodNotAllowed {
|
||||||
|
allow: vec![GET, POST],
|
||||||
|
});
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// oh how i long for inline const patterns
|
||||||
|
(method, ["/", user], _, _) if let Some(user) = user.strip_prefix('~') => {
|
||||||
|
if matches!(method, GET) {
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(method, ["/", user, repo], params, _) if user.starts_with('~') => {
|
||||||
|
if matches!(method, GET) {
|
||||||
|
let revision = match params.as_ref().and_then(|x| x.iter().filter_map(|y| ["branch", "commit", "tag"].contains(&y.0).then(|| y)).last()) {
|
||||||
|
Some(("branch", branch)) => Some(Revision::Branch(branch.to_string())),
|
||||||
|
Some(("commit", commit)) => Some(Revision::Commit(commit.to_string())),
|
||||||
|
Some(("tag", tag)) => Some(Revision::Tag(tag.to_string())),
|
||||||
|
_ => None
|
||||||
|
};
|
||||||
|
Page::new(PageKind::Repo(Repo::new(user.to_string(), repo.to_string(), revision, None)), None).render(&mut response.body).unwrap();
|
||||||
|
} else {
|
||||||
|
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
(method, ["/", project], _, _) if project.starts_with('+') => {
|
||||||
|
if matches!(method, GET) {
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(method, ["/", project, repo], _, _)
|
||||||
|
if let Some(project) = project.strip_prefix('+') =>
|
||||||
|
{
|
||||||
|
if matches!(method, GET) {
|
||||||
|
todo!()
|
||||||
|
} else {
|
||||||
|
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(method, ["/", "res", x @ ..], _, _) => {
|
||||||
|
if matches!(method, GET) {
|
||||||
|
match get_res(x.join("/"), &mut response.body) {
|
||||||
|
Ok(_) => response.status = Some(Okay),
|
||||||
|
Err(e) => { eyap!(e.to_string()); },
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.status = Some(MethodNotAllowed { allow: vec![GET] });
|
||||||
|
}
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
response.status = Some(NotFound);
|
||||||
|
self
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct FeStorage {
|
||||||
|
// TODO: tera template cache
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Frontend<FeStorage, FeConfig> {}
|
||||||
|
|
||||||
|
impl FrontendImpl<TcpStream> for Frontend<FeStorage, FeConfig> {
|
||||||
|
type FeConfig = FeConfig;
|
||||||
|
type Request = TcpStream;
|
||||||
|
type Response = Vec<u8>;
|
||||||
|
|
||||||
|
fn init(config: FeConfig) -> Self {
|
||||||
|
// TODO: load tera templates into FeStorage
|
||||||
|
Frontend {
|
||||||
|
state: self::FeStorage {},
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_request(&self, subj: Self::Request) -> Self::Response {
|
||||||
|
subj.set_read_timeout(Some(self.config.read_timeout))
|
||||||
|
.and_then(|_| subj.set_write_timeout(Some(self.config.write_timeout))).unwrap(/* TODO: what do we wanna do here? */);
|
||||||
|
eyap!("handling");
|
||||||
|
let stream_read = BufReader::new(subj);
|
||||||
|
let buf: &mut Vec<u8> = &mut vec![];
|
||||||
|
|
||||||
|
stream_read.take(8192).read_until(b'\n', buf).unwrap(/*TODO*/);
|
||||||
|
let (request, response) = match <(Request, Response)>::new(buf) {
|
||||||
|
(Some(request), response) => (request, response),
|
||||||
|
(None, response) => return (&response).into(),
|
||||||
|
};
|
||||||
|
let mut pair = (request, response);
|
||||||
|
let (_request, response) = pair.route();
|
||||||
|
|
||||||
|
response.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-14
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 silt <silt@tebibyte.media>
|
* Copyright (c) 2025, 2026 silt <silt@tebibyte.media>
|
||||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
*
|
*
|
||||||
* This file is part of Mintee.
|
* This file is part of Mintee.
|
||||||
@@ -18,30 +18,20 @@
|
|||||||
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
* along with Mintee. If not, see https://www.gnu.org/licenses/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{error::Error, thread::available_parallelism, time::Duration};
|
use std::{error::Error, io::Write, net::TcpListener, process::exit, sync::Arc, thread::available_parallelism, time::Duration};
|
||||||
|
|
||||||
use futures::{self, channel::mpsc, executor::ThreadPool};
|
use futures::{channel::mpsc, executor::ThreadPool};
|
||||||
|
|
||||||
mod manager;
|
mod manager;
|
||||||
use manager::Pool;
|
|
||||||
|
|
||||||
|
|
||||||
mod http_fe;
|
mod http_fe;
|
||||||
use http_fe::FrontendImpl;
|
use http_fe::FrontendImpl;
|
||||||
|
|
||||||
mod gem_fe;
|
mod gem_fe;
|
||||||
|
|
||||||
// mod util;
|
|
||||||
// use crate::;
|
|
||||||
use mintee::util::yapper::eyap;
|
use mintee::util::yapper::eyap;
|
||||||
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
// let http_fe = http_fe::Frontend::init(http_fe::FeConfig::init("0.0.0.0:8080", Duration::new(2, 0), Duration::new(2, 0))?);
|
|
||||||
|
|
||||||
// let pool = Pool::<32>::new();
|
|
||||||
|
|
||||||
|
|
||||||
let pool = ThreadPool::builder()
|
let pool = ThreadPool::builder()
|
||||||
.pool_size(available_parallelism()?.into()) // TODO: or optional value from config
|
.pool_size(available_parallelism()?.into()) // TODO: or optional value from config
|
||||||
.name_prefix("mintfe-worker:")
|
.name_prefix("mintfe-worker:")
|
||||||
@@ -53,10 +43,26 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
})
|
})
|
||||||
.create()?;
|
.create()?;
|
||||||
|
|
||||||
// let (tx, rx) = mpsc::unbounded::<todo!()>();
|
let (tx, rx) = mpsc::unbounded::<i32>();
|
||||||
|
|
||||||
|
let http_listener = TcpListener::bind("0.0.0.0:8080").unwrap_or_else(|e| {
|
||||||
|
eyap!(&e);
|
||||||
|
exit(1)
|
||||||
|
});
|
||||||
|
eyap!("http_listener bound to tcp 0.0.0.0:8080");
|
||||||
|
|
||||||
|
let http_fe = Arc::new(http_fe::Frontend::init(http_fe::FeConfig::init(Duration::new(2, 0), Duration::new(2, 0))?)) ;
|
||||||
|
eyap!("initialized http_fe");
|
||||||
|
|
||||||
|
for mut conn in http_listener.incoming().map(|x| x.unwrap()) {
|
||||||
|
let http_fe = http_fe.clone();
|
||||||
|
pool.spawn_ok(async move {
|
||||||
|
eyap!("incoming request from {}", conn.peer_addr().unwrap());
|
||||||
|
let response = http_fe.handle_request(conn.try_clone().unwrap()).await;
|
||||||
|
let a = conn.write_all(&response);
|
||||||
|
eyap!("handled!");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,49 +19,23 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
array,
|
|
||||||
error::Error,
|
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
thread::{self, JoinHandle},
|
|
||||||
};
|
};
|
||||||
use futures::executor;
|
|
||||||
|
|
||||||
use mintee::util::yapper::{yap, eyap};
|
|
||||||
|
|
||||||
pub struct Frontend<S, C> {
|
pub struct Frontend<S, C> {
|
||||||
/// Holds data necessary for and private to the implementor.
|
/// Data necessary for and private to the implementor.
|
||||||
pub storage: S,
|
pub state: S,
|
||||||
|
|
||||||
/// Holds data to be set during initialization.
|
/// Data to be set during initialization.
|
||||||
pub config: C,
|
pub config: C,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait FrontendImpl<Request>: Iterator where Request: Read, Self::Response: Into<Vec<u8>> {
|
pub trait FrontendImpl<Request> where Self::Request: Read, Self::Response: Into<Vec<u8>> {
|
||||||
type FeConfig: ?Sized;
|
type FeConfig: ?Sized;
|
||||||
type Request: Read;
|
type Request: Read;
|
||||||
type Response: Write;
|
type Response: Write;
|
||||||
fn init(storage: Self::FeConfig) -> Self;
|
|
||||||
fn handle_request(&self, subj: Request) -> Result<Self::Response, Box<dyn Error>>;
|
|
||||||
// fn send_reply(&self, subj: Self::Response) -> Result<(), Box<dyn Error>>;
|
|
||||||
// NOTE: handle_request().or_else(handle_error())
|
|
||||||
fn handle_error(&mut self, res: Result<Self::Response, Box<dyn Error>>) -> Self::Response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: split frontend management code and Frontend trait stuff into diff files
|
fn init(config: Self::FeConfig) -> Self;
|
||||||
|
async fn handle_request(&self, subj: Request) -> Self::Response;
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Pool<const N: usize> {
|
|
||||||
threads: [JoinHandle<()>; N],
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<const N: usize> Pool<N> {
|
|
||||||
pub fn new() -> Result<Self, Box<dyn Error>> {
|
|
||||||
Ok(Pool {
|
|
||||||
threads: array::from_fn(|id| {
|
|
||||||
thread::spawn(move || {
|
|
||||||
eyap!("started thread #{:?}", id);
|
|
||||||
})
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-62
@@ -19,73 +19,34 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
env::{ args, current_dir },
|
env::current_dir,
|
||||||
error::Error,
|
error::Error,
|
||||||
|
io::stdout,
|
||||||
};
|
};
|
||||||
|
|
||||||
use gix::open;
|
use mintee::backend::render::{ Page, PageKind, Repo };
|
||||||
use tera::{ Context, Tera };
|
|
||||||
|
|
||||||
use mintee::backend::git::repo_to_context;
|
|
||||||
|
|
||||||
/*
|
|
||||||
impl From<GitRepo> for Context {
|
|
||||||
fn from(repo: GitRepo) -> Self {
|
|
||||||
let mut ctx = Context::new();
|
|
||||||
|
|
||||||
let directory = format!("{}/{}", repo.owner, repo.name);
|
|
||||||
|
|
||||||
let main_branch = repo.branches
|
|
||||||
.iter()
|
|
||||||
.find(|b| b.name == "main")
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let latest_commit = &main_branch.commits[0];
|
|
||||||
let hash = latest_commit.hash.clone();
|
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
|
|
||||||
for e in latest_commit.entries.iter() {
|
|
||||||
let entry = SampleEntry {
|
|
||||||
class: e.kind.to_string(),
|
|
||||||
last_commit: hash.clone(),
|
|
||||||
last_commit_message: latest_commit.message.clone().unwrap(),
|
|
||||||
last_commit_time: latest_commit.time,
|
|
||||||
path: e.path.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
entries.push(entry);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* stubs til we have a real database */
|
|
||||||
ctx.insert("user", "anon");
|
|
||||||
ctx.insert("site", "TiB.");
|
|
||||||
ctx.insert("notif_count", "");
|
|
||||||
ctx.insert("ticket_count", "(47)");
|
|
||||||
|
|
||||||
ctx.insert("readme_content", "this is a readme");
|
|
||||||
|
|
||||||
ctx.insert("owner", &repo.owner);
|
|
||||||
ctx.insert("branch", &main_branch.name);
|
|
||||||
ctx.insert("repo", &repo.name);
|
|
||||||
ctx.insert("directory", &directory);
|
|
||||||
ctx.insert("entries", &entries);
|
|
||||||
|
|
||||||
ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn Error>> {
|
fn main() -> Result<(), Box<dyn Error>> {
|
||||||
if let Some(templates) = args().collect::<Vec<_>>().get(1) {
|
let path = current_dir()?;
|
||||||
let tera = Tera::new(templates.as_str())?;
|
|
||||||
let ctx = repo_to_context(open(current_dir()?)?)?;
|
|
||||||
|
|
||||||
println!("{}", tera.render("code.html", &ctx)?);
|
let name = String::from_utf8(
|
||||||
|
path.file_name().unwrap().to_os_string().into_encoded_bytes()
|
||||||
|
)?;
|
||||||
|
let entity = String::from_utf8(
|
||||||
|
path.parent().unwrap().as_os_str().to_os_string().into_encoded_bytes()
|
||||||
|
)?;
|
||||||
|
|
||||||
Ok(())
|
let repo = Repo::new(
|
||||||
} else {
|
entity,
|
||||||
eprintln!("Usage: {} template_glob", args().collect::<Vec<_>>()[0]);
|
name,
|
||||||
std::process::exit(64);
|
None,
|
||||||
}
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
let page = Page::new(
|
||||||
|
PageKind::Repo(repo),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
page.render(&mut stdout())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user