Plan on-demand map unpacking instead of unpacking every zip upfront
Nothing on the mirror's read path reads unpacked maps, and ArcViewer loads the zips directly, so phase 1 step 9 is dropped and a deferred phase 2 step 9 describes a small on-demand adapter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+36
-12
@@ -18,7 +18,7 @@ Written 2026-11-08. The numbers under "Facts" were checked live on that date.
|
|||||||
| Source of truth | A raw, append-only **archive** of unmodified API responses and replay files. The SQLite database can always be rebuilt from it, and so can any future replacement system. |
|
| Source of truth | A raw, append-only **archive** of unmodified API responses and replay files. The SQLite database can always be rebuilt from it, and so can any future replacement system. |
|
||||||
| Exporter and sync | TypeScript in this repo, building on `src/lib/server/beatleader.ts`, `beatleaderAuth.ts`, `scoresaber.ts` and `beatsaver.ts`. It writes only to the archive. |
|
| Exporter and sync | TypeScript in this repo, building on `src/lib/server/beatleader.ts`, `beatleaderAuth.ts`, `scoresaber.ts` and `beatsaver.ts`. It writes only to the archive. |
|
||||||
| BeatLeader auth | OAuth 2.0 (`profile offline_access`). The sync owns its own chain of refresh tokens (`scripts/archive/cli.ts auth login`, seeded 2026-11-08). Unranked ratings do **not** need it: they come from the anonymous `/leaderboards/hash/{hash}` (see Facts). |
|
| BeatLeader auth | OAuth 2.0 (`profile offline_access`). The sync owns its own chain of refresh tokens (`scripts/archive/cli.ts auth login`, seeded 2026-11-08). Unranked ratings do **not** need it: they come from the anonymous `/leaderboards/hash/{hash}` (see Facts). |
|
||||||
| Map files | Archive the BeatSaver zip of every map I've played, using `beatsaver.ts`. Unpack them into the server's `MapsPath`, so the server and ArcViewer never need BeatSaver. |
|
| Map files | Archive the BeatSaver zip of every map I've played, using `beatsaver.ts`. Don't unpack them ahead of time: ArcViewer loads the zip directly, and the server unpacks on demand only if a mirror code path ever needs map files (phase 2, step 9). Neither ever needs BeatSaver. |
|
||||||
| Sync schedule | Daily at 17:00 America/Los_Angeles, incineroar's timezone. |
|
| Sync schedule | Daily at 17:00 America/Los_Angeles, incineroar's timezone. |
|
||||||
| Access | Only incineroar and snorlax, through Caddy's `remote_ip` allowlist. |
|
| Access | Only incineroar and snorlax, through Caddy's `remote_ip` allowlist. |
|
||||||
| Map data scope | Only maps I've played: BeatLeader leaderboards and ratings, plus the BeatSaver metadata and zip. |
|
| Map data scope | Only maps I've played: BeatLeader leaderboards and ratings, plus the BeatSaver metadata and zip. |
|
||||||
@@ -256,7 +256,7 @@ gitea pleb/beatleader-website fork, branch `pleb/main`
|
|||||||
state.json sync cursors (last timepost per source/feed)
|
state.json sync cursors (last timepost per source/feed)
|
||||||
db/app.db, db/storage.db SQLite, rebuilt from the archive
|
db/app.db, db/storage.db SQLite, rebuilt from the archive
|
||||||
storage/ S3 replacement (containers as dirs); replays/ and ssreplays/ symlink into archive/
|
storage/ S3 replacement (containers as dirs); replays/ and ssreplays/ symlink into archive/
|
||||||
maps/<hash>/ MapsPath: archive zips unpacked (server + /cdn/maps)
|
maps/<hash>/ MapsPath: unpack cache, filled on demand from archive zips (phase 2, step 9)
|
||||||
auth/beatleader_tokens.json the sync's own OAuth token chain (rotates on refresh)
|
auth/beatleader_tokens.json the sync's own OAuth token chain (rotates on refresh)
|
||||||
secrets → EnvironmentFile from sops BL_CLIENT_ID, BL_CLIENT_SECRET
|
secrets → EnvironmentFile from sops BL_CLIENT_ID, BL_CLIENT_SECRET
|
||||||
```
|
```
|
||||||
@@ -373,10 +373,10 @@ Steps:
|
|||||||
`CustomWIPLevels`, and copy those whose hash is in `missing.json` to
|
`CustomWIPLevels`, and copy those whose hash is in `missing.json` to
|
||||||
`game-levels/<hash>/`. Whatever is still missing stays listed; those replays
|
`game-levels/<hash>/`. Whatever is still missing stays listed; those replays
|
||||||
just can't render the map.
|
just can't render the map.
|
||||||
9. **Unpack for the server.** Extract each zip, or copy each recovered folder,
|
9. **Unpack for the server: dropped 2026-11-08**, replaced by on-demand
|
||||||
to `maps/<hash>/`. Then the server's `MapDownloader` never goes to the
|
unpacking (phase 2, step 9). ArcViewer loads `url=/cdn/maps/<hash>.zip`
|
||||||
network, and ArcViewer can load `url=/cdn/maps/<hash>.zip` (serve the zips)
|
(Caddy serves the archive's zips) instead of `id=<bsr>` from BeatSaver, so
|
||||||
instead of `id=<bsr>` from BeatSaver.
|
it never needs unpacked folders.
|
||||||
10. **Profile and history.** Save `/player/{id}?stats=true`, `/history`,
|
10. **Profile and history.** Save `/player/{id}?stats=true`, `/history`,
|
||||||
`/history/triangle`, `/accgraph`, and `/user` (strip anything secret).
|
`/history/triangle`, `/accgraph`, and `/user` (strip anything secret).
|
||||||
11. **ScoreSaber main.** Page through all scores (22 pages), then download the
|
11. **ScoreSaber main.** Page through all scores (22 pages), then download the
|
||||||
@@ -416,9 +416,8 @@ Played hashes 2560
|
|||||||
Archive size 21.78 GB
|
Archive size 21.78 GB
|
||||||
```
|
```
|
||||||
|
|
||||||
Steps 8 (game-folder recovery), 9 (unpack into `maps/`) and 13 (local mod
|
Steps 8 (game-folder recovery) and 13 (local mod replays) are still open.
|
||||||
replays) are still open. Step 9 is needed before phase 4; it can happen when
|
Step 9 was dropped in favour of on-demand unpacking.
|
||||||
the archive moves to `/var/lib/plebleader`.
|
|
||||||
|
|
||||||
The 29 missing hashes are all "hash unknown to BeatSaver", and none belongs to
|
The 29 missing hashes are all "hash unknown to BeatSaver", and none belongs to
|
||||||
a legacy score with a replay (all 53 of those maps are archived). The 8 hashes
|
a legacy score with a replay (all 53 of those maps are archived). The 8 hashes
|
||||||
@@ -515,6 +514,30 @@ small and in its own commit, so upstream rebases stay manageable.
|
|||||||
`MapsPath`, `MirrorMode`). Use development OpenIddict certs, or generate a
|
`MapsPath`, `MirrorMode`). Use development OpenIddict certs, or generate a
|
||||||
self-signed pfx into the state dir.
|
self-signed pfx into the state dir.
|
||||||
|
|
||||||
|
9. **On-demand map unpacking (deferred; do it only when needed).**
|
||||||
|
`MapDownloader.Map(hash)` (in the `MapPostprocessor` submodule) returns
|
||||||
|
`MapsPath/<hash>/` when it exists and otherwise downloads from BeatSaver.
|
||||||
|
Add a step before the download: if `<MapZipsPath>/<hash>.zip` exists
|
||||||
|
(the archive's `beatsaver/zips/`), extract it to a temp folder under
|
||||||
|
`MapsPath`, check it contains `Info.dat`, rename it to `<hash>/` and return
|
||||||
|
that. Accept upper- and lowercase hashes, like `Map` does. In `MirrorMode`,
|
||||||
|
never fall through to BeatSaver: return null instead. That makes
|
||||||
|
`MapsPath` a cache that can be deleted at any time.
|
||||||
|
- **Technical debt: low.** It's about 20 lines and changes no
|
||||||
|
interfaces. The catch is where it lives: `MapDownloader` is in
|
||||||
|
`MapPostprocessor`, so editing it means a fourth fork. Its methods aren't
|
||||||
|
virtual, so they can't be overridden. Without a fork, add a small
|
||||||
|
`Utils/LocalMapZips.EnsureUnpacked(hash)` in the server and call it
|
||||||
|
before `_downloader.Map(...)` at the three call sites
|
||||||
|
(`ReplayController`, `EarthDayController`, `MapRefresh`). That's a few
|
||||||
|
extra lines per rebase and no submodule fork, so prefer it.
|
||||||
|
- **Why deferred:** no read path of the mirror calls `MapDownloader` today.
|
||||||
|
Its callers are `MapRefresh` (off in `MirrorMode`), the Earth Day event
|
||||||
|
controller, and replay upload and recalculation in `ReplayController`.
|
||||||
|
It becomes necessary if the mirror ever recalculates score stats
|
||||||
|
(for example the optional `.dat` → `.bsor` conversion in phase 5) or
|
||||||
|
accepts replay uploads.
|
||||||
|
|
||||||
**Check:** the server starts on an empty SQLite file, `/swagger/index.html`
|
**Check:** the server starts on an empty SQLite file, `/swagger/index.html`
|
||||||
loads, and `GET /player/76561199407393962` returns 404 rather than 500.
|
loads, and `GET /player/76561199407393962` returns 404 rather than 500.
|
||||||
|
|
||||||
@@ -532,8 +555,9 @@ and upserts by upstream id.
|
|||||||
- `leaderboards/<hash>/latest` updates the difficulty ratings, so maps you
|
- `leaderboards/<hash>/latest` updates the difficulty ratings, so maps you
|
||||||
played but have no best score for still get them.
|
played but have no best score for still get them.
|
||||||
- `beatsaver/maps/<hash>.json` fills in `Song` fields that BeatLeader leaves
|
- `beatsaver/maps/<hash>.json` fills in `Song` fields that BeatLeader leaves
|
||||||
out (BeatSaver key, description, tags, upload date). The server reads the
|
out (BeatSaver key, description, tags, upload date). The server
|
||||||
map files themselves from `maps/<hash>/`, filled in phase 1, step 9.
|
doesn't read map files on any mirror path; if one appears, phase 2, step 9
|
||||||
|
unpacks them on demand.
|
||||||
- `players/<id>/profile/latest` → `Player`, `PlayerScoreStats`,
|
- `players/<id>/profile/latest` → `Player`, `PlayerScoreStats`,
|
||||||
`ProfileSettings`, plus history rows from `/history`. Insert the legacy id as
|
`ProfileSettings`, plus history rows from `/history`. Insert the legacy id as
|
||||||
an `AccountLink` or alt pointing to the main player, so `PlayerIdToMain`
|
an `AccountLink` or alt pointing to the main player, so `PlayerIdToMain`
|
||||||
@@ -673,7 +697,7 @@ day's play is usually over.
|
|||||||
JSON, the replay, the score stats, and a leaderboard refresh for that hash.
|
JSON, the replay, the score stats, and a leaderboard refresh for that hash.
|
||||||
2. **Attempts:** skipped (see Decisions).
|
2. **Attempts:** skipped (see Decisions).
|
||||||
3. **New maps:** for every hash not in the archive yet, do phase 1, steps 6–9:
|
3. **New maps:** for every hash not in the archive yet, do phase 1, steps 6–9:
|
||||||
BeatLeader leaderboard, BeatSaver JSON and zip, then unpack into `maps/`.
|
BeatLeader leaderboard, then the BeatSaver JSON and zip (nothing to unpack).
|
||||||
The game's own folders are only a manual fallback, since sync can't mount
|
The game's own folders are only a manual fallback, since sync can't mount
|
||||||
Windows. List unresolved hashes in the run summary.
|
Windows. List unresolved hashes in the run summary.
|
||||||
4. **Profile:** one dated snapshot per run, plus `/history`.
|
4. **Profile:** one dated snapshot per run, plus `/history`.
|
||||||
|
|||||||
Reference in New Issue
Block a user