mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +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
@@ -2,9 +2,12 @@ package loki
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
bolt "go.etcd.io/bbolt"
|
||||
)
|
||||
|
||||
// API interne entre la façade Rust (rust/loki-rs) et ce serveur.
|
||||
@@ -56,12 +59,107 @@ func withInternalAPI(next http.Handler) http.Handler {
|
||||
switch r.URL.Path {
|
||||
case "/internal/v1/auth":
|
||||
handleInternalAuth(w, r)
|
||||
case "/internal/v1/store":
|
||||
handleInternalStore(w, r)
|
||||
case "/internal/v1/store/all":
|
||||
handleInternalStoreAll(w, r)
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// --- Store ----------------------------------------------------------------
|
||||
//
|
||||
// Accès générique à la base pour les routes déjà portées en Rust (lot 4) : le
|
||||
// Go reste l'unique propriétaire du fichier bbolt jusqu'au lot qui le
|
||||
// migrera ; Rust lit et écrit À TRAVERS lui. Les écritures passent par
|
||||
// putBytes/update, donc invalident le cache de ce process comme une écriture
|
||||
// locale.
|
||||
//
|
||||
// GET /internal/v1/store?bucket=B&key=K 200 + valeur brute | 404 absente
|
||||
// PUT /internal/v1/store?bucket=B&key=K corps = valeur → 204
|
||||
// DELETE /internal/v1/store?bucket=B&key=K → 204
|
||||
// GET /internal/v1/store/all?bucket=B 200 + {"clé":"valeur",…}
|
||||
//
|
||||
// Base illisible : 503 — l'appelant distingue « rien » de « pas pu lire »,
|
||||
// comme getBytesErr.
|
||||
|
||||
// internalStoreMaxPut borne une écriture : la plus grosse valeur légitime (le
|
||||
// journal d'une discussion) reste très en dessous.
|
||||
const internalStoreMaxPut = 64 << 20
|
||||
|
||||
func handleInternalStore(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
bucket, key := q.Get("bucket"), q.Get("key")
|
||||
if bucket == "" || key == "" {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "bucket et key requis"})
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodGet:
|
||||
var val []byte
|
||||
var found bool
|
||||
err := view(bucket, func(b *bolt.Bucket) error {
|
||||
if v := b.Get([]byte(key)); v != nil {
|
||||
val, found = append([]byte(nil), v...), true
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
sendJSON(w, http.StatusServiceUnavailable, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if !found {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Write(val)
|
||||
case http.MethodPut:
|
||||
val, err := io.ReadAll(io.LimitReader(r.Body, internalStoreMaxPut+1))
|
||||
if err != nil || len(val) > internalStoreMaxPut {
|
||||
sendJSON(w, http.StatusRequestEntityTooLarge, map[string]any{"error": "valeur trop grande ou illisible"})
|
||||
return
|
||||
}
|
||||
if err := putBytes(bucket, key, val); err != nil {
|
||||
sendJSON(w, http.StatusServiceUnavailable, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case http.MethodDelete:
|
||||
err := update(bucket, func(b *bolt.Bucket) error { return b.Delete([]byte(key)) })
|
||||
if err != nil {
|
||||
sendJSON(w, http.StatusServiceUnavailable, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.Header().Set("Allow", "GET, PUT, DELETE")
|
||||
sendJSON(w, http.StatusMethodNotAllowed, map[string]any{"error": "méthode non permise"})
|
||||
}
|
||||
}
|
||||
|
||||
func handleInternalStoreAll(w http.ResponseWriter, r *http.Request) {
|
||||
bucket := r.URL.Query().Get("bucket")
|
||||
if r.Method != http.MethodGet || bucket == "" {
|
||||
sendJSON(w, http.StatusBadRequest, map[string]any{"error": "GET ?bucket= attendu"})
|
||||
return
|
||||
}
|
||||
m := map[string]string{}
|
||||
err := view(bucket, func(b *bolt.Bucket) error {
|
||||
return b.ForEach(func(k, v []byte) error {
|
||||
m[string(k)] = string(v)
|
||||
return nil
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
sendJSON(w, http.StatusServiceUnavailable, map[string]any{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
sendJSON(w, http.StatusOK, m)
|
||||
}
|
||||
|
||||
// handleInternalAuth : de quoi authentifier une requête de pilotage côté Rust.
|
||||
// Base illisible → 503 : la façade ferme alors l'accès, elle n'ouvre jamais
|
||||
// sur une panne d'E/S (cf. readWebKeyErr).
|
||||
|
||||
@@ -2,6 +2,7 @@ package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -73,3 +74,59 @@ func TestAPIInterneLaisseLeResteAuMux(t *testing.T) {
|
||||
t.Fatalf("relais au mux : %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func appelStore(t *testing.T, method, path string, body string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
var rd io.Reader
|
||||
if body != "" {
|
||||
rd = strings.NewReader(body)
|
||||
}
|
||||
r := httptest.NewRequest(method, path, rd)
|
||||
r.Header.Set(internalTokenHeader, jetonTest)
|
||||
w := httptest.NewRecorder()
|
||||
withInternalAPI(http.NotFoundHandler()).ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func TestAPIInterneStore(t *testing.T) {
|
||||
testHome(t)
|
||||
t.Setenv(internalTokenEnv, jetonTest)
|
||||
if w := appelStore(t, "GET", "/internal/v1/store?bucket=state&key=absente", ""); w.Code != 404 {
|
||||
t.Fatalf("clé absente : %d", w.Code)
|
||||
}
|
||||
if w := appelStore(t, "PUT", "/internal/v1/store?bucket=state&key=k", "valeur\x00binaire"); w.Code != 204 {
|
||||
t.Fatalf("put : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
if got := getStr(bkState, "k"); got != "valeur\x00binaire" {
|
||||
t.Fatalf("écrit : %q", got)
|
||||
}
|
||||
w := appelStore(t, "GET", "/internal/v1/store?bucket=state&key=k", "")
|
||||
if w.Code != 200 || w.Body.String() != "valeur\x00binaire" {
|
||||
t.Fatalf("get : %d %q", w.Code, w.Body.String())
|
||||
}
|
||||
if err := putStr(bkConfig, "PORT", "8081"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w = appelStore(t, "GET", "/internal/v1/store/all?bucket=config", "")
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &m); err != nil || m["PORT"] != "8081" {
|
||||
t.Fatalf("all : %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
// Écriture via l'API = écriture locale : le cache de config est à jour.
|
||||
_ = ReadConfig()
|
||||
if w := appelStore(t, "PUT", "/internal/v1/store?bucket=config&key=PORT", "9000"); w.Code != 204 {
|
||||
t.Fatalf("put config : %d", w.Code)
|
||||
}
|
||||
if LLMPort() != 9000 {
|
||||
t.Fatalf("cache non invalidé : port %d", LLMPort())
|
||||
}
|
||||
if w := appelStore(t, "DELETE", "/internal/v1/store?bucket=state&key=k", ""); w.Code != 204 {
|
||||
t.Fatalf("delete : %d", w.Code)
|
||||
}
|
||||
if getStr(bkState, "k") != "" {
|
||||
t.Fatal("clé non supprimée")
|
||||
}
|
||||
if w := appelStore(t, "GET", "/internal/v1/store?bucket=state", ""); w.Code != 400 {
|
||||
t.Fatalf("sans clé : %d", w.Code)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user