1use crate::state::{Durable, Kind, now_ms, ulid};
9use serde::{Deserialize, Serialize};
10use serde_json::{Value, json};
11use std::collections::BTreeMap;
12
13pub const MAX_INLINE_BYTES: usize = 4 * 1024 * 1024;
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
17pub struct Artifact {
18 pub id: String,
19 pub name: String,
20 pub mime: String,
21 pub size: u64,
22 pub sha256: String,
23 pub content: Value,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub created_by: Option<String>,
26 #[serde(default)]
27 pub sensitive: bool,
28 #[serde(default)]
29 pub created: u64,
30 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub owner: Option<String>,
33}
34
35impl Artifact {
36 pub fn meta(&self) -> Value {
37 json!({"id": self.id, "name": self.name, "mime": self.mime, "size": self.size, "sha256": self.sha256, "created_by": self.created_by, "sensitive": self.sensitive, "created": self.created, "owner": self.owner})
38 }
39}
40
41pub struct NewArtifact<'a> {
43 pub name: &'a str,
44 pub mime: Option<&'a str>,
45 pub content: Value,
46 pub created_by: Option<&'a str>,
47 pub sensitive: bool,
48 pub owner: Option<&'a str>,
49}
50
51#[derive(Default)]
53pub struct Artifacts {
54 map: BTreeMap<String, Artifact>,
55}
56
57impl Artifacts {
58 pub fn new() -> Artifacts {
59 Artifacts::default()
60 }
61
62 pub fn restore(&mut self, envelopes: &[crate::store::Envelope]) -> usize {
64 let mut n = 0;
65 for env in envelopes {
66 if let Ok(a) = serde_json::from_value::<Artifact>(env.state.clone()) {
67 self.map.insert(a.id.clone(), a);
68 n += 1;
69 }
70 }
71 n
72 }
73
74 pub fn create(&mut self, d: &Durable, spec: NewArtifact<'_>) -> Result<Value, String> {
76 let NewArtifact {
77 name,
78 mime,
79 content,
80 created_by,
81 sensitive,
82 owner,
83 } = spec;
84 if name.trim().is_empty() {
85 return Err("artifact.create: name must be non-empty".into());
86 }
87 let serialized = match &content {
88 Value::String(s) => s.clone(),
89 other => other.to_string(),
90 };
91 if serialized.len() > MAX_INLINE_BYTES {
92 return Err(format!(
93 "artifact.create: content is {} bytes; the inline cap is {MAX_INLINE_BYTES}",
94 serialized.len()
95 ));
96 }
97 let mime = mime.map(str::to_string).unwrap_or_else(|| {
98 if content.is_string() {
99 "text/plain".into()
100 } else {
101 "application/json".into()
102 }
103 });
104 let a = Artifact {
105 id: ulid::new(),
106 name: name.trim().to_string(),
107 mime,
108 size: serialized.len() as u64,
109 sha256: crate::sha::sha256_hex(serialized.as_bytes()),
110 content,
111 created_by: created_by.map(str::to_string),
112 sensitive,
113 created: now_ms(),
114 owner: owner.map(str::to_string),
115 };
116 d.put(
117 Kind::Artifact,
118 &a.id,
119 serde_json::to_value(&a).unwrap_or(Value::Null),
120 Some(a.sha256.clone()),
121 )
122 .map_err(|e| e.to_string())?;
123 let meta = a.meta();
124 self.map.insert(a.id.clone(), a);
125 Ok(meta)
126 }
127
128 pub fn get(&self, id: &str) -> Option<&Artifact> {
130 self.map.get(id)
131 }
132
133 pub fn get_value(&self, id: &str) -> Result<Value, String> {
136 let a = self
137 .map
138 .get(id)
139 .ok_or_else(|| format!("no such artifact {id:?}"))?;
140 let mut v = a.meta();
141 v["content"] = a.content.clone();
142 Ok(v)
143 }
144
145 pub fn delete(&mut self, d: &Durable, id: &str) -> Result<Value, String> {
147 if self.map.remove(id).is_none() {
148 return Err(format!("no such artifact {id:?}"));
149 }
150 d.delete(Kind::Artifact, id).map_err(|e| e.to_string())?;
151 Ok(json!({"ok": true, "id": id}))
152 }
153
154 pub fn list(&self, prefix: Option<&str>, limit: Option<usize>, owner: Option<&str>) -> Value {
156 let limit = limit.unwrap_or(100).max(1);
157 let mut items: Vec<Value> = self
158 .map
159 .values()
160 .filter(|a| prefix.is_none_or(|p| a.name.starts_with(p)))
161 .filter(|a| owner.is_none_or(|o| a.owner.as_deref() == Some(o)))
162 .map(Artifact::meta)
163 .collect();
164 let truncated = items.len() > limit;
165 items.truncate(limit);
166 json!({"artifacts": items, "truncated": truncated})
167 }
168
169 pub fn len(&self) -> usize {
170 self.map.len()
171 }
172 pub fn is_empty(&self) -> bool {
173 self.map.is_empty()
174 }
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::state::Policy;
181 use crate::store::memory::MemoryStore;
182 use std::sync::Arc;
183
184 #[test]
185 fn create_get_list_delete_and_restore() {
186 let d = Durable::new(
187 Arc::new(MemoryStore::new()),
188 "agentd",
189 "i",
190 Policy::default(),
191 None,
192 );
193 let mut a = Artifacts::new();
194 let m = a
195 .create(
196 &d,
197 NewArtifact {
198 name: "report.md",
199 mime: None,
200 content: json!("# hi"),
201 created_by: Some("root"),
202 sensitive: false,
203 owner: Some("task-1"),
204 },
205 )
206 .unwrap();
207 let id = m["id"].as_str().unwrap().to_string();
208 assert_eq!(m["mime"], json!("text/plain"));
209 assert_eq!(m["size"], json!(4));
210 let m2 = a
211 .create(
212 &d,
213 NewArtifact {
214 name: "data.json",
215 mime: None,
216 content: json!({"a": 1}),
217 created_by: None,
218 sensitive: true,
219 owner: None,
220 },
221 )
222 .unwrap();
223 assert_eq!(m2["mime"], json!("application/json"));
224 assert_eq!(a.get_value(&id).unwrap()["content"], json!("# hi"));
225 assert_eq!(
226 a.list(Some("rep"), None, None)["artifacts"]
227 .as_array()
228 .unwrap()
229 .len(),
230 1
231 );
232 assert_eq!(
233 a.list(None, None, Some("task-1"))["artifacts"]
234 .as_array()
235 .unwrap()
236 .len(),
237 1
238 );
239 assert_eq!(a.list(None, Some(1), None)["truncated"], json!(true));
240 assert!(
241 a.create(
242 &d,
243 NewArtifact {
244 name: "",
245 mime: None,
246 content: json!(1),
247 created_by: None,
248 sensitive: false,
249 owner: None,
250 },
251 )
252 .is_err()
253 );
254 let mut b = Artifacts::new();
256 assert_eq!(b.restore(d.restore().unwrap().of(Kind::Artifact)), 2);
257 assert!(b.get(&id).is_some());
258 b.delete(&d, &id).unwrap();
259 assert!(b.delete(&d, &id).is_err());
260 assert_eq!(d.restore().unwrap().of(Kind::Artifact).len(), 1);
261 }
262}