|
@@ -238,6 +238,17 @@ function gallery_archive_buffer(string $slug): string
|
|
|
return gallery_path($slug, '.archive.buf');
|
|
return gallery_path($slug, '.archive.buf');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/**
|
|
|
|
|
+ * The gallery's visit counters. Deliberately a sidecar rather than fields in
|
|
|
|
|
+ * the gallery file: every visitor writes it, and the gallery file — hundreds of
|
|
|
|
|
+ * image entries — would be rewritten in full on each page view, in contention
|
|
|
|
|
+ * with uploads and archive builds.
|
|
|
|
|
+ */
|
|
|
|
|
+function gallery_stats_file(string $slug): string
|
|
|
|
|
+{
|
|
|
|
|
+ return gallery_path($slug, '.stats.json');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function gallery_load(string $slug): ?array
|
|
function gallery_load(string $slug): ?array
|
|
|
{
|
|
{
|
|
|
try {
|
|
try {
|
|
@@ -268,6 +279,8 @@ function gallery_delete(string $slug): void
|
|
|
}
|
|
}
|
|
|
@unlink($file . '.lock');
|
|
@unlink($file . '.lock');
|
|
|
@unlink(gallery_archive_file($slug) . '.lock');
|
|
@unlink(gallery_archive_file($slug) . '.lock');
|
|
|
|
|
+ @unlink(gallery_stats_file($slug));
|
|
|
|
|
+ @unlink(gallery_stats_file($slug) . '.lock');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -296,13 +309,72 @@ function gallery_append_image(string $slug, array $image): ?int
|
|
|
return count($gallery['images'] ?? []);
|
|
return count($gallery['images'] ?? []);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/** Counter name => the timestamp field recording when it last moved. */
|
|
|
|
|
+const GALLERY_COUNTERS = [
|
|
|
|
|
+ 'views' => 'last_viewed_at',
|
|
|
|
|
+ 'downloads' => 'last_download_at',
|
|
|
|
|
+];
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Count one event on a gallery. Silently does nothing for an unknown slug or
|
|
|
|
|
+ * counter, so a stray link cannot litter the data directory with stats for
|
|
|
|
|
+ * galleries that never existed.
|
|
|
|
|
+ */
|
|
|
|
|
+function gallery_record_hit(string $slug, string $counter): void
|
|
|
|
|
+{
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!isset(GALLERY_COUNTERS[$counter]) || !is_file(gallery_file($slug))) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (InvalidArgumentException) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ json_update(gallery_stats_file($slug), function (array $stats) use ($counter) {
|
|
|
|
|
+ $stats[$counter] = (int)($stats[$counter] ?? 0) + 1;
|
|
|
|
|
+ $stats[GALLERY_COUNTERS[$counter]] = date('Y-m-d H:i:s');
|
|
|
|
|
+ return $stats;
|
|
|
|
|
+ });
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** One gallery opened by a visitor. */
|
|
|
|
|
+function gallery_record_view(string $slug): void
|
|
|
|
|
+{
|
|
|
|
|
+ gallery_record_hit($slug, 'views');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/** One ZIP archive handed out to a visitor. */
|
|
|
|
|
+function gallery_record_download(string $slug): void
|
|
|
|
|
+{
|
|
|
|
|
+ gallery_record_hit($slug, 'downloads');
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * A gallery's stats, with every counter present. Galleries that were never
|
|
|
|
|
+ * opened simply read as zero, with null timestamps.
|
|
|
|
|
+ */
|
|
|
|
|
+function gallery_stats(string $slug): array
|
|
|
|
|
+{
|
|
|
|
|
+ try {
|
|
|
|
|
+ $stored = json_read(gallery_stats_file($slug));
|
|
|
|
|
+ } catch (InvalidArgumentException) {
|
|
|
|
|
+ $stored = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ $out = [];
|
|
|
|
|
+ foreach (GALLERY_COUNTERS as $counter => $timestamp) {
|
|
|
|
|
+ $out[$counter] = (int)($stored[$counter] ?? 0);
|
|
|
|
|
+ $out[$timestamp] = $stored[$timestamp] ?? null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return $out;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
/** All galleries, newest first. */
|
|
/** All galleries, newest first. */
|
|
|
function galleries_all(): array
|
|
function galleries_all(): array
|
|
|
{
|
|
{
|
|
|
$out = [];
|
|
$out = [];
|
|
|
foreach (glob(DATA_DIR . '/galleries/*.json') ?: [] as $file) {
|
|
foreach (glob(DATA_DIR . '/galleries/*.json') ?: [] as $file) {
|
|
|
- // Skip the archive-build sidecars that live in the same directory.
|
|
|
|
|
- if (str_ends_with($file, '.archive.json')) {
|
|
|
|
|
|
|
+ // Skip the sidecars (archive build state, visit counter) that live in
|
|
|
|
|
+ // the same directory and match the same glob.
|
|
|
|
|
+ if (str_ends_with($file, '.archive.json') || str_ends_with($file, '.stats.json')) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
$g = json_read($file);
|
|
$g = json_read($file);
|