0 ? $size : max(1, min(200, (int)($_GET['limit'] ?? 50))); $offset = $page > 0 ? ($page - 1) * $limit : max(0, (int)($_GET['offset'] ?? 0)); if ($page > 0) { $list = FileService::list($u['id'], $limit, $offset); Json::ok([ 'items' => $list, 'total' => FileService::fileCount($u['id']), 'page' => $page, 'size' => $limit, ]); return; } $list = FileService::list($u['id'], $limit, $offset); Json::ok([ 'items' => $list, 'total' => FileService::fileCount($u['id']), 'limit' => $limit, 'offset' => $offset, ]); } public function create(): void { $u = Auth::require(); MembershipService::requireActiveMember($u['id']); $body = Json::readBody(); $name = (string)($body['name'] ?? 'untitled.soon'); $json = (string)($body['json'] ?? '{}'); Json::ok(FileService::create($u['id'], $name, $json)); } public function update(int $id): void { $u = Auth::require(); MembershipService::requireActiveMember($u['id']); $body = Json::readBody(); $name = (string)($body['name'] ?? 'untitled.soon'); $json = (string)($body['json'] ?? '{}'); $version = isset($body['version']) ? (int)$body['version'] : null; Json::ok(FileService::update($u['id'], $id, $name, $json, $version)); } public function delete(int $id): void { $u = Auth::require(); FileService::softDelete($u['id'], $id); Json::ok(['id' => $id]); } public function download(int $id): void { $u = Auth::require(); $accept = (string)($_SERVER['HTTP_ACCEPT'] ?? ''); $jsonRead = stripos($accept, 'application/json') !== false && stripos($accept, 'application/octet-stream') === false; if (!$jsonRead) { MembershipService::requireActiveMember($u['id']); } $row = FileService::fetch($u['id'], $id); $name = (string)$row['name']; $json = (string)$row['json']; $size = strlen($json); $etag = '"' . md5($json) . '"'; if (isset($_SERVER['HTTP_IF_NONE_MATCH']) && trim($_SERVER['HTTP_IF_NONE_MATCH']) === $etag) { http_response_code(304); exit; } header('Content-Type: application/octet-stream'); header('Content-Disposition: attachment; filename="' . rawurlencode($name) . '"'); header('Content-Length: ' . $size); header('ETag: ' . $etag); header('Accept-Ranges: bytes'); header('Cache-Control: private, max-age=0, must-revalidate'); $start = 0; $end = $size - 1; if (isset($_SERVER['HTTP_RANGE']) && preg_match('/bytes=(\d+)-(\d*)/', $_SERVER['HTTP_RANGE'], $m)) { $start = (int)$m[1]; if ($m[2] !== '') $end = (int)$m[2]; if ($end >= $size) $end = $size - 1; http_response_code(206); header('Content-Range: bytes ' . $start . '-' . $end . '/' . $size); header('Content-Length: ' . ($end - $start + 1)); } $out = fopen('php://output', 'wb'); fwrite($out, substr($json, $start, $end - $start + 1)); fclose($out); exit; } }