1use crate::*;
2
3async fn datasets() -> Result<Value> {
4 host::call(json!({"operation":"storage.datasets"})).await
5}
6fn leaves(vdev: &Value, out: &mut Vec<Value>) {
7 if let Some(children) = vdev["vdevs"].as_object() {
8 for child in children.values() {
9 leaves(child, out);
10 }
11 } else {
12 let errors = number(&vdev["read_errors"])
13 + number(&vdev["write_errors"])
14 + number(&vdev["checksum_errors"]);
15 let mut issues = Vec::new();
16 if vdev["state"] != "ONLINE" {
17 issues.push(string(&vdev["state"]).to_lowercase());
18 }
19 if errors > 0.0 {
20 issues.push(format!(
21 "{errors} error{}",
22 if errors == 1.0 { "" } else { "s" }
23 ));
24 }
25 out.push(json!({"name":vdev["name"],"state":vdev["state"],"read":vdev["read_errors"],"write":vdev["write_errors"],"checksum":vdev["checksum_errors"],"smart":null,"issue":if issues.is_empty() {None} else {Some(issues.join(", "))}}));
26 }
27}
28async fn pool() -> Result<Value> {
29 let value = host::call(json!({"operation":"storage.pool"})).await?;
30 let name = string(&value["name"]);
31 let pool = &value["status"]["pools"][name];
32 let mut output = value["summary"].clone();
33 output["name"] = json!(name);
34 output["state"] = pool["state"].clone();
35 output["scan"] = Value::Null;
36 let mut vdevs = Vec::new();
37 let mut striped = Vec::new();
38 for vdev in pool["vdevs"][name]["vdevs"]
39 .as_object()
40 .into_iter()
41 .flat_map(|v| v.values())
42 {
43 if vdev["vdevs"].is_object() {
44 let mut disks = Vec::new();
45 leaves(vdev, &mut disks);
46 vdevs.push(json!({"name":vdev["name"],"state":vdev["state"],"disks":disks}));
47 } else {
48 leaves(vdev, &mut striped);
49 }
50 }
51 if !striped.is_empty() {
52 vdevs.insert(
53 0,
54 json!({"name":name,"state":pool["state"],"disks":striped}),
55 );
56 }
57 for kind in ["logs", "l2cache", "special", "dedup", "spares"] {
58 let children: Vec<_> = pool[kind]
59 .as_object()
60 .into_iter()
61 .flat_map(|v| v.values())
62 .collect();
63 if children.is_empty() {
64 continue;
65 }
66 let worst = children
67 .iter()
68 .find(|v| v["state"] != "ONLINE" && v["state"] != "AVAIL")
69 .unwrap_or(&children[0]);
70 let mut disks = Vec::new();
71 for child in &children {
72 leaves(child, &mut disks);
73 }
74 vdevs.push(json!({"name":if kind=="l2cache" {"cache"} else {kind},"state":worst["state"],"disks":disks}));
75 }
76 output["vdevs"] = json!(vdevs);
77 let scan = &pool["scan_stats"];
78 if ["SCANNING", "FINISHED", "CANCELED"].contains(&string(&scan["state"])) {
79 output["scan"] = json!({"kind":if scan["function"]=="RESILVER" {"resilver"} else {"scrub"},"state":string(&scan["state"]).to_lowercase(),"start":scan["start_time"],"end":if scan["state"]=="SCANNING" {Value::Null} else {scan["end_time"].clone()},"examined":scan["examined"],"total":scan["to_examine"],"repaired":scan["processed"],"errors":scan["errors"]});
80 }
81 output["errors"] = json!(if number(&pool["error_count"]) > 0.0 {
82 format!("{} data errors", number(&pool["error_count"]))
83 } else {
84 "No known data errors".into()
85 });
86 Ok(output)
87}
88pub async fn snapshots(dataset: &str) -> Result<Value> {
89 validate(dataset, false)?;
90 host::call(json!({"operation":"storage.snapshots","dataset":dataset})).await
91}
92pub fn validate(name: &str, snapshot: bool) -> Result<()> {
93 let regex = if snapshot {
94 r"^[\w.: ][\w.: -]*$"
95 } else {
96 r"^[\w.: ][\w.: -]*(/[\w.: -]+)*$"
97 };
98 if regex::Regex::new(regex).unwrap().is_match(name) {
99 Ok(())
100 } else {
101 Err(Error::new(
102 400,
103 "That isn't a ZFS name. Pick one from the list.",
104 ))
105 }
106}
107async fn existing(dataset: &str, from: &str, to: &str) -> Result<()> {
108 validate(from, true)?;
109 validate(to, true)?;
110 let values = snapshots(dataset).await?;
111 for name in [from, to] {
112 if !array(&values).iter().any(|s| s["name"] == name) {
113 return Err(Error::new(404, format!("No snapshot {dataset}@{name}")));
114 }
115 }
116 Ok(())
117}
118pub fn unescape(name: &str) -> String {
119 let bytes = name.as_bytes();
120 let mut out = Vec::new();
121 let mut i = 0;
122 while i < bytes.len() {
123 if bytes[i] == b'\\'
124 && i + 4 < bytes.len()
125 && bytes[i + 1..i + 5]
126 .iter()
127 .all(|b| (b'0'..=b'7').contains(b))
128 {
129 let octal = std::str::from_utf8(&bytes[i + 1..i + 5]).unwrap();
130 out.push(u16::from_str_radix(octal, 8).unwrap() as u8);
131 i += 5;
132 } else {
133 out.push(bytes[i]);
134 i += 1;
135 }
136 }
137 String::from_utf8_lossy(&out).into_owned()
138}
139pub async fn route(
140 app: Arc<App>,
141 method: &Method,
142 parts: &[&str],
143 query: &HashMap<String, String>,
144 body: Value,
145) -> Result<Response> {
146 let value = match parts {
147 [] if method == Method::GET => {
148 let found=app.cache.get("storage".into(),Duration::from_secs(30),move || async move {
149 let (pool,mut datasets)=tokio::try_join!(pool(),datasets())?;let root=array(&datasets).iter().find(|d| d["name"]==pool["name"]).ok_or_else(|| Error::new(502,"The storage pool has no root dataset."))?;let held:f64=array(&datasets).iter().map(|d| number(&d["usedbysnapshots"])).sum();let space=json!({"live":number(&root["used"])-held,"held":held,"free":root["available"]});
150 let media=env("STUDIO_MEDIA_ROOT",&format!("{}/clover/Media",env("STUDIO_STORE_ROOT","/srv")));for dataset in datasets.as_array_mut().unwrap() {let mount=dataset["mountpoint"].as_str().map(str::to_owned);dataset["link"]=mount.as_ref().map(|p| apps::file_link(p,false)).unwrap_or(Value::Null);dataset["media"]=mount.and_then(|p| std::path::Path::new(&p).strip_prefix(&media).ok().map(|p| p.to_string_lossy().into_owned())).map(|p| json!(p)).unwrap_or(Value::Null);}Ok(json!({"pool":pool,"datasets":datasets,"space":space}))
151 }).await?;
152 return Ok(found.response());
153 }
154 ["snapshots"] if method == Method::GET => {
155 let dataset = query
156 .get("dataset")
157 .ok_or_else(|| Error::new(400, "Pick a dataset."))?;
158 json!(
159 array(&snapshots(dataset).await?)
160 .iter()
161 .filter(|s| !string(&s["name"]).starts_with("index-"))
162 .cloned()
163 .collect::<Vec<_>>()
164 )
165 }
166 ["reclaim"] if method == Method::GET => {
167 let dataset = query
168 .get("dataset")
169 .ok_or_else(|| Error::new(400, "Pick a dataset."))?;
170 let from = query
171 .get("from")
172 .ok_or_else(|| Error::new(400, "Pick a snapshot."))?;
173 let to = query
174 .get("to")
175 .ok_or_else(|| Error::new(400, "Pick a snapshot."))?;
176 existing(dataset, from, to).await?;
177 let value = host::call(
178 json!({"operation":"storage.reclaim","dataset":dataset,"from":from,"to":to}),
179 )
180 .await?;
181 let bytes = string(&value)
182 .lines()
183 .find_map(|line| line.strip_prefix("reclaim\t"))
184 .and_then(|s| s.parse::<f64>().ok())
185 .unwrap_or(0.0);
186 json!({"bytes":bytes})
187 }
188 ["destroy"] if method == Method::POST => {
189 let (dataset, from, to) = (
190 string(&body["dataset"]),
191 string(&body["from"]),
192 string(&body["to"]),
193 );
194 existing(dataset, from, to).await?;
195 host::call(
196 json!({"operation":"storage.destroy","dataset":dataset,"from":from,"to":to}),
197 )
198 .await?;
199 app.cache.invalidate("storage");
200 Value::Null
201 }
202 ["removed"] if method == Method::GET => {
203 let dataset = query
204 .get("dataset")
205 .ok_or_else(|| Error::new(400, "Pick a dataset."))?;
206 let snapshot = query
207 .get("snapshot")
208 .ok_or_else(|| Error::new(400, "Pick a snapshot."))?;
209 validate(dataset, false)?;
210 validate(snapshot, true)?;
211 let values = datasets().await?;
212 let mount = array(&values)
213 .iter()
214 .find(|d| d["name"] == *dataset)
215 .and_then(|d| d["mountpoint"].as_str())
216 .ok_or_else(|| {
217 Error::new(
218 409,
219 format!("{dataset} isn't mounted, so its deleted files can't be listed"),
220 )
221 })?;
222 let value = host::call(
223 json!({"operation":"storage.removed","dataset":dataset,"snapshot":snapshot}),
224 )
225 .await?;
226 let text = string(&value);
227 let mut files = Vec::new();
228 for line in text.lines() {
229 if let Some(path) = line.strip_prefix("-\t") {
230 let path = unescape(path);
231 let Ok(relative) = std::path::Path::new(&path).strip_prefix(mount) else {
232 continue;
233 };
234 let saved = std::path::Path::new(mount)
235 .join(".zfs/snapshot")
236 .join(snapshot)
237 .join(relative);
238 if let Ok(info) = tokio::fs::symlink_metadata(saved).await
239 && info.is_file()
240 {
241 files.push(json!({"path":relative,"size":info.len()}));
242 }
243 }
244 }
245 files.sort_by(|a, b| number(&b["size"]).total_cmp(&number(&a["size"])));
246 json!({"count":files.len(),"largest":files.into_iter().take(4).collect::<Vec<_>>()})
247 }
248 _ => return Err(Error::new(404, "Not Found")),
249 };
250 Ok(if value.is_null() {
251 StatusCode::NO_CONTENT.into_response()
252 } else {
253 Document::new(value).response()
254 })
255}