Files
bitvid/js/storage/s3-client.js
T
thePR0M3TH3ANandClaude Opus 4.8 513f75e4da 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]>
2026-06-20 14:55:41 -04:00

116 lines
2.7 KiB
JavaScript

// js/storage/s3-client.js
const AWS_SDK_SPECIFIER = "https://esm.sh/@aws-sdk/[email protected]?bundle";
const disableNetworkImports = Boolean(
globalThis?.__BITVID_DISABLE_NETWORK_IMPORTS__
);
let awsSdk = null;
let awsSdkLoadError = null;
let awsSdkLoadPromise = null;
function assignAwsSdk(module) {
if (!module) {
return;
}
awsSdk = {
S3Client: module.S3Client,
CreateBucketCommand: module.CreateBucketCommand,
CreateMultipartUploadCommand: module.CreateMultipartUploadCommand,
UploadPartCommand: module.UploadPartCommand,
CompleteMultipartUploadCommand: module.CompleteMultipartUploadCommand,
AbortMultipartUploadCommand: module.AbortMultipartUploadCommand,
ListBucketsCommand: module.ListBucketsCommand,
HeadBucketCommand: module.HeadBucketCommand,
PutBucketCorsCommand: module.PutBucketCorsCommand,
DeleteObjectCommand: module.DeleteObjectCommand,
PutObjectCommand: module.PutObjectCommand,
GetObjectCommand: module.GetObjectCommand,
ListObjectsV2Command: module.ListObjectsV2Command,
};
}
async function loadAwsSdkModule() {
if (awsSdk || awsSdkLoadError) {
return awsSdk;
}
if (!awsSdkLoadPromise) {
if (disableNetworkImports) {
awsSdkLoadError = new Error("S3 SDK network import has been disabled.");
return null;
}
awsSdkLoadPromise = import(AWS_SDK_SPECIFIER)
.then((module) => {
assignAwsSdk(module);
return awsSdk;
})
.catch((error) => {
awsSdkLoadError = error || new Error("Failed to load S3 SDK module");
throw awsSdkLoadError;
});
}
try {
return await awsSdkLoadPromise;
} catch (error) {
awsSdkLoadError = error;
throw error;
}
}
export function requireAwsSdk() {
if (awsSdk) {
return awsSdk;
}
if (awsSdkLoadError) {
throw awsSdkLoadError;
}
throw new Error("S3 SDK has not finished loading. Call ensureS3SdkLoaded() first.");
}
export function ensureS3SdkLoaded() {
return loadAwsSdkModule();
}
export function getS3SdkLoadError() {
return awsSdkLoadError;
}
export function isS3SdkAvailable() {
return Boolean(awsSdk);
}
export function makeS3Client({
endpoint,
region,
accessKeyId,
secretAccessKey,
forcePathStyle,
}) {
if (!accessKeyId || !secretAccessKey) {
throw new Error("Missing S3 credentials");
}
const { S3Client } = requireAwsSdk();
return new S3Client({
region: region || "auto",
endpoint,
credentials: { accessKeyId, secretAccessKey },
forcePathStyle: Boolean(forcePathStyle),
});
}
if (!disableNetworkImports) {
// Begin loading immediately in environments that support network imports.
loadAwsSdkModule().catch(() => {
// Errors are captured for later inspection by getS3SdkLoadError().
});
}