use std::{collections::HashMap as StdHashMap, sync::Arc};
use gloo_net::http::Request;
use scc::HashMap;
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexManifest {
pub version: u32,
pub chunk_size: usize,
pub total_size: u64,
pub files: StdHashMap<String, FileManifest>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileManifest {
pub size: usize,
pub chunks: Vec<String>,
}
#[derive(Debug)]
pub enum DirectoryError {
Network(String),
Parse(String),
NotFound(String),
}
impl std::fmt::Display for DirectoryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
DirectoryError::Network(e) => write!(f, "Network error: {e}"),
DirectoryError::Parse(e) => write!(f, "Parse error: {e}"),
DirectoryError::NotFound(e) => write!(f, "Not found: {e}"),
}
}
}
impl From<DirectoryError> for JsValue {
fn from(err: DirectoryError) -> Self {
JsValue::from_str(&err.to_string())
}
}
#[derive(Clone)]
pub struct HttpDirectory {
base_url: String,
manifest: Arc<IndexManifest>,
chunk_cache: Arc<HashMap<String, Vec<u8>>>,
}
impl HttpDirectory {
pub async fn new(base_url: &str) -> Result<Self, DirectoryError> {
let manifest_url = format!("{}/manifest.json", base_url.trim_end_matches('/'));
let response = Request::get(&manifest_url)
.send()
.await
.map_err(|e| DirectoryError::Network(e.to_string()))?;
if !response.ok() {
return Err(DirectoryError::Network(format!(
"Failed to fetch manifest: HTTP {}",
response.status()
)));
}
let manifest_text = response
.text()
.await
.map_err(|e| DirectoryError::Network(e.to_string()))?;
let manifest: IndexManifest = serde_json::from_str(&manifest_text)
.map_err(|e| DirectoryError::Parse(e.to_string()))?;
Ok(Self {
base_url: base_url.trim_end_matches('/').to_string(),
manifest: Arc::new(manifest),
chunk_cache: Arc::new(HashMap::new()),
})
}
pub fn with_manifest(base_url: &str, manifest: IndexManifest) -> Self {
Self {
base_url: base_url.trim_end_matches('/').to_string(),
manifest: Arc::new(manifest),
chunk_cache: Arc::new(HashMap::new()),
}
}
pub fn manifest(&self) -> &IndexManifest {
&self.manifest
}
pub async fn load_file(&self, filename: &str) -> Result<Vec<u8>, DirectoryError> {
let file_manifest =
self.manifest.files.get(filename).ok_or_else(|| {
DirectoryError::NotFound(format!("File not in manifest: {filename}"))
})?;
let mut data = Vec::with_capacity(file_manifest.size);
for chunk_name in &file_manifest.chunks {
let chunk_data = self.load_chunk(chunk_name).await?;
data.extend(chunk_data);
}
data.truncate(file_manifest.size);
Ok(data)
}
pub async fn load_range(
&self,
filename: &str,
start: usize,
end: usize,
) -> Result<Vec<u8>, DirectoryError> {
let file_manifest =
self.manifest.files.get(filename).ok_or_else(|| {
DirectoryError::NotFound(format!("File not in manifest: {filename}"))
})?;
if start >= file_manifest.size || end > file_manifest.size || start >= end {
return Err(DirectoryError::NotFound(format!(
"Invalid range: {}-{} for file of size {}",
start, end, file_manifest.size
)));
}
let chunk_size = self.manifest.chunk_size;
let start_chunk_idx = start / chunk_size;
let end_chunk_idx = (end - 1) / chunk_size;
let mut full_data = Vec::new();
for idx in start_chunk_idx..=end_chunk_idx {
if idx < file_manifest.chunks.len() {
let chunk_name = &file_manifest.chunks[idx];
let chunk_data = self.load_chunk(chunk_name).await?;
full_data.extend(chunk_data);
}
}
let data_start = start - (start_chunk_idx * chunk_size);
let data_end = data_start + (end - start);
if data_end > full_data.len() {
return Err(DirectoryError::NotFound(
"Range exceeds available data".to_string(),
));
}
Ok(full_data[data_start..data_end].to_vec())
}
async fn load_chunk(&self, chunk_name: &str) -> Result<Vec<u8>, DirectoryError> {
if let Some(entry) = self.chunk_cache.get_async(chunk_name).await {
return Ok(entry.get().clone());
}
let chunk_url = format!("{}/{}", self.base_url, chunk_name);
let response = Request::get(&chunk_url)
.send()
.await
.map_err(|e| DirectoryError::Network(e.to_string()))?;
if !response.ok() {
return Err(DirectoryError::Network(format!(
"Failed to fetch chunk {}: HTTP {}",
chunk_name,
response.status()
)));
}
let bytes = response
.binary()
.await
.map_err(|e| DirectoryError::Network(e.to_string()))?;
let _ = self
.chunk_cache
.insert_async(chunk_name.to_string(), bytes.clone())
.await;
Ok(bytes)
}
pub fn cached_chunk_count(&self) -> usize {
self.chunk_cache.len()
}
pub fn clear_cache(&self) {
self.chunk_cache.clear_sync();
}
pub fn total_size(&self) -> u64 {
self.manifest.total_size
}
pub fn list_files(&self) -> Vec<&str> {
self.manifest.files.keys().map(|s| s.as_str()).collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_manifest() -> IndexManifest {
let mut files = StdHashMap::new();
files.insert(
"test.bin".to_string(),
FileManifest {
size: 1000,
chunks: vec!["chunk_0000.bin".to_string(), "chunk_0001.bin".to_string()],
},
);
IndexManifest {
version: 1,
chunk_size: 512,
total_size: 1000,
files,
}
}
#[test]
fn test_manifest_deserialization() {
let json = r#"{
"version": 1,
"chunk_size": 65536,
"total_size": 100000,
"files": {
"data.bin": {
"size": 100000,
"chunks": ["chunk_0000.bin", "chunk_0001.bin"]
}
}
}"#;
let manifest: IndexManifest = serde_json::from_str(json).unwrap();
assert_eq!(manifest.version, 1);
assert_eq!(manifest.chunk_size, 65536);
assert!(manifest.files.contains_key("data.bin"));
}
#[test]
fn test_with_manifest() {
let manifest = create_test_manifest();
let dir = HttpDirectory::with_manifest("https://example.com/search", manifest);
assert_eq!(dir.total_size(), 1000);
assert_eq!(dir.list_files().len(), 1);
assert!(dir.list_files().contains(&"test.bin"));
}
#[test]
fn test_directory_error_display() {
let err = DirectoryError::Network("connection refused".to_string());
assert!(err.to_string().contains("Network error"));
let err = DirectoryError::NotFound("file.bin".to_string());
assert!(err.to_string().contains("Not found"));
}
}