1use crate::rt::PluginError;
9use base64::Engine as _;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ItemStat {
16 pub name: String,
18 pub updated_time: i64,
20}
21
22pub trait Storage: Sized {
25 fn from_config(config: &Value) -> Result<Self, PluginError>;
27
28 fn list_items(&self) -> Result<Vec<ItemStat>, PluginError>;
29 fn get_item(&self, name: &str) -> Result<String, PluginError>;
30 fn put_item(&self, name: &str, content: &str) -> Result<(), PluginError>;
31 fn delete_item(&self, name: &str) -> Result<(), PluginError>;
32 fn get_resource(&self, resource_id: &str) -> Result<Vec<u8>, PluginError>;
33 fn put_resource(&self, resource_id: &str, data: &[u8]) -> Result<(), PluginError>;
34 fn delete_resource(&self, resource_id: &str) -> Result<(), PluginError>;
35 fn init_new(&self) -> Result<(), PluginError>;
36}
37
38fn str_param<'a>(params: &'a Value, key: &str) -> Result<&'a str, PluginError> {
39 params
40 .get(key)
41 .and_then(Value::as_str)
42 .ok_or_else(|| PluginError::invalid(format!("缺参数 {key}")))
43}
44
45pub fn dispatch_storage<S: Storage>(method: &str, params: &Value) -> Option<Result<Value, PluginError>> {
48 let op = method.strip_prefix("storage.")?;
49 Some(run::<S>(op, params))
50}
51
52fn run<S: Storage>(op: &str, params: &Value) -> Result<Value, PluginError> {
53 let b64 = base64::engine::general_purpose::STANDARD;
54 let config = params.get("config").cloned().unwrap_or(Value::Null);
55 let s = S::from_config(&config)?;
56 match op {
57 "list_items" => {
58 let items = s.list_items()?;
59 Ok(json!({ "items": items }))
60 }
61 "get_item" => {
62 let content = s.get_item(str_param(params, "name")?)?;
63 Ok(json!({ "content": content }))
64 }
65 "put_item" => {
66 s.put_item(str_param(params, "name")?, str_param(params, "content")?)?;
67 Ok(json!({}))
68 }
69 "delete_item" => {
70 s.delete_item(str_param(params, "name")?)?;
71 Ok(json!({}))
72 }
73 "get_resource" => {
74 let data = s.get_resource(str_param(params, "resource_id")?)?;
75 Ok(json!({ "data_b64": b64.encode(data) }))
76 }
77 "put_resource" => {
78 let data = b64
79 .decode(str_param(params, "data_b64")?)
80 .map_err(|e| PluginError::invalid(format!("data_b64 解码失败: {e}")))?;
81 s.put_resource(str_param(params, "resource_id")?, &data)?;
82 Ok(json!({}))
83 }
84 "delete_resource" => {
85 s.delete_resource(str_param(params, "resource_id")?)?;
86 Ok(json!({}))
87 }
88 "init_new" => {
89 s.init_new()?;
90 Ok(json!({}))
91 }
92 other => Err(PluginError::unsupported(format!("未知 storage 方法: {other}"))),
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99 use std::cell::RefCell;
100
101 struct Mem;
103 thread_local! {
104 static LAST: RefCell<Option<(String, Vec<u8>)>> = const { RefCell::new(None) };
105 }
106
107 impl Storage for Mem {
108 fn from_config(config: &Value) -> Result<Self, PluginError> {
109 if config.get("fail").is_some() {
110 return Err(PluginError::invalid("bad config"));
111 }
112 Ok(Mem)
113 }
114 fn list_items(&self) -> Result<Vec<ItemStat>, PluginError> {
115 Ok(vec![ItemStat { name: "a".repeat(32) + ".md", updated_time: 42 }])
116 }
117 fn get_item(&self, name: &str) -> Result<String, PluginError> {
118 Ok(format!("content of {name}"))
119 }
120 fn put_item(&self, _: &str, _: &str) -> Result<(), PluginError> {
121 Ok(())
122 }
123 fn delete_item(&self, _: &str) -> Result<(), PluginError> {
124 Ok(())
125 }
126 fn get_resource(&self, _: &str) -> Result<Vec<u8>, PluginError> {
127 Ok(vec![0xde, 0xad, 0xbe, 0xef])
128 }
129 fn put_resource(&self, id: &str, data: &[u8]) -> Result<(), PluginError> {
130 LAST.with(|l| *l.borrow_mut() = Some((id.to_string(), data.to_vec())));
131 Ok(())
132 }
133 fn delete_resource(&self, _: &str) -> Result<(), PluginError> {
134 Ok(())
135 }
136 fn init_new(&self) -> Result<(), PluginError> {
137 Ok(())
138 }
139 }
140
141 #[test]
142 fn routes_and_encodes() {
143 let r = dispatch_storage::<Mem>("storage.list_items", &json!({"config": {}}))
144 .unwrap()
145 .unwrap();
146 assert_eq!(r["items"][0]["updated_time"], 42);
147
148 let r = dispatch_storage::<Mem>("storage.get_resource", &json!({"config": {}, "resource_id": "x"}))
149 .unwrap()
150 .unwrap();
151 assert_eq!(r["data_b64"], "3q2+7w=="); dispatch_storage::<Mem>(
154 "storage.put_resource",
155 &json!({"config": {}, "resource_id": "y", "data_b64": "3q2+7w=="}),
156 )
157 .unwrap()
158 .unwrap();
159 LAST.with(|l| {
160 let (id, data) = l.borrow().clone().unwrap();
161 assert_eq!(id, "y");
162 assert_eq!(data, vec![0xde, 0xad, 0xbe, 0xef]);
163 });
164 }
165
166 #[test]
167 fn non_storage_method_passes_through() {
168 assert!(dispatch_storage::<Mem>("command", &json!({})).is_none());
169 }
170
171 #[test]
172 fn bad_config_and_bad_b64_are_invalid() {
173 let e = dispatch_storage::<Mem>("storage.list_items", &json!({"config": {"fail": 1}}))
174 .unwrap()
175 .unwrap_err();
176 assert_eq!(e.code, "invalid");
177 let e = dispatch_storage::<Mem>(
178 "storage.put_resource",
179 &json!({"config": {}, "resource_id": "y", "data_b64": "!!!"}),
180 )
181 .unwrap()
182 .unwrap_err();
183 assert_eq!(e.code, "invalid");
184 }
185}