summaryrefslogtreecommitdiffstats
path: root/web/index.php
diff options
context:
space:
mode:
Diffstat (limited to 'web/index.php')
-rw-r--r--web/index.php144
1 files changed, 131 insertions, 13 deletions
diff --git a/web/index.php b/web/index.php
index 361fdcf..fd0a396 100644
--- a/web/index.php
+++ b/web/index.php
@@ -19,9 +19,10 @@ declare(strict_types=1);
19 * bin/client/zagan-bet-client-3.1.1-linux-x86_64 19 * bin/client/zagan-bet-client-3.1.1-linux-x86_64
20 * bin/server/zagan-bet-server-3.1.1-linux-x86_64 20 * bin/server/zagan-bet-server-3.1.1-linux-x86_64
21 * 21 *
22 * The hashes are computed per request with hash_file(). That is a few tens of 22 * SHA-256 values are cached in a JSON file keyed on size and mtime, so a file
23 * milliseconds for a handful of builds; add a cache keyed on mtime if the 23 * is only re-read when it changes. The cache lives in the system temp
24 * directory ever grows. 24 * directory by default (the web root is often not writable by the PHP user);
25 * set ZAGAN_HASH_CACHE to put it somewhere else.
25 */ 26 */
26 27
27const KINDS = [ 28const KINDS = [
@@ -68,10 +69,99 @@ function human_size(int $bytes): string
68} 69}
69 70
70/** 71/**
71 * @return array{version:string,platform:string,arch:string,file:string,size:int,mtime:int,sha256:string}|null 72 * Hash to use for a published file, reusing the cached value when the file has
73 * not changed since it was hashed.
74 *
75 * A cached hash is valid only when the size and mtime still match and the hash
76 * was taken at or after the file's mtime, so a restored or touched file is
77 * re-read.
78 *
79 * @param array{files:array<string,array{size:int,mtime:int,sha256:string,hashed_at:int}>} $cache
80 */
81function cached_hash(
82 string $key,
83 string $path,
84 int $size,
85 int $mtime,
86 array &$cache,
87 bool &$dirty
88): ?string {
89 $entry = $cache['files'][$key] ?? null;
90 if (
91 is_array($entry)
92 && ($entry['size'] ?? null) === $size
93 && ($entry['mtime'] ?? null) === $mtime
94 && (int) ($entry['hashed_at'] ?? 0) >= $mtime
95 && isset($entry['sha256'])
96 && is_string($entry['sha256'])
97 && preg_match('/\A[0-9a-f]{64}\z/', $entry['sha256']) === 1
98 ) {
99 return $entry['sha256'];
100 }
101
102 $hash = hash_file('sha256', $path);
103 if ($hash === false) {
104 return null;
105 }
106
107 $cache['files'][$key] = [
108 'size' => $size,
109 'mtime' => $mtime,
110 'sha256' => $hash,
111 'hashed_at' => time(),
112 ];
113 $dirty = true;
114
115 return $hash;
116}
117
118/** @return array{files:array<string,array{size:int,mtime:int,sha256:string,hashed_at:int}>} */
119function load_hash_cache(string $path): array
120{
121 $raw = @file_get_contents($path);
122 if ($raw === false || $raw === '') {
123 return ['files' => []];
124 }
125 $decoded = json_decode($raw, true);
126 if (!is_array($decoded) || !isset($decoded['files']) || !is_array($decoded['files'])) {
127 return ['files' => []];
128 }
129
130 return ['files' => $decoded['files']];
131}
132
133/**
134 * Best effort: a read-only web root, a full disk or a racing writer must never
135 * break the page, so failures are ignored and the next request recomputes.
136 *
137 * @param array{files:array<string,array{size:int,mtime:int,sha256:string,hashed_at:int}>} $cache
72 */ 138 */
73function read_release(string $kind, string $directory, string $entry): ?array 139function save_hash_cache(string $path, array $cache): void
74{ 140{
141 $json = json_encode($cache, JSON_UNESCAPED_SLASHES);
142 if ($json === false) {
143 return;
144 }
145 $directory = dirname($path);
146 $temporary = @tempnam($directory, '.zagan-hashes-');
147 if ($temporary === false) {
148 return;
149 }
150 if (@file_put_contents($temporary, $json, LOCK_EX) === false || !@rename($temporary, $path)) {
151 @unlink($temporary);
152 }
153}
154
155/**
156 * @return array{version:string,platform:string,arch:string,file:string,size:int,mtime:int,sha256:string}|null
157 */
158function read_release(
159 string $kind,
160 string $directory,
161 string $entry,
162 array &$cache,
163 bool &$dirty
164): ?array {
75 $pattern = '/^zagan-bet-' . preg_quote($kind, '/') 165 $pattern = '/^zagan-bet-' . preg_quote($kind, '/')
76 . '-v?(\d+\.\d+\.\d+)-([a-z]+)-([a-z0-9_]+)(\.exe)?$/i'; 166 . '-v?(\d+\.\d+\.\d+)-([a-z]+)-([a-z0-9_]+)(\.exe)?$/i';
77 if (preg_match($pattern, $entry, $matches) !== 1) { 167 if (preg_match($pattern, $entry, $matches) !== 1) {
@@ -92,8 +182,13 @@ function read_release(string $kind, string $directory, string $entry): ?array
92 } 182 }
93 183
94 $size = filesize($real); 184 $size = filesize($real);
95 $hash = hash_file('sha256', $real); 185 $mtime = filemtime($real);
96 if ($size === false || $hash === false) { 186 if ($size === false || $mtime === false) {
187 return null;
188 }
189
190 $hash = cached_hash($kind . '/' . $entry, $real, $size, $mtime, $cache, $dirty);
191 if ($hash === null) {
97 return null; 192 return null;
98 } 193 }
99 194
@@ -103,7 +198,7 @@ function read_release(string $kind, string $directory, string $entry): ?array
103 'arch' => strtolower($matches[3]), 198 'arch' => strtolower($matches[3]),
104 'file' => $entry, 199 'file' => $entry,
105 'size' => $size, 200 'size' => $size,
106 'mtime' => filemtime($real) ?: 0, 201 'mtime' => $mtime,
107 'sha256' => $hash, 202 'sha256' => $hash,
108 ]; 203 ];
109} 204}
@@ -113,6 +208,16 @@ if ($base === '.' || $base === '/') {
113 $base = ''; 208 $base = '';
114} 209}
115 210
211$cachePath = getenv('ZAGAN_HASH_CACHE');
212if (!is_string($cachePath) || $cachePath === '') {
213 $cachePath = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
214 . DIRECTORY_SEPARATOR . 'zagan-bet-hashes-'
215 . substr(hash('sha256', __DIR__), 0, 16) . '.json';
216}
217$cache = load_hash_cache($cachePath);
218$cacheDirty = false;
219$seen = [];
220
116$releases = []; 221$releases = [];
117$newest = 0; 222$newest = 0;
118 223
@@ -127,10 +232,11 @@ foreach (KINDS as $kind => $meta) {
127 if ($entry === '.' || $entry === '..') { 232 if ($entry === '.' || $entry === '..') {
128 continue; 233 continue;
129 } 234 }
130 $release = read_release($kind, $directory, $entry); 235 $release = read_release($kind, $directory, $entry, $cache, $cacheDirty);
131 if ($release === null) { 236 if ($release === null) {
132 continue; 237 continue;
133 } 238 }
239 $seen[$kind . '/' . $entry] = true;
134 $release['href'] = $base . '/bin/' . $kind . '/' . rawurlencode($release['file']); 240 $release['href'] = $base . '/bin/' . $kind . '/' . rawurlencode($release['file']);
135 $found[] = $release; 241 $found[] = $release;
136 $newest = max($newest, $release['mtime']); 242 $newest = max($newest, $release['mtime']);
@@ -156,19 +262,30 @@ foreach (KINDS as $kind => $meta) {
156 262
157 $releases[$kind] = $found; 263 $releases[$kind] = $found;
158} 264}
265
266// Drop cache entries for files that are no longer published.
267foreach (array_keys($cache['files']) as $key) {
268 if (!isset($seen[$key])) {
269 unset($cache['files'][$key]);
270 $cacheDirty = true;
271 }
272}
273if ($cacheDirty) {
274 save_hash_cache($cachePath, $cache);
275}
159?> 276?>
160<!doctype html> 277<!doctype html>
161<html lang="en"> 278<html lang="en">
162<head> 279<head>
163<meta charset="utf-8"> 280<meta charset="utf-8">
164<meta name="viewport" content="width=device-width, initial-scale=1"> 281<meta name="viewport" content="width=device-width, initial-scale=1">
165<title>Download · zagan.bet</title> 282<title>żagań.bet</title>
166<meta name="description" content="Official zagan.bet client and server downloads."> 283<meta name="description" content="Official zagan.bet client and server downloads.">
167<meta http-equiv="Content-Security-Policy" content="default-src 'none'"> 284<meta http-equiv="Content-Security-Policy" content="default-src 'none'">
168</head> 285</head>
169<body> 286<body>
170 287
171<h1>zagan.bet</h1> 288<h1>żagań.bet</h1>
172<p>Official client and server builds.</p> 289<p>Official client and server builds.</p>
173<?php if ($newest > 0): ?> 290<?php if ($newest > 0): ?>
174<p>Builds published <?= e(gmdate('j M Y H:i', $newest)) ?> UTC.</p> 291<p>Builds published <?= e(gmdate('j M Y H:i', $newest)) ?> UTC.</p>
@@ -181,7 +298,7 @@ foreach (KINDS as $kind => $meta) {
181<?php else: ?> 298<?php else: ?>
182<ul> 299<ul>
183<?php foreach ($releases['client'] as $release): ?> 300<?php foreach ($releases['client'] as $release): ?>
184<li><a href="<?= e($release['href']) ?>" download><?= e(PLATFORMS[$release['platform']] ?? ucfirst($release['platform'])) ?> <?= e($release['arch']) ?><?php if (isset(ARCHES[$release['arch']])): ?> (<?= e(ARCHES[$release['arch']]) ?>)<?php endif; ?> — version <?= e($release['version']) ?> — <?= e(human_size($release['size'])) ?></a></li> 301<li><a href="<?= e($release['href']) ?>" download><?= e(PLATFORMS[$release['platform']] ?? ucfirst($release['platform'])) ?> <?= e($release['arch']) ?><?php if (isset(ARCHES[$release['arch']])): ?> (<?= e(ARCHES[$release['arch']]) ?>)<?php endif; ?> v<?= e($release['version']) ?> — <?= e(human_size($release['size'])) ?></a></li>
185<?php endforeach; ?> 302<?php endforeach; ?>
186</ul> 303</ul>
187<?php endif; ?> 304<?php endif; ?>
@@ -193,7 +310,7 @@ foreach (KINDS as $kind => $meta) {
193<?php else: ?> 310<?php else: ?>
194<ul> 311<ul>
195<?php foreach ($releases['server'] as $release): ?> 312<?php foreach ($releases['server'] as $release): ?>
196<li><a href="<?= e($release['href']) ?>" download><?= e(PLATFORMS[$release['platform']] ?? ucfirst($release['platform'])) ?> <?= e($release['arch']) ?><?php if (isset(ARCHES[$release['arch']])): ?> (<?= e(ARCHES[$release['arch']]) ?>)<?php endif; ?> — version <?= e($release['version']) ?> — <?= e(human_size($release['size'])) ?></a></li> 313<li><a href="<?= e($release['href']) ?>" download><?= e(PLATFORMS[$release['platform']] ?? ucfirst($release['platform'])) ?> <?= e($release['arch']) ?><?php if (isset(ARCHES[$release['arch']])): ?> (<?= e(ARCHES[$release['arch']]) ?>)<?php endif; ?> v<?= e($release['version']) ?> — <?= e(human_size($release['size'])) ?></a></li>
197<?php endforeach; ?> 314<?php endforeach; ?>
198</ul> 315</ul>
199<?php endif; ?> 316<?php endif; ?>
@@ -222,5 +339,6 @@ echo e($sums);
222 339
223<p>Windows: run the <code>.exe</code> from Windows Terminal or another real terminal.</p> 340<p>Windows: run the <code>.exe</code> from Windows Terminal or another real terminal.</p>
224 341
342<p>When lost, execute <code>help</code> command inside zagan.bet client. Server binary supports <code>--help</code> flag</p>
225</body> 343</body>
226</html> 344</html>