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:
@@ -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 ───────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user