Onset: version control where nobody waits and nothing gets lost. The on command, its core and onset-server.
| 5 | use std::sync::Arc; |
| 6 |
| 7 | use anyhow::Result; |
| 8 | use serde::Serialize; |
| 9 | use serde_json::json; |
| 10 | use tiny_http::{Header, Method, Request, Response, Server}; |
| 11 | use onset_cli::host::LandRequest; |
| 12 | use onset_cli::wire::{self, Hold, MineFile, PutChange, Rule, SaveRecord, ShareRequest}; |
| 13 | use onset_core::Id; |
| 14 |
| 15 | use crate::state::{Access, Problem, Res, State, problem}; |
| 16 |
| 17 | /// Biggest request body accepted (a bundle of a big change). |
| 18 | const MAX_BODY: u64 = 512 << 20; |
| 19 |
| 20 | pub fn serve(data: &Path, listen: &str, threads: usize) -> Result<()> { |
| 21 | // Create the schema once before the workers start. |
| 22 | State::open(data)?; |
| 23 | let server = Arc::new(Server::http(listen).map_err(|e| anyhow::anyhow!("can't listen on {listen}: {e}"))?); |
| 24 | eprintln!("onset-server on http://{listen}, data in {}", data.display()); |
| 25 | let mut handles = Vec::new(); |
| 26 | for _ in 0..threads.max(1) { |
| 27 | let server = server.clone(); |
| 28 | let data = data.to_path_buf(); |
| 29 | handles.push(std::thread::spawn(move || { |
| 30 | let state = State::open(&data).expect("open state"); |
| 31 | for request in server.incoming_requests() { |
| 32 | handle(&state, request); |
| 33 | } |
| 34 | })); |
| 35 | } |
| 36 | for h in handles { |
| 37 | let _ = h.join(); |
| 38 | } |
| 39 | Ok(()) |
| 40 | } |
| 41 |
| 42 | enum Body { |
| 43 | Json(serde_json::Value), |
| 44 | Bytes(Vec<u8>), |
| 45 | } |
| 46 |
| 47 | fn handle(state: &State, mut request: Request) { |
| 48 | let started = std::time::Instant::now(); |
| 49 | let method = request.method().clone(); |
| 50 | let url = request.url().to_string(); |
| 51 | let result = route(state, &mut request); |
| 52 | let (status, body) = match result { |
| 53 | Ok((status, body)) => (status, body), |
| 54 | Err(p) => (p.status, Body::Json(json!({ "error": { "code": p.code, "message": p.message, "holder": p.holder } }))), |
| 55 | }; |
| 56 | let response = match body { |
| 57 | Body::Json(v) => Response::from_data(serde_json::to_vec(&v).unwrap_or_default()) |
| 58 | .with_status_code(status) |
| 59 | .with_header(Header::from_bytes("content-type", "application/json").unwrap()), |
| 60 | Body::Bytes(b) => Response::from_data(b).with_status_code(status).with_header(Header::from_bytes("content-type", "application/octet-stream").unwrap()), |
| 61 | }; |
| 62 | if onset_cli::paths::var_os("SERVER_LOG").is_some() { |
| 63 | eprintln!("{method} {url} → {status} in {:?}", started.elapsed()); |
| 64 | } |
| 65 | let _ = request.respond(response); |
| 66 | } |
| 67 |
| 68 | fn ok<T: Serialize>(value: T) -> Res<(u16, Body)> { |
| 69 | Ok((200, Body::Json(serde_json::to_value(value).map_err(|e| problem(500, "failed", e.to_string()))?))) |
| 70 | } |
| 71 |
| 72 | fn read_body(request: &mut Request) -> Res<Vec<u8>> { |
| 73 | let mut body = Vec::new(); |
| 74 | request.as_reader().take(MAX_BODY + 1).read_to_end(&mut body).map_err(|e| problem(400, "bad_usage", e.to_string()))?; |
| 75 | if body.len() as u64 > MAX_BODY { |
| 76 | return Err(problem(413, "too_big", "request too big")); |
| 77 | } |
| 78 | Ok(body) |
| 79 | } |
| 80 |
| 81 | fn json_body<T: serde::de::DeserializeOwned>(request: &mut Request) -> Res<T> { |
| 82 | let body = read_body(request)?; |
| 83 | serde_json::from_slice(&body).map_err(|e| problem(400, "bad_usage", format!("bad request body: {e}"))) |
| 84 | } |
| 85 |
| 86 | fn query(url: &str, key: &str) -> Option<String> { |
| 87 | url.split_once('?')?.1.split('&').find_map(|kv| kv.strip_prefix(&format!("{key}=")).map(decode)) |
| 88 | } |
| 89 |
| 90 | /// A query value with its %XX escapes (and + for spaces) undone. |
| 91 | fn decode(text: &str) -> String { |
| 92 | let bytes = text.as_bytes(); |
| 93 | let mut out = Vec::with_capacity(bytes.len()); |
| 94 | let mut i = 0; |
| 95 | while i < bytes.len() { |
| 96 | match bytes[i] { |
| 97 | b'%' if let Some(b) = text.get(i + 1..i + 3).and_then(|h| u8::from_str_radix(h, 16).ok()) => { |
| 98 | out.push(b); |
| 99 | i += 3; |
| 100 | } |
| 101 | b'+' => { |
| 102 | out.push(b' '); |
| 103 | i += 1; |
| 104 | } |
| 105 | b => { |
| 106 | out.push(b); |
| 107 | i += 1; |
| 108 | } |
| 109 | } |
| 110 | } |
| 111 | String::from_utf8_lossy(&out).into_owned() |
| 112 | } |
| 113 |
| 114 | fn route(state: &State, request: &mut Request) -> Res<(u16, Body)> { |
| 115 | let url = request.url().to_string(); |
| 116 | let path = url.split('?').next().unwrap_or(""); |
| 117 | let method = request.method().clone(); |
| 118 | // vcs.json for machines from before Onset had its name. |
| 119 | if path == "/.well-known/onset.json" || path == "/.well-known/vcs.json" { |
| 120 | return ok(json!({ "api": "/v1" })); |
| 121 | } |
| 122 | let Some(rest) = path.strip_prefix("/v1/") else { return Err(problem(404, "not_found", "no such endpoint")) }; |
| 123 | let parts: Vec<&str> = rest.split('/').filter(|p| !p.is_empty()).collect(); |
| 124 |
| 125 | let token = request |
| 126 | .headers() |
| 127 | .iter() |
| 128 | .find(|h| h.field.equiv("authorization")) |
| 129 | .and_then(|h| h.value.as_str().strip_prefix("Bearer ").map(str::to_string)); |
| 130 | let user = match token { |
| 131 | Some(t) => Some(state.user_for_token(&t)?.ok_or_else(|| problem(401, "not_logged_in", "this token isn't valid here"))?), |
| 132 | None => None, |
| 133 | }; |
| 134 |
| 135 | match (&method, parts.as_slice()) { |
| 136 | (Method::Get, ["me"]) => { |
| 137 | let me = user.ok_or_else(|| problem(401, "not_logged_in", "sign in first"))?; |
| 138 | ok(me) |
| 139 | } |
| 140 | (Method::Post, ["projects"]) => { |
| 141 | let me = user.ok_or_else(|| problem(401, "not_logged_in", "sign in first"))?; |
| 142 | #[derive(serde::Deserialize)] |
| 143 | struct New { |
| 144 | owner: String, |
| 145 | name: String, |
| 146 | #[serde(default)] |
| 147 | public: bool, |
| 148 | } |
| 149 | let new: New = json_body(request)?; |
| 150 | if !state.may_create_under(&new.owner, &me.handle)? { |
| 151 | return Err(problem(403, "not_allowed", format!("{} belongs to someone else", new.owner))); |
| 152 | } |
| 153 | let key = format!("{}/{}", new.owner, new.name); |
| 154 | if state.project_exists(&key)?.is_some() { |
| 155 | return Err(problem(409, "exists", format!("{key} already exists"))); |
| 156 | } |
| 157 | state.add_project(&key, &me.handle, new.public)?; |
| 158 | let access = state.access(&key, Some(me))?; |
| 159 | ok(view(state, &key, &access)?) |
| 160 | } |
| 161 | (_, ["projects", owner, name, tail @ ..]) => { |
| 162 | let key = format!("{owner}/{name}"); |
| 163 | let access = state.access(&key, user)?; |
| 164 | project_route(state, request, &method, &key, &access, tail, &url) |
| 165 | } |
| 166 | _ => Err(problem(404, "not_found", "no such endpoint")), |
| 167 | } |
| 168 | } |
| 169 |
| 170 | fn view(state: &State, key: &str, access: &Access) -> Res<wire::ProjectView> { |
| 171 | let (owner, name) = key.split_once('/').unwrap(); |
| 172 | Ok(wire::ProjectView { owner: owner.into(), name: name.into(), public: state.public(key)?, main: state.main_for(key, access)?, can: access.can.clone(), git: None, git_stopped: None, git_mine: None, import_pending: false }) |
| 173 | } |
| 174 |
| 175 | fn need_user(access: &Access) -> Res<()> { |
| 176 | if access.user.is_none() { |
| 177 | return Err(problem(401, "not_logged_in", "sign in first")); |
| 178 | } |
| 179 | Ok(()) |
| 180 | } |
| 181 |
| 182 | fn project_route(state: &State, request: &mut Request, method: &Method, key: &str, access: &Access, tail: &[&str], url: &str) -> Res<(u16, Body)> { |
| 183 | match (method, tail) { |
| 184 | (Method::Get, []) => ok(view(state, key, access)?), |
| 185 | (Method::Get, ["history"]) => { |
| 186 | let path = query(url, "path").unwrap_or_default(); |
| 187 | let limit: usize = query(url, "limit").and_then(|s| s.parse().ok()).unwrap_or(50).clamp(1, 1000); |
| 188 | ok(state.history(key, access, &path, limit)?) |
| 189 | } |
| 190 | (Method::Get, ["blame"]) => ok(state.blame(key, access, &query(url, "path").unwrap_or_default())?), |
| 191 | (Method::Get, ["file"]) => { |
| 192 | let version = match query(url, "version") { |
| 193 | Some(v) => Some(v.parse::<u64>().map_err(|_| problem(400, "bad_usage", "versions are whole numbers"))?), |
| 194 | None => None, |
| 195 | }; |
| 196 | ok(state.read_path(key, access, version, &query(url, "path").unwrap_or_default())?) |
| 197 | } |
| 198 | (Method::Get, ["versions"]) => { |
| 199 | let since: u64 = query(url, "since").and_then(|s| s.parse().ok()).unwrap_or(0); |
| 200 | let limit: i64 = query(url, "limit").and_then(|s| s.parse().ok()).unwrap_or(5000).min(5000); |
| 201 | let versions = state.versions_since(key, since, limit)?; |
| 202 | ok(json!({ "versions": state.versions_for(key, access, versions)?, "purges": state.purges(key)?.len() })) |
| 203 | } |
| 204 | (Method::Post, ["purge"]) => { |
| 205 | #[derive(serde::Deserialize)] |
| 206 | struct Purge { |
| 207 | paths: Vec<String>, |
| 208 | #[serde(default)] |
| 209 | dry_run: bool, |
| 210 | } |
| 211 | let p: Purge = json_body(request)?; |
| 212 | ok(state.purge(key, access, &p.paths, p.dry_run)?) |
| 213 | } |
| 214 | (Method::Get, ["purges"]) => ok(json!({ "purges": state.purges(key)? })), |
| 215 | (Method::Get, ["lines"]) => ok(state.lines(key, access)?), |
| 216 | (Method::Post, ["lines"]) => { |
| 217 | #[derive(serde::Deserialize)] |
| 218 | struct NewLine { |
| 219 | name: String, |
| 220 | from: u64, |
| 221 | } |
| 222 | let l: NewLine = json_body(request)?; |
| 223 | ok(state.add_line(key, access, &l.name, l.from)?) |
| 224 | } |
| 225 | (Method::Get, ["lines", "versions"]) => { |
| 226 | let limit: i64 = query(url, "limit").and_then(|s| s.parse().ok()).unwrap_or(50); |
| 227 | ok(state.line_versions(key, access, &query(url, "name").unwrap_or_default(), limit)?) |
| 228 | } |
| 229 | (Method::Post, ["objects", "missing"]) => { |
| 230 | need_user(access)?; |
| 231 | let ids = wire::decode_ids(&read_body(request)?); |
| 232 | Ok((200, Body::Bytes(wire::encode_ids(&state.missing(&ids))))) |
| 233 | } |
| 234 | (Method::Post, ["objects"]) => { |
| 235 | need_user(access)?; |
| 236 | let body = read_body(request)?; |
| 237 | let stored = state.store.ingest_uploaded(&body).map_err(|e| problem(400, "bad_usage", format!("{e:#}")))?; |
| 238 | ok(json!({ "stored": stored })) |
| 239 | } |
| 240 | (Method::Get, ["trees", tree]) => { |
| 241 | let tree = Id::parse(tree).ok_or_else(|| problem(400, "bad_usage", "not a tree id"))?; |
| 242 | if !state.may_read_tree(key, access, &tree)? { |
| 243 | return Err(problem(404, "not_found", "no such tree")); |
| 244 | } |
| 245 | if query(url, "folders").as_deref() == Some("1") { |
| 246 | return Ok((200, Body::Bytes(state.folder_bundle(&tree)?))); |
| 247 | } |
| 248 | let have = query(url, "have").and_then(|h| Id::parse(&h)).filter(|h| state.may_read_tree(key, access, h).unwrap_or(false)); |
| 249 | Ok((200, Body::Bytes(state.tree_bundle(&tree, have.as_ref())?))) |
| 250 | } |
| 251 | // A lazy workspace: its files' sizes, and files as they're opened. |
| 252 | (Method::Get, ["trees", tree, "sizes"]) => { |
| 253 | let tree = Id::parse(tree).filter(|t| state.may_read_tree(key, access, t).unwrap_or(false)).ok_or_else(|| problem(404, "not_found", "no such tree"))?; |
| 254 | ok(json!({ "sizes": state.file_sizes(&tree)? })) |
| 255 | } |
| 256 | (Method::Post, ["trees", tree, "files"]) => { |
| 257 | let tree = Id::parse(tree).filter(|t| state.may_read_tree(key, access, t).unwrap_or(false)).ok_or_else(|| problem(404, "not_found", "no such tree"))?; |
| 258 | #[derive(serde::Deserialize)] |
| 259 | struct Paths { |
| 260 | paths: Vec<String>, |
| 261 | } |
| 262 | let p: Paths = json_body(request)?; |
| 263 | Ok((200, Body::Bytes(state.files_bundle(&tree, &p.paths)?))) |
| 264 | } |
| 265 | (Method::Get, ["changes"]) => ok(json!({ "changes": state.changes(key, access)? })), |
| 266 | (Method::Get, ["changes", prefix]) => { |
| 267 | let (change, saves) = state.find_change(key, access, prefix)?; |
| 268 | ok(json!({ "change": change, "saves": saves })) |
| 269 | } |
| 270 | (Method::Put, ["changes", id]) => { |
| 271 | let put: PutChange = json_body(request)?; |
| 272 | if put.change.id != *id { |
| 273 | return Err(problem(400, "bad_usage", "change id mismatch")); |
| 274 | } |
| 275 | let change = state.put_change(key, access, put.change, &put.workspace, &put.label)?; |
| 276 | if let Some(save) = &put.save { |
| 277 | state.add_save(key, access, id, &SaveRecord { workspace: put.workspace.clone(), ..save.clone() })?; |
| 278 | } |
| 279 | ok(json!({ "change": change })) |
| 280 | } |
| 281 | (Method::Post, ["changes", id, "hold"]) => { |
| 282 | let hold: Hold = json_body(request)?; |
| 283 | ok(json!({ "previous": state.hold(key, access, id, &hold.workspace, &hold.label)? })) |
| 284 | } |
| 285 | (Method::Post, ["changes", id, "saves"]) => { |
| 286 | let save: SaveRecord = json_body(request)?; |
| 287 | state.add_save(key, access, id, &save)?; |
| 288 | ok(json!({})) |
| 289 | } |
| 290 | (Method::Post, ["changes", id, "approve"]) => ok(state.approve(key, access, id)?), |
| 291 | // Agent sessions kept with changes (on sessions). |
| 292 | (Method::Put, ["changes", id, "sessions"]) => { |
| 293 | let row: onset_cli::db::SessionRow = json_body(request)?; |
| 294 | ok(state.keep_session(key, access, id, &row)?) |
| 295 | } |
| 296 | (Method::Get, ["changes", id, "sessions"]) => ok(json!({ "sessions": state.sessions_of(key, access, id)? })), |
| 297 | (Method::Post, ["changes", id, "sessions", "delete"]) => ok(json!({ "deleted": state.delete_sessions(key, access, Some(id))? })), |
| 298 | (Method::Get, ["changes", id, "sessions", n]) => { |
| 299 | let n: u64 = n.parse().map_err(|_| problem(400, "bad_usage", "sessions are numbered"))?; |
| 300 | Ok((200, Body::Bytes(state.session_bundle(key, access, id, n)?))) |
| 301 | } |
| 302 | (Method::Get, ["sessions"]) => ok(state.my_sessions(key, access)?), |
| 303 | (Method::Post, ["sessions", "delete"]) => ok(json!({ "deleted": state.delete_sessions(key, access, None)? })), |
| 304 | (Method::Get, ["settings"]) => ok(json!({ "approvals": state.approvals_required(key)? })), |
| 305 | (Method::Put, ["settings"]) => { |
| 306 | #[derive(serde::Deserialize)] |
| 307 | struct Settings { |
| 308 | approvals: Option<u32>, |
| 309 | } |
| 310 | let settings: Settings = json_body(request)?; |
| 311 | if let Some(n) = settings.approvals { |
| 312 | state.set_approvals(key, access, n)?; |
| 313 | } |
| 314 | ok(json!({ "approvals": state.approvals_required(key)? })) |
| 315 | } |
| 316 | (Method::Get, ["tags"]) => ok(json!({ "tags": state.db.tags(key)? })), |
| 317 | (Method::Post, ["tags"]) => { |
| 318 | #[derive(serde::Deserialize)] |
| 319 | struct NewTag { |
| 320 | name: String, |
| 321 | n: u64, |
| 322 | } |
| 323 | let t: NewTag = json_body(request)?; |
| 324 | ok(state.set_tag(key, access, &t.name, t.n)?) |
| 325 | } |
| 326 | (Method::Post, ["tags", "remove"]) => { |
| 327 | #[derive(serde::Deserialize)] |
| 328 | struct Name { |
| 329 | name: String, |
| 330 | } |
| 331 | let t: Name = json_body(request)?; |
| 332 | state.remove_tag(key, access, &t.name)?; |
| 333 | ok(json!({})) |
| 334 | } |
| 335 | (Method::Get, ["groups"]) => ok(json!({ "groups": state.groups(key)? })), |
| 336 | (Method::Put, ["groups"]) => { |
| 337 | #[derive(serde::Deserialize)] |
| 338 | struct Group { |
| 339 | name: String, |
| 340 | #[serde(default)] |
| 341 | members: Vec<String>, |
| 342 | } |
| 343 | let group: Group = json_body(request)?; |
| 344 | let name = group.name.trim_start_matches('@'); |
| 345 | let valid = |s: &str| !s.is_empty() && s.len() <= 64 && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'); |
| 346 | if !valid(name) || ["team", "everyone", "me"].contains(&name) || !group.members.iter().all(|m| valid(m.trim_start_matches('@'))) { |
| 347 | return Err(problem(400, "bad_usage", "group names and members are letters, digits, - and _")); |
| 348 | } |
| 349 | ok(json!({ "groups": state.set_group(key, access, name, &group.members)? })) |
| 350 | } |
| 351 | (Method::Post, ["changes", id, "access"]) => { |
| 352 | let share: ShareRequest = json_body(request)?; |
| 353 | ok(json!({ "shared": state.share(key, access, id, &share.who, share.edit)? })) |
| 354 | } |
| 355 | (Method::Post, ["import"]) => { |
| 356 | #[derive(serde::Deserialize)] |
| 357 | struct Import { |
| 358 | versions: Vec<onset_core::version::Version>, |
| 359 | } |
| 360 | let import: Import = json_body(request)?; |
| 361 | let main = state.import(key, access, import.versions)?; |
| 362 | ok(json!({ "main": main })) |
| 363 | } |
| 364 | // This server has no git view: git IDs from an import aren't needed. |
| 365 | (Method::Post, ["git", "ids"]) => { |
| 366 | if !access.can.admin { |
| 367 | return Err(problem(403, "not_allowed", "only the project's owners can do that")); |
| 368 | } |
| 369 | let _ = read_body(request)?; |
| 370 | ok(json!({ "added": 0 })) |
| 371 | } |
| 372 | (Method::Post, ["git", "resume"]) => Err(problem(400, "bad_usage", "this server has no git views")), |
| 373 | (Method::Post, ["check"]) => ok(state.check(key, access)?), |
| 374 | (Method::Post, ["rollback"]) => Err(problem(400, "bad_usage", "this server keeps no earlier copies to roll back to; back up its data folder")), |
| 375 | (Method::Post, ["edits"]) => { |
| 376 | // Landing without a workspace: file edits on a version, in one call. |
| 377 | #[derive(serde::Deserialize)] |
| 378 | struct Edits { |
| 379 | #[serde(default)] |
| 380 | base: Option<u64>, |
| 381 | #[serde(default)] |
| 382 | title: String, |
| 383 | #[serde(default)] |
| 384 | body: String, |
| 385 | #[serde(default)] |
| 386 | files: std::collections::BTreeMap<String, Option<String>>, |
| 387 | #[serde(default)] |
| 388 | files_base64: std::collections::BTreeMap<String, String>, |
| 389 | #[serde(default)] |
| 390 | executable: Vec<String>, |
| 391 | #[serde(default)] |
| 392 | allow_secrets: bool, |
| 393 | /// The caller's own key for these edits: sending it again answers as the first time. |
| 394 | #[serde(default)] |
| 395 | key: Option<String>, |
| 396 | } |
| 397 | use base64::Engine; |
| 398 | let edits: Edits = json_body(request)?; |
| 399 | let title = edits.title.trim(); |
| 400 | if title.is_empty() { |
| 401 | return Err(problem(400, "bad_usage", "edits need a title")); |
| 402 | } |
| 403 | let kind = |p: &str| if edits.executable.iter().any(|e| e == p) { onset_core::tree::EntryKind::Exec } else { onset_core::tree::EntryKind::File }; |
| 404 | let mut put = Vec::new(); |
| 405 | let mut delete = Vec::new(); |
| 406 | for (path, text) in &edits.files { |
| 407 | match text { |
| 408 | Some(t) => put.push(onset_core::tree::Put { path: path.clone(), kind: kind(path), content: t.clone().into_bytes() }), |
| 409 | None => delete.push(path.clone()), |
| 410 | } |
| 411 | } |
| 412 | for (path, b64) in &edits.files_base64 { |
| 413 | let content = base64::engine::general_purpose::STANDARD.decode(b64).map_err(|_| problem(400, "bad_usage", format!("{path}: content isn't base64")))?; |
| 414 | put.push(onset_core::tree::Put { path: path.clone(), kind: kind(path), content }); |
| 415 | } |
| 416 | if put.is_empty() && delete.is_empty() { |
| 417 | return Err(problem(400, "bad_usage", "send at least one file")); |
| 418 | } |
| 419 | let (change, landed) = state.land_edits(key, access, edits.base, put, delete, title, &edits.body, edits.allow_secrets, edits.key.as_deref())?; |
| 420 | let mut out = serde_json::to_value(&landed).map_err(|e| problem(500, "failed", e.to_string()))?; |
| 421 | out["change"] = json!(change); |
| 422 | ok(out) |
| 423 | } |
| 424 | (Method::Post, ["land"]) => { |
| 425 | let land: LandRequest = json_body(request)?; |
| 426 | // The change's record and newest save come along with the landing. |
| 427 | if let Some(put) = &land.record { |
| 428 | if put.change.id != land.change { |
| 429 | return Err(problem(400, "bad_usage", "change id mismatch")); |
| 430 | } |
| 431 | state.put_change(key, access, put.change.clone(), &put.workspace, &put.label)?; |
| 432 | if let Some(save) = &put.save { |
| 433 | state.add_save(key, access, &land.change, &SaveRecord { workspace: put.workspace.clone(), ..save.clone() })?; |
| 434 | } |
| 435 | } |
| 436 | ok(state.land(key, access, &land)?) |
| 437 | } |
| 438 | (Method::Get, ["rules"]) => ok(json!({ "rules": state.rules(key)? })), |
| 439 | (Method::Put, ["rules"]) => { |
| 440 | let rule: Rule = json_body(request)?; |
| 441 | ok(json!({ "rules": state.set_rule(key, access, &rule)? })) |
| 442 | } |
| 443 | (Method::Get, ["mine"]) => { |
| 444 | need_user(access)?; |
| 445 | ok(json!({ "files": state.mine(key, &access.user.as_ref().unwrap().handle)? })) |
| 446 | } |
| 447 | (Method::Put, ["mine"]) => { |
| 448 | need_user(access)?; |
| 449 | #[derive(serde::Deserialize)] |
| 450 | struct Files { |
| 451 | files: Vec<MineFile>, |
| 452 | } |
| 453 | let files: Files = json_body(request)?; |
| 454 | state.set_mine(key, &access.user.as_ref().unwrap().handle, &files.files)?; |
| 455 | ok(json!({})) |
| 456 | } |
| 457 | (Method::Get, ["mine", "bundle"]) => { |
| 458 | need_user(access)?; |
| 459 | let files = state.mine(key, &access.user.as_ref().unwrap().handle)?; |
| 460 | let mut ids = Vec::new(); |
| 461 | for f in files { |
| 462 | ids.extend(state.store.file_parts(&f.file)?); |
| 463 | } |
| 464 | Ok((200, Body::Bytes(state.store.bundle(&ids)?))) |
| 465 | } |
| 466 | _ => Err(Problem { status: 404, code: "not_found", message: "no such endpoint".into(), holder: None }), |
| 467 | } |
| 468 | } |
| 469 |