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
Some checks failed
CI / rust (push) Failing after 1m5s
This commit is contained in:
parent
bb8f4bb6a2
commit
50e0b97a7f
17
.4nk-sync.yml
Normal file
17
.4nk-sync.yml
Normal 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
7
.cursor/rules.md
Normal 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.
|
20
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal file
20
.gitea/ISSUE_TEMPLATE/bug_report.md
Normal 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
|
21
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal file
21
.gitea/ISSUE_TEMPLATE/feature_request.md
Normal 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é.
|
13
.gitea/PULL_REQUEST_TEMPLATE.md
Normal file
13
.gitea/PULL_REQUEST_TEMPLATE.md
Normal 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
30
.gitea/workflows/ci.yml
Normal 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
|
34
.gitea/workflows/release.yml
Normal file
34
.gitea/workflows/release.yml
Normal 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
5
AGENTS.md
Normal 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
6
CHANGELOG.md
Normal 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
9
CODE_OF_CONDUCT.md
Normal 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
8
CONTRIBUTING.md
Normal 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
51
Cargo.lock
generated
@ -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"
|
||||
|
13
Cargo.toml
13
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"
|
||||
|
19
LICENSE
Normal file
19
LICENSE
Normal 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
22
README.md
Normal 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
5
SECURITY.md
Normal 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
18
docs/README.md
Normal 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 }` 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.
|
144
src/lib.rs
Normal file
144
src/lib.rs
Normal 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)
|
||||
}
|
217
src/main.rs
217
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<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
17
tests/http_api.rs
Normal 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);
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user