initial commit
This commit is contained in:
commit
664f0bc626
30 changed files with 15669 additions and 0 deletions
1
.envrc
Normal file
1
.envrc
Normal file
|
@ -0,0 +1 @@
|
||||||
|
use flake
|
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
/target
|
||||||
|
.env
|
||||||
|
config.toml
|
||||||
|
.direnv
|
||||||
|
target/
|
||||||
|
target-bin/
|
||||||
|
secrets/
|
3625
Cargo.lock
generated
Normal file
3625
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
92
Cargo.toml
Normal file
92
Cargo.toml
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
[workspace]
|
||||||
|
members = [
|
||||||
|
"chir-rs-config",
|
||||||
|
"chir-rs-db",
|
||||||
|
"chir-rs-gemini",
|
||||||
|
"chir-rs-http",
|
||||||
|
"chir-rs-http-api",
|
||||||
|
]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "chir-rs"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
chir-rs-config = { version = "0.1.0", path = "chir-rs-config" }
|
||||||
|
chir-rs-db = { version = "0.1.0", path = "chir-rs-db" }
|
||||||
|
chir-rs-gemini = { version = "0.1.0", path = "chir-rs-gemini" }
|
||||||
|
chir-rs-http = { version = "0.1.0", path = "chir-rs-http" }
|
||||||
|
color-eyre = { version = "0.6.3", features = ["issue-url"] }
|
||||||
|
dotenvy = "0.15.7"
|
||||||
|
eyre = "0.6.12"
|
||||||
|
sentry = { version = "0.34.0", default-features = false, features = [
|
||||||
|
"backtrace",
|
||||||
|
"contexts",
|
||||||
|
"debug-images",
|
||||||
|
"panic",
|
||||||
|
"metrics",
|
||||||
|
"reqwest",
|
||||||
|
"rustls",
|
||||||
|
] }
|
||||||
|
sentry-eyre = "0.2.0"
|
||||||
|
sentry-tracing = { version = "0.34.0", features = ["backtrace"] }
|
||||||
|
tokio = { version = "1.41.1", features = ["macros", "rt-multi-thread"] }
|
||||||
|
tracing-error = "0.2.0"
|
||||||
|
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
deprecated-safe = "forbid"
|
||||||
|
elided_lifetimes_in_paths = "warn"
|
||||||
|
explicit_outlives_requirements = "warn"
|
||||||
|
impl-trait-overcaptures = "warn"
|
||||||
|
keyword-idents-2024 = "forbid"
|
||||||
|
let-underscore-drop = "warn"
|
||||||
|
macro-use-extern-crate = "deny"
|
||||||
|
meta-variable-misuse = "deny"
|
||||||
|
missing-abi = "forbid"
|
||||||
|
missing-copy-implementations = "warn"
|
||||||
|
missing-debug-implementations = "deny"
|
||||||
|
missing-docs = "warn"
|
||||||
|
missing-unsafe-on-extern = "deny"
|
||||||
|
non-local-definitions = "warn"
|
||||||
|
redundant-lifetimes = "warn"
|
||||||
|
single-use-lifetimes = "warn"
|
||||||
|
trivial-casts = "warn"
|
||||||
|
trivial-numeric-casts = "warn"
|
||||||
|
unit-bindings = "deny"
|
||||||
|
unnameable-types = "warn"
|
||||||
|
unreachable-pub = "warn"
|
||||||
|
unsafe-code = "forbid"
|
||||||
|
unused-crate-dependencies = "warn"
|
||||||
|
unused-extern-crates = "warn"
|
||||||
|
unused-import-braces = "warn"
|
||||||
|
unused-lifetimes = "warn"
|
||||||
|
unused-macro-rules = "warn"
|
||||||
|
unused-qualifications = "warn"
|
||||||
|
variant-size-differences = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
module-name-repetitions = "allow"
|
||||||
|
alloc-instead-of-core = "warn"
|
||||||
|
allow-attributes-without-reason = "deny"
|
||||||
|
assertions-on-result-states = "forbid"
|
||||||
|
clone-on-ref-ptr = "warn"
|
||||||
|
empty-drop = "warn"
|
||||||
|
expect-used = "deny"
|
||||||
|
inline-asm-x86-att-syntax = "forbid"
|
||||||
|
missing-docs-in-private-items = "warn"
|
||||||
|
panic = "deny"
|
||||||
|
panic-in-result-fn = "forbid"
|
||||||
|
rc-buffer = "warn"
|
||||||
|
rc-mutex = "deny"
|
||||||
|
unwrap-used = "forbid"
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
codegen-units = 1
|
||||||
|
lto = true
|
||||||
|
debug = "full"
|
||||||
|
strip = "none"
|
60
chir-rs-config/Cargo.toml
Normal file
60
chir-rs-config/Cargo.toml
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
[package]
|
||||||
|
name = "chir-rs-config"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
atty = "0.2.14"
|
||||||
|
eyre = "0.6.12"
|
||||||
|
sentry-core = "0.34.0"
|
||||||
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
|
toml = "0.8.19"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
deprecated-safe = "forbid"
|
||||||
|
elided_lifetimes_in_paths = "warn"
|
||||||
|
explicit_outlives_requirements = "warn"
|
||||||
|
impl-trait-overcaptures = "warn"
|
||||||
|
keyword-idents-2024 = "forbid"
|
||||||
|
let-underscore-drop = "warn"
|
||||||
|
macro-use-extern-crate = "deny"
|
||||||
|
meta-variable-misuse = "deny"
|
||||||
|
missing-abi = "forbid"
|
||||||
|
missing-copy-implementations = "warn"
|
||||||
|
missing-debug-implementations = "deny"
|
||||||
|
missing-docs = "warn"
|
||||||
|
missing-unsafe-on-extern = "deny"
|
||||||
|
non-local-definitions = "warn"
|
||||||
|
redundant-lifetimes = "warn"
|
||||||
|
single-use-lifetimes = "warn"
|
||||||
|
trivial-casts = "warn"
|
||||||
|
trivial-numeric-casts = "warn"
|
||||||
|
unit-bindings = "deny"
|
||||||
|
unnameable-types = "warn"
|
||||||
|
unreachable-pub = "warn"
|
||||||
|
unsafe-code = "forbid"
|
||||||
|
unused-crate-dependencies = "warn"
|
||||||
|
unused-extern-crates = "warn"
|
||||||
|
unused-import-braces = "warn"
|
||||||
|
unused-lifetimes = "warn"
|
||||||
|
unused-macro-rules = "warn"
|
||||||
|
unused-qualifications = "warn"
|
||||||
|
variant-size-differences = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
module-name-repetitions = "allow"
|
||||||
|
alloc-instead-of-core = "warn"
|
||||||
|
allow-attributes-without-reason = "deny"
|
||||||
|
assertions-on-result-states = "forbid"
|
||||||
|
clone-on-ref-ptr = "warn"
|
||||||
|
empty-drop = "warn"
|
||||||
|
expect-used = "deny"
|
||||||
|
inline-asm-x86-att-syntax = "forbid"
|
||||||
|
missing-docs-in-private-items = "warn"
|
||||||
|
panic = "deny"
|
||||||
|
panic-in-result-fn = "forbid"
|
||||||
|
rc-buffer = "warn"
|
||||||
|
rc-mutex = "deny"
|
||||||
|
unwrap-used = "forbid"
|
197
chir-rs-config/src/lib.rs
Normal file
197
chir-rs-config/src/lib.rs
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
//! Configuration file support
|
||||||
|
|
||||||
|
use std::{
|
||||||
|
net::SocketAddr,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use eyre::{self, Context, Result};
|
||||||
|
use sentry_core::types::Dsn;
|
||||||
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
|
|
||||||
|
/// Returns the default logging level.
|
||||||
|
///
|
||||||
|
/// This is set to `debug` on debug builds and `warn` on release builds.
|
||||||
|
#[must_use]
|
||||||
|
fn default_log_level() -> String {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
"debug".to_string()
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
"warn".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The Logging format to use
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub enum LogFormat {
|
||||||
|
/// Full human-readable logging output
|
||||||
|
Full,
|
||||||
|
/// Compact single-line logging format
|
||||||
|
Compact,
|
||||||
|
/// Pretty and sparse logging output, intended for development.
|
||||||
|
Pretty,
|
||||||
|
/// JSON output
|
||||||
|
Json,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for LogFormat {
|
||||||
|
fn default() -> Self {
|
||||||
|
if atty::is(atty::Stream::Stdout) {
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
{
|
||||||
|
Self::Pretty
|
||||||
|
}
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
{
|
||||||
|
Self::Full
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Self::Json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration for monitoring and logging
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct LoggingConfig {
|
||||||
|
/// Sentry DSN
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
#[serde(default)]
|
||||||
|
pub sentry_dsn: Option<Dsn>,
|
||||||
|
/// Log Level to output to stdout.
|
||||||
|
/// By default, this is `"debug"` in debug builds and `"warn"` in release builds.
|
||||||
|
#[serde(default = "default_log_level")]
|
||||||
|
pub log_level: String,
|
||||||
|
/// Logging style used by tracing.
|
||||||
|
///
|
||||||
|
/// There are three default values:
|
||||||
|
///
|
||||||
|
/// - If the log output is not a tty (for example systemd-journald or a file), the log output will be [`LogFormat::Json`]
|
||||||
|
/// - If the log output is a tty AND the program runs in debug mode, the log output will be [`LogFormat::Pretty`]
|
||||||
|
/// - If the log output is a tty AND the program runs in release mode, the log output will be [`LogFormat::Full`]
|
||||||
|
#[serde(default)]
|
||||||
|
pub log_style: LogFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_socket_addrs<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<SocketAddr>, D::Error> {
|
||||||
|
/// Internal representation of socket addresses
|
||||||
|
///
|
||||||
|
/// It’s either a single one, or a list of them
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
enum OneOrVec {
|
||||||
|
/// Just a single socket address
|
||||||
|
One(SocketAddr),
|
||||||
|
/// List of socket addresses
|
||||||
|
Vec(Vec<SocketAddr>),
|
||||||
|
}
|
||||||
|
let one_or_vec: OneOrVec = OneOrVec::deserialize(d)?;
|
||||||
|
|
||||||
|
match one_or_vec {
|
||||||
|
OneOrVec::One(socket_addr) => Ok(vec![socket_addr]),
|
||||||
|
OneOrVec::Vec(vec) => Ok(vec),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn serialize_socket_addrs<S: Serializer>(
|
||||||
|
socket_addrs: &Vec<SocketAddr>,
|
||||||
|
s: S,
|
||||||
|
) -> Result<S::Ok, S::Error> {
|
||||||
|
if socket_addrs.len() == 1 {
|
||||||
|
socket_addrs[0].serialize(s)
|
||||||
|
} else {
|
||||||
|
socket_addrs.serialize(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default socket addresses to use for http
|
||||||
|
#[expect(clippy::expect_used, reason = "Expect on hard-coded string literal")]
|
||||||
|
fn default_http_socket_addrs() -> Vec<SocketAddr> {
|
||||||
|
vec!["[::1]:5621"
|
||||||
|
.parse()
|
||||||
|
.expect("Hard coded string literal, should never happen")]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the default socket addresses to use for gemini
|
||||||
|
#[expect(clippy::expect_used, reason = "Expect on hard-coded string literal")]
|
||||||
|
fn default_gemini_socket_addrs() -> Vec<SocketAddr> {
|
||||||
|
vec!["[::]:1965"
|
||||||
|
.parse()
|
||||||
|
.expect("Hard coded string literal, should never happen")]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration used for axum’s HTTP config
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct Http {
|
||||||
|
/// Which IP addresses and ports to bind to
|
||||||
|
///
|
||||||
|
/// The default is `[::1]:5621`.
|
||||||
|
#[serde(serialize_with = "serialize_socket_addrs")]
|
||||||
|
#[serde(deserialize_with = "deserialize_socket_addrs")]
|
||||||
|
#[serde(default = "default_http_socket_addrs")]
|
||||||
|
pub listen: Vec<SocketAddr>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Configuration used for the gemini server
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct Gemini {
|
||||||
|
/// Which IP addresses and ports to bind to
|
||||||
|
///
|
||||||
|
/// The default is `[::]:1965`.
|
||||||
|
#[serde(serialize_with = "serialize_socket_addrs")]
|
||||||
|
#[serde(deserialize_with = "deserialize_socket_addrs")]
|
||||||
|
#[serde(default = "default_gemini_socket_addrs")]
|
||||||
|
pub listen: Vec<SocketAddr>,
|
||||||
|
/// Host name to run under
|
||||||
|
pub host: String,
|
||||||
|
/// Path to the private key
|
||||||
|
pub private_key: PathBuf,
|
||||||
|
/// Path to the certificate
|
||||||
|
pub certificate: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Database configuration file
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct Database {
|
||||||
|
/// Path to the database
|
||||||
|
pub path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Root configuration file
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
|
pub struct ChirRs {
|
||||||
|
/// Logging and monitoring related settings
|
||||||
|
pub logging: LoggingConfig,
|
||||||
|
/// HTTP Configuration
|
||||||
|
pub http: Http,
|
||||||
|
/// Gemini Configuration
|
||||||
|
pub gemini: Gemini,
|
||||||
|
/// Database Configuration
|
||||||
|
pub database: Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChirRs {
|
||||||
|
/// Reads chir.rs configuration from file
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if the path cannot be read, doesn’t contain valid UTF-8 text, or TOML in the expected configuration format.
|
||||||
|
pub fn read(fname: impl AsRef<Path>) -> Result<Self> {
|
||||||
|
let config = std::fs::read_to_string(fname.as_ref())
|
||||||
|
.with_context(|| format!("Reading config file {:?}", fname.as_ref()))?;
|
||||||
|
toml::de::from_str(&config)
|
||||||
|
.with_context(|| format!("Deserializing config file {:?}", fname.as_ref()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads chir.rs configuration from a file pointed to by the `CHIR_RS_CONFIG` environment variable
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Like [`ChirRs::read`], but it also returns an error if the value of `CHIR_RS_CONFIG` is missing or invalid.
|
||||||
|
pub fn read_from_env() -> Result<Self> {
|
||||||
|
let fname = std::env::var("CHIR_RS_CONFIG")
|
||||||
|
.context("Reading CHIR_RS_CONFIG environment variable")?;
|
||||||
|
Self::read(fname)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM 'file' WHERE 'id' = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 1
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "3b729cf7297b569253249000a3cc0de67106e426a1ae4e7cdac6eb85eeeb3849"
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE 'file' SET 'file_path' = ?, 'mime' = ?, 'b3hash' = ? WHERE 'id' = ?",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 4
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "88de2bc8bea8b9b5f1de736af16ed3cc0a2711b37687e595619fa001fa2633bc"
|
||||||
|
}
|
67
chir-rs-db/Cargo.toml
Normal file
67
chir-rs-db/Cargo.toml
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
[package]
|
||||||
|
name = "chir-rs-db"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bincode = "2.0.0-rc.3"
|
||||||
|
serde = { version = "1.0.215", features = ["derive"] }
|
||||||
|
sqlx = { version = "0.8.2", features = [
|
||||||
|
"runtime-tokio",
|
||||||
|
"sqlite",
|
||||||
|
"derive",
|
||||||
|
"macros",
|
||||||
|
"migrate",
|
||||||
|
] }
|
||||||
|
eyre = "0.6.12"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
blake3 = { version = "1.5.4", features = ["serde"] }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
deprecated-safe = "forbid"
|
||||||
|
elided_lifetimes_in_paths = "warn"
|
||||||
|
explicit_outlives_requirements = "warn"
|
||||||
|
impl-trait-overcaptures = "warn"
|
||||||
|
keyword-idents-2024 = "forbid"
|
||||||
|
let-underscore-drop = "warn"
|
||||||
|
macro-use-extern-crate = "deny"
|
||||||
|
meta-variable-misuse = "deny"
|
||||||
|
missing-abi = "forbid"
|
||||||
|
missing-copy-implementations = "warn"
|
||||||
|
missing-debug-implementations = "deny"
|
||||||
|
missing-docs = "warn"
|
||||||
|
missing-unsafe-on-extern = "deny"
|
||||||
|
non-local-definitions = "warn"
|
||||||
|
redundant-lifetimes = "warn"
|
||||||
|
single-use-lifetimes = "warn"
|
||||||
|
trivial-casts = "warn"
|
||||||
|
trivial-numeric-casts = "warn"
|
||||||
|
unit-bindings = "deny"
|
||||||
|
unnameable-types = "warn"
|
||||||
|
unreachable-pub = "warn"
|
||||||
|
unsafe-code = "forbid"
|
||||||
|
unused-crate-dependencies = "warn"
|
||||||
|
unused-extern-crates = "warn"
|
||||||
|
unused-import-braces = "warn"
|
||||||
|
unused-lifetimes = "warn"
|
||||||
|
unused-macro-rules = "warn"
|
||||||
|
unused-qualifications = "warn"
|
||||||
|
variant-size-differences = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
module-name-repetitions = "allow"
|
||||||
|
alloc-instead-of-core = "warn"
|
||||||
|
allow-attributes-without-reason = "deny"
|
||||||
|
assertions-on-result-states = "forbid"
|
||||||
|
clone-on-ref-ptr = "warn"
|
||||||
|
empty-drop = "warn"
|
||||||
|
expect-used = "deny"
|
||||||
|
inline-asm-x86-att-syntax = "forbid"
|
||||||
|
missing-docs-in-private-items = "warn"
|
||||||
|
panic = "deny"
|
||||||
|
panic-in-result-fn = "forbid"
|
||||||
|
rc-buffer = "warn"
|
||||||
|
rc-mutex = "deny"
|
||||||
|
unwrap-used = "forbid"
|
6
chir-rs-db/build.rs
Normal file
6
chir-rs-db/build.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
//! Ensures that migration additions force a rebuild
|
||||||
|
// generated by `sqlx migrate build-script`
|
||||||
|
fn main() {
|
||||||
|
// trigger recompilation when a new migration is added
|
||||||
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
}
|
10
chir-rs-db/migrations/20241123151924_add-file-table.sql
Normal file
10
chir-rs-db/migrations/20241123151924_add-file-table.sql
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
-- Add migration script here
|
||||||
|
|
||||||
|
create table 'file' (
|
||||||
|
id integer primary key not null,
|
||||||
|
file_path text not null,
|
||||||
|
mime text not null,
|
||||||
|
b3hash blob not null
|
||||||
|
);
|
||||||
|
create unique index file_path_mime on 'file' (file_path, mime);
|
||||||
|
create index file_path on 'file' (file_path);
|
190
chir-rs-db/src/file.rs
Normal file
190
chir-rs-db/src/file.rs
Normal file
|
@ -0,0 +1,190 @@
|
||||||
|
//! File related APIs
|
||||||
|
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
use blake3::Hash;
|
||||||
|
use eyre::Context as _;
|
||||||
|
use eyre::Result;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::{prelude::FromRow, query_as, sqlite::SqliteRow};
|
||||||
|
use sqlx::{query, Row as _};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
use crate::Database;
|
||||||
|
|
||||||
|
/// File record
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
pub struct File {
|
||||||
|
/// ID of the file record
|
||||||
|
id: u64,
|
||||||
|
/// Path this file is mounted at
|
||||||
|
pub file_path: String,
|
||||||
|
/// MIME type of file
|
||||||
|
pub mime: String,
|
||||||
|
/// blake3 hash of the file to serve
|
||||||
|
pub b3hash: Hash,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Encode for File {
|
||||||
|
fn encode<E: bincode::enc::Encoder>(
|
||||||
|
&self,
|
||||||
|
encoder: &mut E,
|
||||||
|
) -> std::result::Result<(), bincode::error::EncodeError> {
|
||||||
|
self.id.encode(encoder)?;
|
||||||
|
self.file_path.encode(encoder)?;
|
||||||
|
self.mime.encode(encoder)?;
|
||||||
|
self.b3hash.as_bytes().encode(encoder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Decode for File {
|
||||||
|
fn decode<D: bincode::de::Decoder>(
|
||||||
|
decoder: &mut D,
|
||||||
|
) -> std::result::Result<Self, bincode::error::DecodeError> {
|
||||||
|
let id = u64::decode(decoder)?;
|
||||||
|
let file_path = String::decode(decoder)?;
|
||||||
|
let mime = String::decode(decoder)?;
|
||||||
|
let b3hash = <[u8; 32]>::decode(decoder)?;
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
file_path,
|
||||||
|
mime,
|
||||||
|
b3hash: Hash::from_bytes(b3hash),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> FromRow<'r, SqliteRow> for File {
|
||||||
|
fn from_row(row: &'r SqliteRow) -> std::result::Result<Self, sqlx::Error> {
|
||||||
|
let id = u64::try_from(row.try_get::<i64, _>("id")?).unwrap_or_default();
|
||||||
|
let file_path: String = row.try_get("file_path")?;
|
||||||
|
let mime: String = row.try_get("mime")?;
|
||||||
|
let b3hash: Vec<u8> = row.try_get("b3hash")?;
|
||||||
|
|
||||||
|
if b3hash.len() != 32 {
|
||||||
|
return Err(sqlx::Error::ColumnDecode {
|
||||||
|
index: "b3hash".to_string(),
|
||||||
|
source: Box::new(std::io::Error::other("invalid b3 hash len")),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut b3hash_arr = [0u8; 32];
|
||||||
|
b3hash_arr.copy_from_slice(&b3hash);
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
id,
|
||||||
|
file_path,
|
||||||
|
mime,
|
||||||
|
b3hash: Hash::from_bytes(b3hash_arr),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl File {
|
||||||
|
/// Attempts to load a file by path and mime type
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if a database error occurs while loading.
|
||||||
|
#[instrument(skip(db))]
|
||||||
|
pub async fn get_by_path_mime(db: &Database, path: &str, mime: &str) -> Result<Option<Self>> {
|
||||||
|
query_as("SELECT * FROM 'file' WHERE 'path' = ? AND 'mime' = ?")
|
||||||
|
.bind(path)
|
||||||
|
.bind(mime)
|
||||||
|
.fetch_optional(&*db.0)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Loading file path {path} with mime type {mime}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to load any files by path.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if a database error occurs while loading.
|
||||||
|
#[instrument(skip(db))]
|
||||||
|
pub async fn get_by_path(db: &Database, path: &str) -> Result<Vec<Self>> {
|
||||||
|
query_as("SELECT * FROM 'file' WHERE 'path' = ?")
|
||||||
|
.bind(path)
|
||||||
|
.fetch_all(&*db.0)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Loading files with path {path}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a paginated view into the file table
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if a database error occurs while loading.
|
||||||
|
#[instrument(skip(db))]
|
||||||
|
pub async fn list(db: &Database, after: i64, limit: usize) -> Result<Vec<Self>> {
|
||||||
|
let limit: i64 = limit.min(100).try_into().unwrap_or(100); // reasonable limit for pagination size
|
||||||
|
query_as("SELECT * FROM 'file' WHERE id > ? LIMIT ?")
|
||||||
|
.bind(after)
|
||||||
|
.bind(limit)
|
||||||
|
.fetch_all(&*db.0)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Loading up to {limit} files after id {after}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new file
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if a database error occurs when writing, or there is a conflict
|
||||||
|
#[instrument(skip(db))]
|
||||||
|
pub async fn new(db: &Database, path: &str, mime: &str, hash: &Hash) -> Result<()> {
|
||||||
|
query_as("INSERT INTO 'file' ('file_path', 'mime', 'b3hash') VALUES (?, ?, ?) RETURNING *")
|
||||||
|
.bind(path)
|
||||||
|
.bind(mime)
|
||||||
|
.bind(hash.as_bytes().as_slice())
|
||||||
|
.fetch_one(&*db.0)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Inserting new file {path} with mime type {mime}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Deletes a file record from the database. This does not perform any actual file deletion.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if removing the entry from the database fails.
|
||||||
|
#[instrument(skip(db))]
|
||||||
|
pub async fn delete(self, db: &Database) -> Result<()> {
|
||||||
|
let id: i64 = self.id.try_into()?;
|
||||||
|
query!("DELETE FROM 'file' WHERE 'id' = ?", id)
|
||||||
|
.execute(&*db.0)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Deleting file {} with mime type {}",
|
||||||
|
self.file_path, self.mime
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the immutable ID of the object
|
||||||
|
#[must_use]
|
||||||
|
pub const fn id(&self) -> u64 {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates the file with new information
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// THis function returns an error if updating the entry in the database fails
|
||||||
|
#[instrument(skip(db))]
|
||||||
|
pub async fn update(&self, db: &Database) -> Result<()> {
|
||||||
|
let id: i64 = self.id.try_into()?;
|
||||||
|
let b3hash = self.b3hash.as_bytes().as_slice();
|
||||||
|
query!(
|
||||||
|
"UPDATE 'file' SET 'file_path' = ?, 'mime' = ?, 'b3hash' = ? WHERE 'id' = ?",
|
||||||
|
self.file_path,
|
||||||
|
self.mime,
|
||||||
|
b3hash,
|
||||||
|
id
|
||||||
|
)
|
||||||
|
.execute(&*db.0)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Deleting file {} with mime type {}",
|
||||||
|
self.file_path, self.mime
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
27
chir-rs-db/src/lib.rs
Normal file
27
chir-rs-db/src/lib.rs
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
//! Chir.rs database models
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use eyre::{Context, Result};
|
||||||
|
use sqlx::{migrate, SqlitePool};
|
||||||
|
use tracing::instrument;
|
||||||
|
|
||||||
|
pub mod file;
|
||||||
|
|
||||||
|
/// Opaque database handle
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub struct Database(Arc<SqlitePool>);
|
||||||
|
|
||||||
|
/// Opens the database
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if a connection to the database could not be established
|
||||||
|
#[instrument]
|
||||||
|
pub async fn open_database(path: &str) -> Result<Database> {
|
||||||
|
let pool = SqlitePool::connect(path)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Opening database {path}"))?;
|
||||||
|
migrate!().run(&pool).await?;
|
||||||
|
Ok(Database(Arc::new(pool)))
|
||||||
|
}
|
62
chir-rs-gemini/Cargo.toml
Normal file
62
chir-rs-gemini/Cargo.toml
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
[package]
|
||||||
|
name = "chir-rs-gemini"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bytes = "1.8.0"
|
||||||
|
chir-rs-config = { version = "0.1.0", path = "../chir-rs-config" }
|
||||||
|
chir-rs-db = { version = "0.1.0", path = "../chir-rs-db" }
|
||||||
|
eyre = "0.6.12"
|
||||||
|
rustls = "0.23.18"
|
||||||
|
tokio = { version = "1.41.1", features = ["net"] }
|
||||||
|
tokio-rustls = "0.26.0"
|
||||||
|
tracing = "0.1.40"
|
||||||
|
[lints.rust]
|
||||||
|
deprecated-safe = "forbid"
|
||||||
|
elided_lifetimes_in_paths = "warn"
|
||||||
|
explicit_outlives_requirements = "warn"
|
||||||
|
impl-trait-overcaptures = "warn"
|
||||||
|
keyword-idents-2024 = "forbid"
|
||||||
|
let-underscore-drop = "warn"
|
||||||
|
macro-use-extern-crate = "deny"
|
||||||
|
meta-variable-misuse = "deny"
|
||||||
|
missing-abi = "forbid"
|
||||||
|
missing-copy-implementations = "warn"
|
||||||
|
missing-debug-implementations = "deny"
|
||||||
|
missing-docs = "warn"
|
||||||
|
missing-unsafe-on-extern = "deny"
|
||||||
|
non-local-definitions = "warn"
|
||||||
|
redundant-lifetimes = "warn"
|
||||||
|
single-use-lifetimes = "warn"
|
||||||
|
trivial-casts = "warn"
|
||||||
|
trivial-numeric-casts = "warn"
|
||||||
|
unit-bindings = "deny"
|
||||||
|
unnameable-types = "warn"
|
||||||
|
unreachable-pub = "warn"
|
||||||
|
unsafe-code = "forbid"
|
||||||
|
unused-crate-dependencies = "warn"
|
||||||
|
unused-extern-crates = "warn"
|
||||||
|
unused-import-braces = "warn"
|
||||||
|
unused-lifetimes = "warn"
|
||||||
|
unused-macro-rules = "warn"
|
||||||
|
unused-qualifications = "warn"
|
||||||
|
variant-size-differences = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
module-name-repetitions = "allow"
|
||||||
|
alloc-instead-of-core = "warn"
|
||||||
|
allow-attributes-without-reason = "deny"
|
||||||
|
assertions-on-result-states = "forbid"
|
||||||
|
clone-on-ref-ptr = "warn"
|
||||||
|
empty-drop = "warn"
|
||||||
|
expect-used = "deny"
|
||||||
|
inline-asm-x86-att-syntax = "forbid"
|
||||||
|
missing-docs-in-private-items = "warn"
|
||||||
|
panic = "deny"
|
||||||
|
panic-in-result-fn = "forbid"
|
||||||
|
rc-buffer = "warn"
|
||||||
|
rc-mutex = "deny"
|
||||||
|
unwrap-used = "forbid"
|
53
chir-rs-gemini/src/lib.rs
Normal file
53
chir-rs-gemini/src/lib.rs
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
//! Gemini server implementation for chir.rs
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use bytes::BytesMut;
|
||||||
|
use chir_rs_config::ChirRs;
|
||||||
|
use chir_rs_db::Database;
|
||||||
|
use eyre::Result;
|
||||||
|
use rustls::pki_types::{pem::PemObject, CertificateDer, PrivateKeyDer};
|
||||||
|
use tokio::{
|
||||||
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
|
net::TcpListener,
|
||||||
|
};
|
||||||
|
use tokio_rustls::TlsAcceptor;
|
||||||
|
use tracing::{error, info};
|
||||||
|
|
||||||
|
/// entrypoint for the gemini server
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// This function returns an error if starting the gemini server fails
|
||||||
|
pub async fn main(cfg: Arc<ChirRs>, _: Database) -> Result<()> {
|
||||||
|
let certs =
|
||||||
|
CertificateDer::pem_file_iter(&cfg.gemini.certificate)?.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let key = PrivateKeyDer::from_pem_file(&cfg.gemini.private_key)?;
|
||||||
|
let config = rustls::ServerConfig::builder_with_provider(Arc::new(
|
||||||
|
rustls::crypto::aws_lc_rs::default_provider(),
|
||||||
|
))
|
||||||
|
.with_safe_default_protocol_versions()?
|
||||||
|
.with_no_client_auth()
|
||||||
|
.with_single_cert(certs, key)?;
|
||||||
|
let acceptor = TlsAcceptor::from(Arc::new(config));
|
||||||
|
let listener = TcpListener::bind(&*cfg.gemini.listen).await?;
|
||||||
|
info!("Starting Gemini server on {:?}", cfg.gemini.listen);
|
||||||
|
loop {
|
||||||
|
let (stream, _peer_addr) = listener.accept().await?;
|
||||||
|
let acceptor = acceptor.clone();
|
||||||
|
let fut = async move {
|
||||||
|
let mut stream = acceptor.accept(stream).await?;
|
||||||
|
let mut request = BytesMut::with_capacity(4096);
|
||||||
|
stream.read_buf(&mut request).await?;
|
||||||
|
println!("{request:?}");
|
||||||
|
stream.write_all(b"51\r\n").await?;
|
||||||
|
stream.shutdown().await?;
|
||||||
|
Ok::<_, eyre::Report>(())
|
||||||
|
};
|
||||||
|
tokio::spawn(async move {
|
||||||
|
if let Err(err) = fut.await {
|
||||||
|
error!("Failed to handle request: {err:?}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
66
chir-rs-http-api/Cargo.toml
Normal file
66
chir-rs-http-api/Cargo.toml
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
[package]
|
||||||
|
name = "chir-rs-http-api"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
axum = ["axum-core", "async-trait", "bytes", "mime", "tracing"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
async-trait = { version = "0.1.83", optional = true }
|
||||||
|
axum-core = { version = "0.4.5", optional = true }
|
||||||
|
bincode = "2.0.0-rc.3"
|
||||||
|
bytes = { version = "1.8.0", optional = true }
|
||||||
|
http = "1.1.0"
|
||||||
|
mime = { version = "0.3.17", optional = true }
|
||||||
|
thiserror = "2.0.3"
|
||||||
|
tracing = { version = "0.1.40", optional = true }
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
deprecated-safe = "forbid"
|
||||||
|
elided_lifetimes_in_paths = "warn"
|
||||||
|
explicit_outlives_requirements = "warn"
|
||||||
|
impl-trait-overcaptures = "warn"
|
||||||
|
keyword-idents-2024 = "forbid"
|
||||||
|
let-underscore-drop = "warn"
|
||||||
|
macro-use-extern-crate = "deny"
|
||||||
|
meta-variable-misuse = "deny"
|
||||||
|
missing-abi = "forbid"
|
||||||
|
missing-copy-implementations = "warn"
|
||||||
|
missing-debug-implementations = "deny"
|
||||||
|
missing-docs = "warn"
|
||||||
|
missing-unsafe-on-extern = "deny"
|
||||||
|
non-local-definitions = "warn"
|
||||||
|
redundant-lifetimes = "warn"
|
||||||
|
single-use-lifetimes = "warn"
|
||||||
|
trivial-casts = "warn"
|
||||||
|
trivial-numeric-casts = "warn"
|
||||||
|
unit-bindings = "deny"
|
||||||
|
unnameable-types = "warn"
|
||||||
|
unreachable-pub = "warn"
|
||||||
|
unsafe-code = "forbid"
|
||||||
|
unused-crate-dependencies = "warn"
|
||||||
|
unused-extern-crates = "warn"
|
||||||
|
unused-import-braces = "warn"
|
||||||
|
unused-lifetimes = "warn"
|
||||||
|
unused-macro-rules = "warn"
|
||||||
|
unused-qualifications = "warn"
|
||||||
|
variant-size-differences = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
module-name-repetitions = "allow"
|
||||||
|
alloc-instead-of-core = "warn"
|
||||||
|
allow-attributes-without-reason = "deny"
|
||||||
|
assertions-on-result-states = "forbid"
|
||||||
|
clone-on-ref-ptr = "warn"
|
||||||
|
empty-drop = "warn"
|
||||||
|
expect-used = "deny"
|
||||||
|
inline-asm-x86-att-syntax = "forbid"
|
||||||
|
missing-docs-in-private-items = "warn"
|
||||||
|
panic = "deny"
|
||||||
|
panic-in-result-fn = "forbid"
|
||||||
|
rc-buffer = "warn"
|
||||||
|
rc-mutex = "deny"
|
||||||
|
unwrap-used = "forbid"
|
88
chir-rs-http-api/src/axum/bincode.rs
Normal file
88
chir-rs-http-api/src/axum/bincode.rs
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
//! Binary serialization format
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use axum_core::{
|
||||||
|
extract::{FromRequest, Request},
|
||||||
|
response::{IntoResponse, Response},
|
||||||
|
};
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
use bytes::{BufMut as _, Bytes, BytesMut};
|
||||||
|
use http::{header::CONTENT_TYPE, HeaderValue, StatusCode};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
|
use crate::errors::APIError;
|
||||||
|
|
||||||
|
/// Bincode wrapper
|
||||||
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[must_use]
|
||||||
|
pub struct Bincode<T>(pub T);
|
||||||
|
|
||||||
|
impl IntoResponse for APIError {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
(self.status_code(), Bincode(self)).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<T, S> FromRequest<S> for Bincode<T>
|
||||||
|
where
|
||||||
|
T: Decode,
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = APIError;
|
||||||
|
async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
match req.headers().get(CONTENT_TYPE) {
|
||||||
|
Some(c) if c == "application/x+bincode" => {
|
||||||
|
let bytes = Bytes::from_request(req, state).await?;
|
||||||
|
match bincode::decode_from_slice(&bytes, bincode::config::standard()) {
|
||||||
|
Ok(v) => Ok(Self(v.0)),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Body decode error: {e:?}");
|
||||||
|
Err(APIError::PayloadInvalid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(c) => Err(APIError::ClientInvalidContentType {
|
||||||
|
expected: "application/x+bincode".to_string(),
|
||||||
|
received: c
|
||||||
|
.to_str()
|
||||||
|
.map_or_else(|_| format!("{c:?}"), ToString::to_string),
|
||||||
|
}),
|
||||||
|
_ => Err(APIError::ClientMissingContentType {
|
||||||
|
expected: "application/x+bincode".to_string(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> IntoResponse for Bincode<T>
|
||||||
|
where
|
||||||
|
T: Encode,
|
||||||
|
{
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
let mut buf = BytesMut::with_capacity(128).writer();
|
||||||
|
match bincode::encode_into_std_write(self.0, &mut buf, bincode::config::standard()) {
|
||||||
|
Ok(_) => (
|
||||||
|
[(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static("application/x+bincode"),
|
||||||
|
)],
|
||||||
|
buf.into_inner().freeze(),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to encode bincode response: {err:?}");
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
[(
|
||||||
|
CONTENT_TYPE,
|
||||||
|
HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
|
||||||
|
)],
|
||||||
|
"internal server error".to_string(),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
40
chir-rs-http-api/src/axum/error.rs
Normal file
40
chir-rs-http-api/src/axum/error.rs
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
//! Error type impls
|
||||||
|
|
||||||
|
use axum_core::extract::rejection::{
|
||||||
|
BytesRejection, FailedToBufferBody, LengthLimitError, UnknownBodyError,
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::errors::APIError;
|
||||||
|
|
||||||
|
impl From<LengthLimitError> for APIError {
|
||||||
|
fn from(_: LengthLimitError) -> Self {
|
||||||
|
Self::PayloadTooBig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<UnknownBodyError> for APIError {
|
||||||
|
fn from(_: UnknownBodyError) -> Self {
|
||||||
|
Self::PayloadLoadError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<FailedToBufferBody> for APIError {
|
||||||
|
fn from(value: FailedToBufferBody) -> Self {
|
||||||
|
match value {
|
||||||
|
FailedToBufferBody::LengthLimitError(length_limit_error) => length_limit_error.into(),
|
||||||
|
FailedToBufferBody::UnknownBodyError(unknown_body_error) => unknown_body_error.into(),
|
||||||
|
rest => Self::Unknown(format!("Unknown FailedToBufferBody: {rest:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<BytesRejection> for APIError {
|
||||||
|
fn from(value: BytesRejection) -> Self {
|
||||||
|
match value {
|
||||||
|
BytesRejection::FailedToBufferBody(failed_to_buffer_body) => {
|
||||||
|
failed_to_buffer_body.into()
|
||||||
|
}
|
||||||
|
rest => Self::Unknown(format!("Unknown BytesRejection: {rest:?}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
chir-rs-http-api/src/axum/mod.rs
Normal file
4
chir-rs-http-api/src/axum/mod.rs
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
//! Axum support
|
||||||
|
|
||||||
|
pub mod bincode;
|
||||||
|
mod error;
|
51
chir-rs-http-api/src/errors/mod.rs
Normal file
51
chir-rs-http-api/src/errors/mod.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
//! Main error type
|
||||||
|
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
use http::StatusCode;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
/// The main error type
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Encode, Decode, Error)]
|
||||||
|
pub enum APIError {
|
||||||
|
/// Returned when the client sends the wrong content type to the server.
|
||||||
|
#[error("Invalid content type: Expected {expected}, Received {received}")]
|
||||||
|
ClientInvalidContentType {
|
||||||
|
/// Expected value of the content type
|
||||||
|
expected: String,
|
||||||
|
/// Received value of the content type
|
||||||
|
received: String,
|
||||||
|
},
|
||||||
|
/// Returned when the client does not send a content type header
|
||||||
|
#[error("Missing content type: Expected {expected}")]
|
||||||
|
ClientMissingContentType {
|
||||||
|
/// Expected value of the content type
|
||||||
|
expected: String,
|
||||||
|
},
|
||||||
|
/// Returned when the client payload is too large.
|
||||||
|
#[error("Invalid payload: Too large")]
|
||||||
|
PayloadTooBig,
|
||||||
|
/// Returned when there is an unknown error loading the client payloud
|
||||||
|
#[error("Failed to load payload")]
|
||||||
|
PayloadLoadError,
|
||||||
|
/// Returned when the client payload is malformed
|
||||||
|
#[error("Invalid payload")]
|
||||||
|
PayloadInvalid,
|
||||||
|
/// Returned when the error is unknown
|
||||||
|
#[error("Unknown Error")]
|
||||||
|
Unknown(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl APIError {
|
||||||
|
/// Returns the HTTP Status code of the error
|
||||||
|
#[must_use]
|
||||||
|
pub const fn status_code(&self) -> StatusCode {
|
||||||
|
match *self {
|
||||||
|
Self::ClientInvalidContentType { .. } | Self::ClientMissingContentType { .. } => {
|
||||||
|
StatusCode::UNSUPPORTED_MEDIA_TYPE
|
||||||
|
}
|
||||||
|
Self::PayloadTooBig => StatusCode::PAYLOAD_TOO_LARGE,
|
||||||
|
Self::PayloadLoadError | Self::PayloadInvalid => StatusCode::BAD_REQUEST,
|
||||||
|
Self::Unknown(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
6
chir-rs-http-api/src/lib.rs
Normal file
6
chir-rs-http-api/src/lib.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
//! API Type Definitions for chir.rs
|
||||||
|
|
||||||
|
#[cfg(feature = "axum")]
|
||||||
|
pub mod axum;
|
||||||
|
pub mod errors;
|
||||||
|
pub mod readiness;
|
12
chir-rs-http-api/src/readiness.rs
Normal file
12
chir-rs-http-api/src/readiness.rs
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
//! APIs for testing readiness
|
||||||
|
|
||||||
|
use bincode::{Decode, Encode};
|
||||||
|
|
||||||
|
/// Current Ready State
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Encode, Decode)]
|
||||||
|
pub enum ReadyState {
|
||||||
|
/// Indicates that this service is ready to receive requests
|
||||||
|
Ready,
|
||||||
|
/// Indicates that this service is not yet ready to receive requests
|
||||||
|
NotReady,
|
||||||
|
}
|
67
chir-rs-http/Cargo.toml
Normal file
67
chir-rs-http/Cargo.toml
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
[package]
|
||||||
|
name = "chir-rs-http"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
axum = { version = "0.7.9", features = ["tracing"] }
|
||||||
|
axum-prometheus = "0.7.0"
|
||||||
|
chir-rs-config = { version = "0.1.0", path = "../chir-rs-config" }
|
||||||
|
chir-rs-db = { version = "0.1.0", path = "../chir-rs-db" }
|
||||||
|
chir-rs-http-api = { version = "0.1.0", path = "../chir-rs-http-api", features = [
|
||||||
|
"axum",
|
||||||
|
] }
|
||||||
|
eyre = "0.6.12"
|
||||||
|
sentry-tower = { version = "0.34.0", features = ["axum", "axum-matched-path"] }
|
||||||
|
tokio = { version = "1.41.1", features = ["net"] }
|
||||||
|
tower-http = { version = "0.6.2", features = ["trace"] }
|
||||||
|
tracing = "0.1.40"
|
||||||
|
|
||||||
|
[lints.rust]
|
||||||
|
deprecated-safe = "forbid"
|
||||||
|
elided_lifetimes_in_paths = "warn"
|
||||||
|
explicit_outlives_requirements = "warn"
|
||||||
|
impl-trait-overcaptures = "warn"
|
||||||
|
keyword-idents-2024 = "forbid"
|
||||||
|
let-underscore-drop = "warn"
|
||||||
|
macro-use-extern-crate = "deny"
|
||||||
|
meta-variable-misuse = "deny"
|
||||||
|
missing-abi = "forbid"
|
||||||
|
missing-copy-implementations = "warn"
|
||||||
|
missing-debug-implementations = "deny"
|
||||||
|
missing-docs = "warn"
|
||||||
|
missing-unsafe-on-extern = "deny"
|
||||||
|
non-local-definitions = "warn"
|
||||||
|
redundant-lifetimes = "warn"
|
||||||
|
single-use-lifetimes = "warn"
|
||||||
|
trivial-casts = "warn"
|
||||||
|
trivial-numeric-casts = "warn"
|
||||||
|
unit-bindings = "deny"
|
||||||
|
unnameable-types = "warn"
|
||||||
|
unreachable-pub = "warn"
|
||||||
|
unsafe-code = "forbid"
|
||||||
|
unused-crate-dependencies = "warn"
|
||||||
|
unused-extern-crates = "warn"
|
||||||
|
unused-import-braces = "warn"
|
||||||
|
unused-lifetimes = "warn"
|
||||||
|
unused-macro-rules = "warn"
|
||||||
|
unused-qualifications = "warn"
|
||||||
|
variant-size-differences = "warn"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
nursery = { level = "warn", priority = -1 }
|
||||||
|
pedantic = { level = "warn", priority = -1 }
|
||||||
|
module-name-repetitions = "allow"
|
||||||
|
alloc-instead-of-core = "warn"
|
||||||
|
allow-attributes-without-reason = "deny"
|
||||||
|
assertions-on-result-states = "forbid"
|
||||||
|
clone-on-ref-ptr = "warn"
|
||||||
|
empty-drop = "warn"
|
||||||
|
expect-used = "deny"
|
||||||
|
inline-asm-x86-att-syntax = "forbid"
|
||||||
|
missing-docs-in-private-items = "warn"
|
||||||
|
panic = "deny"
|
||||||
|
panic-in-result-fn = "forbid"
|
||||||
|
rc-buffer = "warn"
|
||||||
|
rc-mutex = "deny"
|
||||||
|
unwrap-used = "forbid"
|
73
chir-rs-http/src/lib.rs
Normal file
73
chir-rs-http/src/lib.rs
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
//! HTTP server implementation for chir-rs
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::{
|
||||||
|
extract::{MatchedPath, Request, State},
|
||||||
|
routing::get,
|
||||||
|
Router,
|
||||||
|
};
|
||||||
|
use axum_prometheus::PrometheusMetricLayer;
|
||||||
|
use chir_rs_config::ChirRs;
|
||||||
|
use chir_rs_db::{file::File, Database};
|
||||||
|
use chir_rs_http_api::{axum::bincode::Bincode, readiness::ReadyState};
|
||||||
|
use eyre::{Context, Result};
|
||||||
|
use tokio::net::TcpListener;
|
||||||
|
use tower_http::trace::TraceLayer;
|
||||||
|
use tracing::{info, info_span};
|
||||||
|
|
||||||
|
/// Application state
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AppState {
|
||||||
|
/// Database handle
|
||||||
|
pub db: Database,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entrypoint for the HTTP server component
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// This function returns an error if the startup of the server fails.
|
||||||
|
///
|
||||||
|
/// Errors it encounters during runtime should be automatically handled.
|
||||||
|
pub async fn main(cfg: Arc<ChirRs>, db: Database) -> Result<()> {
|
||||||
|
let (prometheus_layer, metric_handle) = PrometheusMetricLayer::pair();
|
||||||
|
let app = Router::new()
|
||||||
|
// Routes here
|
||||||
|
.route("/.api/readyz", get(|| async { Bincode(ReadyState::Ready) }))
|
||||||
|
.route(
|
||||||
|
"/.api/metrics",
|
||||||
|
get(|| async move { metric_handle.render() }),
|
||||||
|
)
|
||||||
|
.route(
|
||||||
|
"/",
|
||||||
|
get(|State(state): State<AppState>| async move {
|
||||||
|
Bincode(File::list(&state.db, 0, 100).await.ok())
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.with_state(AppState { db })
|
||||||
|
.layer(
|
||||||
|
TraceLayer::new_for_http().make_span_with(|request: &Request<_>| {
|
||||||
|
let matched_path = request
|
||||||
|
.extensions()
|
||||||
|
.get::<MatchedPath>()
|
||||||
|
.map(MatchedPath::as_str);
|
||||||
|
|
||||||
|
info_span!(
|
||||||
|
"http_request",
|
||||||
|
method = ?request.method(),
|
||||||
|
matched_path,
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.layer(prometheus_layer)
|
||||||
|
.layer(sentry_tower::NewSentryLayer::<Request>::new_from_top())
|
||||||
|
.layer(sentry_tower::SentryHttpLayer::with_transaction());
|
||||||
|
let listener = TcpListener::bind(&*cfg.http.listen)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("Binding to TCP {:?}", cfg.http.listen))?;
|
||||||
|
info!("Starting HTTP server on {:?}", cfg.http.listen);
|
||||||
|
axum::serve(listener, app)
|
||||||
|
.await
|
||||||
|
.context("Starting Axum Server")?;
|
||||||
|
Ok(())
|
||||||
|
}
|
15
config-example.toml
Normal file
15
config-example.toml
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
[logging]
|
||||||
|
#sentry_dsn = "…"
|
||||||
|
log_level = "debug"
|
||||||
|
log_style = "Pretty"
|
||||||
|
[http]
|
||||||
|
listen = "[::1]:5621"
|
||||||
|
# Alternatively:
|
||||||
|
# listen = ["127.0.0.1:5621", "10.0.0.0:12345", "[::1]:5621"]
|
||||||
|
[gemini]
|
||||||
|
listen = "[::]:1965"
|
||||||
|
host = "lotte.chir.rs"
|
||||||
|
private_key = "secrets/server.key"
|
||||||
|
certificate = "secrets/server.crt"
|
||||||
|
[database]
|
||||||
|
path = "secrets/test.db"
|
141
flake.lock
Normal file
141
flake.lock
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"cargo2nix": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-compat": "flake-compat",
|
||||||
|
"flake-utils": [
|
||||||
|
"flake-utils"
|
||||||
|
],
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
],
|
||||||
|
"rust-overlay": [
|
||||||
|
"rust-overlay"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1726552619,
|
||||||
|
"narHash": "sha256-ytTBILVMnRZYvjiLYz+J6IFf/TOXdGuP6RDesMx9qgA=",
|
||||||
|
"owner": "DarkKirb",
|
||||||
|
"repo": "cargo2nix",
|
||||||
|
"rev": "baa12124e2de09e1cbbdac320f14809fa55af1a2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "DarkKirb",
|
||||||
|
"ref": "master",
|
||||||
|
"repo": "cargo2nix",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat": {
|
||||||
|
"flake": false,
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696426674,
|
||||||
|
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "edolstra",
|
||||||
|
"repo": "flake-compat",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-compat_2": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1696426674,
|
||||||
|
"narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=",
|
||||||
|
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||||
|
"revCount": 57,
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz?rev=0f9255e01c2351cc7d116c072cb317785dd33b33&revCount=57"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"type": "tarball",
|
||||||
|
"url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1732604498,
|
||||||
|
"narHash": "sha256-jLSaysOTd7nj4sR0FG5ruTz2QBBPR/dInANBIDtJLog=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "096ede7f2b180f03d53fae3d69e4f0d84e199825",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"cargo2nix": "cargo2nix",
|
||||||
|
"flake-compat": "flake-compat_2",
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1732588352,
|
||||||
|
"narHash": "sha256-J2/hxOO1VtBA/u+a+9E+3iJpWT3xsBdghgYAVfoGCJo=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "414e748aae5c9e6ca63c5aafffda03e5dad57ceb",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
73
flake.nix
Normal file
73
flake.nix
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
{
|
||||||
|
description = "rust-template";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
rust-overlay = {
|
||||||
|
url = "github:oxalica/rust-overlay";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
|
|
||||||
|
cargo2nix = {
|
||||||
|
url = "github:DarkKirb/cargo2nix/master";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
inputs.flake-utils.follows = "flake-utils";
|
||||||
|
inputs.rust-overlay.follows = "rust-overlay";
|
||||||
|
};
|
||||||
|
flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{
|
||||||
|
self,
|
||||||
|
nixpkgs,
|
||||||
|
flake-utils,
|
||||||
|
rust-overlay,
|
||||||
|
cargo2nix,
|
||||||
|
...
|
||||||
|
}@inputs:
|
||||||
|
flake-utils.lib.eachSystem [ "x86_64-linux" ] (
|
||||||
|
system:
|
||||||
|
let
|
||||||
|
overlays = [
|
||||||
|
cargo2nix.overlays.default
|
||||||
|
(import rust-overlay)
|
||||||
|
];
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system overlays;
|
||||||
|
};
|
||||||
|
rustPkgs = pkgs.rustBuilder.makePackageSet {
|
||||||
|
packageFun = import ./Cargo.nix;
|
||||||
|
rustChannel = "stable";
|
||||||
|
rustVersion = "latest";
|
||||||
|
packageOverrides = pkgs: pkgs.rustBuilder.overrides.all;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
rec {
|
||||||
|
devShells.default =
|
||||||
|
with pkgs;
|
||||||
|
mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
cargo2nix.packages.${system}.cargo2nix
|
||||||
|
rustfilt
|
||||||
|
gdb
|
||||||
|
sqlx-cli
|
||||||
|
cargo-expand
|
||||||
|
sqlite
|
||||||
|
];
|
||||||
|
};
|
||||||
|
packages = pkgs.lib.mapAttrs (_: v: (v { }).overrideAttrs { dontStrip = true; }) rustPkgs.workspace;
|
||||||
|
nixosModules.default = import ./nixos {
|
||||||
|
inherit inputs system;
|
||||||
|
};
|
||||||
|
checks = pkgs.lib.mapAttrs (_: v: pkgs.rustBuilder.runTests v { }) rustPkgs.workspace;
|
||||||
|
hydraJobs = {
|
||||||
|
inherit packages checks;
|
||||||
|
};
|
||||||
|
formatter = pkgs.nixfmt-rfc-style;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
# Trick renovate into working: "github:NixOS/nixpkgs/nixpkgs-unstable"
|
4
gencert.sh
Executable file
4
gencert.sh
Executable file
|
@ -0,0 +1,4 @@
|
||||||
|
#!/usr/bin/env nix-shell
|
||||||
|
#! nix-shell -p openssh -i bash
|
||||||
|
|
||||||
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout secrets/server.key -out secrets/server.crt
|
103
src/main.rs
Normal file
103
src/main.rs
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
//! Main entrypoint for the chir-rs web server
|
||||||
|
|
||||||
|
use core::str::FromStr;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use chir_rs_config::ChirRs;
|
||||||
|
use eyre::{Context, Result};
|
||||||
|
// implicitly used
|
||||||
|
use sentry_eyre as _;
|
||||||
|
use tokio::try_join;
|
||||||
|
use tracing_error::ErrorLayer;
|
||||||
|
use tracing_subscriber::{
|
||||||
|
fmt::format::JsonFields, layer::SubscriberExt as _, util::SubscriberInitExt as _, Layer,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
color_eyre::install().ok();
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
|
// NO THREADS BEFORE THIS POINT
|
||||||
|
|
||||||
|
let cfg = ChirRs::read_from_env().context("Reading chir.rs configuration")?;
|
||||||
|
|
||||||
|
let _guard = sentry::init(sentry::ClientOptions {
|
||||||
|
dsn: cfg.logging.sentry_dsn.clone(),
|
||||||
|
release: sentry::release_name!(),
|
||||||
|
traces_sample_rate: 1.0,
|
||||||
|
attach_stacktrace: true,
|
||||||
|
debug: cfg!(debug_assertions),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
let log_filter = tracing_subscriber::EnvFilter::from_str(&cfg.logging.log_level)
|
||||||
|
.with_context(|| format!("Setting log filter to {}", cfg.logging.log_level))?;
|
||||||
|
|
||||||
|
match cfg.logging.log_style {
|
||||||
|
chir_rs_config::LogFormat::Full => {
|
||||||
|
let log_format = tracing_subscriber::fmt::format();
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.event_format(log_format)
|
||||||
|
.with_filter(log_filter),
|
||||||
|
)
|
||||||
|
.with(ErrorLayer::default())
|
||||||
|
.with(sentry_tracing::layer())
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
chir_rs_config::LogFormat::Compact => {
|
||||||
|
let log_format = tracing_subscriber::fmt::format().compact();
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.event_format(log_format)
|
||||||
|
.with_filter(log_filter),
|
||||||
|
)
|
||||||
|
.with(ErrorLayer::default())
|
||||||
|
.with(sentry_tracing::layer())
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
chir_rs_config::LogFormat::Pretty => {
|
||||||
|
let log_format = tracing_subscriber::fmt::format().pretty();
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.event_format(log_format)
|
||||||
|
.with_filter(log_filter),
|
||||||
|
)
|
||||||
|
.with(ErrorLayer::default())
|
||||||
|
.with(sentry_tracing::layer())
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
chir_rs_config::LogFormat::Json => {
|
||||||
|
let log_format = tracing_subscriber::fmt::format().json();
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.event_format(log_format)
|
||||||
|
.fmt_fields(JsonFields::new())
|
||||||
|
.with_filter(log_filter),
|
||||||
|
)
|
||||||
|
.with(ErrorLayer::default())
|
||||||
|
.with(sentry_tracing::layer())
|
||||||
|
.init();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.context("Building thread pool for tokio")?
|
||||||
|
.block_on(async move {
|
||||||
|
let cfg = Arc::new(cfg);
|
||||||
|
let db = chir_rs_db::open_database(&cfg.database.path).await?;
|
||||||
|
try_join!(
|
||||||
|
chir_rs_http::main(Arc::clone(&cfg), db.clone()),
|
||||||
|
chir_rs_gemini::main(Arc::clone(&cfg), db.clone())
|
||||||
|
)
|
||||||
|
.context("Starting server components")?;
|
||||||
|
Ok::<_, eyre::Report>(())
|
||||||
|
})
|
||||||
|
.context("Running chir.rs")
|
||||||
|
}
|
Loading…
Reference in a new issue