Alignement template 4NK: lib.rs, docs/, tests/, OSS files, .gitea, .cursor, release CI, .4nk-sync.yml
Some checks failed
CI / rust (push) Failing after 1m5s

This commit is contained in:
Your Name 2025-08-26 10:13:06 +02:00
parent bb8f4bb6a2
commit 50e0b97a7f
20 changed files with 491 additions and 189 deletions

17
.4nk-sync.yml Normal file
View File

@ -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/

7
.cursor/rules.md Normal file
View File

@ -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.

View File

@ -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

View File

@ -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é.

View File

@ -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: #

30
.gitea/workflows/ci.yml Normal file
View File

@ -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

View File

@ -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/*

5
AGENTS.md Normal file
View File

@ -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/`).

6
CHANGELOG.md Normal file
View File

@ -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

9
CODE_OF_CONDUCT.md Normal file
View File

@ -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.

8
CONTRIBUTING.md Normal file
View File

@ -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.

51
Cargo.lock generated
View File

@ -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"

View File

@ -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"

19
LICENSE Normal file
View File

@ -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.

22
README.md Normal file
View File

@ -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).

5
SECURITY.md Normal file
View File

@ -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.

18
docs/README.md Normal file
View File

@ -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 }``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.

144
src/lib.rs Normal file
View File

@ -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<S: Into<String>>(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<SystemTime>,
) -> 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<Vec<u8>, 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<u64>,
}
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)
}

View File

@ -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<u64>,
}
/// 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<SystemTime>) -> 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<Vec<u8>, 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<Response> {
async fn handle_store(mut req: Request<StorageService>, no_ttl_permanent: bool) -> tide::Result<Response> {
// 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<SystemTime> = 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<SystemTime> = 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<Response> {
async fn handle_retrieve(req: Request<StorageService>) -> tide::Result<Response> {
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<Response> {
.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,27 +124,6 @@ async fn handle_retrieve(req: Request<()>) -> tide::Result<Response> {
}
}
/// 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
@ -291,13 +136,21 @@ async fn main() -> tide::Result<()> {
println!("No-TTL requests will use default TTL of {} seconds", DEFAULT_TTL);
}
create_dir_all(STORAGE_DIR)
.await
.expect("Failed to create storage directory.");
let svc = StorageService::new(STORAGE_DIR);
create_dir_all(STORAGE_DIR).await.expect("Failed to create storage directory.");
task::spawn(cleanup_expired_files());
// 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::new();
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?;

17
tests/http_api.rs Normal file
View File

@ -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);
}