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, PartialEq, Eq, Serialize, Deserialize)]
109#[serde(rename_all = "camelCase")]
110pub struct StorageNamespace {
111    pub id: String,
112    pub name: String,
113    pub max_bytes: u64,
114    pub overflow_strategy: StorageOverflowStrategy,
115    pub used_bytes: u64,
116    pub reserved_bytes: u64,
117    pub object_count: u64,
118    pub evicted_object_count: u64,
119    pub evicted_bytes: u64,
120    pub rejected_write_count: u64,
121    pub revision: u64,
122    pub created_at_ms: u64,
123    pub updated_at_ms: u64,
124}
125
126#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
127#[serde(rename_all = "camelCase")]
128pub struct StorageNamespaceList {
129    pub namespaces: Vec<StorageNamespace>,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
133#[serde(rename_all = "camelCase")]
134pub struct StorageSession {
135    pub protocol_version: String,
136    pub repository_id: String,
137    pub namespace: StorageNamespace,
138    pub endpoint: String,
139    pub access_token: String,
140    pub expires_at_ms: u64,
141    pub permissions: Vec<StoragePermission>,
142}
143
144#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
145#[serde(rename_all = "camelCase")]
146pub struct StorageObject {
147    pub key: String,
148    pub object_id: String,
149    pub size_bytes: u64,
150    pub sha256: String,
151    pub content_type: Option<String>,
152    pub committed_at_ms: u64,
153}
154
155#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
156#[serde(rename_all = "camelCase")]
157pub struct StorageObjectList {
158    pub objects: Vec<StorageObject>,
159    pub next_cursor: Option<String>,
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
163#[serde(rename_all = "camelCase")]
164pub enum StorageWriteState {
165    Writable,
166    LowSpace,
167    BlockedByPhysicalSpace,
168    BlockedByStorageLimit,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct StorageRepositorySettings {
174    pub limit_bytes: Option<u64>,
175    pub revision: u64,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "camelCase")]
180pub struct StorageRepositoryStats {
181    pub repository_id: String,
182    pub filesystem_total_bytes: u64,
183    pub filesystem_available_bytes: u64,
184    pub storage_limit_bytes: Option<u64>,
185    pub storage_used_bytes: u64,
186    pub storage_reserved_bytes: u64,
187    pub storage_writable_bytes: u64,
188    pub warning_at_available_bytes: u64,
189    pub stop_at_available_bytes: u64,
190    pub write_state: StorageWriteState,
191    pub settings_revision: u64,
192    pub namespace_count: u64,
193    pub object_count: u64,
194    pub scope_namespace_count: u64,
195    pub scope_object_count: u64,
196    pub scope_used_bytes: u64,
197    pub scope_reserved_bytes: u64,
198    pub active_upload_count: u64,
199    pub pending_deletion_count: u64,
200}
201
202#[cfg(test)]
203mod tests {
204    use super::*;
205    use serde_json::json;
206
207    #[test]
208    fn storage_request_contracts_are_closed_and_camel_case() {
209        let create: StorageCreateNamespaceRequest = serde_json::from_value(json!({
210            "name": "Camera Recordings",
211            "maxBytes": 1024,
212            "overflowStrategy": "evictOldest"
213        }))
214        .unwrap();
215        assert_eq!(
216            create.overflow_strategy,
217            StorageOverflowStrategy::EvictOldest
218        );
219        assert!(
220            serde_json::from_value::<StorageCreateNamespaceRequest>(json!({
221                "name": "recordings",
222                "maxBytes": 1024,
223                "futureField": true
224            }))
225            .is_err()
226        );
227
228        let update: StorageNamespaceUpdate = serde_json::from_value(json!({
229            "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
230            "expectedRevision": 2,
231            "name": "Recordings",
232            "maxBytes": 2048,
233            "overflowStrategy": "reject"
234        }))
235        .unwrap();
236        assert_eq!(update.expected_revision, 2);
237
238        let settings: StorageRepositorySettingsUpdate = serde_json::from_value(json!({
239            "expectedRevision": 3,
240            "limitBytes": null
241        }))
242        .unwrap();
243        assert_eq!(settings.expected_revision, 3);
244        assert_eq!(settings.limit_bytes, None);
245
246        let session: StorageSessionRequest = serde_json::from_value(json!({
247            "namespaceId": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
248            "permissions": ["read", "write", "delete", "list"],
249            "ttlSeconds": 900
250        }))
251        .unwrap();
252        assert_eq!(session.permissions.len(), 4);
253    }
254
255    #[test]
256    fn storage_responses_are_additive_for_consumers() {
257        let namespace: StorageNamespace = serde_json::from_value(json!({
258            "id": "6f082e5e-b8bb-4d6a-bd53-8e87f58258b0",
259            "name": "recordings",
260            "maxBytes": 1024,
261            "overflowStrategy": "reject",
262            "usedBytes": 0,
263            "reservedBytes": 0,
264            "objectCount": 0,
265            "evictedObjectCount": 0,
266            "evictedBytes": 0,
267            "rejectedWriteCount": 0,
268            "revision": 1,
269            "createdAtMs": 100,
270            "updatedAtMs": 100,
271            "futureField": true
272        }))
273        .unwrap();
274        assert_eq!(namespace.max_bytes, 1024);
275
276        let stats: StorageRepositoryStats = serde_json::from_value(json!({
277            "repositoryId": "repository-1",
278            "filesystemTotalBytes": 4096,
279            "filesystemAvailableBytes": 2048,
280            "storageLimitBytes": 3072,
281            "storageUsedBytes": 1024,
282            "storageReservedBytes": 0,
283            "storageWritableBytes": 1024,
284            "warningAtAvailableBytes": 512,
285            "stopAtAvailableBytes": 256,
286            "writeState": "writable",
287            "settingsRevision": 1,
288            "namespaceCount": 1,
289            "objectCount": 2,
290            "scopeNamespaceCount": 1,
291            "scopeObjectCount": 2,
292            "scopeUsedBytes": 1024,
293            "scopeReservedBytes": 0,
294            "activeUploadCount": 0,
295            "pendingDeletionCount": 1,
296            "futureField": true
297        }))
298        .unwrap();
299        assert_eq!(stats.pending_deletion_count, 1);
300    }
301
302    #[test]
303    fn storage_route_and_strategy_constants_are_canonical() {
304        assert_eq!(STORAGE_NODE_TYPE, "storage");
305        assert_eq!(STORAGE_PROTOCOL_VERSION, "storage.v2");
306        assert_eq!(STORAGE_CREATE_NAMESPACE_ROUTE, "/storage/namespace/create");
307        assert_eq!(STORAGE_UPDATE_NAMESPACE_ROUTE, "/storage/namespace/update");
308        assert_eq!(STORAGE_UPDATE_SETTINGS_ROUTE, "/storage/settings/update");
309        assert_eq!(StorageOverflowStrategy::Reject.as_str(), "reject");
310        assert_eq!(
311            StorageOverflowStrategy::parse("evictOldest").unwrap(),
312            StorageOverflowStrategy::EvictOldest
313        );
314        assert!(StorageOverflowStrategy::parse("deleteOldest").is_err());
315    }
316}