mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-20 13:38:11 +02:00
Lot 4 (socle) : store accessible à Rust, routeur /api côté façade
Le Go expose sa base à la façade par l'API interne : lecture, écriture, suppression d'une clé et lecture d'un bucket entier. Il reste l'unique propriétaire du fichier bbolt ; les écritures passent par putBytes/update et invalident son cache comme une écriture locale. Base illisible : 503, pour que Rust distingue « rien » de « pas pu lire ». Côté Rust : client du store (seul point de passage, que la migration finale de la base ne touchera qu'ici), routeur /api avec contexte partagé et garde d'authentification réutilisable ; /api/ping y emménage. Modules réservés aux aides portées ensuite (système, état du moteur, presets, chemins, GGUF). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a672760f3c
commit
bea29bc6a9
14 files changed
+401
-22
No files matched your search
@@ -0,0 +1,136 @@
|
||||
//! Accès à la base de Loki À TRAVERS le Go (`/internal/v1/store`, voir
|
||||
//! `web_internal.go`). Le Go reste l'unique propriétaire du fichier bbolt
|
||||
//! jusqu'au lot qui le migrera ; ce module est l'unique point de passage
|
||||
//! côté Rust, pour que cette migration ne touche qu'ici.
|
||||
|
||||
use bytes::Bytes;
|
||||
use http_body_util::{BodyExt, Full};
|
||||
use hyper::{Method, Request, StatusCode};
|
||||
use hyper_util::client::legacy::{Client, connect::HttpConnector};
|
||||
use hyper_util::rt::TokioExecutor;
|
||||
use std::collections::HashMap;
|
||||
use std::net::SocketAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::auth::INTERNAL_HEADER;
|
||||
|
||||
pub const BK_CONFIG: &str = "config";
|
||||
pub const BK_PREFS: &str = "prefs";
|
||||
pub const BK_STATE: &str = "state";
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Store {
|
||||
client: Client<HttpConnector, Full<Bytes>>,
|
||||
backend: SocketAddr,
|
||||
token: String,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
pub fn new(backend: SocketAddr, token: String) -> Self {
|
||||
Store {
|
||||
client: Client::builder(TokioExecutor::new()).build_http(),
|
||||
backend,
|
||||
token,
|
||||
}
|
||||
}
|
||||
|
||||
async fn call(
|
||||
&self,
|
||||
method: Method,
|
||||
path: &str,
|
||||
body: Vec<u8>,
|
||||
) -> Result<(StatusCode, Bytes), String> {
|
||||
let req = Request::builder()
|
||||
.method(method)
|
||||
.uri(format!("http://{}{path}", self.backend))
|
||||
.header(INTERNAL_HEADER, &self.token)
|
||||
.body(Full::new(Bytes::from(body)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
let fut = async {
|
||||
let resp = self.client.request(req).await.map_err(|e| e.to_string())?;
|
||||
let status = resp.status();
|
||||
let body = resp
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.to_bytes();
|
||||
Ok((status, body))
|
||||
};
|
||||
tokio::time::timeout(Duration::from_secs(10), fut)
|
||||
.await
|
||||
.map_err(|_| "store : délai dépassé".to_string())?
|
||||
}
|
||||
|
||||
/// Valeur brute ; `Ok(None)` = absente, `Err` = pas pu lire (à distinguer
|
||||
/// pour les secrets : comme `getBytesErr`).
|
||||
pub async fn get(&self, bucket: &str, key: &str) -> Result<Option<Vec<u8>>, String> {
|
||||
let path = format!("/internal/v1/store?bucket={}&key={}", enc(bucket), enc(key));
|
||||
match self.call(Method::GET, &path, Vec::new()).await? {
|
||||
(StatusCode::OK, b) => Ok(Some(b.to_vec())),
|
||||
(StatusCode::NOT_FOUND, _) => Ok(None),
|
||||
(s, b) => Err(format!("store {s} : {}", String::from_utf8_lossy(&b))),
|
||||
}
|
||||
}
|
||||
|
||||
/// Comme `getStr` du Go : erreur ou absence → "".
|
||||
pub async fn get_str(&self, bucket: &str, key: &str) -> String {
|
||||
match self.get(bucket, key).await {
|
||||
Ok(Some(v)) => String::from_utf8_lossy(&v).into_owned(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn put(&self, bucket: &str, key: &str, val: Vec<u8>) -> Result<(), String> {
|
||||
let path = format!("/internal/v1/store?bucket={}&key={}", enc(bucket), enc(key));
|
||||
match self.call(Method::PUT, &path, val).await? {
|
||||
(StatusCode::NO_CONTENT, _) => Ok(()),
|
||||
(s, b) => Err(format!("store {s} : {}", String::from_utf8_lossy(&b))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn del(&self, bucket: &str, key: &str) -> Result<(), String> {
|
||||
let path = format!("/internal/v1/store?bucket={}&key={}", enc(bucket), enc(key));
|
||||
match self.call(Method::DELETE, &path, Vec::new()).await? {
|
||||
(StatusCode::NO_CONTENT, _) => Ok(()),
|
||||
(s, b) => Err(format!("store {s} : {}", String::from_utf8_lossy(&b))),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn all(&self, bucket: &str) -> Result<HashMap<String, String>, String> {
|
||||
let path = format!("/internal/v1/store/all?bucket={}", enc(bucket));
|
||||
match self.call(Method::GET, &path, Vec::new()).await? {
|
||||
(StatusCode::OK, b) => serde_json::from_slice(&b).map_err(|e| e.to_string()),
|
||||
(s, b) => Err(format!("store {s} : {}", String::from_utf8_lossy(&b))),
|
||||
}
|
||||
}
|
||||
|
||||
/// `ReadConfig()` du Go : la configuration du moteur ; base illisible →
|
||||
/// configuration vide (comme `allKV`).
|
||||
pub async fn config(&self) -> HashMap<String, String> {
|
||||
self.all(BK_CONFIG).await.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
/// Encodage d'un paramètre de requête (RFC 3986, caractères non réservés
|
||||
/// conservés).
|
||||
pub fn enc(s: &str) -> String {
|
||||
let mut out = String::with_capacity(s.len());
|
||||
for b in s.bytes() {
|
||||
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
|
||||
out.push(b as char);
|
||||
} else {
|
||||
out.push_str(&format!("%{b:02X}"));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn encodage() {
|
||||
assert_eq!(super::enc("sysprompt:a b/é"), "sysprompt%3Aa%20b%2F%C3%A9");
|
||||
assert_eq!(super::enc("PORT"), "PORT");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user