Files
bitvid/tests/storage-key-collection.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 95b51b6e61 feat(r2): delete the underlying storage objects when a video is deleted
Deleting a video removed the Nostr note but left the actual file (plus its
thumbnail and .torrent) publicly downloadable at its R2/S3 URL forever — a
privacy gap and ongoing storage cost. Now the delete flow also removes the
backing objects, best-effort.

- collectVideoStorageKeys() (storagePointer.js): pure helper that derives the
  object keys (video + sibling .torrent + thumbnail) from a note, ONLY for URLs
  under the owner's bucket base — external/3rd-party URLs are never returned.
- R2Service.deleteVideoStorage(): resolves the owner's connection, and deletes
  those keys. Skips cleanly when storage is locked / no creds / no owned
  objects, and never throws (failures are recorded, not propagated) so storage
  cleanup can't block note deletion. R2Service class now exported for testing.
- app.handleDeleteModalConfirm: calls it after handleFullDeleteVideo succeeds.

Tests: tests/storage-key-collection.test.mjs (4),
tests/services/r2-delete-video-storage.test.mjs (4). Full suite green.

Note: superseded versions orphaned by past edits aren't retroactively cleaned;
edit-time cleanup (next) prevents new orphans going forward.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-16 22:09:57 -04:00

64 lines
2.1 KiB
JavaScript

// Scenario (SCN-collect-video-storage-keys):
// Deriving the object keys backing a video note for cleanup must (a) cover the
// video, its sibling .torrent, and the thumbnail, (b) decode percent-encoded
// paths, and (c) NEVER include URLs outside the owner's bucket base (so we
// can't delete objects we don't own). Pure + deterministic.
import test from "node:test";
import assert from "node:assert/strict";
import { collectVideoStorageKeys } from "../js/utils/storagePointer.js";
const BASE = "https://pub.bitvid.network";
test("collects video + .torrent + thumbnail keys for an owned video", () => {
const keys = collectVideoStorageKeys({
videos: [
{
url: `${BASE}/u/np/abc123/my-clip.mp4`,
thumbnail: `${BASE}/u/np/abc123/my-clip.thumb.jpg`,
},
],
publicBaseUrl: BASE,
});
assert.deepEqual(
new Set(keys),
new Set([
"u/np/abc123/my-clip.mp4",
"u/np/abc123/my-clip.torrent",
"u/np/abc123/my-clip.thumb.jpg",
]),
);
});
test("ignores URLs outside the bucket base (never deletes others' objects)", () => {
const keys = collectVideoStorageKeys({
videos: [
{
url: "https://youtube.com/watch?v=abc",
thumbnail: `${BASE}/u/np/h/t.thumb.jpg`, // this one IS ours
},
],
publicBaseUrl: BASE,
});
assert.ok(!keys.some((k) => k.includes("youtube")), "no external host keys");
assert.deepEqual(new Set(keys), new Set(["u/np/h/t.thumb.jpg"]));
});
test("decodes percent-encoded path segments and tolerates a trailing-slash base", () => {
const keys = collectVideoStorageKeys({
videos: [{ url: `${BASE}/u/np/h/my%20clip.mp4` }],
publicBaseUrl: `${BASE}/`,
});
assert.ok(keys.includes("u/np/h/my clip.mp4"), `got ${JSON.stringify(keys)}`);
assert.ok(keys.includes("u/np/h/my clip.torrent"));
});
test("returns nothing when no base url or no owned objects", () => {
assert.deepEqual(collectVideoStorageKeys({ videos: [{ url: `${BASE}/x.mp4` }], publicBaseUrl: "" }), []);
assert.deepEqual(
collectVideoStorageKeys({ videos: [{ url: "https://other.example/x.mp4" }], publicBaseUrl: BASE }),
[],
);
});