mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
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]>
64 lines
2.1 KiB
JavaScript
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 }),
|
|
[],
|
|
);
|
|
});
|