mirror of
https://github.com/djdevin/recflare.git
synced 2026-10-25 23:18:51 -07:00
[cdn] allow 2025 store assets
This commit is contained in:
+2
-2
@@ -15,8 +15,8 @@ route of its own, bundled rather than an asset, because its file is named differ
|
||||
its path.
|
||||
|
||||
Objects are keyed by prefix — `sigs/` (anti-cheat signatures), `room/` (saved room
|
||||
scenes, and room images by their bare `ImageName`), `invention/` (invention data) — and
|
||||
served as `application/octet-stream`; the worker never interprets what it hands back.
|
||||
scenes, and room images by their bare `ImageName`), `invention/` (invention data), `data/`
|
||||
(generic client uploads), `avatar/` (custom avatar item assetbundles) — and served as `application/octet-stream`; the worker never interprets what it hands back.
|
||||
Reads are unauthenticated: a caller needs the exact key, which only comes from an
|
||||
authenticated call to another worker.
|
||||
|
||||
|
||||
+28
-2
@@ -329,6 +329,30 @@ const app = new Hono<App>()
|
||||
(c) => serveAsset(c, `data/${c.req.param('id')}`)
|
||||
)
|
||||
|
||||
// Custom avatar item assetbundles by name. A first-party custom avatar item (see the
|
||||
// `api` worker's `custom_avatar_item`) is rendered from its `CurrentSaves`, each of
|
||||
// which names a built Unity assetbundle by bare filename (`UnityAsset`, e.g.
|
||||
// `anx442dm1a79kp9n4kugkbgd0.assetbundle`) that the client fetches from `/avatar/`.
|
||||
// Streamed from R2 under `avatar/`. The rest of the path is matched so a foldered
|
||||
// upload would resolve the same way as the other blob prefixes.
|
||||
.get(
|
||||
'/avatar/:asset{.+}',
|
||||
describeRoute({
|
||||
tags: ['Assets'],
|
||||
summary: 'Serve a custom avatar item assetbundle',
|
||||
description: [
|
||||
'Streams the object stored under `avatar/<asset>` — the built Unity assetbundle the',
|
||||
'client downloads to render a first-party custom avatar item on one body type. The',
|
||||
'name comes from a save’s `UnityAsset` (or `UnityAsset2`) in the item’s `CurrentSaves`',
|
||||
'(see the `api` worker), a bare filename such as `anx442dm1a79kp9n4kugkbgd0.assetbundle`.',
|
||||
'The worker does not interpret the bytes.',
|
||||
].join(' '),
|
||||
parameters: [keyParam('asset', 'The assetbundle filename.', true), ...CONDITIONAL_HEADERS],
|
||||
responses: assetResponses('The assetbundle'),
|
||||
}),
|
||||
(c) => serveAsset(c, `avatar/${c.req.param('asset')}`)
|
||||
)
|
||||
|
||||
// The generated spec. Documentation only — no request is validated against it (see
|
||||
// openapi.ts). `hide: true` keeps this route out of its own output.
|
||||
app.get(
|
||||
@@ -343,11 +367,13 @@ app.get(
|
||||
description: [
|
||||
'Binary asset delivery for recflare, a private-server reimplementation of the Rec',
|
||||
'Room backend. Streams the blobs the client downloads while playing — anti-cheat',
|
||||
'signatures, saved room scenes, invention data and generic client uploads — out of',
|
||||
'signatures, saved room scenes, invention data, generic client uploads and custom',
|
||||
'avatar item assetbundles — out of',
|
||||
'the shared `recflare-cdn` R2 bucket, plus the JSON config files the client reads',
|
||||
'from `/config/`.',
|
||||
'',
|
||||
'Everything is keyed by prefix (`sigs/`, `room/`, `invention/`, `data/`) and served as',
|
||||
'Everything is keyed by prefix (`sigs/`, `room/`, `invention/`, `data/`, `avatar/`) and',
|
||||
'served as',
|
||||
'`application/octet-stream`; the worker never interprets what it hands back. Reads',
|
||||
'are unauthenticated — a caller needs the exact key, which only comes from an',
|
||||
'authenticated call to another worker.',
|
||||
|
||||
@@ -271,6 +271,21 @@ describe('cdn endpoints', () => {
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
test('GET /avatar/:asset streams the custom avatar item assetbundle from R2', async () => {
|
||||
// A bare filename — what a first-party item's `CurrentSaves[].UnityAsset` names.
|
||||
const name = 'anx442dm1a79kp9n4kugkbgd0.assetbundle'
|
||||
await env.CDN_ASSETS.put(`avatar/${name}`, new Uint8Array([7, 8, 9]))
|
||||
const res = await exports.default.fetch(`${ORIGIN}/avatar/${name}`)
|
||||
expect(res.status).toBe(200)
|
||||
expect(res.headers.get('content-type')).toBe('application/octet-stream')
|
||||
expect(new Uint8Array(await res.arrayBuffer())).toEqual(new Uint8Array([7, 8, 9]))
|
||||
})
|
||||
|
||||
test('GET /avatar/:asset 404s when the assetbundle is absent', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/avatar/missing.assetbundle`)
|
||||
expect(res.status).toBe(404)
|
||||
})
|
||||
|
||||
test('GET /openapi.json documents every route', async () => {
|
||||
const res = await exports.default.fetch(`${ORIGIN}/openapi.json`)
|
||||
expect(res.status).toBe(200)
|
||||
@@ -293,6 +308,7 @@ describe('cdn endpoints', () => {
|
||||
)
|
||||
expect([...documented].sort()).toEqual([
|
||||
'GET /',
|
||||
'GET /avatar/{asset}',
|
||||
'GET /config/LoadingScreenTipData',
|
||||
'GET /config/{name}',
|
||||
'GET /data/{id}',
|
||||
|
||||
Reference in New Issue
Block a user