From 3b600fe3211981b4aaff258d6b900938199669d2 Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Sun, 4 Jan 2026 11:58:18 +0700 Subject: [PATCH 01/20] feat: create file store cache and implement up to dir creation --- cot/src/cache/store.rs | 1 + cot/src/cache/store/file.rs | 88 +++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 cot/src/cache/store/file.rs diff --git a/cot/src/cache/store.rs b/cot/src/cache/store.rs index ef278cbb..f42c3f5f 100644 --- a/cot/src/cache/store.rs +++ b/cot/src/cache/store.rs @@ -5,6 +5,7 @@ //! provide a simple asynchronous interface for putting, getting, and managing //! cached values, optionally with expiration policies. +pub mod file; pub mod memory; #[cfg(feature = "redis")] pub mod redis; diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs new file mode 100644 index 00000000..229cec9c --- /dev/null +++ b/cot/src/cache/store/file.rs @@ -0,0 +1,88 @@ +//! File based cache store implementation. +//! +//! This implementation uses file system for caching +//! +//! TODO: add example + +use std::borrow::Cow; +use std::path::Path; + +use serde_json::Value; +use thiserror::Error; + +use crate::cache::store::{CacheStore, CacheStoreError, CacheStoreResult}; +use crate::error::error_impl::impl_into_cot_error; + +const ERROR_PREFIX: &str = "file based cache store error:"; + +/// Errors specific to the file based cache store. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum FileCacheStoreError { + /// An error occured during directory creation + #[error("{ERROR_PREFIX} file dir creation error: {0}")] + DirCreation(Box), + + // TODO: add more errors + + // To fullfil trait + /// TODO: add docs + #[error("{ERROR_PREFIX} serialization error: {0}")] + Serialize(Box), + + /// TODO: add docs + #[error("{ERROR_PREFIX} deserialization error: {0}")] + Deserialize(Box), +} + +impl_into_cot_error!(FileCacheStoreError); + +impl From for CacheStoreError { + fn from(err: FileCacheStoreError) -> Self { + let full = err.to_string(); + + match err { + FileCacheStoreError::Serialize(_) => CacheStoreError::Serialize(full), + FileCacheStoreError::Deserialize(_) => CacheStoreError::Deserialize(full), + _ => CacheStoreError::Backend(full), + } + } +} + +/// File based cache store implementation +/// +/// This implementation uses file system for caching +/// +/// TODO: add example + +#[derive(Debug, Clone)] +pub struct FileStore { + dir_path: Cow<'static, Path>, +} + +impl FileStore { + /// TODO: add docs + pub fn new(dir: impl Into>) -> CacheStoreResult { + let dir_path = dir.into(); + + let store = Self { dir_path }; + store.create_dir_sync_root()?; + + Ok(store) + } + + fn create_dir_sync_root(&self) -> CacheStoreResult<()> { + std::fs::create_dir_all(&self.dir_path) + .map_err(|e| FileCacheStoreError::DirCreation(Box::new(e)))?; + + Ok(()) + } + + async fn create_dir_root(&self) -> CacheStoreResult<()> { + tokio::fs::create_dir_all(&self.dir_path) + .await + .map_err(|e| FileCacheStoreError::DirCreation(Box::new(e)))?; + + Ok(()) + } +} From 5126b35502d00d3d39ae6a7d50628b2ee8ea5a4b Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Sun, 4 Jan 2026 15:34:19 +0700 Subject: [PATCH 02/20] feat: add md5 dependency --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 8d793847..a15c60c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,6 +100,7 @@ humantime = "2" indexmap = "2" insta = { version = "1", features = ["filters"] } insta-cmd = "0.6" +md5 = "0.8" mime = "0.3" mime_guess = { version = "2", default-features = false } mockall = "0.14" From 255330abbf29ed7fc75eeedc755aa7ce7c7a03c9 Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Sun, 4 Jan 2026 19:47:11 +0700 Subject: [PATCH 03/20] feat(fix): use md-5 instead of md5 since its already used by other packages (leaner) --- Cargo.lock | 1 + Cargo.toml | 2 +- cot/Cargo.toml | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 8001be5e..db5e0e3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,6 +707,7 @@ dependencies = [ "http-body-util", "humantime", "indexmap", + "md-5", "mime", "mime_guess", "mockall", diff --git a/Cargo.toml b/Cargo.toml index a15c60c5..56b0dfa9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ humantime = "2" indexmap = "2" insta = { version = "1", features = ["filters"] } insta-cmd = "0.6" -md5 = "0.8" +md-5 = "0.10.6" mime = "0.3" mime_guess = { version = "2", default-features = false } mockall = "0.14" diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 1cb081b6..6139f8cc 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -42,6 +42,7 @@ http-body.workspace = true http.workspace = true humantime.workspace = true indexmap.workspace = true +md-5.workspace = true mime.workspace = true mime_guess.workspace = true multer.workspace = true From e3ae3dbc8939418a10d9b16db80c4f08b38cf69a Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Sun, 4 Jan 2026 19:48:04 +0700 Subject: [PATCH 04/20] feat: add insert and get features; add single case test for said features --- cot/src/cache/store/file.rs | 243 ++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 229cec9c..2e4c96b7 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -4,16 +4,22 @@ //! //! TODO: add example +use chrono::{DateTime, Utc}; +use md5::{Digest, Md5}; use std::borrow::Cow; use std::path::Path; +use tokio::fs::OpenOptions; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; use serde_json::Value; use thiserror::Error; use crate::cache::store::{CacheStore, CacheStoreError, CacheStoreResult}; +use crate::config::Timeout; use crate::error::error_impl::impl_into_cot_error; const ERROR_PREFIX: &str = "file based cache store error:"; +const TEMPFILE_SUFFIX: &str = ".tmp"; /// Errors specific to the file based cache store. #[derive(Debug, Error)] @@ -23,6 +29,14 @@ pub enum FileCacheStoreError { #[error("{ERROR_PREFIX} file dir creation error: {0}")] DirCreation(Box), + /// An error occured during temp file creation + #[error("{ERROR_PREFIX} file temp file creation error: {0}")] + TempFileCreation(Box), + + /// An error occured during write/stream file + #[error("{ERROR_PREFIX} file io error: {0}")] + Io(Box), + // TODO: add more errors // To fullfil trait @@ -85,4 +99,233 @@ impl FileStore { Ok(()) } + + async fn write(&self, key: String, value: Value, expiry: Timeout) -> CacheStoreResult<()> { + self.create_dir_root().await?; // create the dir if not exist + + let key_hash = self.create_key_hash(&key); + let (mut file, file_path) = self.create_file_temp(&key_hash).await?; + + let proc_result: CacheStoreResult<()> = async { + let buffer = self.serialize_data(value, expiry).await?; + + file.write_all(&buffer) + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + + Ok(()) + } + .await; + + if let Err(e) = proc_result { + let _ = tokio::fs::remove_file(&file_path).await; + return Err(e); + } + + // rename + file.sync_all() + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + tokio::fs::rename(file_path, self.dir_path.join(&key_hash)) + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + + Ok(()) + } + + async fn read(&self, key: &str) -> CacheStoreResult> { + let key_hash = self.create_key_hash(key); + let path = self.dir_path.join(&key_hash); + let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await { + Ok(f) => f, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(FileCacheStoreError::Io(Box::new(e)).into()), + }; + + match self.deserialize_data(&mut file).await? { + Some(value) => Ok(Some(value)), + None => { + // delete on expired when read + let _ = tokio::fs::remove_file(&path).await; + Ok(None) + } + } + } + + fn create_key_hash(&self, key: &str) -> String { + let mut hasher = Md5::new(); + hasher.update(key.as_bytes()); + let key_hash_hex = hasher.finalize(); + format!("{:x}", key_hash_hex) + } + + async fn serialize_data(&self, value: Value, expiry: Timeout) -> CacheStoreResult> { + let timeout = expiry.canonicalize(); + let seconds: u64 = match timeout { + Timeout::Never => u64::MAX, + Timeout::AtDateTime(date_time) => date_time.timestamp() as u64, + Timeout::After(_) => unreachable!("should've been converted by canonicalize"), + }; + let timeout_header = seconds.to_le_bytes(); + + let data = serde_json::to_string(&value) + .map_err(|e| FileCacheStoreError::Serialize(Box::new(e)))?; + + let mut buffer: Vec = Vec::with_capacity(8 + data.len()); + buffer.extend_from_slice(&timeout_header); + buffer.extend_from_slice(data.as_bytes()); + + Ok(buffer) + } + + async fn deserialize_data( + &self, + file: &mut tokio::fs::File, + ) -> CacheStoreResult> { + let mut header: [u8; 8] = [0; 8]; + + let _ = file + .read_exact(&mut header) + .await + .map_err(|e| FileCacheStoreError::Deserialize(Box::new(e)))?; + let seconds = u64::from_le_bytes(header); + + let expiry = match seconds { + u64::MAX => Timeout::Never, + _ => { + let date_time = DateTime::from_timestamp(seconds as i64, 0) + .ok_or_else(|| FileCacheStoreError::Deserialize("date time corrupted".into()))? + .with_timezone(&Utc) + .fixed_offset(); + + Timeout::AtDateTime(date_time) + } + }; + + if expiry.is_expired(None) { + return Ok(None); + } + + let mut buffer = Vec::new(); + file.read_to_end(&mut buffer) + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + + let value: Value = serde_json::from_slice(&buffer) + .map_err(|e| FileCacheStoreError::Deserialize(Box::new(e)))?; + + Ok(Some(value)) + } + + async fn create_file_temp( + &self, + key_hash: &str, + ) -> CacheStoreResult<(tokio::fs::File, std::path::PathBuf)> { + let temp_path = self.dir_path.join(format!("{}{TEMPFILE_SUFFIX}", key_hash)); + + let temp_file = tokio::fs::OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&temp_path) + .await + .map_err(|e| FileCacheStoreError::TempFileCreation(Box::new(e)))?; + + Ok((temp_file, temp_path)) + } +} + +impl CacheStore for FileStore { + async fn get(&self, key: &str) -> CacheStoreResult> { + match self.read(key).await? { + Some(value) => Ok(Some(value)), + None => Ok(None), + } + } + + async fn insert(&self, key: String, value: Value, expiry: Timeout) -> CacheStoreResult<()> { + self.write(key, value, expiry).await?; + Ok(()) + } + + async fn remove(&self, key: &str) -> CacheStoreResult<()> { + todo!() + } + + async fn clear(&self) -> CacheStoreResult<()> { + todo!() + } + + async fn approx_size(&self) -> CacheStoreResult { + todo!() + } + + async fn contains_key(&self, key: &str) -> CacheStoreResult { + todo!() + } +} + +#[cfg(test)] +mod tests { + use tempfile::tempdir; + + use crate::cache::store::CacheStore; + use crate::cache::store::file::FileStore; + use crate::config::Timeout; + + fn make_store_path() -> std::path::PathBuf { + tempdir().expect("failed to create dir").keep() + } + + #[cot::test] + async fn test_create_dir() { + let path = make_store_path(); + let _ = FileStore::new(path.clone()).expect("failed to init store"); + + assert!(path.exists()); + assert!(path.is_dir()); + + tokio::fs::remove_dir_all(path) + .await + .expect("failed to cleanup tempdir"); + } + + #[cot::test] + async fn test_create_dir_on_existing() { + let path = make_store_path(); + let _ = FileStore::new(path.clone()).expect("failed to init store"); + let _ = FileStore::new(path.clone()).expect("failed to init second store"); + + assert!(path.exists()); + assert!(path.is_dir()); + + tokio::fs::remove_dir_all(path) + .await + .expect("failed to cleanup tempdir"); + } + + #[cot::test] + async fn test_insert_and_read_single() { + let path = make_store_path(); + + let store = FileStore::new(path.clone()).expect("failed to init store"); + let key = "test_key".to_string(); + let value = serde_json::json!({ "id": 1, "message": "hello world" }); + + store + .insert(key.clone(), value.clone(), Timeout::Never) + .await + .expect("failed to insert data to store"); + + let retrieved = store.read(&key).await.expect("failed to read from store"); + + assert!(retrieved.is_some(), "retrieved value should not be None"); + assert_eq!( + retrieved.unwrap(), + value, + "retrieved value does not match inserted value" + ); + + let _ = tokio::fs::remove_dir_all(&path).await; + } } From a3ceea691a3c31a9c64d8314a98b2e2467244b53 Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Sun, 4 Jan 2026 21:06:45 +0700 Subject: [PATCH 05/20] feat: refactor file open then implement exist and remove; add test for read after remove --- cot/src/cache/store/file.rs | 55 +++++++++++++++++++++++++++++++------ 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 2e4c96b7..29323266 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -134,19 +134,16 @@ impl FileStore { } async fn read(&self, key: &str) -> CacheStoreResult> { - let key_hash = self.create_key_hash(key); - let path = self.dir_path.join(&key_hash); - let mut file = match tokio::fs::OpenOptions::new().read(true).open(&path).await { - Ok(f) => f, - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(e) => return Err(FileCacheStoreError::Io(Box::new(e)).into()), + let (mut file, file_path) = match self.file_open(key).await? { + Some(f) => f, + None => return Ok(None), }; match self.deserialize_data(&mut file).await? { Some(value) => Ok(Some(value)), None => { // delete on expired when read - let _ = tokio::fs::remove_file(&path).await; + let _ = tokio::fs::remove_file(&file_path).await; Ok(None) } } @@ -233,6 +230,19 @@ impl FileStore { Ok((temp_file, temp_path)) } + + async fn file_open( + &self, + key: &str, + ) -> CacheStoreResult> { + let key_hash = self.create_key_hash(key); + let path = self.dir_path.join(&key_hash); + match tokio::fs::OpenOptions::new().read(true).open(&path).await { + Ok(f) => Ok(Some((f, path))), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), + Err(e) => return Err(FileCacheStoreError::Io(Box::new(e)).into()), + } + } } impl CacheStore for FileStore { @@ -249,7 +259,13 @@ impl CacheStore for FileStore { } async fn remove(&self, key: &str) -> CacheStoreResult<()> { - todo!() + if let Some((_file, file_path)) = self.file_open(key).await? { + tokio::fs::remove_file(file_path) + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + } + + Ok(()) } async fn clear(&self) -> CacheStoreResult<()> { @@ -261,7 +277,7 @@ impl CacheStore for FileStore { } async fn contains_key(&self, key: &str) -> CacheStoreResult { - todo!() + Ok(self.file_open(key).await?.is_some()) } } @@ -328,4 +344,25 @@ mod tests { let _ = tokio::fs::remove_dir_all(&path).await; } + + #[cot::test] + async fn test_insert_and_read_after_delete_single() { + let path = make_store_path(); + + let store = FileStore::new(path.clone()).expect("failed to init store"); + let key = "test_key".to_string(); + let value = serde_json::json!({ "id": 1, "message": "hello world" }); + + store + .insert(key.clone(), value.clone(), Timeout::Never) + .await + .expect("failed to insert data to store"); + + store.remove(&key).await.expect("failed to delete entry"); + + let retrieved = store.read(&key).await.expect("failed to read from store"); + assert!(retrieved.is_none(), "retrieved value should not be Some"); + + let _ = tokio::fs::remove_dir_all(&path).await; + } } From 5b0d7288cef4c371a210f2032999dc0eb8c1350b Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Sun, 4 Jan 2026 21:27:50 +0700 Subject: [PATCH 06/20] feat: implement clear and its test --- cot/src/cache/store/file.rs | 39 ++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 29323266..8a252bf2 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -269,7 +269,17 @@ impl CacheStore for FileStore { } async fn clear(&self) -> CacheStoreResult<()> { - todo!() + if let Err(e) = tokio::fs::remove_dir_all(&self.dir_path).await { + // if not found try to continue, don't dip + if e.kind() != std::io::ErrorKind::NotFound { + return Err(FileCacheStoreError::Io(Box::new(e)).into()); + } + } + // even though write is self healing, this minimizes result variants on other methods + tokio::fs::create_dir_all(&self.dir_path) + .await + .map_err(|e| FileCacheStoreError::DirCreation(Box::new(e)))?; + Ok(()) } async fn approx_size(&self) -> CacheStoreResult { @@ -365,4 +375,31 @@ mod tests { let _ = tokio::fs::remove_dir_all(&path).await; } + + #[cot::test] + async fn test_clear_double_free() { + let path = make_store_path(); + + let store = FileStore::new(path.clone()).expect("failed to init store"); + let key = "test_key".to_string(); + let value = serde_json::json!({ "id": 1, "message": "hello world" }); + + store + .insert(key.clone(), value.clone(), Timeout::Never) + .await + .expect("failed to insert data to store"); + + store.clear().await.expect("failed to clear"); + store + .clear() + .await + .expect("failed to clear the second time"); + + let retrieved = store.read(&key).await.expect("failed to read from store"); + + assert!(path.is_dir(), "path must be dir"); + assert!(retrieved.is_none(), "retrieved value should not be Some"); + + let _ = tokio::fs::remove_dir_all(&path).await; + } } From 3fee79b76690b08d6ae8d099487566e5f003ea0f Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Sun, 4 Jan 2026 21:55:42 +0700 Subject: [PATCH 07/20] feat: add approx size and its test case --- cot/src/cache/store/file.rs | 46 ++++++++++++++++++++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 8a252bf2..cb428467 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -283,7 +283,23 @@ impl CacheStore for FileStore { } async fn approx_size(&self) -> CacheStoreResult { - todo!() + let mut entries = match tokio::fs::read_dir(&self.dir_path).await { + Ok(e) => e, + Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(0), + Err(e) => return Err(FileCacheStoreError::Io(Box::new(e)).into()), + }; + + let mut total_size: usize = 0; + + while let Ok(Some(entry)) = entries.next_entry().await { + if let Ok(meta) = entry.metadata().await { + if meta.is_file() { + total_size += meta.len() as usize; + } + } + } + + Ok(total_size) } async fn contains_key(&self, key: &str) -> CacheStoreResult { @@ -293,6 +309,7 @@ impl CacheStore for FileStore { #[cfg(test)] mod tests { + use aide::IntoApi; use tempfile::tempdir; use crate::cache::store::CacheStore; @@ -402,4 +419,31 @@ mod tests { let _ = tokio::fs::remove_dir_all(&path).await; } + + #[cot::test] + async fn test_approx_size() { + let path = make_store_path(); + + let store = FileStore::new(path.clone()).expect("failed to init store"); + let key = "test_key".to_string(); + let value = serde_json::json!({ "id": 1, "message": "hello world" }); + + store + .insert(key.clone(), value.clone(), Timeout::Never) + .await + .expect("failed to insert data to store"); + + let data_buffer: Vec = + serde_json::to_vec(&value).expect("failed to convert value into vector"); + let data_length: usize = 8 + data_buffer.len(); // extra 8 for header size + + let entry_length = store + .approx_size() + .await + .expect("failed to get approx file"); + + assert_eq!(data_length, entry_length); + + let _ = tokio::fs::remove_dir_all(&path).await; + } } From 1fe3bc5c4031a5385668a13808dbad2415adfbf2 Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Tue, 6 Jan 2026 00:03:33 +0700 Subject: [PATCH 08/20] chore: fix unused imports and clean docs --- cot/src/cache/store/file.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index cb428467..03dedd88 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -38,13 +38,11 @@ pub enum FileCacheStoreError { Io(Box), // TODO: add more errors - - // To fullfil trait - /// TODO: add docs + /// An error occured during data serialization #[error("{ERROR_PREFIX} serialization error: {0}")] Serialize(Box), - /// TODO: add docs + /// An error occured during data deserialization #[error("{ERROR_PREFIX} deserialization error: {0}")] Deserialize(Box), } @@ -220,7 +218,7 @@ impl FileStore { ) -> CacheStoreResult<(tokio::fs::File, std::path::PathBuf)> { let temp_path = self.dir_path.join(format!("{}{TEMPFILE_SUFFIX}", key_hash)); - let temp_file = tokio::fs::OpenOptions::new() + let temp_file = OpenOptions::new() .write(true) .create(true) .truncate(true) @@ -237,7 +235,7 @@ impl FileStore { ) -> CacheStoreResult> { let key_hash = self.create_key_hash(key); let path = self.dir_path.join(&key_hash); - match tokio::fs::OpenOptions::new().read(true).open(&path).await { + match OpenOptions::new().read(true).open(&path).await { Ok(f) => Ok(Some((f, path))), Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), Err(e) => return Err(FileCacheStoreError::Io(Box::new(e)).into()), @@ -309,7 +307,6 @@ impl CacheStore for FileStore { #[cfg(test)] mod tests { - use aide::IntoApi; use tempfile::tempdir; use crate::cache::store::CacheStore; From 92c2db6386fc18d9ebfd956d93103d1f9cc557bc Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Tue, 6 Jan 2026 00:16:32 +0700 Subject: [PATCH 09/20] feat: implement eviction on contains_key --- cot/src/cache/store/file.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 03dedd88..59bf6490 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -301,7 +301,21 @@ impl CacheStore for FileStore { } async fn contains_key(&self, key: &str) -> CacheStoreResult { - Ok(self.file_open(key).await?.is_some()) + let Ok(Some(mut file_tuple)) = self.file_open(key).await else { + return Ok(false); + }; + + // cache eviction on contains_key() based on TTL + // currently parse the whole data, but can be optimized by checking TTL only + if self.deserialize_data(&mut file_tuple.0).await.is_ok() { + return Ok(true); + } + + tokio::fs::remove_file(&file_tuple.1) + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + + Ok(false) } } From 40da36d6eef3a513494648135866d2b4b660949b Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Tue, 6 Jan 2026 11:07:24 +0700 Subject: [PATCH 10/20] refactor deserialize logic --- cot/src/cache/store/file.rs | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 59bf6490..2d2ffc66 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -9,7 +9,7 @@ use md5::{Digest, Md5}; use std::borrow::Cow; use std::path::Path; use tokio::fs::OpenOptions; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom}; use serde_json::Value; use thiserror::Error; @@ -173,10 +173,7 @@ impl FileStore { Ok(buffer) } - async fn deserialize_data( - &self, - file: &mut tokio::fs::File, - ) -> CacheStoreResult> { + async fn parse_expiry(&self, file: &mut tokio::fs::File) -> CacheStoreResult { let mut header: [u8; 8] = [0; 8]; let _ = file @@ -198,10 +195,34 @@ impl FileStore { }; if expiry.is_expired(None) { - return Ok(None); + return Ok(false); + } + + // This may look inefficient, but this ensures portability + // By making this method reset its own cursor, + // the logic is reusable without the risk of forgetting to reset cursor + file.seek(SeekFrom::Start(0)) + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + + Ok(true) + } + + async fn deserialize_data( + &self, + file: &mut tokio::fs::File, + ) -> CacheStoreResult> { + match self.parse_expiry(file).await? { + true => {} + false => return Ok(None), } let mut buffer = Vec::new(); + + // advances cursor by the expiry header offset + file.seek(SeekFrom::Start(8)) + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; file.read_to_end(&mut buffer) .await .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; @@ -306,8 +327,7 @@ impl CacheStore for FileStore { }; // cache eviction on contains_key() based on TTL - // currently parse the whole data, but can be optimized by checking TTL only - if self.deserialize_data(&mut file_tuple.0).await.is_ok() { + if self.parse_expiry(&mut file_tuple.0).await.is_ok() { return Ok(true); } From 12c4458f32d9172fd3717066773050e293f054da Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Tue, 6 Jan 2026 12:00:56 +0700 Subject: [PATCH 11/20] add expiration integrity and contains key tests --- cot/src/cache/store/file.rs | 66 +++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 2d2ffc66..303d3adc 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -37,7 +37,6 @@ pub enum FileCacheStoreError { #[error("{ERROR_PREFIX} file io error: {0}")] Io(Box), - // TODO: add more errors /// An error occured during data serialization #[error("{ERROR_PREFIX} serialization error: {0}")] Serialize(Box), @@ -322,12 +321,12 @@ impl CacheStore for FileStore { } async fn contains_key(&self, key: &str) -> CacheStoreResult { - let Ok(Some(mut file_tuple)) = self.file_open(key).await else { + let Ok(Some(mut file_tuple)) = self.file_open(&key).await else { return Ok(false); }; // cache eviction on contains_key() based on TTL - if self.parse_expiry(&mut file_tuple.0).await.is_ok() { + if let true = self.parse_expiry(&mut file_tuple.0).await? { return Ok(true); } @@ -341,6 +340,9 @@ impl CacheStore for FileStore { #[cfg(test)] mod tests { + use std::time::Duration; + + use chrono::Utc; use tempfile::tempdir; use crate::cache::store::CacheStore; @@ -477,4 +479,62 @@ mod tests { let _ = tokio::fs::remove_dir_all(&path).await; } + + #[cot::test] + async fn test_contains_key() { + let path = make_store_path(); + + let store = FileStore::new(path.clone()).expect("failed to init store"); + let key = "test_key".to_string(); + let value = serde_json::json!({ "id": 1, "message": "hello world" }); + + store + .insert(key.clone(), value.clone(), Timeout::Never) + .await + .expect("failed to insert data to store"); + + let exist = store + .contains_key(&key) + .await + .expect("failed to check key existence"); + + assert!(exist); + + let _ = tokio::fs::remove_dir_all(&path).await; + } + + #[cot::test] + async fn test_expiration_integrity() { + let path = make_store_path(); + + let store = FileStore::new(path.clone()).expect("failed to init store"); + let key = "test_key".to_string(); + let value = serde_json::json!({ "id": 1, "message": "hello world" }); + + let past = Utc::now() - Duration::from_secs(1); + let past_fixed = past.fixed_offset(); + let expiry = Timeout::AtDateTime(past_fixed); + + store + .insert(key.clone(), value.clone(), expiry) + .await + .expect("failed to insert data to store"); + + // test file is None + let retrieved = store.get(&key).await.expect("failed to read from store"); + assert!(retrieved.is_none()); + + // test file doesn't exist + let exist = store + .contains_key(&key) + .await + .expect("failed to check key existence"); + assert!(!exist); + + // test size is 0 + let size = store.approx_size().await.expect("failed to check size"); + assert_eq!(size, 0); + + let _ = tokio::fs::remove_dir_all(&path).await; + } } From 46e2dd424befc7f6b0d6b8c6042f574dc002fe9f Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Tue, 6 Jan 2026 12:12:33 +0700 Subject: [PATCH 12/20] add impl into cache store error test --- cot/src/cache/store/file.rs | 46 +++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 303d3adc..8e687680 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -345,8 +345,8 @@ mod tests { use chrono::Utc; use tempfile::tempdir; - use crate::cache::store::CacheStore; - use crate::cache::store::file::FileStore; + use crate::cache::store::file::{FileCacheStoreError, FileStore}; + use crate::cache::store::{CacheStore, CacheStoreError}; use crate::config::Timeout; fn make_store_path() -> std::path::PathBuf { @@ -537,4 +537,46 @@ mod tests { let _ = tokio::fs::remove_dir_all(&path).await; } + + #[cot::test] + async fn test_from_file_cache_store_error_to_cache_store_error() { + let file_error = FileCacheStoreError::Io(Box::new(std::io::Error::other("disk failure"))); + let cache_error: CacheStoreError = file_error.into(); + assert_eq!( + cache_error.to_string(), + "cache store error: backend error: file based cache store error: file io error: disk failure" + ); + + let file_error = + FileCacheStoreError::Serialize(Box::new(std::io::Error::other("json fail"))); + let cache_error: CacheStoreError = file_error.into(); + assert_eq!( + cache_error.to_string(), + "cache store error: serialization error: file based cache store error: serialization error: json fail" + ); + + let file_error = + FileCacheStoreError::Deserialize(Box::new(std::io::Error::other("corrupt header"))); + let cache_error: CacheStoreError = file_error.into(); + assert_eq!( + cache_error.to_string(), + "cache store error: deserialization error: file based cache store error: deserialization error: corrupt header" + ); + + let file_error = + FileCacheStoreError::DirCreation(Box::new(std::io::Error::other("permission denied"))); + let cache_error: CacheStoreError = file_error.into(); + assert_eq!( + cache_error.to_string(), + "cache store error: backend error: file based cache store error: file dir creation error: permission denied" + ); + + let file_error = + FileCacheStoreError::TempFileCreation(Box::new(std::io::Error::other("no space left"))); + let cache_error: CacheStoreError = file_error.into(); + assert_eq!( + cache_error.to_string(), + "cache store error: backend error: file based cache store error: file temp file creation error: no space left" + ); + } } From 92ab5e4f4bdf7d993e3a0400532776954b00d58f Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Tue, 6 Jan 2026 12:28:13 +0700 Subject: [PATCH 13/20] add documentation --- cot/src/cache/store/file.rs | 69 ++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 8e687680..c47e52b7 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -1,9 +1,34 @@ -//! File based cache store implementation. +//! File-based cache store implementation. //! -//! This implementation uses file system for caching +//! This store uses the local file system as the backend for caching. It provides +//! atomic writes via sync-then-rename and active validation for TTL-based expiration. //! -//! TODO: add example - +//! # Examples +//! +//! ```no_run +//! # use cot::cache::store::file::FileStore; +//! # use cot::cache::store::{CacheStore, Timeout}; +//! # use std::path::PathBuf; +//! # #[tokio::main] +//! # async fn main() { +//! let path = PathBuf::from("./cache_data"); +//! let store = FileStore::new(path).expect("Failed to initialize store"); +//! +//! let key = "example_key".to_string(); +//! let value = serde_json::json!({"data": "example_value"}); +//! +//! store.insert(key.clone(), value.clone(), Default::default()).await.unwrap(); +//! +//! let retrieved = store.get(&key).await.unwrap(); +//! assert_eq!(retrieved, Some(value)); +//! +//! # } +//! ``` +//! +//! # Expiration Policy +//! +//! Cache files are evicted on `contains_key` and `get`. +//! No background collector is implemented. use chrono::{DateTime, Utc}; use md5::{Digest, Md5}; use std::borrow::Cow; @@ -60,19 +85,45 @@ impl From for CacheStoreError { } } -/// File based cache store implementation +/// A file-backed cache store implementation. /// -/// This implementation uses file system for caching +/// This store uses the local file system for caching. /// -/// TODO: add example - +/// # Examples +/// ```no_run +/// use cot::cache::store::file::FileStore; +/// use std::path::Path; +/// +/// let store = FileStore::new(Path::new("./cache_dir")).unwrap(); +/// ``` #[derive(Debug, Clone)] pub struct FileStore { dir_path: Cow<'static, Path>, } impl FileStore { - /// TODO: add docs + /// Creates a new `FileStore` at the specified directory. + /// + /// This will attempt to create the directory and its parents if they do not exist. + /// + /// # Errors + /// + /// Returns [`FileCacheStoreError::DirCreation`] if the directory cannot be created + /// due to permissions or other I/O issues. + /// + /// # Examples + /// + /// ```no_run + /// use cot::cache::store::file::FileStore; + /// use std::path::PathBuf; + /// + /// // Using a string slice + /// let store = FileStore::new("./cache").unwrap(); + /// + /// // Using a PathBuf + /// let path = PathBuf::from("/var/lib/myapp/cache"); + /// let store = FileStore::new(path).unwrap(); + /// ``` pub fn new(dir: impl Into>) -> CacheStoreResult { let dir_path = dir.into(); From 13747a9aa8560255bc4e83971f00d3fafc2734e1 Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Wed, 7 Jan 2026 19:43:54 +0700 Subject: [PATCH 14/20] fix: refactor code to match clippy lint --- cot/src/cache/store/file.rs | 88 +++++++++++++++++++------------------ 1 file changed, 45 insertions(+), 43 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index c47e52b7..c81ee154 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -151,15 +151,11 @@ impl FileStore { async fn write(&self, key: String, value: Value, expiry: Timeout) -> CacheStoreResult<()> { self.create_dir_root().await?; // create the dir if not exist - let key_hash = self.create_key_hash(&key); + let key_hash = FileStore::create_key_hash(&key); let (mut file, file_path) = self.create_file_temp(&key_hash).await?; let proc_result: CacheStoreResult<()> = async { - let buffer = self.serialize_data(value, expiry).await?; - - file.write_all(&buffer) - .await - .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + self.serialize_data(value, expiry, &mut file).await?; Ok(()) } @@ -182,33 +178,36 @@ impl FileStore { } async fn read(&self, key: &str) -> CacheStoreResult> { - let (mut file, file_path) = match self.file_open(key).await? { - Some(f) => f, - None => return Ok(None), + let Some((mut file, file_path)) = self.file_open(key).await? else { + return Ok(None); }; - match self.deserialize_data(&mut file).await? { - Some(value) => Ok(Some(value)), - None => { - // delete on expired when read - let _ = tokio::fs::remove_file(&file_path).await; - Ok(None) - } + if let Some(value) = self.deserialize_data(&mut file).await? { + Ok(Some(value)) + } else { + // delete on expired when read + let _ = tokio::fs::remove_file(&file_path).await; + Ok(None) } } - fn create_key_hash(&self, key: &str) -> String { + fn create_key_hash(key: &str) -> String { let mut hasher = Md5::new(); hasher.update(key.as_bytes()); let key_hash_hex = hasher.finalize(); - format!("{:x}", key_hash_hex) + format!("{key_hash_hex:x}") } - async fn serialize_data(&self, value: Value, expiry: Timeout) -> CacheStoreResult> { + async fn serialize_data( + &self, + value: Value, + expiry: Timeout, + file: &mut tokio::fs::File, + ) -> CacheStoreResult> { let timeout = expiry.canonicalize(); let seconds: u64 = match timeout { Timeout::Never => u64::MAX, - Timeout::AtDateTime(date_time) => date_time.timestamp() as u64, + Timeout::AtDateTime(date_time) => date_time.timestamp().cast_unsigned(), Timeout::After(_) => unreachable!("should've been converted by canonicalize"), }; let timeout_header = seconds.to_le_bytes(); @@ -220,6 +219,10 @@ impl FileStore { buffer.extend_from_slice(&timeout_header); buffer.extend_from_slice(data.as_bytes()); + file.write_all(&buffer) + .await + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + Ok(buffer) } @@ -232,16 +235,14 @@ impl FileStore { .map_err(|e| FileCacheStoreError::Deserialize(Box::new(e)))?; let seconds = u64::from_le_bytes(header); - let expiry = match seconds { - u64::MAX => Timeout::Never, - _ => { - let date_time = DateTime::from_timestamp(seconds as i64, 0) - .ok_or_else(|| FileCacheStoreError::Deserialize("date time corrupted".into()))? - .with_timezone(&Utc) - .fixed_offset(); - - Timeout::AtDateTime(date_time) - } + let expiry = if seconds == u64::MAX { + Timeout::Never + } else { + let date_time = DateTime::from_timestamp(seconds.cast_signed(), 0) + .ok_or_else(|| FileCacheStoreError::Deserialize("date time corrupted".into()))? + .with_timezone(&Utc) + .fixed_offset(); + Timeout::AtDateTime(date_time) }; if expiry.is_expired(None) { @@ -262,9 +263,8 @@ impl FileStore { &self, file: &mut tokio::fs::File, ) -> CacheStoreResult> { - match self.parse_expiry(file).await? { - true => {} - false => return Ok(None), + if !self.parse_expiry(file).await? { + return Ok(None); } let mut buffer = Vec::new(); @@ -287,7 +287,7 @@ impl FileStore { &self, key_hash: &str, ) -> CacheStoreResult<(tokio::fs::File, std::path::PathBuf)> { - let temp_path = self.dir_path.join(format!("{}{TEMPFILE_SUFFIX}", key_hash)); + let temp_path = self.dir_path.join(format!("{key_hash}{TEMPFILE_SUFFIX}")); let temp_file = OpenOptions::new() .write(true) @@ -304,12 +304,12 @@ impl FileStore { &self, key: &str, ) -> CacheStoreResult> { - let key_hash = self.create_key_hash(key); + let key_hash = FileStore::create_key_hash(key); let path = self.dir_path.join(&key_hash); match OpenOptions::new().read(true).open(&path).await { Ok(f) => Ok(Some((f, path))), - Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(e) => return Err(FileCacheStoreError::Io(Box::new(e)).into()), + Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(e) => Err(FileCacheStoreError::Io(Box::new(e)).into()), } } } @@ -361,10 +361,12 @@ impl CacheStore for FileStore { let mut total_size: usize = 0; while let Ok(Some(entry)) = entries.next_entry().await { - if let Ok(meta) = entry.metadata().await { - if meta.is_file() { - total_size += meta.len() as usize; - } + if let Ok(meta) = entry.metadata().await + && meta.is_file() + { + let current_len = usize::try_from(meta.len()) + .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + total_size += current_len; } } @@ -372,12 +374,12 @@ impl CacheStore for FileStore { } async fn contains_key(&self, key: &str) -> CacheStoreResult { - let Ok(Some(mut file_tuple)) = self.file_open(&key).await else { + let Ok(Some(mut file_tuple)) = self.file_open(key).await else { return Ok(false); }; // cache eviction on contains_key() based on TTL - if let true = self.parse_expiry(&mut file_tuple.0).await? { + if self.parse_expiry(&mut file_tuple.0).await? { return Ok(true); } From f0df2039f5f4501422b59a8dd5d6729995eec671 Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Wed, 7 Jan 2026 20:13:37 +0700 Subject: [PATCH 15/20] fix: doc examples --- cot/src/cache/store/file.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index c81ee154..40b1e82e 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -7,7 +7,8 @@ //! //! ```no_run //! # use cot::cache::store::file::FileStore; -//! # use cot::cache::store::{CacheStore, Timeout}; +//! # use cot::cache::store::CacheStore; +//! # use cot::config::Timeout; //! # use std::path::PathBuf; //! # #[tokio::main] //! # async fn main() { @@ -118,7 +119,8 @@ impl FileStore { /// use std::path::PathBuf; /// /// // Using a string slice - /// let store = FileStore::new("./cache").unwrap(); + /// let path = PathBuf::from("./cache"); + /// let store = FileStore::new(path).unwrap(); /// /// // Using a PathBuf /// let path = PathBuf::from("/var/lib/myapp/cache"); From 30689189323397df730aa4f1946dba24b2ff50fa Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Wed, 7 Jan 2026 20:20:01 +0700 Subject: [PATCH 16/20] feat: change date serialization to i64 --- cot/src/cache/store/file.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 40b1e82e..f00c627d 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -207,9 +207,9 @@ impl FileStore { file: &mut tokio::fs::File, ) -> CacheStoreResult> { let timeout = expiry.canonicalize(); - let seconds: u64 = match timeout { - Timeout::Never => u64::MAX, - Timeout::AtDateTime(date_time) => date_time.timestamp().cast_unsigned(), + let seconds: i64 = match timeout { + Timeout::Never => i64::MAX, + Timeout::AtDateTime(date_time) => date_time.timestamp(), Timeout::After(_) => unreachable!("should've been converted by canonicalize"), }; let timeout_header = seconds.to_le_bytes(); @@ -235,12 +235,12 @@ impl FileStore { .read_exact(&mut header) .await .map_err(|e| FileCacheStoreError::Deserialize(Box::new(e)))?; - let seconds = u64::from_le_bytes(header); + let seconds = i64::from_le_bytes(header); - let expiry = if seconds == u64::MAX { + let expiry = if seconds == i64::MAX { Timeout::Never } else { - let date_time = DateTime::from_timestamp(seconds.cast_signed(), 0) + let date_time = DateTime::from_timestamp(seconds, 0) .ok_or_else(|| FileCacheStoreError::Deserialize("date time corrupted".into()))? .with_timezone(&Utc) .fixed_offset(); From e8d5ca0dec0d366756e701ffa9ccda74fef5eb74 Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Wed, 7 Jan 2026 20:26:19 +0700 Subject: [PATCH 17/20] feat: improve error handling and casting logic in approx size --- cot/src/cache/store/file.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index f00c627d..d2868e3c 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -360,19 +360,20 @@ impl CacheStore for FileStore { Err(e) => return Err(FileCacheStoreError::Io(Box::new(e)).into()), }; - let mut total_size: usize = 0; + let mut total_size: u64 = 0; while let Ok(Some(entry)) = entries.next_entry().await { if let Ok(meta) = entry.metadata().await && meta.is_file() { - let current_len = usize::try_from(meta.len()) - .map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; - total_size += current_len; + total_size += meta.len(); } } - Ok(total_size) + let wrapped_size = + usize::try_from(total_size).map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; + + Ok(wrapped_size) } async fn contains_key(&self, key: &str) -> CacheStoreResult { From 4a18edf3659b75604b504bf1badaa2635b851680 Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Wed, 7 Jan 2026 20:28:12 +0700 Subject: [PATCH 18/20] doc: annotate error handling on meta.len() --- cot/src/cache/store/file.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index d2868e3c..64b29553 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -370,6 +370,8 @@ impl CacheStore for FileStore { } } + // when error is triggered, this would be because capacity overflow + // of trying to wrap usize on a 32-bit system let wrapped_size = usize::try_from(total_size).map_err(|e| FileCacheStoreError::Io(Box::new(e)))?; From 63f91f75f3ec0ec6b29c8c4d12a34c4b9b370fbe Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Wed, 7 Jan 2026 20:36:11 +0700 Subject: [PATCH 19/20] fix: refactor package declaration of md-5 into md5 --- Cargo.toml | 2 +- cot/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 56b0dfa9..0960264a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,7 +100,7 @@ humantime = "2" indexmap = "2" insta = { version = "1", features = ["filters"] } insta-cmd = "0.6" -md-5 = "0.10.6" +md5 = { package = "md-5", version = "0.10.6"} mime = "0.3" mime_guess = { version = "2", default-features = false } mockall = "0.14" diff --git a/cot/Cargo.toml b/cot/Cargo.toml index 6139f8cc..df24c714 100644 --- a/cot/Cargo.toml +++ b/cot/Cargo.toml @@ -42,7 +42,7 @@ http-body.workspace = true http.workspace = true humantime.workspace = true indexmap.workspace = true -md-5.workspace = true +md5.workspace = true mime.workspace = true mime_guess.workspace = true multer.workspace = true From ccd8c5386919267b0c2c40082a9b0a032a59957e Mon Sep 17 00:00:00 2001 From: achsanalfitra Date: Wed, 7 Jan 2026 22:15:35 +0700 Subject: [PATCH 20/20] fix: refactor code to match cargo fmt --- cot/src/cache/store/file.rs | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/cot/src/cache/store/file.rs b/cot/src/cache/store/file.rs index 64b29553..5583d107 100644 --- a/cot/src/cache/store/file.rs +++ b/cot/src/cache/store/file.rs @@ -1,7 +1,8 @@ //! File-based cache store implementation. //! -//! This store uses the local file system as the backend for caching. It provides -//! atomic writes via sync-then-rename and active validation for TTL-based expiration. +//! This store uses the local file system as the backend for caching. It +//! provides atomic writes via sync-then-rename and active validation for +//! TTL-based expiration. //! //! # Examples //! @@ -30,15 +31,15 @@ //! //! Cache files are evicted on `contains_key` and `get`. //! No background collector is implemented. -use chrono::{DateTime, Utc}; -use md5::{Digest, Md5}; use std::borrow::Cow; use std::path::Path; -use tokio::fs::OpenOptions; -use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom}; +use chrono::{DateTime, Utc}; +use md5::{Digest, Md5}; use serde_json::Value; use thiserror::Error; +use tokio::fs::OpenOptions; +use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt, SeekFrom}; use crate::cache::store::{CacheStore, CacheStoreError, CacheStoreResult}; use crate::config::Timeout; @@ -92,9 +93,10 @@ impl From for CacheStoreError { /// /// # Examples /// ```no_run -/// use cot::cache::store::file::FileStore; /// use std::path::Path; /// +/// use cot::cache::store::file::FileStore; +/// /// let store = FileStore::new(Path::new("./cache_dir")).unwrap(); /// ``` #[derive(Debug, Clone)] @@ -105,19 +107,21 @@ pub struct FileStore { impl FileStore { /// Creates a new `FileStore` at the specified directory. /// - /// This will attempt to create the directory and its parents if they do not exist. + /// This will attempt to create the directory and its parents if they do not + /// exist. /// /// # Errors /// - /// Returns [`FileCacheStoreError::DirCreation`] if the directory cannot be created - /// due to permissions or other I/O issues. + /// Returns [`FileCacheStoreError::DirCreation`] if the directory cannot be + /// created due to permissions or other I/O issues. /// /// # Examples /// /// ```no_run - /// use cot::cache::store::file::FileStore; /// use std::path::PathBuf; /// + /// use cot::cache::store::file::FileStore; + /// /// // Using a string slice /// let path = PathBuf::from("./cache"); /// let store = FileStore::new(path).unwrap(); @@ -346,7 +350,8 @@ impl CacheStore for FileStore { return Err(FileCacheStoreError::Io(Box::new(e)).into()); } } - // even though write is self healing, this minimizes result variants on other methods + // even though write is self healing, this minimizes result variants on other + // methods tokio::fs::create_dir_all(&self.dir_path) .await .map_err(|e| FileCacheStoreError::DirCreation(Box::new(e)))?;