diff --git a/.4nk-sync.yml b/.4nk-sync.yml new file mode 100644 index 0000000..df0ace7 --- /dev/null +++ b/.4nk-sync.yml @@ -0,0 +1,17 @@ +template: + repo: nicolas.cantu/4NK_project_template + host: git.4nkweb.com +sync: + include: + - .gitea/ + - .cursor/ + - docs/ + - AGENTS.md + - CONTRIBUTING.md + - CODE_OF_CONDUCT.md + - SECURITY.md + - CHANGELOG.md + exclude: + - target/ + - storage/ + - .git/ diff --git a/.cursor/rules.md b/.cursor/rules.md new file mode 100644 index 0000000..207790e --- /dev/null +++ b/.cursor/rules.md @@ -0,0 +1,7 @@ +# Règles Cursor du projet + +- Compiler régulièrement: `cargo build`. +- Lancer les tests souvent: `cargo test`. +- Mettre à jour la documentation (`docs/`) à chaque changement fonctionnel. +- Respecter le style Rust, `cargo fmt` et `cargo clippy -D warnings`. +- PRs doivent inclure tests et docs. diff --git a/.gitea/ISSUE_TEMPLATE/bug_report.md b/.gitea/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..ab2648a --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Rapport de bug +about: Signaler un problème +labels: bug +--- + +## Description + +Décrivez le bug. + +## Reproduction + +1. Étapes +2. Résultat observé +3. Résultat attendu + +## Contexte +- Version +- OS / Arch +- Logs pertinents diff --git a/.gitea/ISSUE_TEMPLATE/feature_request.md b/.gitea/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..78c77e9 --- /dev/null +++ b/.gitea/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,21 @@ +--- +name: Demande de fonctionnalité +about: Proposer une idée +labels: enhancement +--- + +## Problème / Contexte + +Quel problème résout la fonctionnalité ? + +## Proposition + +Décrivez la solution souhaitée. + +## Alternatives + +Solutions alternatives envisagées. + +## Impacts + +Tests, docs, compatibilité. diff --git a/.gitea/PULL_REQUEST_TEMPLATE.md b/.gitea/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..b705374 --- /dev/null +++ b/.gitea/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,13 @@ +# Objet + +Décrivez brièvement les changements. + +## Checklist +- [ ] Tests ajoutés/mis à jour (`tests/`) +- [ ] Documentation mise à jour (`docs/`) +- [ ] `cargo fmt` OK +- [ ] `cargo clippy` sans warnings +- [ ] `CHANGELOG.md` mis à jour si nécessaire + +## Liens +- Issue liée: # diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..ab3e3f6 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,30 @@ +name: CI + +on: + push: + pull_request: + +jobs: + rust: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - name: Format check + run: cargo fmt --all -- --check + - name: Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + - name: Build + run: cargo build --verbose + - name: Test + run: cargo test --all --verbose diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..8bc1e04 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,34 @@ +name: Release + +on: + push: + tags: + - 'v*.*.*' + +jobs: + build-release: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + - name: Build + run: cargo build --release + - name: Archive artifact + shell: bash + run: | + mkdir -p dist + if [[ "$RUNNER_OS" == "Windows" ]]; then + cp target/release/sdk_storage.exe dist/ + else + cp target/release/sdk_storage dist/ + fi + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: sdk_storage-${{ runner.os }} + path: dist/* diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..0ac2bb2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,5 @@ +# Agents & Automations + +- Compilation régulière: `cargo build`. +- Lancement des tests: `cargo test`. +- Mise à jour de la documentation dès qu'une fonctionnalité change (`docs/`). diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c55eaf2 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,6 @@ +# Changelog + +## 0.1.0 +- Refactor vers `src/lib.rs` et service `StorageService` +- Ajout `docs/` (README) et `tests/` (test intégration service) +- API HTTP Tide conservée; nettoyage TTL périodique 60s diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..32309a3 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,9 @@ +# Code de Conduite + +Nous nous engageons à offrir une communauté ouverte, accueillante et respectueuse. + +- Pas de harcèlement. +- Respect des avis techniques et des personnes. +- Suivre les consignes des mainteneurs. + +Signalez tout problème via les issues du dépôt. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..b60d0c7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,8 @@ +# Contribuer à sdk_storage + +Merci de proposer des issues et des Pull Requests. + +- Discutez via une issue avant une modification majeure. +- Travaillez sur une branche `feature/...`. +- Ajoutez systématiquement des tests (`tests/`) et mettez à jour la documentation (`docs/`). +- Assurez-vous que `cargo fmt`, `cargo clippy` et `cargo test` passent localement. diff --git a/Cargo.lock b/Cargo.lock index e6833ce..360f61d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -817,6 +817,18 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + [[package]] name = "ghash" version = "0.3.1" @@ -1336,6 +1348,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "rand" version = "0.7.3" @@ -1463,6 +1481,7 @@ dependencies = [ "hex", "serde", "serde_json", + "tempfile", "tide", ] @@ -1794,6 +1813,20 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "tempfile" +version = "3.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230" +dependencies = [ + "cfg-if 1.0.0", + "fastrand 2.2.0", + "getrandom 0.3.3", + "once_cell", + "rustix 0.38.41", + "windows-sys 0.59.0", +] + [[package]] name = "thiserror" version = "1.0.69" @@ -2013,6 +2046,15 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + [[package]] name = "wasm-bindgen" version = "0.2.95" @@ -2271,6 +2313,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.6.0", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index c829b2b..100ae95 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,11 @@ version = "0.1.0" edition = "2021" [dependencies] -tide = "0.16.0" -async-std = { version = "1.8.0", features = ["attributes"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -hex = "0.4.3" +tide = "0.16" +async-std = { version = "1", features = ["attributes"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +hex = "0.4" + +[dev-dependencies] +tempfile = "3" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9cf1062 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c866137 --- /dev/null +++ b/README.md @@ -0,0 +1,22 @@ +# sdk_storage + +Voir la documentation détaillée dans `docs/`. + +## Démarrage rapide + +- Construire: `cargo build` +- Lancer: `cargo run -- --permanent` (clé sans TTL = permanente) +- Tester: `cargo test` + +## API + +- POST `/store` { key(hex64), value(hex), ttl? (s) } +- GET `/retrieve/:key` + +## Contribution + +Voir `CONTRIBUTING.md`, `CODE_OF_CONDUCT.md`, `SECURITY.md`. + +## Licence + +Voir `LICENSE` (MIT). diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..50ac807 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,5 @@ +# Politique de Sécurité + +- Ne divulguez pas publiquement les vulnérabilités. +- Ouvrez une issue privée si possible ou contactez les mainteneurs. +- Merci d'inclure des étapes de reproduction et l'impact. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..b69c667 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,18 @@ +# Documentation du projet sdk_storage + +Ce dossier documente l'API HTTP, l'architecture et les décisions techniques. + +## API + +- POST `/store` : stocke une valeur hex pour une clé hex 64 chars, `ttl` optionnel (secondes). Quand `--permanent` est passé au binaire, l'absence de `ttl` rend la donnée permanente. +- GET `/retrieve/:key` : retourne `{ key, value }` où `value` est encodée en hex. + +## Architecture + +- Service `StorageService` (voir `src/lib.rs`) encapsule la logique de stockage, récupération et nettoyage TTL. +- `src/main.rs` démarre Tide avec état `StorageService` et une boucle de nettoyage périodique (60s). + +## REX technique + +- Refactor initial de la logique depuis `main.rs` vers `lib.rs` pour testabilité et séparation des responsabilités. +- Durées TTL maintenant validées dans le handler, calcul d'expiration converti en `SystemTime` avant l'appel service. diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..9d1a5e0 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,144 @@ +use async_std::fs::{create_dir_all, read_dir, read_to_string, remove_file, File}; +use async_std::io::WriteExt; +use async_std::path::Path; +use async_std::stream::StreamExt; +use serde::{Deserialize, Serialize}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use tide::StatusCode; + +#[derive(Clone, Debug)] +pub struct StorageService { + storage_dir: String, +} + +impl StorageService { + pub fn new>(storage_dir: S) -> Self { + Self { + storage_dir: storage_dir.into(), + } + } + + fn get_file_path(&self, key: &str) -> String { + let dir_name = format!("{}/{}", self.storage_dir, &key[..2]); + let file_path = format!("{}/{}", dir_name, &key[2..]); + file_path + } + + pub async fn store_data( + &self, + key: &str, + value: &[u8], + expires_at: Option, + ) -> Result<(), tide::Error> { + let file_name = self.get_file_path(key); + let file_path = Path::new(&file_name); + + if file_path.exists().await { + return Err(tide::Error::from_str(StatusCode::Conflict, "Key already exists")); + } + + create_dir_all(file_path.parent().ok_or(tide::Error::from_str( + StatusCode::InternalServerError, + "File path doesn't have parent", + ))?) + .await + .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; + + let metadata_path = format!("{}.meta", file_name); + + let mut file = File::create(&file_path) + .await + .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; + file.write_all(value) + .await + .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; + + let metadata = Metadata { + expires_at: expires_at.map(system_time_to_unix), + }; + + let metadata_json = serde_json::to_string(&metadata) + .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; + let mut meta_file = File::create(&metadata_path) + .await + .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; + meta_file + .write_all(metadata_json.as_bytes()) + .await + .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; + + Ok(()) + } + + pub async fn retrieve_data(&self, key: &str) -> Result, String> { + let file_path = format!("{}/{}/{}", self.storage_dir, &key[..2], &key[2..]); + + let mut file = File::open(&file_path) + .await + .map_err(|_| "Key not found.".to_string())?; + let mut buffer = Vec::new(); + async_std::io::ReadExt::read_to_end(&mut file, &mut buffer) + .await + .map_err(|e| e.to_string())?; + Ok(buffer) + } + + pub async fn cleanup_expired_files_once(&self) -> Result<(), String> { + let mut entries = read_dir(&self.storage_dir) + .await + .map_err(|e| format!("Failed to read storage dir: {}", e))?; + let now = system_time_to_unix(SystemTime::now()); + while let Some(entry) = entries.next().await { + let e = entry.map_err(|e| format!("entry returned error: {}", e))?; + let path = e.path(); + if path.is_dir().await { + if let Ok(mut sub_entries) = read_dir(&path).await { + while let Some(sub_entry) = sub_entries.next().await { + if let Ok(sub_entry) = sub_entry { + let file_path = sub_entry.path(); + if file_path.extension() == Some("meta".as_ref()) { + self.handle_file_cleanup(now, &file_path).await?; + } + } + } + } + } + } + Ok(()) + } + + async fn handle_file_cleanup(&self, now: u64, meta_path: &Path) -> Result<(), String> { + let meta_content = read_to_string(meta_path) + .await + .map_err(|e| format!("Failed to read metadata: {}", e.to_string()))?; + let metadata: Metadata = serde_json::from_str(&meta_content) + .map_err(|e| format!("Failed to parse metadata: {}", e.to_string()))?; + + if metadata.expires_at.is_some() && metadata.expires_at.unwrap() < now { + let data_file_path = meta_path.with_extension(""); + remove_file(&data_file_path) + .await + .map_err(|e| format!("Failed to remove data file: {}", e.to_string()))?; + remove_file(meta_path) + .await + .map_err(|e| format!("Failed to remove metadata file: {}", e.to_string()))?; + } + Ok(()) + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Metadata { + pub expires_at: Option, +} + +pub fn system_time_to_unix(system_time: SystemTime) -> u64 { + system_time + .duration_since(UNIX_EPOCH) + .expect("SystemTime before UNIX_EPOCH!") + .as_secs() +} + +pub fn unix_to_system_time(unix_timestamp: u64) -> SystemTime { + UNIX_EPOCH + Duration::from_secs(unix_timestamp) +} diff --git a/src/main.rs b/src/main.rs index 0d6807f..b8b052c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,80 +1,16 @@ -use async_std::fs::{create_dir_all, read_dir, read_to_string, remove_file, File}; use serde::{Deserialize, Serialize}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{Duration, SystemTime}; use std::env; - -use async_std::io::WriteExt; -use async_std::path::Path; -use async_std::stream::StreamExt; use async_std::task; +use async_std::fs::create_dir_all; use tide::{Request, Response, StatusCode}; +use sdk_storage::StorageService; const STORAGE_DIR: &str = "./storage"; const PORT: u16 = 8081; -const MIN_TTL: u64 = 60; // 1 minute -const DEFAULT_TTL: u64 = 86400; // 1 day -const MAX_TTL: u64 = 31_536_000; // 1 year, to be discussed - -/// Scans storage and removes expired files -async fn cleanup_expired_files() { - loop { - // Traverse storage directory - let mut entries = match read_dir(STORAGE_DIR).await { - Ok(entry) => entry, - Err(e) => { - eprintln!("Failed to read storage dir: {}", e); - task::sleep(Duration::from_secs(60)).await; - continue; - } - }; - - let now = system_time_to_unix(SystemTime::now()); - while let Some(entry) = entries.next().await { - let e = match entry { - Ok(e) => e, - Err(e) => { - eprintln!("entry returned error: {}", e); - continue; - } - }; - let path = e.path(); - if path.is_dir().await { - if let Ok(mut sub_entries) = read_dir(&path).await { - while let Some(sub_entry) = sub_entries.next().await { - if let Ok(sub_entry) = sub_entry { - let file_path = sub_entry.path(); - if file_path.extension() == Some("meta".as_ref()) { - if let Err(err) = handle_file_cleanup(now, &file_path).await { - eprintln!("Error cleaning file {:?}: {}", file_path, err); - } - } - } - } - } - } - } - // Sleep for 1 minute before next cleanup - task::sleep(Duration::from_secs(60)).await; - } -} - -#[derive(Debug, Deserialize, Serialize)] -struct Metadata { - expires_at: Option, -} - -/// Converts a `SystemTime` to a UNIX timestamp (seconds since UNIX epoch). -fn system_time_to_unix(system_time: SystemTime) -> u64 { - system_time - .duration_since(UNIX_EPOCH) - .expect("SystemTime before UNIX_EPOCH!") - .as_secs() -} - -/// Converts a UNIX timestamp (seconds since UNIX epoch) back to `SystemTime`. -fn unix_to_system_time(unix_timestamp: u64) -> SystemTime { - UNIX_EPOCH + Duration::from_secs(unix_timestamp) -} +const MIN_TTL: u64 = 60; +const DEFAULT_TTL: u64 = 86400; +const MAX_TTL: u64 = 31_536_000; #[derive(Deserialize)] struct StoreRequest { @@ -84,84 +20,12 @@ struct StoreRequest { } #[derive(Serialize)] -struct ApiResponse { - message: String, -} +struct ApiResponse { message: String } #[derive(Serialize)] -struct RetrieveResponse { - key: String, - value: String, -} +struct RetrieveResponse { key: String, value: String } -async fn get_file_path(key: &str) -> String { - let dir_name = format!("{}/{}", STORAGE_DIR, &key[..2]); - let file_path = format!("{}/{}", dir_name, &key[2..]); - - file_path -} - -/// Store data on the filesystem -async fn store_data(key: &str, value: &[u8], expires_at: Option) -> Result<(), tide::Error> { - let file_name = get_file_path(key).await; - let file_path = Path::new(&file_name); - - // Check if key exists - if file_path.exists().await { - return Err(tide::Error::from_str( - StatusCode::Conflict, - "Key already exists", - )); - } - - create_dir_all(file_path.parent().ok_or(tide::Error::from_str( - StatusCode::InternalServerError, - "File path doesn't have parent", - ))?) - .await - .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; - - let metadata_path = format!("{}.meta", file_name); - - let mut file = File::create(&file_path) - .await - .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; - file.write_all(value) - .await - .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; - - let metadata = Metadata { - expires_at: expires_at.map(|e| system_time_to_unix(e)), - }; - - let metadata_json = serde_json::to_string(&metadata) - .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; - let mut meta_file = File::create(&metadata_path) - .await - .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; - meta_file - .write_all(metadata_json.as_bytes()) - .await - .map_err(|e| tide::Error::new(StatusCode::InternalServerError, e))?; - - Ok(()) -} - -async fn retrieve_data(key: &str) -> Result, String> { - let file_path = format!("{}/{}/{}", STORAGE_DIR, &key[..2], &key[2..]); - - let mut file = File::open(&file_path) - .await - .map_err(|_| "Key not found.".to_string())?; - let mut buffer = Vec::new(); - async_std::io::ReadExt::read_to_end(&mut file, &mut buffer) - .await - .map_err(|e| e.to_string())?; - Ok(buffer) -} - -/// Handler for the /store endpoint -async fn handle_store(mut req: Request<()>, no_ttl_permanent: bool) -> tide::Result { +async fn handle_store(mut req: Request, no_ttl_permanent: bool) -> tide::Result { // Parse the JSON body let data: StoreRequest = match req.body_json().await { Ok(data) => data, @@ -201,16 +65,16 @@ async fn handle_store(mut req: Request<()>, no_ttl_permanent: bool) -> tide::Res Some(Duration::from_secs(DEFAULT_TTL)) }; - let expires_at: Option = if let Some(live_for) = live_for { - let now = SystemTime::now(); - Some(now - .checked_add(live_for) - .ok_or(tide::Error::from_str(StatusCode::BadRequest, "Invalid ttl"))?) - } else { - None + let expires_at: Option = match live_for { + Some(lf) => Some( + SystemTime::now() + .checked_add(lf) + .ok_or(tide::Error::from_str(StatusCode::BadRequest, "Invalid ttl"))? + ), + None => None, }; - // Decode the value from Base64 + // Decode hex value let value_bytes = match hex::decode(&data.value) { Ok(value) => value, Err(e) => { @@ -221,7 +85,8 @@ async fn handle_store(mut req: Request<()>, no_ttl_permanent: bool) -> tide::Res }; // Store the data - match store_data(&data.key, &value_bytes, expires_at).await { + let svc = req.state(); + match svc.store_data(&data.key, &value_bytes, expires_at).await { Ok(()) => Ok(Response::builder(StatusCode::Ok) .body(serde_json::to_value(&ApiResponse { message: "Data stored successfully.".to_string(), @@ -235,7 +100,7 @@ async fn handle_store(mut req: Request<()>, no_ttl_permanent: bool) -> tide::Res } } -async fn handle_retrieve(req: Request<()>) -> tide::Result { +async fn handle_retrieve(req: Request) -> tide::Result { let key: String = req.param("key")?.to_string(); if key.len() != 64 || !key.chars().all(|c| c.is_ascii_hexdigit()) { @@ -244,7 +109,8 @@ async fn handle_retrieve(req: Request<()>) -> tide::Result { .build()); } - match retrieve_data(&key).await { + let svc = req.state(); + match svc.retrieve_data(&key).await { Ok(value) => { let encoded_value = hex::encode(value); Ok(Response::builder(StatusCode::Ok) @@ -258,46 +124,33 @@ async fn handle_retrieve(req: Request<()>) -> tide::Result { } } -/// Checks a metadata file and deletes the associated data file if expired -async fn handle_file_cleanup(now: u64, meta_path: &Path) -> Result<(), String> { - let meta_content = read_to_string(meta_path) - .await - .map_err(|e| format!("Failed to read metadata: {}", e.to_string()))?; - let metadata: Metadata = serde_json::from_str(&meta_content) - .map_err(|e| format!("Failed to parse metadata: {}", e.to_string()))?; - - if metadata.expires_at.is_some() && metadata.expires_at.unwrap() < now { - let data_file_path = meta_path.with_extension(""); - remove_file(&data_file_path) - .await - .map_err(|e| format!("Failed to remove data file: {}", e.to_string()))?; - remove_file(meta_path) - .await - .map_err(|e| format!("Failed to remove metadata file: {}", e.to_string()))?; - println!("Removed expired file: {:?}", data_file_path); - } - Ok(()) -} - #[async_std::main] async fn main() -> tide::Result<()> { // Parse command line arguments let args: Vec = env::args().collect(); let no_ttl_permanent = args.iter().any(|arg| arg == "--permanent"); - + if no_ttl_permanent { println!("No-TTL requests will be treated as permanent"); } else { println!("No-TTL requests will use default TTL of {} seconds", DEFAULT_TTL); } - - create_dir_all(STORAGE_DIR) - .await - .expect("Failed to create storage directory."); - task::spawn(cleanup_expired_files()); + let svc = StorageService::new(STORAGE_DIR); + create_dir_all(STORAGE_DIR).await.expect("Failed to create storage directory."); - let mut app = tide::new(); + // background cleanup loop + let svc_clone = svc.clone(); + task::spawn(async move { + loop { + if let Err(e) = svc_clone.cleanup_expired_files_once().await { + eprintln!("cleanup error: {}", e); + } + task::sleep(std::time::Duration::from_secs(60)).await; + } + }); + + let mut app = tide::with_state(svc); app.at("/store").post(move |req| handle_store(req, no_ttl_permanent)); app.at("/retrieve/:key").get(handle_retrieve); app.listen(format!("0.0.0.0:{}", PORT)).await?; diff --git a/tests/http_api.rs b/tests/http_api.rs new file mode 100644 index 0000000..b6c9817 --- /dev/null +++ b/tests/http_api.rs @@ -0,0 +1,17 @@ +use sdk_storage::{StorageService, unix_to_system_time}; +use tempfile::TempDir; + +#[async_std::test] +async fn store_and_retrieve_hex_in_tempdir() { + let td = TempDir::new().unwrap(); + let dir_path = td.path().to_string_lossy().to_string(); + let svc = StorageService::new(dir_path); + + let key = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + let value = b"hello"; + let expires = Some(unix_to_system_time(60 + sdk_storage::system_time_to_unix(std::time::SystemTime::now()))); + + svc.store_data(key, value, expires).await.unwrap(); + let got = svc.retrieve_data(key).await.unwrap(); + assert_eq!(got, value); +}