mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 11:52:25 +00:00
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:
1 parent
bbb8dd0fd8
commit
513f75e4da
6 files changed
+233
-2
No files matched your search
@@ -3,11 +3,13 @@
|
||||
// State-free helpers extracted from r2Service.js to keep that service under the
|
||||
// file-size budget. No behavior change.
|
||||
|
||||
import { userLogger } from "../utils/logger.js";
|
||||
import { userLogger, devLogger } from "../utils/logger.js";
|
||||
import { calculateTorrentInfoHash } from "../utils/torrentHash.js";
|
||||
import { buildPublicUrl } from "../r2.js";
|
||||
import { sanitizeBucketName } from "../storage/r2-mgmt.js";
|
||||
import { truncateMiddle } from "../utils/formatters.js";
|
||||
import { safeEncodeNpub } from "../utils/nostrHelpers.js";
|
||||
import { ensureS3SdkLoaded } from "../storage/s3-client.js";
|
||||
|
||||
const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
|
||||
|
||||
@@ -207,3 +209,93 @@ export async function verifyPublicAccess(ctx, { settings, npub }) {
|
||||
return { success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a provider-aware S3 client from resolved connection settings.
|
||||
* Cloudflare R2 always uses path-style (via makeR2Client); a generic S3 bucket
|
||||
* honors the connection's forcePathStyle. Shared by storage list/delete paths.
|
||||
*/
|
||||
export function buildProviderClient(settings, { makeR2Client, makeS3Client } = {}) {
|
||||
const accessKeyId = (settings?.accessKeyId || "").trim();
|
||||
const secretAccessKey = (settings?.secretAccessKey || "").trim();
|
||||
const useR2 =
|
||||
settings?.provider === "cloudflare_r2" || Boolean(settings?.accountId);
|
||||
if (useR2) {
|
||||
return makeR2Client({
|
||||
accountId: settings.accountId,
|
||||
endpoint: settings.endpoint,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
region: settings.region,
|
||||
});
|
||||
}
|
||||
return makeS3Client({
|
||||
endpoint: settings.endpoint,
|
||||
region: settings.region,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
forcePathStyle: Boolean(settings?.forcePathStyle),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List the object keys under a user's storage prefix (u/<npub>/). `ctx` is the
|
||||
* R2Service instance (resolveConnection / makeR2Client / makeS3Client /
|
||||
* listObjects). Unlock-gated; returns { ok, reason, keys }.
|
||||
*/
|
||||
export async function listVideoStorageObjects(
|
||||
ctx,
|
||||
{ npub = "", pubkey = "", credentials = null } = {},
|
||||
) {
|
||||
let resolvedNpub = (npub || "").trim();
|
||||
if (!resolvedNpub && pubkey) {
|
||||
resolvedNpub = safeEncodeNpub(pubkey) || "";
|
||||
}
|
||||
let settings = credentials;
|
||||
if (!settings && resolvedNpub) {
|
||||
try {
|
||||
settings = await ctx.resolveConnection(resolvedNpub);
|
||||
} catch (err) {
|
||||
devLogger.warn("[R2Service] listVideoStorageObjects: resolve failed:", err);
|
||||
}
|
||||
}
|
||||
if (!settings) {
|
||||
return { ok: false, reason: "no-connection", keys: [] };
|
||||
}
|
||||
if (
|
||||
!(settings.accessKeyId || "").trim() ||
|
||||
!(settings.secretAccessKey || "").trim()
|
||||
) {
|
||||
return { ok: false, reason: "storage-locked", keys: [] };
|
||||
}
|
||||
const bucket = (settings.bucket || "").trim();
|
||||
if (!bucket) {
|
||||
return { ok: false, reason: "missing-bucket", keys: [] };
|
||||
}
|
||||
let s3;
|
||||
try {
|
||||
const ensureSdk =
|
||||
typeof ctx.ensureS3SdkLoaded === "function"
|
||||
? ctx.ensureS3SdkLoaded
|
||||
: ensureS3SdkLoaded;
|
||||
await ensureSdk();
|
||||
s3 = buildProviderClient(settings, {
|
||||
makeR2Client: ctx.makeR2Client,
|
||||
makeS3Client: ctx.makeS3Client,
|
||||
});
|
||||
} catch (err) {
|
||||
devLogger.warn("[R2Service] listVideoStorageObjects: client build failed:", err);
|
||||
return { ok: false, reason: "client-error", keys: [] };
|
||||
}
|
||||
try {
|
||||
const keys = await ctx.listObjects({
|
||||
s3,
|
||||
bucket,
|
||||
prefix: resolvedNpub ? `u/${resolvedNpub}/` : "",
|
||||
});
|
||||
return { ok: true, keys };
|
||||
} catch (err) {
|
||||
devLogger.warn("[R2Service] listVideoStorageObjects: list failed:", err);
|
||||
return { ok: false, reason: "list-error", keys: [] };
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user