Compare commits

...

14 Commits
master ... dev

Author SHA1 Message Date
omaroughriss
120a3dc8d0 Add CICD to storage
All checks were successful
Build and Push to Registry / build-and-push (push) Successful in 1m28s
2025-09-08 19:06:03 +02:00
44472bef1e Rework api to take and return binary directly without hex serialization
Some checks failed
CI / rust (push) Failing after 30s
2025-09-04 12:55:55 +02:00
f1021295f7 Add logging 2025-09-04 12:54:55 +02:00
Your Name
1c42b86f90 chore(release): bump to v0.2.2
Some checks failed
CI / rust (push) Failing after 29s
Docker Image / docker (push) Failing after 19s
Release / build-release (ubuntu-latest) (push) Failing after 1m2s
Release / build-release (windows-latest) (push) Has been cancelled
2025-08-26 14:24:04 +02:00
Your Name
7aebbca98d feat(health): add /health endpoint + tests + docs
Some checks failed
CI / rust (push) Failing after 30s
Docker Image / docker (push) Failing after 19s
2025-08-26 11:16:33 +02:00
Your Name
8f456f0cd5 chore-gitignore-update
Some checks failed
CI / rust (push) Failing after 30s
Docker Image / docker (push) Failing after 40s
2025-08-26 11:13:36 +02:00
Your Name
87e89317f5 chore(ci): CODEOWNERS + doc variables registre Docker
Some checks failed
CI / rust (push) Failing after 29s
Docker Image / docker (push) Failing after 18s
2025-08-26 11:05:40 +02:00
Your Name
7cc675c129 docs: ajout guide de release
Some checks failed
CI / rust (push) Failing after 30s
Docker Image / docker (push) Failing after 18s
2025-08-26 11:03:06 +02:00
Your Name
62a972594d test(api): couverture complète endpoints + docs contrats
Some checks failed
Docker Image / docker (push) Failing after 18s
CI / rust (push) Failing after 30s
Release / build-release (ubuntu-latest) (push) Failing after 57s
Release / build-release (windows-latest) (push) Has been cancelled
2025-08-26 10:34:11 +02:00
Your Name
0950da48d6 chore(release): update CHANGELOG for v0.2.0
Some checks failed
Release / build-release (ubuntu-latest) (push) Failing after 1m8s
CI / rust (push) Failing after 29s
Docker Image / docker (push) Failing after 18s
Release / build-release (windows-latest) (push) Has been cancelled
2025-08-26 10:26:01 +02:00
Your Name
790ffb4032 docs: api json spec
Some checks failed
CI / rust (push) Failing after 31s
Docker Image / docker (push) Failing after 22s
2025-08-26 10:23:15 +02:00
Your Name
53c6301be2 docs: ajout des pages alignées avec le template 4NK
Some checks failed
CI / rust (push) Failing after 30s
Docker Image / docker (push) Failing after 19s
2025-08-26 10:19:55 +02:00
Your Name
6907e4baf1 feat(docker): Dockerfile, .dockerignore, CI docker, docs
Some checks failed
CI / rust (push) Failing after 30s
Docker Image / docker (push) Failing after 1m24s
2025-08-26 10:17:35 +02:00
Your Name
50e0b97a7f 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
2025-08-26 10:13:06 +02:00
35 changed files with 1546 additions and 294 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.

6
.dockerignore Normal file
View File

@ -0,0 +1,6 @@
target
.git
storage
**/*.log
**/*.tmp
**/*.swp

44
.github/workflows/dev.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: Build and Push to Registry
on:
push:
branches: [ dev ]
env:
REGISTRY: git.4nkweb.com
IMAGE_NAME: 4nk/sdk_storage
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up SSH agent
uses: webfactory/ssh-agent@v0.9.1
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.USER }}
password: ${{ secrets.TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
ssh: default
build-args: |
CONF=${{ secrets.CONF }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ gitea.sha }}

11
.gitignore vendored
View File

@ -1,2 +1,13 @@
/target
/storage
/dist
/.env
/.env.local
/.env.production
/.env.test
.vscode/
.idea/
*.log
*.tmp
*.swp
*.swo

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

18
CHANGELOG.md Normal file
View File

@ -0,0 +1,18 @@
# Changelog
## 0.2.0
- Ajout Dockerfile multi-stage et `.dockerignore`
- CI: workflows build/test, release et build/push Docker
- Documentation étendue dans `docs/` (architecture, guides, API JSON)
- Tests renforcés: conflit de clé et suppression des expirés
## 0.2.2
- Endpoint `/health` ajouté et testé
- Spec JSON/API et docs monitoring mises à jour
- CI Docker: tagging multi-tags (`latest` et `vX.Y.Z` sur tag)
- Version et déploiements alignés
## 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

3
CODEOWNERS Normal file
View File

@ -0,0 +1,3 @@
# Code owners par défaut du dépôt
# Ajustez ces lignes si vous utilisez des équipes/alias différents.
* @nicolas.cantu

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.

615
Cargo.lock generated
View File

@ -2,6 +2,21 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aead"
version = "0.3.2"
@ -56,6 +71,15 @@ dependencies = [
"opaque-debug",
]
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
@ -236,6 +260,18 @@ dependencies = [
"pin-project-lite 0.2.15",
]
[[package]]
name = "async-native-tls"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e9e7a929bd34c68a82d58a4de7f86fffdaf97fb2af850162a7bb19dd7269b33"
dependencies = [
"async-std",
"native-tls",
"thiserror",
"url",
]
[[package]]
name = "async-process"
version = "2.3.0"
@ -365,6 +401,21 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.75"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
dependencies = [
"addr2line",
"cfg-if 1.0.0",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets 0.52.6",
]
[[package]]
name = "base-x"
version = "0.2.11"
@ -507,6 +558,17 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "config"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3"
dependencies = [
"lazy_static",
"nom",
"serde",
]
[[package]]
name = "const_fn"
version = "0.4.10"
@ -536,6 +598,16 @@ dependencies = [
"version_check",
]
[[package]]
name = "core-foundation"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@ -557,6 +629,15 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba"
[[package]]
name = "crossbeam-queue"
version = "0.3.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
dependencies = [
"crossbeam-utils",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
@ -592,6 +673,33 @@ dependencies = [
"cipher",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if 1.0.0",
"hashbrown",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "deadpool"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d126179d86aee4556e54f5f3c6bf6d9884e7cc52cef82f77ee6f90a7747616d"
dependencies = [
"async-trait",
"config",
"crossbeam-queue",
"num_cpus",
"serde",
"tokio",
]
[[package]]
name = "digest"
version = "0.9.0"
@ -618,6 +726,19 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "erased-serde"
version = "0.4.5"
@ -696,6 +817,21 @@ dependencies = [
"web-sys",
]
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.2.1"
@ -705,6 +841,21 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
@ -712,6 +863,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
@ -720,6 +872,17 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
@ -765,6 +928,12 @@ dependencies = [
"syn 2.0.87",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
@ -777,9 +946,13 @@ version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite 0.2.15",
"pin-utils",
"slab",
@ -817,6 +990,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"
@ -827,6 +1012,12 @@ dependencies = [
"polyval",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "gloo-timers"
version = "0.3.0"
@ -839,6 +1030,12 @@ dependencies = [
"wasm-bindgen",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hermit-abi"
version = "0.3.9"
@ -851,6 +1048,12 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.4.3"
@ -893,8 +1096,14 @@ version = "6.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1947510dc91e2bf586ea5ffb412caad7673264e14bb39fb9078da114a94ce1a5"
dependencies = [
"async-h1",
"async-native-tls",
"async-std",
"async-trait",
"cfg-if 1.0.0",
"dashmap",
"deadpool",
"futures",
"http-types",
"log",
]
@ -927,6 +1136,12 @@ version = "1.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
[[package]]
name = "humantime"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f"
[[package]]
name = "iana-time-zone"
version = "0.1.61"
@ -1115,6 +1330,28 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "io-uring"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"libc",
]
[[package]]
name = "is-terminal"
version = "0.4.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [
"hermit-abi 0.5.2",
"libc",
"windows-sys 0.59.0",
]
[[package]]
name = "itoa"
version = "1.0.11"
@ -1140,10 +1377,29 @@ dependencies = [
]
[[package]]
name = "libc"
version = "0.2.164"
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "433bfe06b8c75da9b2e3fbea6e5329ff87748f0b144ef75306e674c3f6f7c13f"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]]
name = "lexical-core"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe"
dependencies = [
"arrayvec",
"bitflags 1.3.2",
"cfg-if 1.0.0",
"ryu",
"static_assertions",
]
[[package]]
name = "libc"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "linux-raw-sys"
@ -1163,6 +1419,16 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "lock_api"
version = "0.4.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.22"
@ -1179,6 +1445,70 @@ version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "mime_guess"
version = "2.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.59.0",
]
[[package]]
name = "native-tls"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
dependencies = [
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "nom"
version = "5.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08959a387a676302eebf4ddbcbc611da04285579f76f88ee0506c63b1a61dd4b"
dependencies = [
"lexical-core",
"memchr",
"version_check",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@ -1188,6 +1518,25 @@ dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b"
dependencies = [
"hermit-abi 0.5.2",
"libc",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.20.2"
@ -1200,12 +1549,69 @@ version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]]
name = "openssl"
version = "0.10.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
dependencies = [
"bitflags 2.6.0",
"cfg-if 1.0.0",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.87",
]
[[package]]
name = "openssl-probe"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
[[package]]
name = "openssl-sys"
version = "0.9.109"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]]
name = "parking"
version = "2.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
[[package]]
name = "parking_lot_core"
version = "0.9.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
dependencies = [
"cfg-if 1.0.0",
"libc",
"redox_syscall",
"smallvec",
"windows-targets 0.52.6",
]
[[package]]
name = "percent-encoding"
version = "2.3.1"
@ -1261,6 +1667,12 @@ dependencies = [
"futures-io",
]
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "polling"
version = "2.8.0"
@ -1336,6 +1748,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"
@ -1407,12 +1825,56 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "redox_syscall"
version = "0.5.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77"
dependencies = [
"bitflags 2.6.0",
]
[[package]]
name = "regex"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "route-recognizer"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56770675ebc04927ded3e60633437841581c285dc6236109ea25fbf3beb7b59e"
[[package]]
name = "rustc-demangle"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
[[package]]
name = "rustc_version"
version = "0.2.3"
@ -1455,17 +1917,58 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "schannel"
version = "0.1.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "sdk_storage"
version = "0.1.0"
version = "0.2.2"
dependencies = [
"async-std",
"env_logger",
"hex",
"serde",
"serde_json",
"surf",
"tempfile",
"tide",
]
[[package]]
name = "security-framework"
version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
dependencies = [
"bitflags 2.6.0",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "semver"
version = "0.9.0"
@ -1628,6 +2131,12 @@ dependencies = [
"version_check",
]
[[package]]
name = "static_assertions"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "stdweb"
version = "0.4.20"
@ -1683,6 +2192,28 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "surf"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "718b1ae6b50351982dedff021db0def601677f2120938b070eadb10ba4038dd7"
dependencies = [
"async-native-tls",
"async-std",
"async-trait",
"cfg-if 1.0.0",
"futures-util",
"getrandom 0.2.15",
"http-client",
"http-types",
"log",
"mime_guess",
"once_cell",
"pin-project-lite 0.2.15",
"serde",
"serde_json",
]
[[package]]
name = "sval"
version = "2.13.2"
@ -1794,6 +2325,29 @@ 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 = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@ -1885,6 +2439,20 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tokio"
version = "1.47.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038"
dependencies = [
"backtrace",
"io-uring",
"libc",
"mio",
"pin-project-lite 0.2.15",
"slab",
]
[[package]]
name = "tracing"
version = "0.1.40"
@ -1913,6 +2481,12 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicase"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
[[package]]
name = "unicode-ident"
version = "1.0.13"
@ -1989,6 +2563,12 @@ dependencies = [
"sval_serde",
]
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.5"
@ -2013,6 +2593,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"
@ -2108,6 +2697,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0978bf7171b3d90bac376700cb56d606feb40f251a475a5d6634613564460b22"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@ -2271,6 +2869,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

@ -1,11 +1,16 @@
[package]
name = "sdk_storage"
version = "0.1.0"
version = "0.2.2"
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"
env_logger = "0.10"
[dev-dependencies]
tempfile = "3"
surf = { version = "2", default-features = false, features = ["h1-client"] }

19
Dockerfile Normal file
View File

@ -0,0 +1,19 @@
# syntax=docker/dockerfile:1
FROM rust:1 as builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release
FROM debian:stable-slim
RUN useradd -m -u 10001 appuser && \
apt-get update && apt-get install -y ca-certificates && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY --from=builder /app/target/release/sdk_storage /usr/local/bin/sdk_storage
RUN mkdir -p /app/storage && chown -R appuser:appuser /app
USER appuser
EXPOSE 8081
ENV RUST_LOG=info
ENTRYPOINT ["/usr/local/bin/sdk_storage"]
CMD ["--permanent"]

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.

39
docs/README.md Normal file
View File

@ -0,0 +1,39 @@
# 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).
Voir aussi:
- `architecture.md`
- `configuration.md`
- `guides_principaux.md`
- `guides_techniques.md`
- `guides_test.md`
- `tests_monitoring.md`
- `reseau_de_relais.md`
- `developpement.md`
- `depannage.md`
- `performance.md`
- `api_json_spec.md`
- `api_contrats.md`
- `release_guide.md`
- `ci_docker_registry.md`
## REX technique
- Docker
- Build local: `docker build -t sdk_storage:local .`
- Run: `docker run --rm -p 8081:8081 -v $PWD/storage:/app/storage sdk_storage:local`
- Par défaut `--permanent` est activé via CMD, override possible: `docker run ... sdk_storage -- --permanent`
- 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.

21
docs/api_contrats.md Normal file
View File

@ -0,0 +1,21 @@
# Contrats API
## Garanties de Contrat
- Content-Type JSON, réponses structurées.
- Clé: 64 hex (validation stricte), sinon 400.
- Valeur: hex valide, sinon 400.
- Conflit de clé: 409 si la clé existe déjà.
- TTL: min 60, max 31536000; par défaut 86400 si non `--permanent`.
- Récupération:
- 200 avec `{ key, value }` si trouvée.
- 400 si clé invalide.
- 404 si absente.
## Couverture de Tests
- Stockage et récupération (succès).
- Conflit de clé.
- Suppression des expirés via nettoyage.
- HTTP `/store`: succès, conflit, clé invalide, valeur invalide.
- HTTP `/retrieve`: succès, clé invalide, clé absente.
Voir `api_json_spec.md` pour les schémas et contraintes détaillés.

113
docs/api_json_spec.md Normal file
View File

@ -0,0 +1,113 @@
# Spécification JSON des API
## Généralités
- Content-Type: `application/json; charset=utf-8`
- Encodage des valeurs: chaînes hexadécimales minuscules (0-9a-f).
- Modèle d'erreur: corps `{ "message": string }` avec code HTTP approprié.
## POST /store
- Objet requête: `StoreRequest`
- Objet réponse (succès/erreur): `ApiResponse`
### Schéma JSON (StoreRequest)
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "StoreRequest",
"type": "object",
"additionalProperties": false,
"required": ["key", "value"],
"properties": {
"key": {
"type": "string",
"description": "Clé hexadécimale 64 caractères (32 octets).",
"pattern": "^[0-9a-fA-F]{64}$"
},
"value": {
"type": "string",
"description": "Valeur encodée en hexadécimal.",
"pattern": "^[0-9a-fA-F]+$"
},
"ttl": {
"type": "integer",
"minimum": 60,
"maximum": 31536000,
"description": "Durée de vie en secondes. Si absent: défaut 86400 sauf si mode --permanent (aucune expiration)."
}
}
}
```
### Règles de validation et sémantique
- `key`: exactement 64 caractères hex (
32 octets).
- `value`: chaîne hex valide, longueur paire recommandée (représentation d'octets).
- `ttl`:
- min: 60, max: 31536000.
- si absent et binaire lancé sans `--permanent`: valeur par défaut 86400.
- si absent et binaire lancé avec `--permanent`: aucune expiration.
### Réponses
- 200 OK: `ApiResponse` (message de succès)
- 400 Bad Request: `ApiResponse` (clé/ttl/valeur invalides)
- 409 Conflict: `ApiResponse` (clé déjà existante)
- 500 Internal Server Error: `ApiResponse`
### Schéma JSON (ApiResponse)
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "ApiResponse",
"type": "object",
"additionalProperties": false,
"required": ["message"],
"properties": {
"message": { "type": "string" }
}
}
```
## GET /retrieve/:key
## GET /health
- Aucune donnée d'entrée.
- Réponse 200 avec `ApiResponse` `{ "message": "ok" }`.
- Paramètre de chemin: `key` (hex 64).
- Objet réponse (succès): `RetrieveResponse`
- Objet réponse (erreur): `ApiResponse`
### Contraintes
- `key` doit respecter `^[0-9a-fA-F]{64}$`.
### Réponses
- 200 OK: `RetrieveResponse`
- 400 Bad Request: `ApiResponse` (clé invalide)
- 404 Not Found: `ApiResponse` (clé inconnue)
- 500 Internal Server Error: `ApiResponse`
### Schéma JSON (RetrieveResponse)
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "RetrieveResponse",
"type": "object",
"additionalProperties": false,
"required": ["key", "value"],
"properties": {
"key": {
"type": "string",
"description": "Clé hexadécimale 64 caractères.",
"pattern": "^[0-9a-fA-F]{64}$"
},
"value": {
"type": "string",
"description": "Valeur encodée en hexadécimal.",
"pattern": "^[0-9a-fA-F]+$"
}
}
}
```
## Codes d'état et messages
- Les messages d'erreur sont informatifs mais ne divulguent pas d'informations sensibles.
- Les champs `message` sont destinés à l'humain; ne pas les parser côté client.

13
docs/architecture.md Normal file
View File

@ -0,0 +1,13 @@
# Architecture
## Flux de Données
- Entrées: requêtes HTTP `/store`, `/retrieve/:key`.
- Traitements: validation clés/TTL, encodage hex, stockage FS hiérarchique, métadonnées TTL.
- Sorties: réponses JSON normalisées.
## Composants
- Service `StorageService` (I/O disque, TTL, nettoyage).
- Serveur HTTP Tide (routes, état partagé).
- Nettoyage périodique (60s) basé sur fichiers `.meta`.

View File

@ -0,0 +1,11 @@
# CI - Variables pour Registre Docker
Le workflow `.gitea/workflows/docker.yml` requiert ces secrets:
- `DOCKER_REGISTRY`: registre cible (ex: registry.git.4nkweb.com/4nk)
- `DOCKER_USERNAME`: utilisateur du registre
- `DOCKER_PASSWORD`: mot de passe/token
Configurer ces secrets dans les paramètres du dépôt (ou de lorganisation) côté Gitea.
La cible dimage est `DOCKER_REGISTRY/sdk_storage:latest` (et multi-arch si activé).

8
docs/configuration.md Normal file
View File

@ -0,0 +1,8 @@
# Configuration
## Services Disponibles
- HTTP sur port 8081
## Variables d'Environnement
- `RUST_LOG` (optionnel): niveau de logs.
- Pour Docker, monter `/app/storage` si persistance souhaitée.

15
docs/demarrage_rapide.md Normal file
View File

@ -0,0 +1,15 @@
# Démarrage Rapide
## Prérequis
- Rust stable et Cargo
- Optionnel: Docker
## Installation
- `cargo build`
## Lancement
- `cargo run -- --permanent`
## Docker (optionnel)
- Build: `docker build -t sdk_storage:local .`
- Run: `docker run --rm -p 8081:8081 -v $PWD/storage:/app/storage sdk_storage:local`

12
docs/depannage.md Normal file
View File

@ -0,0 +1,12 @@
# Dépannage
## Problèmes Courants
1. Ports déjà utilisés: changer le port de publication Docker.
2. Permissions storage: vérifier l'UID/GID et droits du volume.
3. Clés invalides: s'assurer d'un hex 64 caractères.
## Logs détaillés
- Exécuter avec `RUST_LOG=info`.
## Healthchecks
- Ajouter une route `/health` (évolution possible) ou ping sur `/retrieve` avec clé connue.

13
docs/developpement.md Normal file
View File

@ -0,0 +1,13 @@
# Développement
## Structure du Projet
- `src/lib.rs`: service métier
- `src/main.rs`: serveur HTTP Tide
- `tests/`: scénarios d'intégration
## Ajout d'un Nouveau Service
- Créer une abstraction dédiée dans `src/lib.rs` ou module séparé.
- Câbler dans `main.rs` via `tide::with_state` si nécessaire.
## Modification de la Configuration
- Mettre à jour `docs/configuration.md` et secrets CI/CD.

View File

@ -0,0 +1,5 @@
# Guides Principaux
- Concepts de base: clés hex 64, valeurs hex, TTL en secondes.
- API: `/store` (POST), `/retrieve/:key` (GET).
- Persistance: système de fichiers, sous-dossiers par préfixe de clé.

View File

@ -0,0 +1,6 @@
# Guides Techniques
- `StorageService`: abstraction des opérations de stockage.
- TTL: sérialisé dans `*.meta` (UNIX timestamp secondes).
- Nettoyage: parcours des dossiers, suppression données expirées.
- Journalisation: sorties standard, intégration possible avec superviseur.

5
docs/guides_test.md Normal file
View File

@ -0,0 +1,5 @@
# Guides de Test
- Tests unitaires recommandés sur `StorageService` via répertoires temporaires.
- Tests d'intégration HTTP optionnels via client HTTP.
- Stratégies: cas TTL min/max, clés invalides, conflits de clé.

9
docs/performance.md Normal file
View File

@ -0,0 +1,9 @@
# Performance
## Ressources Recommandées
- Disque rapide si grand volume d'écritures.
- Mémoire suffisante pour buffers I/O.
## Optimisations
- Paramétrer la taille des blocs et la stratégie de fsync selon contraintes.
- Éviter les collisions de clés, supervision du cleanup TTL.

21
docs/release_guide.md Normal file
View File

@ -0,0 +1,21 @@
# Guide de Release
## Préparation
- Sassurer que la suite `cargo test` est verte.
- Mettre à jour `CHANGELOG.md` avec la version cible.
## Tagging
- Créer un tag annoté: `git tag -a vX.Y.Z -m 'vX.Y.Z'`
- Pousser le tag: `git push origin vX.Y.Z`
## CI
- Build binaire (workflow release) déclenché sur tag `v*.*.*`.
- Build/push image Docker via workflow `docker.yml` (variables `DOCKER_*` requises).
## Assets
- Binaires disponibles via artefacts CI.
- Images Docker taggées `latest` (et potentiellement `vX.Y.Z` selon configuration).
## Post-release
- Mettre à jour la documentation si nécessaire.
- Ouvrir une issue pour les améliorations/retours.

10
docs/reseau_de_relais.md Normal file
View File

@ -0,0 +1,10 @@
# Réseau de Relais
## Architecture Mesh
- À définir selon déploiement.
## Ajout de Nœuds Externes
- Procédure à documenter si nécessaire.
## Configuration Externe
- Ports, sécurité, endpoints à exposer.

9
docs/tests_monitoring.md Normal file
View File

@ -0,0 +1,9 @@
# Tests et Monitoring
## Tests
- Unitaires et intégration via `cargo test`.
## Monitoring
- Healthcheck HTTP: endpoint `/health` retourne `{ "message": "ok" }` et code 200.
- Exposer métriques avec un reverse proxy/sidecar si nécessaire.
- Configurer l'orchestrateur pour vérifier périodiquement `/health`.

263
src/lib.rs Normal file
View File

@ -0,0 +1,263 @@
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::{log, Request, Response, 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)
}
#[derive(Deserialize, Debug)]
pub struct StoreRequest {
pub key: String,
pub value: String,
pub ttl: Option<u64>,
}
#[derive(Serialize)]
pub struct ApiResponse { pub message: String }
#[derive(Serialize)]
pub struct RetrieveResponse { pub key: String, pub value: String }
pub async fn handle_health(_req: Request<StorageService>) -> tide::Result<Response> {
Ok(Response::builder(StatusCode::Ok)
.body(serde_json::to_value(&ApiResponse { message: "ok".into() })?)
.build())
}
pub async fn handle_store(mut req: Request<StorageService>, no_ttl_permanent: bool) -> tide::Result<Response> {
// Extract key from URL parameter
let key: String = req.param("key")?.to_string();
// Validate key format
if key.len() != 64 || !key.chars().all(|c| c.is_ascii_hexdigit()) {
return Ok(Response::builder(StatusCode::BadRequest)
.body("Invalid key: must be a 32 bytes hex string.".to_string())
.build());
}
// Get TTL from query parameter (optional)
let ttl: Option<u64> = req.url().query_pairs()
.find(|(key, _)| key == "ttl")
.and_then(|(_, value)| value.parse().ok());
log::info!("ttl: {:?}", ttl);
let live_for: Option<Duration> = if let Some(ttl) = ttl {
if ttl < 60 {
return Ok(Response::builder(StatusCode::BadRequest)
.body(format!("Invalid ttl: must be at least {} seconds.", 60))
.build());
} else if ttl > 31_536_000 {
return Ok(Response::builder(StatusCode::BadRequest)
.body(format!("Invalid ttl: must be at most {} seconds.", 31_536_000))
.build());
}
Some(Duration::from_secs(ttl))
} else if no_ttl_permanent {
None
} else {
Some(Duration::from_secs(86_400))
};
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,
};
// Read binary data directly from request body
let value_bytes = match req.body_bytes().await {
Ok(bytes) => bytes,
Err(e) => {
return Ok(Response::builder(StatusCode::BadRequest)
.body(format!("Failed to read request body: {}", e))
.build());
}
};
log::info!("received {} bytes", value_bytes.len());
let svc = req.state();
match svc.store_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(),
})?)
.build()),
Err(e) => Ok(Response::builder(e.status())
.body(serde_json::to_value(&ApiResponse {
message: e.to_string(),
})?)
.build()),
}
}
pub 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()) {
return Ok(Response::builder(StatusCode::BadRequest)
.body("Invalid key: must be a 32 bytes hex string.".to_string())
.build());
}
let svc = req.state();
match svc.retrieve_data(&key).await {
Ok(value) => {
Ok(Response::builder(StatusCode::Ok)
.header("Content-Type", "application/octet-stream")
.body(value)
.build())
}
Err(e) => Ok(Response::builder(StatusCode::NotFound).body(e).build()),
}
}
pub fn create_app(no_ttl_permanent: bool, storage_dir: impl Into<String>) -> tide::Server<StorageService> {
let svc = StorageService::new(storage_dir);
let mut app = tide::with_state(svc);
app.at("/health").get(handle_health);
app.at("/store/:key").post(move |req| handle_store(req, no_ttl_permanent));
app.at("/retrieve/:key").get(handle_retrieve);
app
}

View File

@ -1,305 +1,44 @@
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::env;
use async_std::io::WriteExt;
use async_std::path::Path;
use async_std::stream::StreamExt;
use async_std::task;
use tide::{Request, Response, StatusCode};
use async_std::fs::create_dir_all;
use sdk_storage::{StorageService, create_app};
use tide::log;
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
const PORT: u16 = 8080;
const DEFAULT_TTL: u64 = 86400;
/// 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)
}
#[derive(Deserialize)]
struct StoreRequest {
key: String,
value: String,
ttl: Option<u64>,
}
#[derive(Serialize)]
struct ApiResponse {
message: String,
}
#[derive(Serialize)]
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> {
// Parse the JSON body
let data: StoreRequest = match req.body_json().await {
Ok(data) => data,
Err(e) => {
return Ok(Response::builder(StatusCode::BadRequest)
.body(format!("Invalid request: {}", e))
.build());
}
};
// Validate the key
if data.key.len() != 64 || !data.key.chars().all(|c| c.is_ascii_hexdigit()) {
return Ok(Response::builder(StatusCode::BadRequest)
.body("Invalid key: must be a 32 bytes hex string.".to_string())
.build());
}
// Validate the ttl
let live_for: Option<Duration> = if let Some(ttl) = data.ttl {
if ttl < MIN_TTL {
return Ok(Response::builder(StatusCode::BadRequest)
.body(format!(
"Invalid ttl: must be at least {} seconds.",
MIN_TTL
))
.build());
} else if ttl > MAX_TTL {
return Ok(Response::builder(StatusCode::BadRequest)
.body(format!("Invalid ttl: must be at most {} seconds.", MAX_TTL))
.build());
}
Some(Duration::from_secs(ttl))
} else if no_ttl_permanent {
// When no_ttl_permanent is true, requests without TTL are permanent
None
} else {
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
};
// Decode the value from Base64
let value_bytes = match hex::decode(&data.value) {
Ok(value) => value,
Err(e) => {
return Ok(Response::builder(StatusCode::BadRequest)
.body(format!("Invalid request: {}", e))
.build());
}
};
// Store the data
match 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(),
})?)
.build()),
Err(e) => Ok(Response::builder(e.status())
.body(serde_json::to_value(&ApiResponse {
message: e.to_string(),
})?)
.build()),
}
}
async fn handle_retrieve(req: Request<()>) -> tide::Result<Response> {
let key: String = req.param("key")?.to_string();
if key.len() != 64 || !key.chars().all(|c| c.is_ascii_hexdigit()) {
return Ok(Response::builder(StatusCode::BadRequest)
.body("Invalid key: must be a 32 bytes hex string.".to_string())
.build());
}
match retrieve_data(&key).await {
Ok(value) => {
let encoded_value = hex::encode(value);
Ok(Response::builder(StatusCode::Ok)
.body(serde_json::to_value(&RetrieveResponse {
key,
value: encoded_value,
})?)
.build())
}
Err(e) => Ok(Response::builder(StatusCode::NotFound).body(e).build()),
}
}
/// 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<()> {
// Initialize logging
env_logger::init();
log::info!("Starting server");
// Parse command line arguments
let args: Vec<String> = 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();
app.at("/store").post(move |req| handle_store(req, no_ttl_permanent));
app.at("/retrieve/:key").get(handle_retrieve);
// 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 = create_app(no_ttl_permanent, STORAGE_DIR);
app.listen(format!("0.0.0.0:{}", PORT)).await?;
println!("Server running at http://0.0.0.0:{}", PORT);

135
tests/http_api.rs Normal file
View File

@ -0,0 +1,135 @@
use sdk_storage::{StorageService, unix_to_system_time, create_app};
use tempfile::TempDir;
use surf::Client;
#[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);
}
#[async_std::test]
async fn conflict_on_duplicate_key() {
let td = TempDir::new().unwrap();
let dir_path = td.path().to_string_lossy().to_string();
let svc = StorageService::new(dir_path);
let key = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let value = b"data";
svc.store_data(key, value, None).await.unwrap();
let err = svc.store_data(key, value, None).await.err().expect("should error");
assert_eq!(err.status(), tide::StatusCode::Conflict);
}
#[async_std::test]
async fn cleanup_removes_expired() {
let td = TempDir::new().unwrap();
let dir_path = td.path().to_string_lossy().to_string();
let svc = StorageService::new(dir_path.clone());
let key = "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";
let value = b"x";
// expiration très proche: maintenant - 1s
let past = sdk_storage::system_time_to_unix(std::time::SystemTime::now()).saturating_sub(1);
let expires = Some(unix_to_system_time(past));
svc.store_data(key, value, expires).await.unwrap();
// cleanup one-shot
svc.cleanup_expired_files_once().await.unwrap();
let res = svc.retrieve_data(key).await;
assert!(res.is_err(), "expired key should be removed");
}
#[async_std::test]
async fn http_store_success_and_conflicts_and_invalids() {
// app with permanent=false so default TTL applies when missing
let td = TempDir::new().unwrap();
let storage = td.path().to_string_lossy().to_string();
let mut app = create_app(false, storage);
let listener = async_std::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
async_std::task::spawn(async move { app.listen(listener).await.unwrap() });
let client = Client::new();
let base = format!("http://{}", addr);
// success
let key = "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd";
let body = serde_json::json!({"key": key, "value": "01aa", "ttl": 120});
let res = client.post(format!("{}/store", base)).body_json(&body).unwrap().await.unwrap();
assert!(res.status().is_success());
// conflict
let res2 = client.post(format!("{}/store", base)).body_json(&body).unwrap().await.unwrap();
assert_eq!(res2.status(), 409);
// invalid key
let bad = serde_json::json!({"key": "xyz", "value": "01"});
let res3 = client.post(format!("{}/store", base)).body_json(&bad).unwrap().await.unwrap();
assert_eq!(res3.status(), 400);
// invalid value
let badv = serde_json::json!({"key": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", "value": "zz"});
let res4 = client.post(format!("{}/store", base)).body_json(&badv).unwrap().await.unwrap();
assert_eq!(res4.status(), 400);
}
#[async_std::test]
async fn http_retrieve_success_and_invalid_and_notfound() {
let td = TempDir::new().unwrap();
let storage = td.path().to_string_lossy().to_string();
let mut app = create_app(true, storage.clone());
let listener = async_std::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
async_std::task::spawn(async move { app.listen(listener).await.unwrap() });
let client = Client::new();
let base = format!("http://{}", addr);
// prepare stored value (permanent mode)
let svc = StorageService::new(storage);
let key = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff";
svc.store_data(key, b"hi", None).await.unwrap();
// success
let mut res = client.get(format!("{}/retrieve/{}", base, key)).await.unwrap();
assert!(res.status().is_success());
let v: serde_json::Value = res.body_json().await.unwrap();
assert_eq!(v["value"].as_str().unwrap(), hex::encode("hi"));
// invalid key
let res2 = client.get(format!("{}/retrieve/{}", base, "bad")).await.unwrap();
assert_eq!(res2.status(), 400);
// not found
let k2 = "1111111111111111111111111111111111111111111111111111111111111111";
let res3 = client.get(format!("{}/retrieve/{}", base, k2)).await.unwrap();
assert_eq!(res3.status(), 404);
}
#[async_std::test]
async fn http_health_ok() {
let td = TempDir::new().unwrap();
let storage = td.path().to_string_lossy().to_string();
let mut app = create_app(true, storage);
let listener = async_std::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
async_std::task::spawn(async move { app.listen(listener).await.unwrap() });
let client = Client::new();
let base = format!("http://{}", addr);
let mut res = client.get(format!("{}/health", base)).await.unwrap();
assert!(res.status().is_success());
let v: serde_json::Value = res.body_json().await.unwrap();
assert_eq!(v["message"].as_str().unwrap(), "ok");
}