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