diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 65ba5f6..eada928 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -3,7 +3,7 @@
## Requirements
- [mise](https://mise.jdx.dev) — manages pnpm, rust, node (see `mise.toml`)
-- docker or [colima](https://github.com/abiosoft/colima) (for redis)
+- docker or [colima](https://github.com/abiosoft/colima) (for cache)
## Setup
@@ -18,7 +18,7 @@ pnpm install
pnpm run dev
```
-Make sure docker/colima is running. This starts redis, the rust backend, the web client, and the CLI. The app is at [localhost:3000](http://localhost:3000).
+Make sure docker/colima is running. This starts the cache (valkey/redis), the rust backend, the web client, and the CLI. The app is at [localhost:3000](http://localhost:3000).
## Tests
diff --git a/Dockerfile b/Dockerfile
index 1c5738c..ab68bbb 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,6 +25,6 @@ RUN apk add --no-cache curl libgcc
COPY --from=backend /tmp/target/release/cryptgeon .
COPY --from=client /tmp/packages/frontend/build ./frontend
ENV FRONTEND_PATH="./frontend"
-ENV REDIS="redis://redis/"
+ENV CACHE="redis://cache/"
EXPOSE 8000
ENTRYPOINT [ "/app/cryptgeon" ]
diff --git a/README.md b/README.md
index 31668f5..f3c2bc6 100644
--- a/README.md
+++ b/README.md
@@ -63,7 +63,7 @@ client side with the key and then sent to the server. data is store
never persisted to disk. the server never sees the encryption key and cannot decrypt the contents
of the notes even if it tried to.
-> View counts are guaranteed with one running instance of cryptgeon. Multiple instances connected to the same Redis instance can run into race conditions, where a note might be retrieved more than the view count allows.
+> View counts are guaranteed with one running instance of cryptgeon. Multiple instances connected to the same cache instance can run into race conditions, where a note might be retrieved more than the view count allows.
## Screenshot
@@ -73,14 +73,14 @@ of the notes even if it tried to.
| Variable | Default | Description |
| ----------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `REDIS` | `redis://redis/` | Redis URL to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
+| `CACHE` | `redis://cache/` | Cache URL (valkey or redis) to connect to. [According to format](https://docs.rs/redis/latest/redis/#connection-parameters) |
| `SIZE_LIMIT` | `1 KiB` | Max size for body. Accepted values according to [byte-unit](https://docs.rs/byte-unit/).
`512 MiB` is the maximum allowed.
The frontend will show that number including the ~35% encoding overhead. |
| `MAX_VIEWS` | `100` | Maximal number of views. |
| `MAX_EXPIRATION` | `360` | Maximal expiration in minutes. |
| `ALLOW_ADVANCED` | `true` | Allow custom configuration. If set to `false` all notes will be one view only. |
| `ALLOW_FILES` | `true` | Allow uploading files. If set to `false`, users will only be allowed to create text notes. |
| `ID_LENGTH` | `32` | Set the size of the note `id` in bytes. By default this is `32` bytes. This is useful for reducing link size. _This setting does not affect encryption strength_. |
-| `REDIS_PREFIX` | `""` | Optional prefix for all Redis keys. Useful when sharing a Redis instance with other apps via ACL namespaces. |
+| `CACHE_PREFIX` | `""` | Optional prefix for all cache keys. Useful when sharing a cache instance with other apps via ACL namespaces. |
| `VERBOSITY` | `warn` | Verbosity level for the backend. [Possible values](https://docs.rs/env_logger/latest/env_logger/#enabling-logging) are: `error`, `warn`, `info`, `debug`, `trace` |
| `THEME_IMAGE` | `""` | Custom image for replacing the logo. Must be publicly reachable |
| `THEME_TEXT` | `""` | Custom text for replacing the description below the logo |
@@ -107,12 +107,12 @@ Docker is the easiest way. There is the [official image here](https://hub.docker
version: "3.8"
services:
- redis:
- image: redis:7-alpine
+ cache:
+ image: valkey/valkey:7-alpine
# This is required to stay in RAM only.
- command: redis-server --save "" --appendonly no
+ command: valkey-server --save "" --appendonly no
# Set a size limit. See link below on how to customise.
- # https://redis.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
+ # https://valkey.io/docs/latest/operate/rs/databases/memory-performance/eviction-policy/
# --maxmemory 1gb --maxmemory-policy allkeys-lrulpine
# This prevents the creation of an anonymous volume.
tmpfs:
@@ -121,7 +121,7 @@ services:
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- - redis
+ - cache
environment:
# Size limit for a single note.
SIZE_LIMIT: 4 MiB
@@ -130,7 +130,7 @@ services:
# Optional health checks
# healthcheck:
- # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
+ # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"]
# interval: 1m
# timeout: 3s
# retries: 2
diff --git a/docker-compose.dev.yaml b/docker-compose.dev.yaml
index c651a8e..4f21c0d 100644
--- a/docker-compose.dev.yaml
+++ b/docker-compose.dev.yaml
@@ -2,7 +2,7 @@
# For a production file see: README.md
services:
- redis:
+ cache:
image: valkey/valkey:7-alpine
# This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
@@ -19,14 +19,14 @@ services:
build: .
env_file: .env.dev
depends_on:
- - redis
+ - cache
restart: unless-stopped
ports:
- 3000:8000
healthcheck:
- test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/api/live/']
+ test: ['CMD', 'curl', '--fail', 'http://127.0.0.1:8000/healthz']
interval: 1m
timeout: 3s
retries: 2
- start_period: 5s
+ start_period: 5s
\ No newline at end of file
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 3a0b25f..73ef450 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -1,5 +1,5 @@
services:
- redis:
+ cache:
image: valkey/valkey:7-alpine
# This is required to stay in RAM only.
command: valkey-server --save "" --appendonly no
@@ -13,7 +13,7 @@ services:
app:
image: cupcakearmy/cryptgeon:latest
depends_on:
- - redis
+ - cache
environment:
# Size limit for a single note.
SIZE_LIMIT: 4 MiB
@@ -22,8 +22,8 @@ services:
# Optional health checks
# healthcheck:
- # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/api/live/"]
+ # test: ["CMD", "curl", "--fail", "http://127.0.0.1:8000/healthz"]
# interval: 1m
# timeout: 3s
# retries: 2
- # start_period: 5s
+ # start_period: 5s
\ No newline at end of file
diff --git a/package.json b/package.json
index 115e11a..a33f46e 100644
--- a/package.json
+++ b/package.json
@@ -1,22 +1,23 @@
{
"scripts": {
- "dev:docker": "docker compose -f docker-compose.dev.yaml up redis",
+ "dev:docker": "docker compose -f docker-compose.dev.yaml up cache",
"dev:packages": "pnpm --parallel run dev",
- "dev": "run-p dev:*",
+ "dev": "pnpm --parallel run dev:*",
"docker:up": "docker compose -f docker-compose.dev.yaml up",
"docker:build": "docker compose -f docker-compose.dev.yaml build",
"test": "playwright test --project=chrome --project=firefox --project=safari",
"test:local": "playwright test --project=chrome",
- "test:server": "run-s docker:up",
+ "test:server": "docker compose -f docker-compose.dev.yaml up",
"test:dl-browsers": "playwright install",
- "test:prepare": "run-p test:dl-browsers build docker:build",
+ "test:prepare": "pnpm --parallel run test:dl-browsers build docker:build",
"build": "pnpm run --recursive --filter=!@cryptgeon/backend build"
},
"devDependencies": {
"@playwright/test": "^1.60.0",
- "@types/node": "^24.12.4",
- "npm-run-all": "^4.1.5",
- "shelljs": "^0.8.5"
+ "@types/node": "^24.12.4"
},
- "packageManager": "pnpm@11.5.0"
+ "packageManager": "pnpm@11.5.0",
+ "engines": {
+ "node": ">=22"
+ }
}
diff --git a/packages/backend/Cargo.lock b/packages/backend/Cargo.lock
index 7f5c0bd..dba5e3c 100644
--- a/packages/backend/Cargo.lock
+++ b/packages/backend/Cargo.lock
@@ -252,7 +252,7 @@ dependencies = [
[[package]]
name = "cryptgeon"
-version = "2.9.3"
+version = "3.0.0"
dependencies = [
"axum",
"bs62",
@@ -261,6 +261,7 @@ dependencies = [
"lazy_static",
"redis",
"ring",
+ "rmp-serde",
"serde",
"serde_json",
"tokio",
@@ -1004,6 +1005,25 @@ dependencies = [
"windows-sys 0.52.0",
]
+[[package]]
+name = "rmp"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "rmp-serde"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
+dependencies = [
+ "rmp",
+ "serde",
+]
+
[[package]]
name = "rustix"
version = "1.1.4"
diff --git a/packages/backend/Cargo.toml b/packages/backend/Cargo.toml
index 9cb08db..2646a8f 100644
--- a/packages/backend/Cargo.toml
+++ b/packages/backend/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "cryptgeon"
-version = "2.9.3"
+version = "3.0.0"
authors = ["cupcakearmy "]
edition = "2024"
rust-version = "1.95"
@@ -20,8 +20,9 @@ redis = { version = "1", features = ["tls-native-tls"] }
# Utility
serde_json = "1"
+rmp-serde = "1"
lazy_static = "1"
ring = "0.17"
bs62 = "0.1"
byte-unit = "4"
-dotenv = "0.15"
+dotenv = "0.15"
\ No newline at end of file
diff --git a/packages/backend/src/config.rs b/packages/backend/src/config.rs
index 9b1d1ec..b9b4468 100644
--- a/packages/backend/src/config.rs
+++ b/packages/backend/src/config.rs
@@ -34,7 +34,7 @@ pub static ref ID_LENGTH: u32 = std::env::var("ID_LENGTH")
.unwrap_or("32".to_string())
.parse()
.unwrap();
-pub static ref REDIS_PREFIX: String = std::env::var("REDIS_PREFIX")
+pub static ref CACHE_PREFIX: String = std::env::var("CACHE_PREFIX")
.unwrap_or("".to_string())
.parse()
.unwrap();
@@ -50,6 +50,10 @@ pub static ref IMPRINT_HTML: String = std::env::var("IMPRINT_HTML")
.unwrap_or("".to_string())
.parse()
.unwrap();
+pub static ref EXTRA_SIZE_LIMIT: usize = std::env::var("EXTRA_SIZE_LIMIT")
+ .unwrap_or("512".to_string())
+ .parse()
+ .unwrap();
}
// THEME
@@ -78,4 +82,4 @@ lazy_static! {
.unwrap_or("true".to_string())
.parse()
.unwrap();
-}
+}
\ No newline at end of file
diff --git a/packages/backend/src/health/mod.rs b/packages/backend/src/health/mod.rs
index e54f8ac..cec0760 100644
--- a/packages/backend/src/health/mod.rs
+++ b/packages/backend/src/health/mod.rs
@@ -2,9 +2,9 @@ use crate::store;
use axum::http::StatusCode;
pub async fn report_health() -> (StatusCode,) {
- if store::can_reach_redis() {
+ if store::can_reach_cache() {
return (StatusCode::OK,);
} else {
return (StatusCode::SERVICE_UNAVAILABLE,);
}
-}
+}
\ No newline at end of file
diff --git a/packages/backend/src/lock.rs b/packages/backend/src/lock.rs
deleted file mode 100644
index 42deccf..0000000
--- a/packages/backend/src/lock.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-use std::collections::HashMap;
-use std::sync::Arc;
-use tokio::sync::Mutex;
-
-#[derive(Clone)]
-pub struct SharedState {
- pub locks: LockMap,
-}
-
-pub type LockMap = Arc>>>>;
diff --git a/packages/backend/src/main.rs b/packages/backend/src/main.rs
index e738eea..f51a59f 100644
--- a/packages/backend/src/main.rs
+++ b/packages/backend/src/main.rs
@@ -1,13 +1,9 @@
-use std::{collections::HashMap, sync::Arc};
-
use axum::{
Router, ServiceExt,
extract::{DefaultBodyLimit, Request},
routing::{delete, get, post},
};
use dotenv::dotenv;
-use lock::SharedState;
-use tokio::sync::Mutex;
use tower::Layer;
use tower_http::{
compression::CompressionLayer,
@@ -21,7 +17,6 @@ extern crate lazy_static;
mod config;
mod csp;
mod health;
-mod lock;
mod note;
mod status;
mod store;
@@ -30,34 +25,30 @@ mod store;
async fn main() {
dotenv().ok();
- let shared_state = SharedState {
- locks: Arc::new(Mutex::new(HashMap::new())),
- };
-
- if !store::can_reach_redis() {
- println!("cannot reach redis");
- panic!("cannot reach redis");
+ if !store::can_reach_cache() {
+ println!("cannot reach cache");
+ panic!("cannot reach cache");
}
let notes_routes = Router::new()
.route("/", post(note::create))
- .route("/{id}", delete(note::delete))
+ .route("/{id}", delete(note::view))
.route("/{id}", get(note::preview));
- let health_routes = Router::new().route("/live", get(health::report_health));
+ let health_routes = Router::new().route("/healthz", get(health::report_health));
let status_routes = Router::new().route("/status", get(status::get_status));
- let api_routes = Router::new()
+ let v3_routes = Router::new()
.nest("/notes", notes_routes)
- .merge(health_routes)
.merge(status_routes);
+ let api_routes = Router::new().nest("/v3", v3_routes);
+
let index = format!("{}{}", config::FRONTEND_PATH.to_string(), "/index.html");
let serve_dir =
ServeDir::new(config::FRONTEND_PATH.to_string()).not_found_service(ServeFile::new(index));
let app = Router::new()
.nest("/api", api_routes)
+ .merge(health_routes)
.fallback_service(serve_dir)
- // Disabled for now, as svelte inlines scripts
- // .layer(middleware::from_fn(csp::add_csp_header))
.layer(DefaultBodyLimit::max(*config::LIMIT))
.layer(
CompressionLayer::new()
@@ -65,8 +56,7 @@ async fn main() {
.deflate(true)
.gzip(true)
.zstd(true),
- )
- .with_state(shared_state);
+ );
let app = NormalizePathLayer::trim_trailing_slash().layer(app);
@@ -77,4 +67,4 @@ async fn main() {
axum::serve(listener, ServiceExt::::into_make_service(app))
.await
.unwrap();
-}
+}
\ No newline at end of file
diff --git a/packages/backend/src/note/mod.rs b/packages/backend/src/note/mod.rs
index 13bc006..9efd49d 100644
--- a/packages/backend/src/note/mod.rs
+++ b/packages/backend/src/note/mod.rs
@@ -2,4 +2,4 @@ mod model;
mod routes;
pub use model::*;
-pub use routes::*;
+pub use routes::*;
\ No newline at end of file
diff --git a/packages/backend/src/note/model.rs b/packages/backend/src/note/model.rs
index 8200b19..e522f98 100644
--- a/packages/backend/src/note/model.rs
+++ b/packages/backend/src/note/model.rs
@@ -5,22 +5,35 @@ use serde::{Deserialize, Serialize};
use crate::config;
#[derive(Serialize, Deserialize, Clone)]
-pub struct Note {
- pub meta: String,
- pub contents: String,
+pub struct NoteMeta {
+ #[serde(skip_serializing_if = "Option::is_none")]
pub views: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
pub expiration: Option,
+ #[serde(default)]
+ pub extra: Vec,
}
-#[derive(Serialize)]
-pub struct NoteInfo {
- pub meta: String,
+#[derive(Serialize, Deserialize, Clone)]
+pub struct CreateRequest {
+ pub meta: NoteMeta,
+ pub data: Vec,
}
-#[derive(Serialize)]
-pub struct NotePublic {
- pub meta: String,
- pub contents: String,
+#[derive(Serialize, Deserialize)]
+pub struct CreateResponse {
+ pub id: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct MetaResponse {
+ pub meta: NoteMeta,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct NoteResponse {
+ pub meta: NoteMeta,
+ pub data: Vec,
}
pub fn generate_id() -> String {
@@ -32,5 +45,5 @@ pub fn generate_id() -> String {
let _ = sr.fill(&mut id);
result.push_str(&bs62::encode_data(&id));
}
- return result;
-}
+ result
+}
\ No newline at end of file
diff --git a/packages/backend/src/note/routes.rs b/packages/backend/src/note/routes.rs
index bff47ef..de6ce11 100644
--- a/packages/backend/src/note/routes.rs
+++ b/packages/backend/src/note/routes.rs
@@ -2,155 +2,143 @@ use axum::{
extract::Path,
http::StatusCode,
response::{IntoResponse, Response},
- Json,
+ body::Bytes,
};
-use serde::{Deserialize, Serialize};
-use std::{sync::Arc, time::SystemTime};
-use tokio::sync::Mutex;
+use serde::Deserialize;
+use std::time::SystemTime;
-use crate::note::{generate_id, Note, NoteInfo};
+use crate::note::{CreateRequest, generate_id};
use crate::store;
-use crate::{config, lock::SharedState};
+use crate::config;
-use super::NotePublic;
+use super::{CreateResponse, MetaResponse, NoteResponse, NoteMeta};
-pub fn now() -> u32 {
+pub fn now() -> u64 {
SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
- .as_secs() as u32
+ .as_secs()
}
#[derive(Deserialize)]
-pub struct OneNoteParams {
+pub struct NoteParams {
id: String,
}
-pub async fn preview(Path(OneNoteParams { id }): Path) -> Response {
- let note = store::get(&id);
+pub async fn create(body: Bytes) -> Response {
+ let req: CreateRequest = match rmp_serde::from_slice(&body) {
+ Ok(r) => r,
+ Err(_) => return (StatusCode::BAD_REQUEST, "Invalid msgpack").into_response(),
+ };
- match note {
- Ok(Some(n)) => (StatusCode::OK, Json(NoteInfo { meta: n.meta })).into_response(),
- Ok(None) => (StatusCode::NOT_FOUND).into_response(),
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
+ if req.meta.views.is_none() && req.meta.expiration.is_none() {
+ return (StatusCode::BAD_REQUEST, "At least views or expiration must be set").into_response();
}
-}
-#[derive(Serialize, Deserialize)]
-struct CreateResponse {
- id: String,
-}
-
-pub async fn create(Json(mut n): Json) -> Response {
- // let mut n = note.into_inner();
- let id = generate_id();
- // let bad_req = HttpResponse::BadRequest().finish();
- if n.views == None && n.expiration == None {
- return (
- StatusCode::BAD_REQUEST,
- "At least views or expiration must be set",
- )
- .into_response();
+ if req.meta.extra.len() > *config::EXTRA_SIZE_LIMIT {
+ return (StatusCode::BAD_REQUEST, "Extra data too large").into_response();
}
+
+ let mut meta = req.meta;
+
if !*config::ALLOW_ADVANCED {
- n.views = Some(1);
- n.expiration = None;
+ meta.views = Some(1);
+ meta.expiration = None;
}
- match n.views {
+
+ match meta.views {
Some(v) => {
if v > *config::MAX_VIEWS || v < 1 {
return (StatusCode::BAD_REQUEST, "Invalid views").into_response();
}
- n.expiration = None; // views overrides expiration
}
- _ => {}
+ None => {}
}
- match n.expiration {
+
+ let expiration_ts = match meta.expiration {
Some(e) => {
if e > *config::MAX_EXPIRATION || e < 1 {
return (StatusCode::BAD_REQUEST, "Invalid expiration").into_response();
}
- let expiration = now() + (e * 60);
- n.expiration = Some(expiration);
+ Some(now() + (e as u64 * 60))
+ }
+ None => None,
+ };
+
+ let id = generate_id();
+ let views = meta.views.map(|v| v as i64);
+
+ match store::set(&id, &req.data, views, expiration_ts, &meta.extra) {
+ Ok(_) => {
+ let resp = CreateResponse { id };
+ let bytes = rmp_serde::to_vec_named(&resp).unwrap();
+ (StatusCode::OK, Bytes::from(bytes)).into_response()
}
- _ => {}
- }
- match store::set(&id.clone(), &n.clone()) {
- Ok(_) => (StatusCode::OK, Json(CreateResponse { id })).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
-pub async fn delete(
- Path(OneNoteParams { id }): Path,
- state: axum::extract::State,
-) -> Response {
- let mut locks_map = state.locks.lock().await;
- let lock = locks_map
- .entry(id.clone())
- .or_insert_with(|| Arc::new(Mutex::new(())))
- .clone();
- drop(locks_map);
- let _guard = lock.lock().await;
-
- let note = store::get(&id);
- match note {
- Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
+pub async fn preview(Path(NoteParams { id }): Path) -> Response {
+ match store::get_meta(&id) {
+ Ok(Some((views, expiration, extra))) => {
+ let meta = NoteMeta {
+ views: views.map(|v| v as u32),
+ expiration: expiration.map(|e| e as u32),
+ extra,
+ };
+ let resp = MetaResponse { meta };
+ let bytes = rmp_serde::to_vec_named(&resp).unwrap();
+ (StatusCode::OK, Bytes::from(bytes)).into_response()
+ }
Ok(None) => (StatusCode::NOT_FOUND).into_response(),
- Ok(Some(note)) => {
- let mut changed = note.clone();
- if changed.views == None && changed.expiration == None {
- return (StatusCode::BAD_REQUEST).into_response();
- }
- match changed.views {
- Some(v) => {
- changed.views = Some(v - 1);
- let id = id.clone();
- if v <= 1 {
- match store::del(&id) {
- Err(e) => {
- return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
- .into_response();
- }
- _ => {}
- }
- } else {
- match store::set(&id, &changed.clone()) {
- Err(e) => {
- return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
- .into_response();
- }
- _ => {}
- }
- }
- }
- _ => {}
- }
-
- let n = now();
- match changed.expiration {
- Some(e) => {
- if e < n {
- match store::del(&id.clone()) {
- Ok(_) => return (StatusCode::BAD_REQUEST).into_response(),
- Err(e) => {
- return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
- .into_response()
- }
- }
- }
- }
- _ => {}
- }
-
- return (
- StatusCode::OK,
- Json(NotePublic {
- contents: changed.contents,
- meta: changed.meta,
- }),
- )
- .into_response();
- }
+ Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
+
+pub async fn view(Path(NoteParams { id }): Path) -> Response {
+ let (views, expiration, extra) = match store::get_meta(&id) {
+ Ok(Some(v)) => v,
+ _ => return (StatusCode::NOT_FOUND).into_response(),
+ };
+
+ let has_views = views.is_some();
+
+ if has_views {
+ let remaining = match store::decrement_views(&id) {
+ Ok(r) => r,
+ Err(_) => return (StatusCode::NOT_FOUND).into_response(),
+ };
+
+ let data = match store::get_data(&id) {
+ Ok(Some(d)) => d,
+ _ => return (StatusCode::NOT_FOUND).into_response(),
+ };
+
+ if remaining <= 0 {
+ let _ = store::del(&id);
+ }
+
+ let meta = NoteMeta {
+ views: Some(if remaining > 0 { remaining as u32 } else { 0 }),
+ expiration: expiration.map(|e| e as u32),
+ extra,
+ };
+ let resp = NoteResponse { meta, data };
+ let bytes = rmp_serde::to_vec_named(&resp).unwrap();
+ (StatusCode::OK, Bytes::from(bytes)).into_response()
+ } else {
+ let data = match store::get_data(&id) {
+ Ok(Some(d)) => d,
+ _ => return (StatusCode::NOT_FOUND).into_response(),
+ };
+
+ let meta = NoteMeta {
+ views: None,
+ expiration: expiration.map(|e| e as u32),
+ extra,
+ };
+ let resp = NoteResponse { meta, data };
+ let bytes = rmp_serde::to_vec_named(&resp).unwrap();
+ (StatusCode::OK, Bytes::from(bytes)).into_response()
+ }
+}
\ No newline at end of file
diff --git a/packages/backend/src/store.rs b/packages/backend/src/store.rs
index af437eb..7dbe098 100644
--- a/packages/backend/src/store.rs
+++ b/packages/backend/src/store.rs
@@ -1,71 +1,90 @@
-use redis;
use redis::Commands;
use crate::config;
-use crate::note::now;
-use crate::note::Note;
lazy_static! {
- static ref REDIS_CLIENT: String = std::env::var("REDIS")
+ static ref CACHE_URL: String = std::env::var("CACHE")
.unwrap_or("redis://127.0.0.1/".to_string())
.parse()
.unwrap();
}
-fn prefixed(id: &String) -> String {
- format!("{}{}", config::REDIS_PREFIX.as_str(), id)
+fn prefixed(id: &str) -> String {
+ format!("{}{}", config::CACHE_PREFIX.as_str(), id)
}
-fn get_connection() -> Result {
+fn conn() -> Result {
let client =
- redis::Client::open(REDIS_CLIENT.to_string()).map_err(|_| "Unable to connect to redis")?;
- client
- .get_connection()
- .map_err(|_| "Unable to connect to redis")
+ redis::Client::open(CACHE_URL.to_string()).map_err(|_| "Unable to connect to cache")?;
+ client.get_connection().map_err(|_| "Unable to connect to cache")
}
-pub fn can_reach_redis() -> bool {
- let conn = get_connection();
- return match conn {
- Ok(_) => true,
- Err(_) => false,
- };
+pub fn can_reach_cache() -> bool {
+ conn().is_ok()
}
-pub fn set(id: &String, note: &Note) -> Result<(), &'static str> {
+pub fn set(id: &str, data: &[u8], views: Option, expiration: Option, extra: &[u8]) -> Result<(), &'static str> {
let key = prefixed(id);
- let serialized = serde_json::to_string(¬e.clone()).unwrap();
- let mut conn = get_connection()?;
+ let mut c = conn()?;
- conn.set::<_, _, ()>(key.as_str(), serialized)
- .map_err(|_| "Unable to set note in redis")?;
- match note.expiration {
- Some(e) => {
- let seconds = e - now();
- conn.expire::<_, ()>(key.as_str(), seconds as i64)
- .map_err(|_| "Unable to set expiration on note")?
- }
- None => {}
- };
- Ok(())
-}
+ c.hset::<_, _, _, ()>(&key, "data", data).map_err(|_| "Unable to set note")?;
+ c.hset::<_, _, _, ()>(&key, "extra", extra).map_err(|_| "Unable to set note")?;
-pub fn get(id: &String) -> Result