Skip to main content

oxihuman_core/
object_storage_stub.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Object storage (S3-like) stub.
6
7use std::collections::HashMap;
8
9/// Object metadata.
10#[derive(Debug, Clone)]
11pub struct ObjectMeta {
12    pub key: String,
13    pub size_bytes: u64,
14    pub etag: String,
15    pub content_type: String,
16}
17
18impl ObjectMeta {
19    pub fn new(key: &str, size_bytes: u64) -> Self {
20        ObjectMeta {
21            key: key.to_string(),
22            size_bytes,
23            etag: format!("{:x}", size_bytes ^ 0xDEAD_BEEF),
24            content_type: "application/octet-stream".to_string(),
25        }
26    }
27}
28
29/// An object stored in the stub.
30#[derive(Debug, Clone)]
31pub struct StoredObject {
32    pub meta: ObjectMeta,
33    pub data: Vec<u8>,
34}
35
36impl StoredObject {
37    pub fn new(key: &str, data: Vec<u8>) -> Self {
38        let size = data.len() as u64;
39        StoredObject {
40            meta: ObjectMeta::new(key, size),
41            data,
42        }
43    }
44}
45
46/// In-memory object storage stub.
47pub struct ObjectStorage {
48    bucket: String,
49    objects: HashMap<String, StoredObject>,
50}
51
52impl ObjectStorage {
53    pub fn new(bucket: &str) -> Self {
54        ObjectStorage {
55            bucket: bucket.to_string(),
56            objects: HashMap::new(),
57        }
58    }
59
60    pub fn put(&mut self, key: &str, data: Vec<u8>) {
61        self.objects
62            .insert(key.to_string(), StoredObject::new(key, data));
63    }
64
65    pub fn get(&self, key: &str) -> Option<&StoredObject> {
66        self.objects.get(key)
67    }
68
69    pub fn delete(&mut self, key: &str) -> bool {
70        self.objects.remove(key).is_some()
71    }
72
73    pub fn list(&self, prefix: &str) -> Vec<&ObjectMeta> {
74        self.objects
75            .values()
76            .filter(|o| o.meta.key.starts_with(prefix))
77            .map(|o| &o.meta)
78            .collect()
79    }
80
81    pub fn exists(&self, key: &str) -> bool {
82        self.objects.contains_key(key)
83    }
84
85    pub fn object_count(&self) -> usize {
86        self.objects.len()
87    }
88
89    pub fn total_size(&self) -> u64 {
90        self.objects.values().map(|o| o.meta.size_bytes).sum()
91    }
92
93    pub fn bucket(&self) -> &str {
94        &self.bucket
95    }
96}
97
98impl Default for ObjectStorage {
99    fn default() -> Self {
100        Self::new("default-bucket")
101    }
102}
103
104/// Create a new object storage.
105pub fn new_object_storage(bucket: &str) -> ObjectStorage {
106    ObjectStorage::new(bucket)
107}
108
109/// Upload bytes and return the key.
110pub fn upload(storage: &mut ObjectStorage, key: &str, data: &[u8]) -> String {
111    storage.put(key, data.to_vec());
112    key.to_string()
113}
114
115/// Download bytes for a key.
116pub fn download(storage: &ObjectStorage, key: &str) -> Option<Vec<u8>> {
117    storage.get(key).map(|o| o.data.clone())
118}
119
120/// Copy object from one key to another.
121pub fn copy_object(storage: &mut ObjectStorage, src_key: &str, dst_key: &str) -> bool {
122    if let Some(obj) = storage.get(src_key).cloned() {
123        storage.put(dst_key, obj.data);
124        true
125    } else {
126        false
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_put_and_get() {
136        let mut s = new_object_storage("bucket");
137        s.put("key1", b"hello".to_vec());
138        assert!(s.get("key1").is_some());
139    }
140
141    #[test]
142    fn test_get_missing() {
143        let s = new_object_storage("bucket");
144        assert!(s.get("nonexistent").is_none());
145    }
146
147    #[test]
148    fn test_delete() {
149        let mut s = new_object_storage("bucket");
150        s.put("k", b"data".to_vec());
151        assert!(s.delete("k"));
152        assert!(!s.exists("k"));
153    }
154
155    #[test]
156    fn test_list_prefix() {
157        let mut s = new_object_storage("bucket");
158        s.put("images/a.png", b"img".to_vec());
159        s.put("docs/b.txt", b"doc".to_vec());
160        let imgs = s.list("images/");
161        assert_eq!(imgs.len(), 1);
162    }
163
164    #[test]
165    fn test_total_size() {
166        let mut s = new_object_storage("bucket");
167        s.put("a", vec![0u8; 100]);
168        s.put("b", vec![0u8; 200]);
169        assert_eq!(s.total_size(), 300);
170    }
171
172    #[test]
173    fn test_upload_download() {
174        let mut s = new_object_storage("bucket");
175        upload(&mut s, "file.bin", &[1, 2, 3]);
176        let data = download(&s, "file.bin").expect("should succeed");
177        assert_eq!(data, vec![1, 2, 3]);
178    }
179
180    #[test]
181    fn test_copy_object() {
182        let mut s = new_object_storage("bucket");
183        s.put("src", b"payload".to_vec());
184        assert!(copy_object(&mut s, "src", "dst"));
185        assert!(s.exists("dst"));
186    }
187
188    #[test]
189    fn test_copy_missing_returns_false() {
190        let mut s = new_object_storage("bucket");
191        assert!(!copy_object(&mut s, "missing", "dst"));
192    }
193
194    #[test]
195    fn test_object_count() {
196        let mut s = new_object_storage("bucket");
197        s.put("x", vec![]);
198        s.put("y", vec![]);
199        assert_eq!(s.object_count(), 2);
200    }
201
202    #[test]
203    fn test_bucket_name() {
204        let s = ObjectStorage::new("my-bucket");
205        assert_eq!(s.bucket(), "my-bucket");
206    }
207}