feat: file transfer system (avatar/icon download with cacache) (#40)
* docs(architecture): add file transfer design, research, and implementation plan * feat(cache): add chanora_cache crate with cacache-backed blob cache - New chanora_cache crate: content-addressed blob store wrapping cacache - BlobCache API: async put/get/remove/clear/total_size/evict - Key validation: av_ prefix (32 hex chars), ic_ prefix (decimal digits) - Cacache provides crash safety, SSRI integrity, content dedup - Mtime-based eviction via cacache::list_sync + sort by timestamp - 7 unit tests all passing - Added to workspace members * feat(protocol): add file download support for avatars and icons - Add Request::DownloadFile variant with oneshot reply - Add ProtocolClient::download_avatar(client_uid) and download_icon(icon_id) - Track pending file downloads by FiletransferHandle - Handle StreamItem::FileDownload: read bytes from TCP stream - Handle StreamItem::FiletransferFailed: map to ProtocolError - Add ProtocolError::FileTransfer(String) variant - Add path helper tests for avatar/icon download paths - No tsclientlib types leak across the adapter boundary * feat(core): add blob cache wiring and avatar download orchestration - Add chanora_cache dependency to Cargo.toml - Add blob_cache field to ChanoraSession (Arc<Mutex<Option<BlobCache>>>) - Add init_cache() method: creates BlobCache, runs eviction - Add get_avatar() method: cache-first, download on miss, store in cache - Add clear_cache() and cache_size() methods for cache management - Add CoreError::Cache variant for BlobCacheError conversion - Add avatar_cache integration test * feat(bridge): add init_cache, download_avatar, and cache management functions - Add init_cache(dir) bridge function - Add download_avatar(avatar_hash, client_uid) bridge function - Add clear_file_cache() and file_cache_size() bridge functions - Map CoreError::Cache and ProtocolError::FileTransfer in BridgeError * feat(flutter): add cache initialization wiring and avatar download shims - Add wireCache() to app_bootstrap using getApplicationCacheDirectory() - Call wireCache() after wireStorage() in main bootstrap flow - Add Dart-side initCache and downloadAvatar wrapper shims in api.dart - Update Cargo.lock for new chanora_cache dependency * feat(core): FileTransferService with coalescing, throttling, negative cache - New file_transfer module with FileTransferService struct - Semaphore(2) throttles concurrent downloads - In-flight HashMap coalesces duplicate avatar requests - 5-min negative cache short-circuits ServerRejected misses - ChanoraSession delegates get_avatar through the service - connect/disconnect update shared protocol handle - clear_cache/cache_size delegate to service - 2 new unit tests (cached hit, negative cache) * feat(core,bridge): add get_icon with coalescing and negative cache - FileTransferService::get_icon() mirrors get_avatar pattern - ChanoraSession::get_icon() delegates through FileTransferService - Bridge download_icon() exposed for Flutter - Dart downloadIcon() shim added - Uses PREFIX_ICON (ic_<crc32u>) cache key format - 1 new unit test (cached icon hit) * fix(core,protocol): simplify store_protocol and add download size cap - store_protocol: always write to shared Arc<Mutex<Option<ProtocolClient>>>; the FileTransferService holds the same Arc so it sees updates automatically - read_download_bytes: reject downloads exceeding 10 MB to prevent malicious servers from causing OOM
This commit is contained in:
@@ -1465,6 +1465,54 @@ pub async fn init_storage(dir: String) -> Result<(), BridgeError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Configure the bridge blob cache root.
|
||||
pub async fn init_cache(dir: String) -> Result<(), BridgeError> {
|
||||
runtime()
|
||||
.spawn(async move { session().init_cache(&dir).await })
|
||||
.await
|
||||
.map_err(|e| task_join_error("init_cache", e))??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Resolve avatar bytes through the bridge.
|
||||
pub async fn download_avatar(
|
||||
avatar_hash: String,
|
||||
client_uid: String,
|
||||
) -> Result<Option<Vec<u8>>, BridgeError> {
|
||||
runtime()
|
||||
.spawn(async move { session().get_avatar(&avatar_hash, &client_uid).await })
|
||||
.await
|
||||
.map_err(|e| task_join_error("download_avatar", e))?
|
||||
.map_err(BridgeError::from)
|
||||
}
|
||||
|
||||
/// Resolve icon bytes through the bridge.
|
||||
pub async fn download_icon(icon_id: u64) -> Result<Option<Vec<u8>>, BridgeError> {
|
||||
runtime()
|
||||
.spawn(async move { session().get_icon(icon_id).await })
|
||||
.await
|
||||
.map_err(|e| task_join_error("download_icon", e))?
|
||||
.map_err(BridgeError::from)
|
||||
}
|
||||
|
||||
/// Purge cached protocol-owned assets.
|
||||
pub async fn clear_file_cache() -> Result<(), BridgeError> {
|
||||
runtime()
|
||||
.spawn(async move { session().clear_cache().await })
|
||||
.await
|
||||
.map_err(|e| task_join_error("clear_file_cache", e))??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Report the configured file-cache size.
|
||||
pub async fn file_cache_size() -> Result<u64, BridgeError> {
|
||||
runtime()
|
||||
.spawn(async move { session().cache_size().await })
|
||||
.await
|
||||
.map_err(|e| task_join_error("file_cache_size", e))?
|
||||
.map_err(BridgeError::from)
|
||||
}
|
||||
|
||||
/// Bookmark DTO mirroring [`chanora_core::Bookmark`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BridgeBookmark {
|
||||
|
||||
@@ -110,9 +110,13 @@ impl From<chanora_core::CoreError> for BridgeError {
|
||||
code,
|
||||
message,
|
||||
}) => BridgeError::ServerRejected { code, message },
|
||||
chanora_core::CoreError::Protocol(chanora_core::ProtocolError::FileTransfer(p)) => {
|
||||
BridgeError::Connection(format!("file transfer: {p}"))
|
||||
}
|
||||
chanora_core::CoreError::Protocol(p) => BridgeError::Connection(format!("{p}")),
|
||||
chanora_core::CoreError::Audio(a) => BridgeError::Connection(format!("audio: {a}")),
|
||||
chanora_core::CoreError::Storage(s) => BridgeError::Connection(format!("storage: {s}")),
|
||||
chanora_core::CoreError::Cache(c) => BridgeError::Connection(format!("cache: {c}")),
|
||||
other => BridgeError::Unmapped(format!("{other}")),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user