12. Your files
You can upload a file, see it listed, and download it again unchanged.
storage keeps each person's files (B2-backed) behind their sign-in; an app links to it and never holds storage's keys. The check here uploads random bytes, lists them, downloads them and compares.
This is the code serving the zoo right now: read from disk for this request, from commit
77ff74344d (staging). The zoo's own self-check fetches this
page and compares it byte for byte with the file it runs.
exhibits/12-your-files.php sha256 1b6381289153 · raw
1<?php 2// Exhibit 12. A person uploads in storage's own page. The zoo proves the same path works: it asks 3// storage's /app/roundtrip to run the browser's upload (begin, presigned PUT to B2, commit, list), 4// then downloads what storage hands back and compares the bytes with what it sent. 5// call_app("storage", "/app/roundtrip", true, "POST", $bytes) // rc_app_headers("storage", true) 6// The check does this as the robot, through our own /api/12/roundtrip. 7 8function roundtrip_once(): array { 9 $bytes = random_bytes(4096 + random_int(0, 4096)); 10 [$s, $out] = call_app("storage", "/app/roundtrip?path=" . rawurlencode("zoo-roundtrip.bin"), true, "POST", $bytes, ["Content-Type: application/octet-stream"]); 11 $r = json_decode($out, true); 12 if ($s !== 200 || !is_array($r)) return ["error" => "storage's roundtrip answered $s: " . substr($out, 0, 160), "status" => 502]; 13 if (!($r["listed"] ?? false)) return ["error" => "uploaded, but the file was not in the listing", "status" => 502]; 14 // The download is a presigned B2 URL, fetched like a browser would: no assertion on it. 15 $c = curl_init($r["download"]); 16 curl_setopt_array($c, [CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_TIMEOUT => 30]); 17 $back = (string)curl_exec($c); 18 $code = (int)curl_getinfo($c, CURLINFO_HTTP_CODE); 19 if ($code !== 200) return ["error" => "the download answered $code", "status" => 502]; 20 $same = hash_equals(hash("sha256", $bytes), hash("sha256", $back)); 21 return ["size" => strlen($bytes), "got" => strlen($back), "matched" => $same, "sha256" => substr(hash("sha256", $bytes), 0, 12)]; 22} 23 24return [ 25 "n" => 12, "wing" => "Storage", "kind" => "self", 26 "title" => "Your files", 27 "promise" => "You can upload a file, see it listed, and download it again unchanged.", 28 "block" => "storage keeps each person's files (B2-backed) behind their sign-in; an app links to it and never holds storage's keys. The check here uploads random bytes, lists them, downloads them and compares.", 29 "show" => function (?array $me): string { 30 $s = stored(12); 31 $last = $s && ($s["data"]["matched"] ?? false) 32 ? '<p>Last roundtrip, ' . ago((int)$s["at"]) . ': <b>' . (int)$s["data"]["size"] . ' bytes</b> uploaded, listed and downloaded again: <span class="tag ok">matched</span></p>' 33 : '<p class="muted">No matching roundtrip recorded yet.</p>'; 34 return $last . '<p><a href="' . h(rc_app_url("storage")) . '/" target="_blank" rel="noopener">Open storage</a>, upload something, and download it again.</p>'; 35 }, 36 "api" => function (string $do, ?array $me, array $in, bool $post): ?array { 37 if ($do !== "roundtrip" || !$post) return null; 38 if (!$me) return ["error" => "sign in first", "status" => 401]; 39 return roundtrip_once(); 40 }, 41 // The robot asks the zoo to run the roundtrip as it (so the assertion names the robot). 42 "check" => function (): array { 43 require_once ZOO_ROOT . "/lib/robot.php"; 44 [$code, , $j] = robot_at_zoo()->post("https://" . env("RC_HOST") . "/api/12/roundtrip"); 45 if ($code !== 200 || !is_array($j)) return [false, "the roundtrip failed ($code): " . (is_array($j) ? ($j["error"] ?? "") : substr((string)$j, 0, 120))]; 46 if (!$j["matched"]) return [false, "the downloaded copy differs from what was uploaded ({$j['size']} bytes up, {$j['got']} down)", $j]; 47 return [true, "uploaded {$j['size']} random bytes, listed them, downloaded them again: matched (sha256 {$j['sha256']})", $j]; 48 }, 49];