mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
feat(storage): Blossom Phase 1a — upload engine [LOCAL] (#30)
The Blossom upload orchestration, isolated + unit-tested. Only routes when a connection's provider is "blossom", so the S3/R2 path is completely unaffected (that branch can't trigger until the Phase-1b config UI can create a Blossom connection). - blossomService now owns the orchestration (WebTorrent-free, so it's Node- testable): uploadFile (multiServerUpload + BUD-11 auth) and uploadVideo (blob → injected torrent generator → upload .torrent as a 2nd blob → magnet with ws=the Blossom video url + xs=the Blossom .torrent url → blossom storagePointer). Same result shape as the S3 path, so publish is untouched. Exports isBlossomProvider + resolveBlossomServers. - mediaUploader: thin Blossom branch at the top of uploadThumbnail/uploadVideo (delegates to blossomService, injecting generateTorrentMetadata so the service never imports WebTorrent); new getSigner dep. UploadModal + EditModal pass getSigner: () => getActiveSigner(). Tests: tests/blossom-service.test.mjs (7 — guards, mirror, torrent/magnet orchestration, storagePointer). Lint + build clean. Flag still off. NEXT (Phase 1b): the keyless multi-server Storage-pane config UI + connection model (servers in plaintext connection meta — no secret) + resolveActiveConnection Blossom branch, so a server can be configured and uploads run end-to-end. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
02e60f1842
commit
0eafd38ec4
5 files changed
+287
-77
No files matched your search
+103
-36
@@ -1,55 +1,122 @@
|
||||
// Phase 0 skeleton tests for blossomService: the availability gate and the
|
||||
// input guards run without loading the vendored SDK (they throw first). Upload
|
||||
// round-trips against a real Blossom server are exercised in Phase 1. See
|
||||
// docs/blossom-plan.md / TODO #30.
|
||||
// blossomService tests (Node-safe — no WebTorrent). Covers the availability gate,
|
||||
// server-list resolution, input guards, and the uploadVideo orchestration (blob +
|
||||
// injected torrent → magnet + storagePointer). The vendored SDK is mocked via
|
||||
// loadSdk; real-server round-trips are exercised in-browser. See docs/blossom-plan.md.
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import blossomService, { BlossomService } from "../js/services/blossomService.js";
|
||||
import blossomService, {
|
||||
BlossomService,
|
||||
BLOSSOM_PROVIDER,
|
||||
isBlossomProvider,
|
||||
resolveBlossomServers,
|
||||
} from "../js/services/blossomService.js";
|
||||
|
||||
test("isAvailable reflects the FEATURE_BLOSSOM_STORAGE flag (off by default)", () => {
|
||||
const SHA = "a".repeat(64);
|
||||
const signer = async (draft) => ({ ...draft, id: "id", sig: "sig", pubkey: "pk" });
|
||||
|
||||
// A BlossomService whose SDK is mocked: multiServerUpload returns a descriptor
|
||||
// map keyed by server, echoing the file so we can assert which blob was sent.
|
||||
function mockService() {
|
||||
const uploads = [];
|
||||
const svc = new BlossomService();
|
||||
svc.loadSdk = async () => ({
|
||||
createUploadAuth: async (s, sha256, opts) => ({ kind: 24242, sha256, opts }),
|
||||
multiServerUpload: async (servers, file, opts) => {
|
||||
uploads.push({ servers: [...servers], file });
|
||||
// Give each blob a distinct sha256 so video vs .torrent URLs differ.
|
||||
const sha = file?.name?.includes(".torrent") ? "b".repeat(64) : SHA;
|
||||
if (typeof opts?.onAuth === "function") await opts.onAuth(servers[0], sha);
|
||||
const map = new Map();
|
||||
for (const server of servers) {
|
||||
map.set(server, { url: `${server}/${sha}`, sha256: sha, size: 1, type: "x" });
|
||||
}
|
||||
return map;
|
||||
},
|
||||
});
|
||||
return { svc, uploads };
|
||||
}
|
||||
|
||||
test("isBlossomProvider / BLOSSOM_PROVIDER only match 'blossom'", () => {
|
||||
assert.equal(BLOSSOM_PROVIDER, "blossom");
|
||||
assert.equal(isBlossomProvider("blossom"), true);
|
||||
assert.equal(isBlossomProvider("cloudflare_r2"), false);
|
||||
});
|
||||
|
||||
test("isAvailable reflects the flag (off by default)", () => {
|
||||
assert.equal(blossomService.isAvailable(), false);
|
||||
});
|
||||
|
||||
test("uploadFile rejects a missing file before touching the SDK", async () => {
|
||||
const svc = new BlossomService();
|
||||
await assert.rejects(
|
||||
() => svc.uploadFile({ servers: ["https://blossom.example"], signer: async () => ({}) }),
|
||||
/requires a file/,
|
||||
test("resolveBlossomServers de-dupes, trims, and reads meta.servers", () => {
|
||||
assert.deepEqual(
|
||||
resolveBlossomServers({ servers: [" https://a ", "https://a", "https://b", "", " "] }),
|
||||
["https://a", "https://b"],
|
||||
);
|
||||
assert.deepEqual(resolveBlossomServers({ meta: { servers: ["https://c"] } }), ["https://c"]);
|
||||
assert.deepEqual(resolveBlossomServers({}), []);
|
||||
});
|
||||
|
||||
test("uploadFile rejects an empty server list", async () => {
|
||||
const svc = new BlossomService();
|
||||
test("uploadFile guards: file, servers, signer required", async () => {
|
||||
const { svc } = mockService();
|
||||
await assert.rejects(() => svc.uploadFile({ servers: ["https://s"], signer }), /requires a file/);
|
||||
await assert.rejects(
|
||||
() => svc.uploadFile({ file: new Blob(["x"]), servers: [], signer: async () => ({}) }),
|
||||
() => svc.uploadFile({ file: new Blob(["x"]), servers: [], signer }),
|
||||
/at least one server/,
|
||||
);
|
||||
});
|
||||
|
||||
test("uploadFile rejects a non-function signer", async () => {
|
||||
const svc = new BlossomService();
|
||||
await assert.rejects(
|
||||
() =>
|
||||
svc.uploadFile({
|
||||
file: new Blob(["x"]),
|
||||
servers: ["https://blossom.example"],
|
||||
signer: null,
|
||||
}),
|
||||
() => svc.uploadFile({ file: new Blob(["x"]), servers: ["https://s"], signer: null }),
|
||||
/requires a signer/,
|
||||
);
|
||||
});
|
||||
|
||||
test("uploadFile de-dupes and trims the server list before requiring one", async () => {
|
||||
const svc = new BlossomService();
|
||||
// All-blank/dup servers collapse to empty → the "at least one server" guard.
|
||||
await assert.rejects(
|
||||
() =>
|
||||
svc.uploadFile({
|
||||
file: new Blob(["x"]),
|
||||
servers: [" ", "", " "],
|
||||
signer: async () => ({}),
|
||||
}),
|
||||
/at least one server/,
|
||||
);
|
||||
test("uploadFile returns the primary descriptor url + sha256 key, mirrored to all servers", async () => {
|
||||
const { svc, uploads } = mockService();
|
||||
const out = await svc.uploadFile({
|
||||
file: new File([new Uint8Array([1])], "v.mp4"),
|
||||
servers: ["https://a", "https://b"],
|
||||
signer,
|
||||
});
|
||||
assert.equal(out.url, `https://a/${SHA}`);
|
||||
assert.equal(out.key, SHA);
|
||||
assert.deepEqual(uploads[0].servers, ["https://a", "https://b"], "mirrored to both");
|
||||
});
|
||||
|
||||
test("uploadVideo without a torrent returns a blossom storagePointer and no magnet", async () => {
|
||||
const { svc } = mockService();
|
||||
const result = await svc.uploadVideo({
|
||||
file: new File([new Uint8Array([1, 2])], "clip.mp4"),
|
||||
servers: ["https://a"],
|
||||
signer,
|
||||
generateTorrent: async () => ({ hasValidInfoHash: false, torrentFile: null }),
|
||||
});
|
||||
assert.equal(result.url, `https://a/${SHA}`);
|
||||
assert.equal(result.key, SHA);
|
||||
assert.match(result.storagePointer, /blossom/);
|
||||
assert.equal(result.magnet, "");
|
||||
assert.equal(result.hasValidInfoHash, false);
|
||||
});
|
||||
|
||||
test("uploadVideo with a torrent uploads the .torrent as a blob and builds a magnet (ws + xs)", async () => {
|
||||
const { svc, uploads } = mockService();
|
||||
const infoHash = "0".repeat(40);
|
||||
const result = await svc.uploadVideo({
|
||||
file: new File([new Uint8Array([1, 2])], "clip.mp4"),
|
||||
servers: ["https://a"],
|
||||
signer,
|
||||
generateTorrent: async ({ videoPublicUrl }) => {
|
||||
assert.equal(videoPublicUrl, `https://a/${SHA}`, "ws is the blossom video url");
|
||||
return {
|
||||
hasValidInfoHash: true,
|
||||
infoHash,
|
||||
torrentFile: new File([new Uint8Array([9])], "clip.torrent"),
|
||||
};
|
||||
},
|
||||
});
|
||||
assert.equal(uploads.length, 2, "video blob + .torrent blob");
|
||||
assert.equal(result.hasValidInfoHash, true);
|
||||
assert.equal(result.infoHash, infoHash);
|
||||
assert.match(result.magnet, new RegExp(`btih:${infoHash}`));
|
||||
assert.match(result.magnet, /ws=https%3A%2F%2Fa%2F/, "ws = blossom video url");
|
||||
assert.match(result.magnet, /xs=https%3A%2F%2Fa%2F/, "xs = blossom .torrent url");
|
||||
assert.equal(result.torrentUrl, `https://a/${"b".repeat(64)}`);
|
||||
});
|
||||
Reference in new issue
Block a user