use crate::core::{Object, ObjectHash};
use crate::crypto::encryption::{EncryptionConfig, EncryptionManager};
use flate2::read::ZlibDecoder;
use flate2::write::ZlibEncoder;
use flate2::Compression;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
pub struct ObjectStore {
objects_dir: PathBuf,
packs_dir: PathBuf,
encryption: Arc<Mutex<EncryptionManager>>,
}
impl ObjectStore {
pub fn new(repo_path: &Path) -> Self {
let objects_dir = repo_path.join(".lit").join("objects");
let encryption_config = EncryptionConfig::load(repo_path).unwrap_or_default();
let encryption = Arc::new(Mutex::new(EncryptionManager::new_auto(
encryption_config,
repo_path,
)));
ObjectStore {
objects_dir,
packs_dir: crate::storage::pack::packs_dir(repo_path),
encryption,
}
}
pub fn new_with_encryption(repo_path: &Path, passphrase: Option<&str>) -> Result<Self, String> {
let objects_dir = repo_path.join(".lit").join("objects");
let encryption_config = EncryptionConfig::load(repo_path)?;
let mut encryption_manager = EncryptionManager::new(encryption_config);
if let Some(pass) = passphrase {
encryption_manager.initialize(pass)?;
}
let encryption = Arc::new(Mutex::new(encryption_manager));
Ok(ObjectStore {
objects_dir,
packs_dir: crate::storage::pack::packs_dir(repo_path),
encryption,
})
}
fn object_path(&self, hash: &ObjectHash) -> PathBuf {
let hash_str = hash.as_str();
let (dir, file) = hash_str.split_at(4);
self.objects_dir.join(dir).join(file)
}
pub fn write(&self, object: &Object) -> Result<ObjectHash, String> {
let hash = object.hash();
let path = self.object_path(&hash);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| format!("Failed to create object directory: {}", e))?;
}
let data = object.to_bytes();
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default());
encoder
.write_all(&data)
.map_err(|e| format!("Failed to compress object: {}", e))?;
let compressed = encoder
.finish()
.map_err(|e| format!("Failed to finish compression: {}", e))?;
let final_data = {
let encryption = self
.encryption
.lock()
.map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
encryption.encrypt(&compressed)?
};
fs::write(&path, final_data).map_err(|e| format!("Failed to write object: {}", e))?;
Ok(hash)
}
pub fn read(&self, hash: &ObjectHash) -> Result<Object, String> {
let path = self.object_path(hash);
if !path.exists() {
return self.read_packed(hash);
}
let encrypted_data =
fs::read(&path).map_err(|e| format!("Failed to read object: {}", e))?;
let compressed = {
let encryption = self
.encryption
.lock()
.map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
encryption.decrypt(&encrypted_data)?
};
let mut decoder = ZlibDecoder::new(&compressed[..]);
let mut data = Vec::new();
decoder
.read_to_end(&mut data)
.map_err(|e| format!("Failed to decompress object: {}", e))?;
Object::from_bytes(&data)
}
pub fn exists(&self, hash: &ObjectHash) -> bool {
self.object_path(hash).exists()
|| crate::storage::pack::load_all(&self.packs_dir).contains_key(hash.as_str())
}
fn read_packed(&self, hash: &ObjectHash) -> Result<Object, String> {
let packed = crate::storage::pack::load_all(&self.packs_dir);
let Some((pack_path, offset)) = packed.get(hash.as_str()) else {
return Err(format!("Object {} not found", hash.short()));
};
let encryption = self
.encryption
.lock()
.map_err(|e| format!("Failed to acquire encryption lock: {}", e))?;
crate::storage::pack::read_pack_object(pack_path, *offset, &encryption)
.map_err(|e| format!("Failed to read {} from pack: {}", hash.short(), e))
}
pub fn encryption(&self) -> Arc<Mutex<EncryptionManager>> {
Arc::clone(&self.encryption)
}
pub fn list(&self) -> Result<Vec<ObjectHash>, String> {
let mut objects = Vec::new();
for hash in crate::storage::pack::load_all(&self.packs_dir).into_keys() {
objects.push(ObjectHash::from_hex(hash));
}
if !self.objects_dir.exists() {
return Ok(objects);
}
for entry in walkdir::WalkDir::new(&self.objects_dir)
.min_depth(2)
.max_depth(2)
{
let entry = entry.map_err(|e| format!("Failed to read objects: {}", e))?;
if entry.file_type().is_file() {
let path = entry.path();
if let Some(file_name) = path.file_name() {
if let Some(parent) = path.parent() {
if let Some(dir_name) = parent.file_name() {
let hash = format!(
"{}{}",
dir_name.to_string_lossy(),
file_name.to_string_lossy()
);
objects.push(ObjectHash::from_hex(hash));
}
}
}
}
}
objects.sort_by(|a, b| a.as_str().cmp(b.as_str()));
objects.dedup_by(|a, b| a.as_str() == b.as_str());
Ok(objects)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Blob;
use tempfile::TempDir;
#[test]
fn test_object_store() {
let temp_dir = TempDir::new().unwrap();
let repo_path = temp_dir.path();
fs::create_dir_all(repo_path.join(".lit").join("objects")).unwrap();
let store = ObjectStore::new(repo_path);
let content = b"Hello, world!".to_vec();
let blob = Blob::new(content.clone());
let object = Object::Blob(blob);
let hash = store.write(&object).unwrap();
let read_object = store.read(&hash).unwrap();
match read_object {
Object::Blob(blob) => assert_eq!(blob.content, content),
_ => panic!("Expected blob"),
}
}
}