feat(my-videos): bucket object listing for reconciliation (Phase 2, step 2)

Adds the bucket-enumeration primitive the My Videos tab needs to find orphan and
missing files:
- s3 layer: ListObjectsV2Command wired into the SDK wrapper + a paginated
  listObjects({ s3, bucket, prefix, maxKeys }) helper (follows continuation
  tokens, bounded by maxKeys).
- r2Service.listVideoStorageObjects({ npub|pubkey }) — unlock-gated, lists under
  the user's own u/<npub>/ prefix; returns { ok, reason, keys }. Body lives in
  r2ServiceHelpers (with a shared buildProviderClient) so r2Service stays under
  its file-size budget; ensureS3SdkLoaded is injectable via ctx for testing.

Tests + mutation-verified: never lists while locked (storage-locked, no call),
no-connection / missing-bucket gating, and unlocked listing is scoped to the
user's own u/<npub>/ prefix (empty-prefix mutation fails). Removing the lock gate
or widening the prefix each fail a scenario.

Build + full lint green (r2Service back under limit).

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.8 committed 2026-06-20 14:55:41 -04:00
1 parent bbb8dd0fd8
commit 513f75e4da
6 files changed
+233 -2

No files matched your search

+13
View File
@@ -35,6 +35,7 @@ import {
ensureBucketCors,
ensureBucketExists,
deleteObject,
listObjects,
} from "../storage/s3-multipart.js";
import { ensureS3SdkLoaded, makeS3Client } from "../storage/s3-client.js";
import { truncateMiddle } from "../utils/formatters.js";
@@ -60,6 +61,7 @@ import {
createDefaultSettings,
safeDecodeNpub,
verifyPublicAccess as verifyPublicAccessHelper,
listVideoStorageObjects as listVideoStorageObjectsHelper,
} from "./r2ServiceHelpers.js";
const STATUS_VARIANTS = new Set(["info", "success", "error", "warning"]);
@@ -76,6 +78,7 @@ export class R2Service {
ensureBucketExists: ensureBucketExistsOverride,
ensureBucketCors: ensureBucketCorsOverride,
deleteObject: deleteObjectOverride,
listObjects: listObjectsOverride,
} = {}) {
this.listeners = new Map();
this.cloudflareSettings = null;
@@ -85,6 +88,7 @@ export class R2Service {
this.ensureBucketExists = ensureBucketExistsOverride || ensureBucketExists;
this.ensureBucketCors = ensureBucketCorsOverride || ensureBucketCors;
this.deleteObject = deleteObjectOverride || deleteObject;
this.listObjects = listObjectsOverride || listObjects;
}
/**
@@ -226,6 +230,15 @@ export class R2Service {
return summary;
}
/**
* List the object keys under the user's storage prefix (u/<npub>/) so the
* "My Videos" tab can reconcile notes against actual bucket contents. Unlock-
* gated (needs credentials); returns { ok, reason, keys }.
*/
async listVideoStorageObjects(args = {}) {
return listVideoStorageObjectsHelper(this, args);
}
on(event, handler) {
if (typeof handler !== "function") {
return () => {};