PNG bytes) for the signed-in person and read every one back: // begin all, PUT all to B2, commit all, ask for all their URLs, GET them, compare. function save_cards(array $cards): array { $paths = array_keys($cards); [$s, $r] = storage_call("begin", ["files" => array_map(fn($p) => ["path" => $p, "size" => strlen($cards[$p]), "type" => "image/png"], $paths)]); $beg = $r["files"] ?? []; if ($s !== 200 || count(array_filter($beg, fn($f) => isset($f["url"]))) !== count($cards)) return ["error" => "storage would not begin them ($s): " . json_encode($r["error"] ?? $beg[0] ?? $r), "status" => 502]; $puts = put_many(array_map(fn($f, $p) => [$f["url"], $cards[$p], $f["type"]], $beg, $paths)); if (count(array_filter($puts, fn($c) => $c === 200)) !== count($cards)) return ["error" => "B2 refused some PUTs: " . json_encode(array_count_values($puts)), "status" => 502]; [$s, $r] = storage_call("commit", ["ids" => array_column($beg, "id")]); $files = array_column(array_filter($r["files"] ?? [], fn($f) => isset($f["file"])), "file"); if ($s !== 200 || count($files) !== count($cards)) return ["error" => "storage would not commit them all ($s): " . json_encode($r["error"] ?? $r["files"][0] ?? $r), "status" => 502]; [$s, $r] = storage_call("urls", ["ids" => array_column($files, "id")]); $urls = array_column($r["files"] ?? [], "url"); if ($s !== 200 || count($urls) !== count($cards)) return ["error" => "storage would not hand out their URLs ($s)", "status" => 502]; $back = get_many($urls); $matched = 0; foreach ($paths as $i => $p) if (($back[$i][0] ?? 0) === 200 && hash_equals(hash("sha256", $cards[$p]), hash("sha256", $back[$i][1]))) $matched++; return ["files" => $files, "matched" => $matched, "back" => $back]; } function postcard(array $me): array { $png = postcard_png($me); $r = save_cards(["postcards/postcard.png" => $png]); if (isset($r["error"])) return $r; $f = $r["files"][0]; return ["file" => ["id" => $f["id"], "path" => "Apps/Zoo/$f[path]"], "bytes" => strlen($png), "matched" => $r["matched"] === 1, "image" => "data:image/png;base64," . base64_encode($r["back"][0][1])]; } // The check's batch: 50 postcards in one begin, one commit and one urls call, then listed. function postcards_50(array $me): array { $t = microtime(true); $cards = []; for ($i = 1; $i <= 50; $i++) $cards[sprintf("postcards/check/%02d.png", $i)] = postcard_png($me, sprintf("no. %02d of 50", $i)); $r = save_cards($cards); if (isset($r["error"])) return $r; $listed = []; for ($after = 0, $pages = 0; $after !== null && $pages < 20; $pages++) { [$s, $l] = storage_call("list", ["after" => $after, "limit" => 1000]); foreach ($l["files"] ?? [] as $f) $listed[$f["path"]] = true; $after = $l["next"] ?? null; } $ids = array_column($r["files"], "id"); return ["count" => count($cards), "matched" => $r["matched"], "listed" => count(array_intersect_key($listed, $cards)), "path" => "Apps/Zoo/postcards/check/", "ids" => [min($ids), max($ids)], "ms" => (int)((microtime(true) - $t) * 1000)]; } return [ "n" => 13, "try" => "save a postcard", "wing" => "Storage", "kind" => "self", "title" => "Apps can save files for you", "promise" => "An app can save a file into your storage, under its own folder, without ever holding storage's keys.", "block" => 'rc_app_headers("storage", true) proves to storage which app calls and for whom; storage saves only under Apps / . In bulk: app/begin gives presigned PUTs (200 a call), the bytes go straight to B2, app/commit checks them all at once, app/urls hands out downloads.', "files" => ["lib/bulk.php"], "show" => function (?array $me): string { $link = 'your storage, under Apps / Zoo'; $s = stored(13); $last = $s && $s["ok"] ? '

The self-check saves 50 postcards for the robot each time: one begin, 50 PUTs straight to B2, one commit, one call for their 50 URLs, ' . (int)($s["data"]["ms"] ?? 0) . ' ms in all, ' . ago((int)$s["at"]) . '.

' : ""; if (!$me) return '

Sign in to have the zoo save a postcard for you.

' . $last; return '

' . '

It appears in ' . $link . '.

' . $last; }, "api" => function (string $do, ?array $me, array $in, bool $post): ?array { if (!in_array($do, ["postcard", "postcards"], true) || !$post) return null; if (!$me) return ["error" => "sign in first", "status" => 401]; return $do === "postcard" ? postcard($me) : postcards_50($me); }, // The robot has the zoo save 50 postcards into the robot's storage in one batch, read them all // back and find them in the listing. "check" => function (): array { require_once ZOO_ROOT . "/lib/robot.php"; [$code, , $j] = robot_at_zoo()->post("https://" . env("RC_HOST") . "/api/13/postcards"); if ($code !== 200 || !is_array($j)) return [false, "saving 50 postcards failed ($code): " . (is_array($j) ? ($j["error"] ?? "") : substr((string)$j, 0, 120))]; if ($j["matched"] !== 50) return [false, "only {$j['matched']} of 50 postcards came back unchanged", $j]; if ($j["listed"] !== 50) return [false, "only {$j['listed']} of 50 postcards are in the zoo's listing", $j]; return [true, "saved 50 postcards under {$j['path']} for the robot (one begin, PUTs straight to B2, one commit, one call for 50 URLs), fetched all 50 back unchanged and found them listed, in {$j['ms']} ms", $j]; }, "script" => <<<'JS' document.getElementById("postcard")?.addEventListener("click", async (ev) => { const out = document.getElementById("postcard-out"); ev.target.disabled = true; out.textContent = "saving…"; const j = await zoo.call("/api/13/postcard", {}); ev.target.disabled = false; if (j.error) { out.textContent = j.error; return; } out.textContent = "saved to " + j.file.path + " (file " + j.file.id + ", " + j.bytes + " bytes)" + (j.matched ? ", fetched back unchanged" : ", but the copy differs"); document.getElementById("postcard-img").innerHTML = 'your postcard'; }); JS, ];