reddb_server/storage/backend/mod.rs
1//! Storage Backend Abstraction
2//!
3//! Enables RedDB to persist database snapshots to remote storage backends
4//! (S3, R2, DigitalOcean Spaces, GCS, Turso/libSQL, Cloudflare D1).
5//!
6//! The pattern is "snapshot transport":
7//! - On open: download from remote -> local temp file -> open as normal
8//! - On flush: save to local file -> upload to remote
9//!
10//! # Example
11//! ```ignore
12//! use reddb::storage::backend::{S3Backend, S3Config};
13//!
14//! let backend = S3Backend::new(S3Config {
15//! endpoint: "https://s3.amazonaws.com".into(),
16//! bucket: "my-reddb-backups".into(),
17//! key_prefix: "databases/".into(),
18//! access_key: "AKIA...".into(),
19//! secret_key: "...".into(),
20//! region: "us-east-1".into(),
21//! });
22//!
23//! let options = RedDBOptions::persistent("./local-cache")
24//! .with_remote_backend(std::sync::Arc::new(backend), "databases/mydb.rdb");
25//! ```
26
27#[cfg(feature = "backend-d1")]
28pub mod d1;
29pub mod http;
30pub mod local;
31#[cfg(feature = "backend-s3")]
32pub mod s3;
33#[cfg(feature = "backend-turso")]
34pub mod turso;
35
36use std::fmt;
37use std::path::Path;
38
39/// Error type for backend operations.
40#[derive(Debug)]
41pub enum BackendError {
42 /// Network or I/O error during transfer.
43 Transport(String),
44 /// Authentication or authorization failure.
45 Auth(String),
46 /// The requested resource was not found.
47 NotFound(String),
48 /// A compare-and-swap / conditional write precondition failed.
49 PreconditionFailed(String),
50 /// Configuration error.
51 Config(String),
52 /// Backend-specific error.
53 Internal(String),
54}
55
56impl fmt::Display for BackendError {
57 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58 match self {
59 Self::Transport(msg) => write!(f, "backend transport error: {msg}"),
60 Self::Auth(msg) => write!(f, "backend auth error: {msg}"),
61 Self::NotFound(msg) => write!(f, "backend not found: {msg}"),
62 Self::PreconditionFailed(msg) => write!(f, "backend precondition failed: {msg}"),
63 Self::Config(msg) => write!(f, "backend config error: {msg}"),
64 Self::Internal(msg) => write!(f, "backend internal error: {msg}"),
65 }
66 }
67}
68
69impl std::error::Error for BackendError {}
70
71/// Backend-specific version token for one remote object.
72///
73/// S3-compatible backends use ETag, generic HTTP backends use ETag, and
74/// LocalBackend derives a content hash token. Callers must treat the
75/// token as opaque and feed it back through conditional operations only.
76#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct BackendObjectVersion {
78 pub token: String,
79}
80
81impl BackendObjectVersion {
82 pub fn new(token: impl Into<String>) -> Self {
83 Self {
84 token: token.into(),
85 }
86 }
87}
88
89/// Conditional upload semantics for backends that support compare-and-swap.
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub enum ConditionalPut {
92 /// Succeed only when the target object is absent.
93 IfAbsent,
94 /// Succeed only when the target object still has this version token.
95 IfVersion(BackendObjectVersion),
96}
97
98/// Conditional delete semantics for backends that support compare-and-swap.
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub enum ConditionalDelete {
101 /// Succeed only when the target object still has this version token.
102 IfVersion(BackendObjectVersion),
103}
104
105/// Trait for remote storage backends.
106///
107/// Implementations handle downloading and uploading database snapshots
108/// to/from remote storage. Operations are blocking (called during
109/// attach/reclaim lifecycle phases, not in hot query paths).
110pub trait RemoteBackend: Send + Sync {
111 /// Human-readable name of this backend (e.g., "s3", "r2", "turso", "d1").
112 fn name(&self) -> &str;
113
114 /// Download a remote object to a local file path.
115 /// Returns `Ok(true)` if downloaded, `Ok(false)` if remote object doesn't exist.
116 fn download(&self, remote_key: &str, local_path: &Path) -> Result<bool, BackendError>;
117
118 /// Upload a local file to remote storage.
119 fn upload(&self, local_path: &Path, remote_key: &str) -> Result<(), BackendError>;
120
121 /// Check if a remote object exists.
122 fn exists(&self, remote_key: &str) -> Result<bool, BackendError>;
123
124 /// Delete a remote object. Returns Ok(()) even if it didn't exist.
125 fn delete(&self, remote_key: &str) -> Result<(), BackendError>;
126
127 /// List remote objects matching a prefix.
128 fn list(&self, prefix: &str) -> Result<Vec<String>, BackendError>;
129}
130
131/// Backends that can enforce compare-and-swap atomically.
132///
133/// Where `RemoteBackend` is "snapshot transport," `AtomicRemoteBackend`
134/// is the contract callers need when they cannot tolerate lost updates
135/// (writer leases, distributed locks, ledger appenders). Implementing
136/// this trait is a *promise* the backend never silently overwrites a
137/// versioned object — preconditions translate to backend-native
138/// guarantees (S3 ETag + If-Match, FS lock + content-hash CAS, HTTP
139/// servers that honor RFC 7232 preconditions).
140///
141/// Backends that cannot meet that promise (Turso, D1, HTTP servers
142/// without ETag) deliberately do **not** implement this trait, so a
143/// caller that needs CAS will fail at compile time rather than at the
144/// first contended write.
145pub trait AtomicRemoteBackend: RemoteBackend {
146 /// Return the current opaque version token for an object.
147 /// `Ok(None)` means the object does not exist.
148 fn object_version(
149 &self,
150 remote_key: &str,
151 ) -> Result<Option<BackendObjectVersion>, BackendError>;
152
153 /// Upload a local file only if the backend-side condition still
154 /// holds. Returns the new version token on success;
155 /// `BackendError::PreconditionFailed` on contention.
156 fn upload_conditional(
157 &self,
158 local_path: &Path,
159 remote_key: &str,
160 condition: ConditionalPut,
161 ) -> Result<BackendObjectVersion, BackendError>;
162
163 /// Delete a remote object only if the backend-side condition
164 /// still holds. `BackendError::PreconditionFailed` on contention.
165 fn delete_conditional(
166 &self,
167 remote_key: &str,
168 condition: ConditionalDelete,
169 ) -> Result<(), BackendError>;
170}
171
172#[cfg(feature = "backend-d1")]
173pub use d1::{D1Backend, D1Config};
174pub use http::{AtomicHttpBackend, HttpBackend, HttpBackendConfig};
175pub use local::LocalBackend;
176#[cfg(feature = "backend-s3")]
177pub use s3::{S3Backend, S3Config};
178#[cfg(feature = "backend-turso")]
179pub use turso::{TursoBackend, TursoConfig};