@torrent-tv/proxy 2.89.6 → 2.89.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +338 -338
- package/CHANGELOG.md +5 -0
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +765 -765
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +17 -2
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +273 -273
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +416 -416
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/render-run-graph.js +173 -173
- package/server.js +748 -748
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1192 -1192
- package/services/encode/EncodePlan.js +1368 -1368
- package/services/encode/EncodeRun.js +32 -4
- package/services/encode/EncodeRuns.js +19 -7
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2283 -2283
- package/services/encode/RunClock.js +87 -87
- package/services/encode/RunProgress.js +79 -79
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +116 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +43 -43
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/piece-completeness.js +60 -2
- package/services/encode/quality/EncodeCost.js +1043 -1043
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +24 -24
- package/services/encode/quality/QualityController.js +758 -758
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +337 -337
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +3 -1
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +53 -53
- package/services/encode/segment-formats/fmp4.js +476 -476
- package/services/encode/segment-formats/index.js +1 -0
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +8 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1152 -1152
- package/services/server/ViewerRequests.js +785 -785
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1029 -1029
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/returns.js +103 -103
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +264 -264
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/media-time.js +355 -355
- package/services/viewer/playback-readiness.js +635 -635
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-buffer-continuity.test.js +77 -77
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +275 -275
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +426 -426
- package/test/encode-plan-viewers.test.js +725 -725
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +38 -0
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +68 -68
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fixtures/refused-audio-20261002.json +10 -0
- package/test/fmp4-presentation-clock.test.js +4 -2
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +240 -240
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +111 -111
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +291 -291
- package/test/matroska-cues-track.test.js +192 -192
- package/test/media-time.test.js +62 -62
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +207 -207
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +87 -87
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-completeness.test.js +37 -1
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +518 -518
- package/test/playback-start-regressions.test.js +101 -101
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +160 -160
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +64 -64
- package/test/same-height-switch.test.js +663 -663
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +352 -352
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +237 -237
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/supply-margin.test.js +190 -190
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file The browser's log, written beside the proxy's.
|
|
3
|
-
*
|
|
4
|
-
* What these hold is the naming, because the name is the whole point: the two
|
|
5
|
-
* halves of a session have to join without guessing, and a torrent name is
|
|
6
|
-
* arbitrary bytes chosen by a stranger that is about to become a path.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import test from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { join } from "node:path";
|
|
14
|
-
|
|
15
|
-
import { createClientLogFiles } from "../utils/client-log-file.js";
|
|
16
|
-
|
|
17
|
-
/** A directory with a proxy log in it, which is what the sink is given. */
|
|
18
|
-
function aDirectory() {
|
|
19
|
-
const dir = mkdtempSync(join(tmpdir(), "ttv-client-log-"));
|
|
20
|
-
return { dir, proxyLog: join(dir, "proxy.log") };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const aSession = (over = {}) => ({
|
|
24
|
-
sessionId: "abcd1234",
|
|
25
|
-
startedAt: "2026-09-13T16:05:24.123Z",
|
|
26
|
-
torrentName: "Reacher.S04E07.1080p.rus.LostFilm.TV.mkv",
|
|
27
|
-
infoHash: "94dda59fdf252447d6ca5a8775dab2d11a832da5",
|
|
28
|
-
...over
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
test("the file lands beside the proxy log, named by when it started and what is watched", async () => {
|
|
32
|
-
const { dir, proxyLog } = aDirectory();
|
|
33
|
-
const logs = createClientLogFiles(proxyLog);
|
|
34
|
-
logs.write(aSession(), ["first line"]);
|
|
35
|
-
await logs.close();
|
|
36
|
-
|
|
37
|
-
const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
38
|
-
assert.ok(name, "a client log file was written");
|
|
39
|
-
// The stamp is UTC and sorts; the session id joins it to the proxy's lines;
|
|
40
|
-
// the film says which one it is without opening the file.
|
|
41
|
-
assert.match(name, /^client-20260913-160524-abcd1234-Reacher\.S04E07\.1080p\.rus\.LostFilm\.TV\.mkv-94dda59f\.log$/);
|
|
42
|
-
assert.equal(readFileSync(join(dir, name), "utf8"), "first line\n");
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("lines from one session go on appending to the one file", async () => {
|
|
46
|
-
const { dir, proxyLog } = aDirectory();
|
|
47
|
-
const logs = createClientLogFiles(proxyLog);
|
|
48
|
-
logs.write(aSession(), ["one", "two"]);
|
|
49
|
-
logs.write(aSession(), ["three"]);
|
|
50
|
-
await logs.close();
|
|
51
|
-
|
|
52
|
-
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
53
|
-
assert.equal(names.length, 1);
|
|
54
|
-
assert.equal(readFileSync(join(dir, names[0]), "utf8"), "one\ntwo\nthree\n");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("a session that has not chosen a film yet still gets its own file", async () => {
|
|
58
|
-
// The page opening, a proxy being chosen, a connection failing — all before
|
|
59
|
-
// any torrent exists, and all worth keeping.
|
|
60
|
-
const { dir, proxyLog } = aDirectory();
|
|
61
|
-
const logs = createClientLogFiles(proxyLog);
|
|
62
|
-
logs.write(aSession({ torrentName: "", infoHash: "" }), ["before any film"]);
|
|
63
|
-
await logs.close();
|
|
64
|
-
|
|
65
|
-
const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
66
|
-
assert.match(name, /-no-torrent-yet\.log$/);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test("the file keeps its name when the film arrives mid-session", async () => {
|
|
70
|
-
// Keyed by the session, not by the name: the earlier lines belong to the same
|
|
71
|
-
// viewing and must not start a second file.
|
|
72
|
-
const { dir, proxyLog } = aDirectory();
|
|
73
|
-
const logs = createClientLogFiles(proxyLog);
|
|
74
|
-
logs.write(aSession({ torrentName: "", infoHash: "" }), ["before"]);
|
|
75
|
-
logs.write(aSession(), ["after"]);
|
|
76
|
-
await logs.close();
|
|
77
|
-
|
|
78
|
-
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
79
|
-
assert.equal(names.length, 1);
|
|
80
|
-
assert.match(names[0], /-no-torrent-yet\.log$/);
|
|
81
|
-
assert.equal(readFileSync(join(dir, names[0]), "utf8"), "before\nafter\n");
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("a hostile torrent name cannot escape the directory", async () => {
|
|
85
|
-
const { dir, proxyLog } = aDirectory();
|
|
86
|
-
const logs = createClientLogFiles(proxyLog);
|
|
87
|
-
logs.write(aSession({ torrentName: "../../etc/passwd" }), ["x"]);
|
|
88
|
-
await logs.close();
|
|
89
|
-
|
|
90
|
-
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
91
|
-
assert.equal(names.length, 1);
|
|
92
|
-
assert.ok(!names[0].includes(".."), `no traversal in ${names[0]}`);
|
|
93
|
-
assert.ok(!names[0].includes("/"), `no separator in ${names[0]}`);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test("two sessions of one film are two files", async () => {
|
|
97
|
-
const { dir, proxyLog } = aDirectory();
|
|
98
|
-
const logs = createClientLogFiles(proxyLog);
|
|
99
|
-
logs.write(aSession({ sessionId: "aaaa1111" }), ["one viewer"]);
|
|
100
|
-
logs.write(aSession({ sessionId: "bbbb2222", startedAt: "2026-09-13T16:07:11.000Z" }), ["another"]);
|
|
101
|
-
await logs.close();
|
|
102
|
-
|
|
103
|
-
assert.equal(readdirSync(dir).filter((f) => f.startsWith("client-")).length, 2);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("no proxy log path means nothing is written and nothing throws", async () => {
|
|
107
|
-
const logs = createClientLogFiles("");
|
|
108
|
-
logs.write(aSession(), ["dropped"]);
|
|
109
|
-
await logs.close();
|
|
110
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file The browser's log, written beside the proxy's.
|
|
3
|
+
*
|
|
4
|
+
* What these hold is the naming, because the name is the whole point: the two
|
|
5
|
+
* halves of a session have to join without guessing, and a torrent name is
|
|
6
|
+
* arbitrary bytes chosen by a stranger that is about to become a path.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import test from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
|
|
12
|
+
import { tmpdir } from "node:os";
|
|
13
|
+
import { join } from "node:path";
|
|
14
|
+
|
|
15
|
+
import { createClientLogFiles } from "../utils/client-log-file.js";
|
|
16
|
+
|
|
17
|
+
/** A directory with a proxy log in it, which is what the sink is given. */
|
|
18
|
+
function aDirectory() {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), "ttv-client-log-"));
|
|
20
|
+
return { dir, proxyLog: join(dir, "proxy.log") };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const aSession = (over = {}) => ({
|
|
24
|
+
sessionId: "abcd1234",
|
|
25
|
+
startedAt: "2026-09-13T16:05:24.123Z",
|
|
26
|
+
torrentName: "Reacher.S04E07.1080p.rus.LostFilm.TV.mkv",
|
|
27
|
+
infoHash: "94dda59fdf252447d6ca5a8775dab2d11a832da5",
|
|
28
|
+
...over
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("the file lands beside the proxy log, named by when it started and what is watched", async () => {
|
|
32
|
+
const { dir, proxyLog } = aDirectory();
|
|
33
|
+
const logs = createClientLogFiles(proxyLog);
|
|
34
|
+
logs.write(aSession(), ["first line"]);
|
|
35
|
+
await logs.close();
|
|
36
|
+
|
|
37
|
+
const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
38
|
+
assert.ok(name, "a client log file was written");
|
|
39
|
+
// The stamp is UTC and sorts; the session id joins it to the proxy's lines;
|
|
40
|
+
// the film says which one it is without opening the file.
|
|
41
|
+
assert.match(name, /^client-20260913-160524-abcd1234-Reacher\.S04E07\.1080p\.rus\.LostFilm\.TV\.mkv-94dda59f\.log$/);
|
|
42
|
+
assert.equal(readFileSync(join(dir, name), "utf8"), "first line\n");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("lines from one session go on appending to the one file", async () => {
|
|
46
|
+
const { dir, proxyLog } = aDirectory();
|
|
47
|
+
const logs = createClientLogFiles(proxyLog);
|
|
48
|
+
logs.write(aSession(), ["one", "two"]);
|
|
49
|
+
logs.write(aSession(), ["three"]);
|
|
50
|
+
await logs.close();
|
|
51
|
+
|
|
52
|
+
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
53
|
+
assert.equal(names.length, 1);
|
|
54
|
+
assert.equal(readFileSync(join(dir, names[0]), "utf8"), "one\ntwo\nthree\n");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a session that has not chosen a film yet still gets its own file", async () => {
|
|
58
|
+
// The page opening, a proxy being chosen, a connection failing — all before
|
|
59
|
+
// any torrent exists, and all worth keeping.
|
|
60
|
+
const { dir, proxyLog } = aDirectory();
|
|
61
|
+
const logs = createClientLogFiles(proxyLog);
|
|
62
|
+
logs.write(aSession({ torrentName: "", infoHash: "" }), ["before any film"]);
|
|
63
|
+
await logs.close();
|
|
64
|
+
|
|
65
|
+
const [name] = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
66
|
+
assert.match(name, /-no-torrent-yet\.log$/);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("the file keeps its name when the film arrives mid-session", async () => {
|
|
70
|
+
// Keyed by the session, not by the name: the earlier lines belong to the same
|
|
71
|
+
// viewing and must not start a second file.
|
|
72
|
+
const { dir, proxyLog } = aDirectory();
|
|
73
|
+
const logs = createClientLogFiles(proxyLog);
|
|
74
|
+
logs.write(aSession({ torrentName: "", infoHash: "" }), ["before"]);
|
|
75
|
+
logs.write(aSession(), ["after"]);
|
|
76
|
+
await logs.close();
|
|
77
|
+
|
|
78
|
+
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
79
|
+
assert.equal(names.length, 1);
|
|
80
|
+
assert.match(names[0], /-no-torrent-yet\.log$/);
|
|
81
|
+
assert.equal(readFileSync(join(dir, names[0]), "utf8"), "before\nafter\n");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("a hostile torrent name cannot escape the directory", async () => {
|
|
85
|
+
const { dir, proxyLog } = aDirectory();
|
|
86
|
+
const logs = createClientLogFiles(proxyLog);
|
|
87
|
+
logs.write(aSession({ torrentName: "../../etc/passwd" }), ["x"]);
|
|
88
|
+
await logs.close();
|
|
89
|
+
|
|
90
|
+
const names = readdirSync(dir).filter((f) => f.startsWith("client-"));
|
|
91
|
+
assert.equal(names.length, 1);
|
|
92
|
+
assert.ok(!names[0].includes(".."), `no traversal in ${names[0]}`);
|
|
93
|
+
assert.ok(!names[0].includes("/"), `no separator in ${names[0]}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("two sessions of one film are two files", async () => {
|
|
97
|
+
const { dir, proxyLog } = aDirectory();
|
|
98
|
+
const logs = createClientLogFiles(proxyLog);
|
|
99
|
+
logs.write(aSession({ sessionId: "aaaa1111" }), ["one viewer"]);
|
|
100
|
+
logs.write(aSession({ sessionId: "bbbb2222", startedAt: "2026-09-13T16:07:11.000Z" }), ["another"]);
|
|
101
|
+
await logs.close();
|
|
102
|
+
|
|
103
|
+
assert.equal(readdirSync(dir).filter((f) => f.startsWith("client-")).length, 2);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("no proxy log path means nothing is written and nothing throws", async () => {
|
|
107
|
+
const logs = createClientLogFiles("");
|
|
108
|
+
logs.write(aSession(), ["dropped"]);
|
|
109
|
+
await logs.close();
|
|
110
|
+
});
|
|
@@ -1,115 +1,115 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file Files downloaded whole are files, and outlive the torrent that fetched
|
|
3
|
-
* them.
|
|
4
|
-
*
|
|
5
|
-
* Asked for in these words on 2026-09-11: as soon as a torrent is fully
|
|
6
|
-
* downloaded, downloading stops, the torrent is deleted, and the artefacts —
|
|
7
|
-
* what was downloaded — stay for as long as they are wanted.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import test from "node:test";
|
|
11
|
-
import assert from "node:assert/strict";
|
|
12
|
-
import fs from "node:fs/promises";
|
|
13
|
-
import os from "node:os";
|
|
14
|
-
import path from "node:path";
|
|
15
|
-
import { Readable } from "node:stream";
|
|
16
|
-
import { CompletedFiles } from "../services/storage/files/CompletedFiles.js";
|
|
17
|
-
|
|
18
|
-
const INFO_HASH = "abcdef0123456789abcdef0123456789abcdef01";
|
|
19
|
-
|
|
20
|
-
/** @returns {Promise<string>} */
|
|
21
|
-
const directory = () => fs.mkdtemp(path.join(os.tmpdir(), "whole-files-"));
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* @param {Buffer} bytes
|
|
25
|
-
* @param {number} [chunk]
|
|
26
|
-
* @returns {() => NodeJS.ReadableStream}
|
|
27
|
-
*/
|
|
28
|
-
const opens = (bytes, chunk = 7) => () => {
|
|
29
|
-
const parts = [];
|
|
30
|
-
for (let at = 0; at < bytes.length; at += chunk) {
|
|
31
|
-
parts.push(bytes.subarray(at, Math.min(at + chunk, bytes.length)));
|
|
32
|
-
}
|
|
33
|
-
return Readable.from(parts);
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
test("a file read whole is kept whole, and reads back byte for byte", async () => {
|
|
37
|
-
const root = await directory();
|
|
38
|
-
const files = new CompletedFiles({ root });
|
|
39
|
-
const bytes = Buffer.from("the whole of a small film", "utf8");
|
|
40
|
-
try {
|
|
41
|
-
const kept = await files.keep({ infoHash: INFO_HASH, fileIndex: 3, length: bytes.length, name: "film.mkv", open: opens(bytes) });
|
|
42
|
-
assert.ok(kept, "the file was not kept");
|
|
43
|
-
assert.equal(kept.length, bytes.length);
|
|
44
|
-
|
|
45
|
-
const found = files.find(INFO_HASH, 3);
|
|
46
|
-
assert.deepEqual(found, kept, "what was kept is not what is found");
|
|
47
|
-
assert.deepEqual(await fs.readFile(found.path), bytes, "the bytes came back changed");
|
|
48
|
-
} finally {
|
|
49
|
-
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
50
|
-
}
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
test("a read that ends early leaves nothing that looks whole", async () => {
|
|
54
|
-
const root = await directory();
|
|
55
|
-
const files = new CompletedFiles({ root });
|
|
56
|
-
const bytes = Buffer.from("half a film", "utf8");
|
|
57
|
-
try {
|
|
58
|
-
// The torrent says the file is longer than what its read produced — the
|
|
59
|
-
// data went away mid-write, which over a torrent is ordinary.
|
|
60
|
-
const kept = await files.keep({
|
|
61
|
-
infoHash: INFO_HASH,
|
|
62
|
-
fileIndex: 0,
|
|
63
|
-
length: bytes.length + 100,
|
|
64
|
-
open: opens(bytes)
|
|
65
|
-
});
|
|
66
|
-
assert.equal(kept, null, "a short file was kept as whole");
|
|
67
|
-
assert.equal(files.find(INFO_HASH, 0), null);
|
|
68
|
-
assert.deepEqual(
|
|
69
|
-
(await fs.readdir(path.join(root, INFO_HASH))).filter((entry) => entry !== "manifest.json"),
|
|
70
|
-
[],
|
|
71
|
-
"a partial file was left behind"
|
|
72
|
-
);
|
|
73
|
-
} finally {
|
|
74
|
-
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
75
|
-
}
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("what a previous life left is taken up, and what is the wrong size is not", async () => {
|
|
79
|
-
const root = await directory();
|
|
80
|
-
const bytes = Buffer.from("a film from the last time this ran", "utf8");
|
|
81
|
-
try {
|
|
82
|
-
const first = new CompletedFiles({ root });
|
|
83
|
-
await first.keep({ infoHash: INFO_HASH, fileIndex: 2, length: bytes.length, name: "film.mkv", open: opens(bytes) });
|
|
84
|
-
// And one that was being written when the process died, under a name that
|
|
85
|
-
// says it is whole — the only way to tell is its size.
|
|
86
|
-
await fs.writeFile(path.join(root, INFO_HASH, "5"), Buffer.alloc(3));
|
|
87
|
-
|
|
88
|
-
const second = new CompletedFiles({ root });
|
|
89
|
-
const adopted = await second.adopt((infoHash, fileIndex) =>
|
|
90
|
-
infoHash === INFO_HASH && fileIndex === 2 ? bytes.length : 999
|
|
91
|
-
);
|
|
92
|
-
assert.equal(adopted, 1, "the whole file was not taken up, or the short one was");
|
|
93
|
-
assert.equal(second.find(INFO_HASH, 2)?.name, "film.mkv", "the name the torrent gave it was lost");
|
|
94
|
-
assert.equal(second.find(INFO_HASH, 5), null, "a file of the wrong size was taken up");
|
|
95
|
-
} finally {
|
|
96
|
-
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
test("forgetting a torrent removes its files and nobody else's", async () => {
|
|
101
|
-
const root = await directory();
|
|
102
|
-
const files = new CompletedFiles({ root });
|
|
103
|
-
const other = "0123456789abcdef0123456789abcdef01234567";
|
|
104
|
-
const bytes = Buffer.from("kept", "utf8");
|
|
105
|
-
try {
|
|
106
|
-
await files.keep({ infoHash: INFO_HASH, fileIndex: 1, length: bytes.length, name: "one.mkv", open: opens(bytes) });
|
|
107
|
-
await files.keep({ infoHash: other, fileIndex: 1, length: bytes.length, name: "two.mkv", open: opens(bytes) });
|
|
108
|
-
|
|
109
|
-
await files.forget(INFO_HASH);
|
|
110
|
-
assert.equal(files.find(INFO_HASH, 1), null);
|
|
111
|
-
assert.ok(files.find(other, 1), "another torrent's file went with it");
|
|
112
|
-
} finally {
|
|
113
|
-
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
114
|
-
}
|
|
115
|
-
});
|
|
1
|
+
/**
|
|
2
|
+
* @file Files downloaded whole are files, and outlive the torrent that fetched
|
|
3
|
+
* them.
|
|
4
|
+
*
|
|
5
|
+
* Asked for in these words on 2026-09-11: as soon as a torrent is fully
|
|
6
|
+
* downloaded, downloading stops, the torrent is deleted, and the artefacts —
|
|
7
|
+
* what was downloaded — stay for as long as they are wanted.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import test from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import os from "node:os";
|
|
14
|
+
import path from "node:path";
|
|
15
|
+
import { Readable } from "node:stream";
|
|
16
|
+
import { CompletedFiles } from "../services/storage/files/CompletedFiles.js";
|
|
17
|
+
|
|
18
|
+
const INFO_HASH = "abcdef0123456789abcdef0123456789abcdef01";
|
|
19
|
+
|
|
20
|
+
/** @returns {Promise<string>} */
|
|
21
|
+
const directory = () => fs.mkdtemp(path.join(os.tmpdir(), "whole-files-"));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {Buffer} bytes
|
|
25
|
+
* @param {number} [chunk]
|
|
26
|
+
* @returns {() => NodeJS.ReadableStream}
|
|
27
|
+
*/
|
|
28
|
+
const opens = (bytes, chunk = 7) => () => {
|
|
29
|
+
const parts = [];
|
|
30
|
+
for (let at = 0; at < bytes.length; at += chunk) {
|
|
31
|
+
parts.push(bytes.subarray(at, Math.min(at + chunk, bytes.length)));
|
|
32
|
+
}
|
|
33
|
+
return Readable.from(parts);
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
test("a file read whole is kept whole, and reads back byte for byte", async () => {
|
|
37
|
+
const root = await directory();
|
|
38
|
+
const files = new CompletedFiles({ root });
|
|
39
|
+
const bytes = Buffer.from("the whole of a small film", "utf8");
|
|
40
|
+
try {
|
|
41
|
+
const kept = await files.keep({ infoHash: INFO_HASH, fileIndex: 3, length: bytes.length, name: "film.mkv", open: opens(bytes) });
|
|
42
|
+
assert.ok(kept, "the file was not kept");
|
|
43
|
+
assert.equal(kept.length, bytes.length);
|
|
44
|
+
|
|
45
|
+
const found = files.find(INFO_HASH, 3);
|
|
46
|
+
assert.deepEqual(found, kept, "what was kept is not what is found");
|
|
47
|
+
assert.deepEqual(await fs.readFile(found.path), bytes, "the bytes came back changed");
|
|
48
|
+
} finally {
|
|
49
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a read that ends early leaves nothing that looks whole", async () => {
|
|
54
|
+
const root = await directory();
|
|
55
|
+
const files = new CompletedFiles({ root });
|
|
56
|
+
const bytes = Buffer.from("half a film", "utf8");
|
|
57
|
+
try {
|
|
58
|
+
// The torrent says the file is longer than what its read produced — the
|
|
59
|
+
// data went away mid-write, which over a torrent is ordinary.
|
|
60
|
+
const kept = await files.keep({
|
|
61
|
+
infoHash: INFO_HASH,
|
|
62
|
+
fileIndex: 0,
|
|
63
|
+
length: bytes.length + 100,
|
|
64
|
+
open: opens(bytes)
|
|
65
|
+
});
|
|
66
|
+
assert.equal(kept, null, "a short file was kept as whole");
|
|
67
|
+
assert.equal(files.find(INFO_HASH, 0), null);
|
|
68
|
+
assert.deepEqual(
|
|
69
|
+
(await fs.readdir(path.join(root, INFO_HASH))).filter((entry) => entry !== "manifest.json"),
|
|
70
|
+
[],
|
|
71
|
+
"a partial file was left behind"
|
|
72
|
+
);
|
|
73
|
+
} finally {
|
|
74
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("what a previous life left is taken up, and what is the wrong size is not", async () => {
|
|
79
|
+
const root = await directory();
|
|
80
|
+
const bytes = Buffer.from("a film from the last time this ran", "utf8");
|
|
81
|
+
try {
|
|
82
|
+
const first = new CompletedFiles({ root });
|
|
83
|
+
await first.keep({ infoHash: INFO_HASH, fileIndex: 2, length: bytes.length, name: "film.mkv", open: opens(bytes) });
|
|
84
|
+
// And one that was being written when the process died, under a name that
|
|
85
|
+
// says it is whole — the only way to tell is its size.
|
|
86
|
+
await fs.writeFile(path.join(root, INFO_HASH, "5"), Buffer.alloc(3));
|
|
87
|
+
|
|
88
|
+
const second = new CompletedFiles({ root });
|
|
89
|
+
const adopted = await second.adopt((infoHash, fileIndex) =>
|
|
90
|
+
infoHash === INFO_HASH && fileIndex === 2 ? bytes.length : 999
|
|
91
|
+
);
|
|
92
|
+
assert.equal(adopted, 1, "the whole file was not taken up, or the short one was");
|
|
93
|
+
assert.equal(second.find(INFO_HASH, 2)?.name, "film.mkv", "the name the torrent gave it was lost");
|
|
94
|
+
assert.equal(second.find(INFO_HASH, 5), null, "a file of the wrong size was taken up");
|
|
95
|
+
} finally {
|
|
96
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("forgetting a torrent removes its files and nobody else's", async () => {
|
|
101
|
+
const root = await directory();
|
|
102
|
+
const files = new CompletedFiles({ root });
|
|
103
|
+
const other = "0123456789abcdef0123456789abcdef01234567";
|
|
104
|
+
const bytes = Buffer.from("kept", "utf8");
|
|
105
|
+
try {
|
|
106
|
+
await files.keep({ infoHash: INFO_HASH, fileIndex: 1, length: bytes.length, name: "one.mkv", open: opens(bytes) });
|
|
107
|
+
await files.keep({ infoHash: other, fileIndex: 1, length: bytes.length, name: "two.mkv", open: opens(bytes) });
|
|
108
|
+
|
|
109
|
+
await files.forget(INFO_HASH);
|
|
110
|
+
assert.equal(files.find(INFO_HASH, 1), null);
|
|
111
|
+
assert.ok(files.find(other, 1), "another torrent's file went with it");
|
|
112
|
+
} finally {
|
|
113
|
+
await fs.rm(root, { recursive: true, force: true, maxRetries: 10, retryDelay: 20 });
|
|
114
|
+
}
|
|
115
|
+
});
|