Skip to main content

core_api/
storage.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3
4pub const STORAGE_NODE_TYPE: &str = "storage";
5pub const STORAGE_PROTOCOL_VERSION: &str = "storage.v2";
6
7pub const STORAGE_DESCRIBE_ROUTE: &str = "/storage/describe";
8pub const STORAGE_CREATE_NAMESPACE_ROUTE: &str = "/storage/namespace/create";
9pub const STORAGE_NAMESPACE_STATUS_ROUTE: &str = "/storage/namespace/status";
10pub const STORAGE_UPDATE_NAMESPACE_ROUTE: &str = "/storage/namespace/update";
11pub const STORAGE_LIST_NAMESPACES_ROUTE: &str = "/storage/namespaces/list";
12pub const STORAGE_UPDATE_SETTINGS_ROUTE: &str = "/storage/settings/update";
13pub const STORAGE_ISSUE_SESSION_ROUTE: &str = "/storage/session/issue";
14pub const STORAGE_STATS_ROUTE: &str = "/storage/stats";
15pub const STORAGE_READINESS_ROUTE: &str = "/storage/readiness";
16
17#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub enum StorageOverflowStrategy {
20    #[default]
21    Reject,
22    EvictOldest,
23}
24
25impl StorageOverflowStrategy {
26    pub const fn as_str(self) -> &'static str {
27        match self {
28            Self::Reject => "reject",
29            Self::EvictOldest => "evictOldest",
30        }
31    }
32
33    pub fn parse(value: &str) -> Result<Self, UnsupportedStorageOverflowStrategy> {
34        match value {
35            "reject" => Ok(Self::Reject),
36            "evictOldest" => Ok(Self::EvictOldest),
37            _ => Err(UnsupportedStorageOverflowStrategy(value.to_string())),
38        }
39    }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub struct UnsupportedStorageOverflowStrategy(String);
44
45impl fmt::Display for UnsupportedStorageOverflowStrategy {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(
48            formatter,
49            "unsupported storage overflow strategy: {}",
50            self.0
51        )
52    }
53}
54
55impl std::error::Error for UnsupportedStorageOverflowStrategy {}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub enum StoragePermission {
60    Read,
61    Write,
62    Delete,
63    List,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
67#[serde(rename_all = "camelCase", deny_unknown_fields)]
68pub struct StorageCreateNamespaceRequest {
69    pub name: String,
70    pub max_bytes: u64,
71    #[serde(default)]
72    pub overflow_strategy: StorageOverflowStrategy,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76#[serde(rename_all = "camelCase", deny_unknown_fields)]
77pub struct StorageNamespaceUpdate {
78    pub namespace_id: String,
79    pub expected_revision: u64,
80    pub name: String,
81    pub max_bytes: u64,
82    pub overflow_strategy: StorageOverflowStrategy,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86#[serde(rename_all = "camelCase", deny_unknown_fields)]
87pub struct StorageRepositorySettingsUpdate {
88    pub expected_revision: u64,
89    pub limit_bytes: Option<u64>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
93#[serde(rename_all = "camelCase", deny_unknown_fields)]
94pub struct StorageNamespaceIdRequest {
95    pub namespace_id: String,
96}
97
98#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
99#[serde(rename_all = "camelCase", deny_unknown_fields)]
100pub struct StorageSessionRequest {
101    pub namespace_id: String,
102    #[serde(default)]
103    pub permissions: Vec<StoragePermission>,
104    #[serde(default)]
105    pub ttl_seconds: Option<u64>,
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct StorageNamespacePolicy {
111    pub max_bytes: u64,
112    pub overflow_strategy: StorageOverflowStrategy,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct StorageNamespace {
118    pub id: String,
119    pub name: String,
120    pub max_bytes: u64,
121    pub overflow_strategy: StorageOverflowStrategy,
122    pub used_bytes: u64,
123    pub reserved_bytes: u64,
124    pub object_count: u64,
125    pub evicted_object_count: u64,
126    pub evicted_bytes: u64,
127    pub rejected_write_count: u64,
128    pub revision: u64,
129    pub created_at_ms: u64,
130    pub updated_at_ms: u64,
131}
132
133impl StorageNamespace {
134    pub const fn policy(&self) -> StorageNamespacePolicy {
135        StorageNamespacePolicy {
136            max_bytes: self.max_bytes,
137            overflow_strategy: self.overflow_strategy,
138        }
139    }
140}
141
142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
143#[serde(rename_all = "camelCase")]
144pub struct StorageNamespaceList {
145    pub namespaces: Vec<StorageNamespace>,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
149#[serde(rename_all = "camelCase")]
150pub struct StorageSession {
151    pub protocol_version: String,
152    pub repository_id: String,
153    pub namespace: StorageNamespace,
154    pub endpoint: String,
155    pub access_token: String,
156    pub expires_at_ms: u64,
157    pub permissions: Vec<StoragePermission>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
161#[serde(rename_all = "camelCase")]
162pub struct StorageAvailability {
163    pub available: bool,
164    pub protocol_version: String,
165}
166
167#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct StorageObject {
170    pub key: String,
171    pub object_id: String,
172    pub size_bytes: u64,
173    pub sha256: String,
174    pub content_type: Option<String>,
175    pub committed_at_ms: u64,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct StorageObjectList {
181    pub objects: Vec<StorageObject>,
182    pub next_cursor: Option<String>,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub enum StorageWriteState {
188    Writable,
189    LowSpace,
190    BlockedByPhysicalSpace,
191    BlockedByStorageLimit,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
195#[serde(rename_all = "camelCase")]
196pub struct StorageRepositorySettings {
197    pub limit_bytes: Option<u64>,
198    pub revision: u64,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
202#[serde(rename_all = "camelCase")]
203pub struct StorageRepositoryStats {
204    pub repository_id: String,
205    pub filesystem_total_bytes: u64,
206    pub filesystem_available_bytes: u64,
207    pub storage_limit_bytes: Option<u64>,
208    pub storage_used_bytes: u64,
209    pub storage_reserved_bytes: u64,
210    pub storage_writable_bytes: u64,
211    pub warning_at_available_bytes: u64,
212    pub stop_at_available_bytes: u64,
213    pub write_state: StorageWriteState,
214    pub settings_revision: u64,
215    pub namespace_count: u64,
216    pub object_count: u64,
217    pub scope_namespace_count: u64,
218    pub scope_object_count: u64,
219    pub scope_used_bytes: u64,
220    pub scope_reserved_bytes: u64,
221    pub active_upload_count: u64,
222    pub pending_deletion_count: u64,
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use serde_json::json;
229
230    #[test]
231    fn storage_request_contracts_are_closed_and_camel_case() {
232        let create: StorageCreateNamespaceRequest = serde_json::from_value(json!({
233            "name": "Camera Recordings",
234            "maxBytes": 1024,
235            "overflowStrategy": "evictOldest"
236        }))
237        .unwrap();
238        assert_eq!(
239            create.overflow_strategy,
240            StorageOverflowStrategy::EvictOldest
241        );
242        assert!(
243            serde_json::from_value::<StorageCreateNamespaceRequest>(json!({
244                "name": "recordings",
245                "maxBytes": 1024,
246                "futureField": true
247            }))
248            .is_err()
249        );
250
251        let update: StorageNamespaceUpdate = serde_json::from_value(json!({
252            "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
253            "expectedRevision": 2,
254            "name": "Recordings",
255            "maxBytes": 2048,
256            "overflowStrategy": "reject"
257        }))
258        .unwrap();
259        assert_eq!(update.expected_revision, 2);
260
261        let settings: StorageRepositorySettingsUpdate = serde_json::from_value(json!({
262            "expectedRevision": 3,
263            "limitBytes": null
264        }))
265        .unwrap();
266        assert_eq!(settings.expected_revision, 3);
267        assert_eq!(settings.limit_bytes, None);
268
269        let session: StorageSessionRequest = serde_json::from_value(json!({
270            "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
271            "permissions": ["read", "write", "delete", "list"],
272            "ttlSeconds": 900
273        }))
274        .unwrap();
275        assert_eq!(session.permissions.len(), 4);
276    }
277
278    #[test]
279    fn storage_responses_are_additive_for_consumers() {
280        let namespace: StorageNamespace = serde_json::from_value(json!({
281            "id": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
282            "name": "recordings",
283            "maxBytes": 1024,
284            "overflowStrategy": "reject",
285            "usedBytes": 0,
286            "reservedBytes": 0,
287            "objectCount": 0,
288            "evictedObjectCount": 0,
289            "evictedBytes": 0,
290            "rejectedWriteCount": 0,
291            "revision": 1,
292            "createdAtMs": 100,
293            "updatedAtMs": 100,
294            "futureField": true
295        }))
296        .unwrap();
297        assert_eq!(namespace.policy().max_bytes, 1024);
298
299        let stats: StorageRepositoryStats = serde_json::from_value(json!({
300            "repositoryId": "repository-1",
301            "filesystemTotalBytes": 4096,
302            "filesystemAvailableBytes": 2048,
303            "storageLimitBytes": 3072,
304            "storageUsedBytes": 1024,
305            "storageReservedBytes": 0,
306            "storageWritableBytes": 1024,
307            "warningAtAvailableBytes": 512,
308            "stopAtAvailableBytes": 256,
309            "writeState": "writable",
310            "settingsRevision": 1,
311            "namespaceCount": 1,
312            "objectCount": 2,
313            "scopeNamespaceCount": 1,
314            "scopeObjectCount": 2,
315            "scopeUsedBytes": 1024,
316            "scopeReservedBytes": 0,
317            "activeUploadCount": 0,
318            "pendingDeletionCount": 1,
319            "futureField": true
320        }))
321        .unwrap();
322        assert_eq!(stats.pending_deletion_count, 1);
323    }
324
325    #[test]
326    fn storage_route_and_strategy_constants_are_canonical() {
327        assert_eq!(STORAGE_NODE_TYPE, "storage");
328        assert_eq!(STORAGE_PROTOCOL_VERSION, "storage.v2");
329        assert_eq!(STORAGE_CREATE_NAMESPACE_ROUTE, "/storage/namespace/create");
330        assert_eq!(STORAGE_UPDATE_NAMESPACE_ROUTE, "/storage/namespace/update");
331        assert_eq!(STORAGE_UPDATE_SETTINGS_ROUTE, "/storage/settings/update");
332        assert_eq!(StorageOverflowStrategy::Reject.as_str(), "reject");
333        assert_eq!(
334            StorageOverflowStrategy::parse("evictOldest").unwrap(),
335            StorageOverflowStrategy::EvictOldest
336        );
337        assert!(StorageOverflowStrategy::parse("deleteOldest").is_err());
338    }
339}