8.8 KiB
8.8 KiB
id, title, created, depends_on, est_files
| id | title | created | depends_on | est_files | |
|---|---|---|---|---|---|
| 016-intake-and-server-verification-cfe031 | Let a device hand a file to the server for hashing and validation | 2026-09-29 |
|
9 |
016 — Intake: server-side verification of device files
Objective
Implements flow 7 of docs/p2p-architecture.md — the owner's rule "a file must first
be downloaded by the server and checked before its hash is added to the server DB".
After this plan:
POST /api/p2p/intake(device auth) opens a 30-min ticket;PUT /api/p2p/intake/:ticketstreams the bytes intoP2P_INTAKE_DIR(never served). The SERVER hashes while writing, a claimed cid must match,validateMedia()must pass, thenadmitFile()(malware scan only whenP2P_MALWARE_SCAN=1, off by default). Only then does the cid enterp2p_content(origin 'intake'), and the uploader becomes a holder (trust 'challenged').- If the server has no copy of that video, the file is adopted into the media cache
byte-for-byte (
media.adoptFile, new) so it can be streamed again. Otherwise deleted. - Settings → Sharing shows "Verify & share N saved videos" for saves whose hash the server doesn't know yet (old saves, fallback-path saves); one tap uploads them one by one. Nothing uploads automatically in this plan.
Pre-tested: intake 4/4 (valid, hash-mismatch, truncated, non-media, size limits, ticket
reuse, scan-infected), media-cache 27/27 (incl. adoptFile), and the browser flow
(unknown → contribute → accepted) in Chromium.
Context the executor must NOT rediscover
- Patches (apply in this order from the repo root):
plans/patches/016-p2p-intake-new.diff→ newserver/p2p-intake.js(registerIntakeRoutes) +server/p2p-intake.test.jsplans/patches/016-media-cache-adopt.diff→adoptFile(id, src, { sha256, probe, meta })inserver/media-cache.js+ a testplans/patches/016-client-intake.diff→P2PClient.contribute(videoId, { cid, title, channel }),P2PClient.unknownVideos()infrontend/p2p-client.js;OPFS.getFileObject(videoId)infrontend/opfs.js
server/server.jsimports{ createMediaCache, HIGH, LOW } from './media-cache.js';FFMPEGconst exists; ffprobe path isprocess.env.FFPROBE_PATH || 'ffprobe'(used in createMediaCache).- Plan 013/014 added in server.js:
const p2p = registerP2pRoutes(app, …)(exposesgate,requireDevice) andconst p2pHub = createP2pHub(…). Plan 009 importedadmitFile,P2P,* as p2pDb. - Plan 015 added to
renderSettings()the Sharing group ending with:and an<div class="set-row"> <span>This device<small id="p2pDeviceInfo">…</small></span> <span id="p2pHoldingCount" class="set-stat">…</span> </div> </div>(async () => { const info = $('p2pDeviceInfo'); … cnt.textContent = …; })();block. - Settings buttons use
class="btn".videoById(id)(app.js ~7301) returns the known video object (title/channel) or undefined.toast(msg)shows a toast.
Steps
- Apply the three patches (STOP and report on any failure):
git apply plans/patches/016-p2p-intake-new.diff git apply plans/patches/016-media-cache-adopt.diff git apply plans/patches/016-client-intake.diff server/package.json"test" script — append&& bun test --timeout 60000 ./p2p-intake.test.js.server/server.js: a. change the media-cache import toimport { createMediaCache, HIGH, LOW, validateMedia } from './media-cache.js';b. addimport { registerIntakeRoutes } from './p2p-intake.js';c. directly after the/api/p2p/holdersroute (plan 014) add:// Device → server intake: the server hashes, validates (and scans when // P2P_MALWARE_SCAN=1) before a cid is admitted. docs/p2p-architecture.md flow 7. const intake = registerIntakeRoutes(app, { cfg: P2P, p2pDb, gate: p2p.gate, requireDevice: p2p.requireDevice, admitFile, validateMedia: (path, expected, opts) => validateMedia(path, expected, { ...opts, ffmpeg: FFMPEG, ffprobe: process.env.FFPROBE_PATH || 'ffprobe' }), adopt: (videoId, path, info) => media.adoptFile(videoId, path, info), });docker-compose.yml— under the P2P env lines from plan 008 add the commented lines:# P2P_INTAKE_DIR: "/app/data/p2p-intake" # quarantine for device uploads (never served) # P2P_INTAKE_MAX_BYTES: "3221225472" # 3 GiBfrontend/app.jsrenderSettingstemplate — directly BEFORE the closing</div>of the Sharing group (i.e. after theThis devicerow), insert:<div class="set-row hidden" id="p2pContributeRow"> <span> Saved videos the server can't verify yet <small>Uploading lets the server check them (hash + media validation) so other devices can get them. Uses your upload bandwidth.</small> </span> <button id="p2pContributeBtn" class="btn">Verify & share</button> </div>frontend/app.jsrenderSettings— inside the plan-015(async () => { … })();block, after thecnt.textContent = …;line, add:const unknown = window.P2PClient ? await window.P2PClient.unknownVideos() : []; const row = $('p2pContributeRow'); const btn = $('p2pContributeBtn'); if (row && btn && unknown.length) { row.classList.remove('hidden'); btn.textContent = `Verify & share ${unknown.length} saved video${unknown.length === 1 ? '' : 's'}`; btn.onclick = async () => { btn.disabled = true; let ok = 0; for (let i = 0; i < unknown.length; i++) { const v = videoById(unknown[i]) || {}; btn.textContent = `Uploading ${i + 1} of ${unknown.length}…`; const r = await window.P2PClient.contribute(unknown[i], { title: v.title || '', channel: v.channel || '' }); if (r && r.ok) ok++; } toast(`Verified ${ok} of ${unknown.length} saved video${unknown.length === 1 ? '' : 's'}`); btn.disabled = false; row.classList.add('hidden'); }; }
Out of scope / do NOT touch
- No automatic uploads (plan 018 adds server-requested ones). No UI for the admin (plan 019).
- Never serve files from
P2P_INTAKE_DIR; never add a/api/p2p/intakeGET.
Verification
cd /home/user/ytplayer/server && bun install >/dev/null 2>&1
which ffmpeg ffprobe || echo "NO FFMPEG — install it (apt-get install -y ffmpeg); intake/media tests need it"
bun test --timeout 60000 ./p2p-intake.test.js 2>&1 | tail -4
bun test --timeout 60000 ./media-cache.test.js 2>&1 | tail -4
bun run test 2>&1 | grep -E "^ *[0-9]+ (pass|fail)"
bun build server.js --target=bun --outdir=/tmp/ytp-check >/dev/null && echo SERVER_OK
cd .. && node --check frontend/app.js frontend/p2p-client.js frontend/opfs.js && echo FRONT_OK
cd server
bun ../plans/harness/intake-server.js >/tmp/ytp016.log 2>&1 & SRV=$!; sleep 3
cd ../plans/harness && (npm ls playwright >/dev/null 2>&1 || npm i --no-save playwright >/dev/null 2>&1); timeout 90 node intake-check.mjs
kill $SRV; true
Expected: intake 4 pass; media-cache 27 pass; every file 0 fail; SERVER_OK; FRONT_OK;
browser JSON {"firstUnknown":1,"unknown":["upAAAAAAAA9"],"contribute":{"ok":true,"adopted":false,"cidOk":true},"secondAccepted":1,"secondUnknown":0}.
Report format (executor: follow exactly)
Output ONLY the following, no other prose:
git diff(unified) of all changes.- Raw output of the Verification commands.
Findings:— max 10 lines.
Do not commit. Do not push. Do not touch files outside the Steps.
Execution log
- Executor: in-session Agent (haiku). Attempts: 1. Fix rounds: 0.
- Orchestrator re-ran Verification:
p2p-intake.js,p2p-intake.test.js,media-cache.js,media-cache.test.js,p2p-client.js,opfs.jsbyte-identical to the pre-tested 016 versions; intake tests 4 pass; media-cache 27 pass; all 14 server test files 0 fail;SERVER_OK; frontend syntax ok; browser check in Chromium reproduced the expected JSON (unknown file -> contribute -> accepted, cid valid, second report accepted 1 / unknown 0). No leftover processes. - Executor Findings (verbatim): All three patches applied without errors. server/package.json updated to run p2p-intake.test.js. server/server.js updated with validateMedia import and registerIntakeRoutes import and intake route initialization. frontend/app.js updated with HTML and JS for contribute button. docker-compose.yml updated with commented P2P_INTAKE_DIR and P2P_INTAKE_MAX_BYTES. All 4 expected intake tests passed, all 27 media-cache tests passed, full test suite passed with 0 failures across all test files. Server build successful. Frontend syntax check passed. Browser test output matches expected JSON exactly.