mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Posting an external URL used to skip the torrent/webseed benefit entirely. Now the upload modal's external tab auto-derives a WebTorrent webseed from the URL at publish time: fetch the remote file (CORS-permitting), stream it under a hard size cap, compute the infoHash, and publish a magnet with the URL as the webseed (ws=). Best-effort — CORS failure / over the cap / any error silently degrades to URL-only. Only runs when the user didn't paste their own magnet. - js/utils/externalUrlTorrent.js: fetchBlobWithCap (streams with a 500 MB cap, aborts past it, short-circuits on content-length), buildWebseedMagnet, and deriveExternalUrlTorrent (reuses createTorrentMetadata). Pure + injectable → unit tested without network/WebTorrent. - mediaUploader.deriveTorrentForExternalUrl: derives the ws= magnet and, when storage is unlocked, also hosts the tiny .torrent so the webseed can actually bootstrap P2P (xs=) — best-effort, falls back to ws=-only. - UploadModal.handleExternalFlow calls it; deriveExternalWebseed only hosts the .torrent when storage is unlocked, shows progress on the submit button, and never throws. Tests: tests/external-url-torrent.test.mjs (9 — magnet assembly, name parsing, streamed size-cap enforcement + short-circuit, fetch-failed, derive-with-injected- hash, failure propagation). Lint + build green. Next: the same for editing a URL in the Edit modal. Co-Authored-By: Claude Opus 4.8 <[email protected]>
164 lines
6.6 KiB
JavaScript
164 lines
6.6 KiB
JavaScript
// Derive a webseed magnet from an external URL (external-URL P2P). We fetch the
|
|
// remote file (best-effort, CORS-permitting), stream it under a hard size cap,
|
|
// compute the infoHash, and build a ws= magnet. Any failure must degrade to
|
|
// URL-only (the caller catches).
|
|
//
|
|
// test_integrity_note:
|
|
// change_type: ["new_tests"]
|
|
// scenarios:
|
|
// - id: SCN-external-url-torrent
|
|
// given: "a remote URL + an injected fetch/hash"
|
|
// when: "deriveExternalUrlTorrent / fetchBlobWithCap / buildWebseedMagnet run"
|
|
// then: "magnet carries the infoHash + ws=url; oversize/fetch failures throw coded errors"
|
|
// observable_outcomes:
|
|
// - "buildWebseedMagnet: xt/dn/ws present; xs only when torrentUrl given; empty on missing infoHash/url"
|
|
// - "deriveNameFromUrl extracts + decodes the filename"
|
|
// - "fetchBlobWithCap streams under the cap and aborts past it (code too-large)"
|
|
// - "content-length over cap short-circuits before downloading"
|
|
// - "non-ok response -> fetch-failed"
|
|
// - "deriveExternalUrlTorrent returns { infoHash, magnet(ws=url) } via injected hash"
|
|
// determinism_controls:
|
|
// - "injected fetchImpl (fake Response) + createMetadata stub + fileFactory; no network/WebTorrent"
|
|
// anti_cheat_rationale:
|
|
// prevents: ["hard-coded return value", "over-mocking internal logic"]
|
|
// relaxation:
|
|
// did_relax_any_assertion: false
|
|
|
|
import test from "node:test";
|
|
import { strict as assert } from "node:assert";
|
|
import {
|
|
buildWebseedMagnet,
|
|
deriveNameFromUrl,
|
|
fetchBlobWithCap,
|
|
deriveExternalUrlTorrent,
|
|
} from "../js/utils/externalUrlTorrent.js";
|
|
|
|
function streamResponse(bytes, { contentLength, contentType = "video/mp4", ok = true, status = 200, chunkSize = 4 } = {}) {
|
|
const headers = new Map();
|
|
if (contentLength !== null) {
|
|
headers.set("content-length", String(contentLength ?? bytes.length));
|
|
}
|
|
if (contentType) headers.set("content-type", contentType);
|
|
let offset = 0;
|
|
return {
|
|
ok,
|
|
status,
|
|
headers: { get: (k) => headers.get(k.toLowerCase()) ?? null },
|
|
body: {
|
|
getReader() {
|
|
return {
|
|
async read() {
|
|
if (offset >= bytes.length) return { done: true, value: undefined };
|
|
const end = Math.min(offset + chunkSize, bytes.length);
|
|
const value = bytes.slice(offset, end);
|
|
offset = end;
|
|
return { done: false, value };
|
|
},
|
|
async cancel() {},
|
|
};
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
test("buildWebseedMagnet: xt/dn/ws, xs only when torrentUrl is given", () => {
|
|
const m = buildWebseedMagnet({ infoHash: "abc123", url: "https://h/v.mp4", name: "v.mp4" });
|
|
assert.match(m, /^magnet:\?xt=urn:btih:abc123&dn=v\.mp4&ws=https%3A%2F%2Fh%2Fv\.mp4$/);
|
|
const withXs = buildWebseedMagnet({ infoHash: "abc", url: "https://h/v.mp4", torrentUrl: "https://h/v.torrent" });
|
|
assert.match(withXs, /&xs=https%3A%2F%2Fh%2Fv\.torrent$/);
|
|
});
|
|
|
|
test("buildWebseedMagnet: empty when infoHash or url is missing", () => {
|
|
assert.equal(buildWebseedMagnet({ url: "https://h/v.mp4" }), "");
|
|
assert.equal(buildWebseedMagnet({ infoHash: "abc" }), "");
|
|
assert.equal(buildWebseedMagnet({}), "");
|
|
});
|
|
|
|
test("deriveNameFromUrl extracts + decodes the filename", () => {
|
|
assert.equal(deriveNameFromUrl("https://h/path/My%20Clip.mp4"), "My Clip.mp4");
|
|
assert.equal(deriveNameFromUrl("https://h/path/"), "");
|
|
assert.equal(deriveNameFromUrl("not a url"), "");
|
|
});
|
|
|
|
test("fetchBlobWithCap streams the whole body under the cap", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
|
const progress = [];
|
|
const blob = await fetchBlobWithCap("https://h/v.mp4", {
|
|
maxBytes: 1000,
|
|
onProgress: (p) => progress.push(p.received),
|
|
fetchImpl: async () => streamResponse(bytes, { chunkSize: 4 }),
|
|
});
|
|
assert.equal(blob.size, 10);
|
|
assert.deepEqual(progress, [4, 8, 10]);
|
|
});
|
|
|
|
test("fetchBlobWithCap aborts a stream that exceeds the cap (too-large)", async () => {
|
|
const bytes = new Uint8Array(20);
|
|
await assert.rejects(
|
|
() =>
|
|
fetchBlobWithCap("https://h/v.mp4", {
|
|
maxBytes: 8,
|
|
// no content-length header → only the streaming guard can catch it
|
|
fetchImpl: async () => streamResponse(bytes, { contentLength: null, chunkSize: 4 }),
|
|
}),
|
|
(err) => err.code === "too-large",
|
|
);
|
|
});
|
|
|
|
test("fetchBlobWithCap short-circuits on content-length over the cap (no download)", async () => {
|
|
let readerMade = false;
|
|
await assert.rejects(
|
|
() =>
|
|
fetchBlobWithCap("https://h/big.mp4", {
|
|
maxBytes: 100,
|
|
fetchImpl: async () => ({
|
|
ok: true,
|
|
status: 200,
|
|
headers: { get: (k) => (k.toLowerCase() === "content-length" ? "999999" : null) },
|
|
body: { getReader() { readerMade = true; return { read: async () => ({ done: true }) }; } },
|
|
}),
|
|
}),
|
|
(err) => err.code === "too-large" && err.total === 999999,
|
|
);
|
|
assert.equal(readerMade, false, "must not start reading the body when the size is known-too-big");
|
|
});
|
|
|
|
test("fetchBlobWithCap: non-ok response -> fetch-failed", async () => {
|
|
await assert.rejects(
|
|
() => fetchBlobWithCap("https://h/404", { fetchImpl: async () => ({ ok: false, status: 403, headers: { get: () => null } }) }),
|
|
(err) => err.code === "fetch-failed" && err.status === 403,
|
|
);
|
|
});
|
|
|
|
test("deriveExternalUrlTorrent returns infoHash + ws= magnet via the injected hasher", async () => {
|
|
const bytes = new Uint8Array([1, 2, 3, 4]);
|
|
const seen = { fileName: null, urlList: null };
|
|
const result = await deriveExternalUrlTorrent("https://host/clips/My%20Clip.mp4", {
|
|
maxBytes: 1000,
|
|
fetchImpl: async () => streamResponse(bytes),
|
|
fileFactory: (blob, name) => ({ blob, name }),
|
|
createMetadata: async (file, urlList) => {
|
|
seen.fileName = file.name;
|
|
seen.urlList = urlList;
|
|
return { infoHash: "deadbeef", torrentFile: new Uint8Array([9]) };
|
|
},
|
|
});
|
|
assert.equal(result.infoHash, "deadbeef");
|
|
assert.equal(seen.fileName, "My Clip.mp4", "file name derived from the URL");
|
|
assert.deepEqual(seen.urlList, ["https://host/clips/My%20Clip.mp4"], "url passed as the webseed");
|
|
assert.match(result.magnet, /xt=urn:btih:deadbeef/);
|
|
assert.match(result.magnet, /ws=https%3A%2F%2Fhost%2Fclips%2FMy%2520Clip\.mp4/);
|
|
assert.ok(result.torrentFile, "torrentFile returned for optional .torrent hosting");
|
|
});
|
|
|
|
test("deriveExternalUrlTorrent propagates a fetch failure (caller degrades to URL-only)", async () => {
|
|
await assert.rejects(
|
|
() =>
|
|
deriveExternalUrlTorrent("https://host/blocked.mp4", {
|
|
fetchImpl: async () => ({ ok: false, status: 0, headers: { get: () => null } }),
|
|
createMetadata: async () => ({ infoHash: "x" }),
|
|
}),
|
|
(err) => err.code === "fetch-failed",
|
|
);
|
|
});
|