Skip to main content

rws_storage/storage/
mod.rs

1//! File / object storage abstraction.
2//!
3//! `FormMultipartData::parse()` hands back raw bytes with no place to put
4//! them. This module gives handlers a single `Storage` trait so the same
5//! upload code works against local disk in development and an S3-compatible
6//! bucket (AWS S3, Cloudflare R2, MinIO), Azure Blob Storage or Google Cloud
7//! Storage in production.
8//!
9//! # Local disk (requires the `storage-local` feature)
10//!
11//! ```rust
12//! # extern crate rws_storage as rust_web_server;
13//! # #[cfg(feature = "storage-local")]
14//! # fn example() -> Result<(), rust_web_server::storage::StorageError> {
15//! use rust_web_server::storage::{LocalStorage, Storage};
16//!
17//! let dir = std::env::temp_dir().join("rws-storage-doctest");
18//! let store = LocalStorage::new(dir.to_str().unwrap()).with_base_url("/uploads");
19//!
20//! let key = store.put("avatars/42.png", b"...png bytes...", "image/png")?;
21//! assert_eq!("/uploads/avatars/42.png", store.url(&key));
22//!
23//! let bytes = store.get(&key)?;
24//! store.delete(&key)?;
25//! # std::fs::remove_dir_all(&dir).ok();
26//! # Ok(())
27//! # }
28//! ```
29//!
30//! # S3-compatible object storage (requires the `storage-s3` feature)
31//!
32//! ```rust,no_run
33//! # extern crate rws_storage as rust_web_server;
34//! # #[cfg(feature = "storage-s3")]
35//! # fn example() -> Result<(), rust_web_server::storage::StorageError> {
36//! use rust_web_server::storage::{S3Storage, Storage};
37//!
38//! // Reads RWS_S3_BUCKET, RWS_S3_REGION, RWS_S3_ACCESS_KEY, RWS_S3_SECRET_KEY,
39//! // and optionally RWS_S3_ENDPOINT (for R2 / MinIO / any S3-compatible host).
40//! let store = S3Storage::from_env()?;
41//!
42//! store.put("avatars/42.png", b"...png bytes...", "image/png")?;
43//! let bytes = store.get("avatars/42.png")?;
44//! store.delete("avatars/42.png")?;
45//! # Ok(())
46//! # }
47//! ```
48//!
49//! # Azure Blob Storage (requires the `storage-azure` feature)
50//!
51//! ```rust,no_run
52//! # extern crate rws_storage as rust_web_server;
53//! # #[cfg(feature = "storage-azure")]
54//! # fn example() -> Result<(), rust_web_server::storage::StorageError> {
55//! use rust_web_server::storage::{AzureBlobStorage, Storage};
56//!
57//! // Reads RWS_AZURE_ACCOUNT, RWS_AZURE_CONTAINER, and optionally
58//! // RWS_AZURE_ACCOUNT_KEY (falls back to Managed Identity when unset) and
59//! // RWS_AZURE_ENDPOINT (for Azurite / a private endpoint).
60//! let store = AzureBlobStorage::from_env()?;
61//!
62//! store.put("avatars/42.png", b"...png bytes...", "image/png")?;
63//! let bytes = store.get("avatars/42.png")?;
64//! store.delete("avatars/42.png")?;
65//! # Ok(())
66//! # }
67//! ```
68
69
70// `StorageError` and the AWS / Azure credential helpers live in rws-server
71// (`secrets` uses them too); the backends are here.
72pub use rws_server::storage::*;
73
74#[cfg(feature = "storage-azure")]
75mod azure_blob;
76#[cfg(feature = "storage-azure")]
77mod azure_signature;
78
79#[cfg(feature = "storage-gcs")]
80mod gcs;
81#[cfg(feature = "storage-gcs")]
82mod gcs_credentials;
83
84#[cfg(feature = "storage-local")]
85mod local;
86#[cfg(feature = "storage-s3")]
87mod s3;
88
89#[cfg(feature = "storage-local")]
90pub use local::LocalStorage;
91#[cfg(feature = "storage-s3")]
92pub use s3::{S3Config, S3Storage};
93#[cfg(feature = "storage-azure")]
94pub use azure_blob::{AzureBlobConfig, AzureBlobStorage};
95#[cfg(feature = "storage-gcs")]
96pub use gcs::{GcsConfig, GcsStorage};
97
98
99// ── Storage ──────────────────────────────────────────────────────────────────
100
101/// Backend-independent object storage.
102///
103/// Implement this trait to plug a custom backend (a database-backed store,
104/// ...) into the same handler code that already works against
105/// [`LocalStorage`], [`S3Storage`], [`AzureBlobStorage`] and `GcsStorage`.
106pub trait Storage: Send + Sync {
107    /// Store `data` under `key`, overwriting any existing object at that key.
108    /// Returns the key that was stored (backends that normalize or namespace
109    /// keys may return a different string than the one passed in).
110    fn put(&self, key: &str, data: &[u8], content_type: &str) -> Result<String, StorageError>;
111
112    /// Retrieve the bytes stored under `key`.
113    fn get(&self, key: &str) -> Result<Vec<u8>, StorageError>;
114
115    /// Delete the object stored under `key`. Backends generally treat
116    /// deleting a missing key as a no-op success, matching S3's semantics.
117    fn delete(&self, key: &str) -> Result<(), StorageError>;
118
119    /// A URL for retrieving `key`. For [`LocalStorage`] this is only
120    /// meaningful if a base URL was configured (e.g. because the directory
121    /// is also served as static files); for [`S3Storage`] it's the object's
122    /// HTTPS URL. This performs no I/O and never fails.
123    fn url(&self, key: &str) -> String;
124    /// Store everything `reader` yields under `key` — for a body too large
125    /// to hold in memory, e.g. a streamed upload ([`Request::body`] read as a
126    /// stream, #31). [`LocalStorage`] writes as it reads and [`S3Storage`]
127    /// uploads in parts (S3 multipart upload), so memory stays at one part;
128    /// the default reads everything first and calls [`Storage::put`].
129    ///
130    /// [`Request::body`]: crate::request::Request::body
131    fn put_stream(&self, key: &str, reader: &mut dyn std::io::Read, content_type: &str) -> Result<String, StorageError> {
132        let mut data = Vec::new();
133        reader.read_to_end(&mut data).map_err(|e| StorageError::new(format!("failed to read the data for '{key}': {e}")))?;
134        self.put(key, &data, content_type)
135    }
136}