Skip to main content

haste_document_storage/
types.rs

1use std::{
2    collections::BTreeMap,
3    fmt::{self, Debug, Display},
4    pin::Pin,
5    str::FromStr,
6    task::{Context, Poll},
7};
8
9use bytes::{Bytes, BytesMut};
10use futures_core::Stream;
11use futures_util::StreamExt as _;
12use time::OffsetDateTime;
13
14use crate::error::StorageError;
15
16/// Longest key accepted. Google Cloud Storage and S3 both allow 1024 bytes.
17const MAX_KEY_BYTES: usize = 1024;
18
19/// A validated object key such as `tenant/project/Binary/123`.
20///
21/// Keys are non-empty UTF-8 of at most 1024 bytes with no control characters,
22/// no leading slash and no empty, `.` or `..` segments. Slashes mean nothing
23/// to an object store, but they keep related documents listable by prefix and
24/// keep keys safe for a filesystem-backed implementation.
25#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct DocumentKey(String);
27
28impl DocumentKey {
29    /// Validates `key` against the rules above.
30    ///
31    /// # Errors
32    ///
33    /// Returns [`StorageError::InvalidKey`] naming the broken rule.
34    pub fn new(key: impl Into<String>) -> Result<Self, StorageError> {
35        let key = key.into();
36        let problem = if key.is_empty() {
37            Some("must not be empty")
38        } else if key.len() > MAX_KEY_BYTES {
39            Some("must be at most 1024 bytes")
40        } else if key.starts_with('/') {
41            Some("must not start with a slash")
42        } else if key.chars().any(char::is_control) {
43            Some("must not contain control characters")
44        } else if key
45            .split('/')
46            .any(|segment| matches!(segment, "" | "." | ".."))
47        {
48            Some("must not contain empty, '.' or '..' segments")
49        } else {
50            None
51        };
52
53        match problem {
54            Some(problem) => Err(StorageError::InvalidKey(format!("'{key}' {problem}"))),
55            None => Ok(Self(key)),
56        }
57    }
58
59    /// Wraps a name read back from a backend without validating it. Only
60    /// backends should call this; whatever is in the store is reported as is.
61    #[must_use]
62    pub fn from_backend(name: String) -> Self {
63        Self(name)
64    }
65
66    #[must_use]
67    pub fn as_str(&self) -> &str {
68        &self.0
69    }
70}
71
72impl Display for DocumentKey {
73    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74        f.write_str(&self.0)
75    }
76}
77
78impl AsRef<str> for DocumentKey {
79    fn as_ref(&self) -> &str {
80        &self.0
81    }
82}
83
84impl FromStr for DocumentKey {
85    type Err = StorageError;
86
87    fn from_str(key: &str) -> Result<Self, Self::Err> {
88        Self::new(key)
89    }
90}
91
92impl From<DocumentKey> for String {
93    fn from(key: DocumentKey) -> Self {
94        key.0
95    }
96}
97
98/// A document to store: its bytes and what the backend keeps with them.
99#[derive(Debug, Clone, Default)]
100pub struct Document {
101    pub body: Bytes,
102    /// MIME type the backend serves the body with; defaults to
103    /// `application/octet-stream`.
104    pub content_type: Option<String>,
105    /// Free-form pairs stored with the object, for example the owning tenant
106    /// and resource.
107    pub metadata: BTreeMap<String, String>,
108}
109
110impl Document {
111    #[must_use]
112    pub fn new(body: impl Into<Bytes>) -> Self {
113        Self {
114            body: body.into(),
115            ..Self::default()
116        }
117    }
118
119    #[must_use]
120    pub fn with_content_type(mut self, content_type: impl Into<String>) -> Self {
121        self.content_type = Some(content_type.into());
122        self
123    }
124
125    #[must_use]
126    pub fn with_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
127        self.metadata.insert(key.into(), value.into());
128        self
129    }
130}
131
132/// What a backend knows about a stored document without reading its body.
133#[derive(Debug, Clone, PartialEq, Eq)]
134pub struct DocumentMetadata {
135    pub key: DocumentKey,
136    /// Body length in bytes.
137    pub size: u64,
138    pub content_type: Option<String>,
139    /// Backend version tag; changes whenever the body does.
140    pub etag: Option<String>,
141    pub last_modified: Option<OffsetDateTime>,
142    /// The pairs given as [`Document::metadata`] when the document was stored.
143    pub metadata: BTreeMap<String, String>,
144}
145
146/// A document body as a stream of chunks. Forward it chunk by chunk (for
147/// example into an HTTP response) or gather it with [`DocumentBody::collect`].
148pub struct DocumentBody(Pin<Box<dyn Stream<Item = Result<Bytes, StorageError>> + Send>>);
149
150impl DocumentBody {
151    pub fn from_stream<S>(stream: S) -> Self
152    where
153        S: Stream<Item = Result<Bytes, StorageError>> + Send + 'static,
154    {
155        Self(Box::pin(stream))
156    }
157
158    /// Reads the whole body into memory. Check [`DocumentMetadata::size`]
159    /// first when the document may be large.
160    ///
161    /// # Errors
162    ///
163    /// Returns the first error the stream yields.
164    pub async fn collect(mut self) -> Result<Bytes, StorageError> {
165        let mut buffer = BytesMut::new();
166        while let Some(chunk) = self.0.next().await {
167            buffer.extend_from_slice(&chunk?);
168        }
169        Ok(buffer.freeze())
170    }
171}
172
173impl Stream for DocumentBody {
174    type Item = Result<Bytes, StorageError>;
175
176    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
177        self.0.as_mut().poll_next(cx)
178    }
179}
180
181impl Debug for DocumentBody {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        f.write_str("DocumentBody(..)")
184    }
185}
186
187/// A stored document: its metadata and body.
188#[derive(Debug)]
189pub struct StoredDocument {
190    pub metadata: DocumentMetadata,
191    pub body: DocumentBody,
192}
193
194/// Selects one page of a listing.
195#[derive(Debug, Clone, Default)]
196pub struct ListRequest {
197    /// Only keys starting with this are returned; `None` lists everything.
198    pub prefix: Option<String>,
199    /// Token from the previous [`ListPage`]; `None` starts from the beginning.
200    pub page_token: Option<String>,
201    /// Upper bound on documents per page; the backend applies its own cap.
202    pub page_size: Option<u32>,
203}
204
205/// One page of a listing, in key order.
206#[derive(Debug, Clone)]
207pub struct ListPage {
208    pub documents: Vec<DocumentMetadata>,
209    /// Present when more documents follow.
210    pub next_page_token: Option<String>,
211}