feat(server): connection pool, session persistence, blob storage in SqlStore

- Replace Mutex<Connection> with Vec<Mutex<Connection>> pool (default 4)
  with try_lock fast-path and blocking fallback
- Add SessionRecord struct and session CRUD to Store trait (default no-ops)
- Implement session persistence in SqlStore (sessions table, migration 009)
- Add blob upload/download with SHA-256 verified staging assembly
  (blobs + blob_staging tables, migration 010)
- All 35 server tests pass, FileBackedStore unaffected
This commit is contained in:
2026-03-04 12:09:03 +01:00
parent f09dbe10ce
commit 6273ab668d
4 changed files with 346 additions and 59 deletions

View File

@@ -22,6 +22,14 @@ pub enum StorageError {
DuplicateUser(String),
}
/// A persisted session record mapping a bearer token to an authenticated user.
pub struct SessionRecord {
pub username: String,
pub identity_key: Vec<u8>,
pub created_at: u64,
pub expires_at: u64,
}
fn lock<T>(m: &Mutex<T>) -> Result<std::sync::MutexGuard<'_, T>, StorageError> {
m.lock()
.map_err(|e| StorageError::Io(format!("lock poisoned: {e}")))
@@ -199,6 +207,55 @@ pub trait Store: Send + Sync {
/// Return the number of registered devices for an identity.
fn device_count(&self, identity_key: &[u8]) -> Result<usize, StorageError>;
// ── Session persistence ────────────────────────────────────────────────
/// Store a session token → record mapping.
fn store_session(&self, _token: &[u8], _record: &SessionRecord) -> Result<(), StorageError> {
Ok(())
}
/// Retrieve a session record by bearer token.
fn get_session(&self, _token: &[u8]) -> Result<Option<SessionRecord>, StorageError> {
Ok(None)
}
/// Delete all sessions whose `expires_at` <= `now`. Returns count deleted.
fn delete_expired_sessions(&self, _now: u64) -> Result<usize, StorageError> {
Ok(0)
}
/// Delete a single session by token.
fn delete_session(&self, _token: &[u8]) -> Result<(), StorageError> {
Ok(())
}
// ── Blob storage ───────────────────────────────────────────────────────
/// Append a chunk to the staging area for an in-progress upload.
/// When all chunks have arrived (sum of chunk sizes == `total_size`), assembles the blob,
/// verifies its SHA-256 hash against `blob_hash`, inserts into permanent storage, and
/// returns `Some(blob_id)`. Otherwise returns `None`.
fn store_blob_chunk(
&self,
_blob_hash: &[u8],
_chunk: &[u8],
_offset: u64,
_total_size: u64,
_mime_type: &str,
) -> Result<Option<Vec<u8>>, StorageError> {
Err(StorageError::Io("blob storage not supported".into()))
}
/// Read a slice of a completed blob. Returns `(chunk_data, total_size, mime_type)`.
fn get_blob_chunk(
&self,
_blob_id: &[u8],
_offset: u64,
_length: u32,
) -> Result<Option<(Vec<u8>, u64, String)>, StorageError> {
Err(StorageError::Io("blob storage not supported".into()))
}
}
// ── ChannelKey ───────────────────────────────────────────────────────────────