From c02db755acefa80074fb64d4bab348b47db0fe0f Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:47:05 -0500 Subject: [PATCH] Refactor R2 key prefix for deterministic identifiers --- js/r2.js | 11 ++++++----- js/services/attachmentService.js | 3 ++- js/services/r2Service.js | 8 ++++++-- js/services/s3UploadService.js | 7 ++++++- js/ui/components/UploadModal.js | 21 +++++++++++++++++++-- js/ui/profileModalController.js | 4 +++- 6 files changed, 42 insertions(+), 12 deletions(-) diff --git a/js/r2.js b/js/r2.js index 0c8d2776..fcaa8b08 100644 --- a/js/r2.js +++ b/js/r2.js @@ -211,13 +211,13 @@ function guessExtension(file) { } } -export function buildR2Key(npub, file) { - const now = new Date(); - const year = now.getUTCFullYear(); - const month = String(now.getUTCMonth() + 1).padStart(2, "0"); +export function buildR2Key(npub, file, identifier = "") { const safeNpub = String(npub || "") .toLowerCase() .replace(/[^a-z0-9]/g, ""); + const safeIdentifier = String(identifier || "") + .toLowerCase() + .replace(/[^a-z0-9]/g, ""); const baseName = typeof file?.name === "string" ? file.name : "video"; const withoutExt = baseName.replace(/\.[^/.]+$/, ""); const slug = withoutExt @@ -226,7 +226,8 @@ export function buildR2Key(npub, file) { .replace(/^-+|-+$/g, ""); const safeSlug = slug || "video"; const ext = guessExtension(file); - return `u/${safeNpub}/${year}/${month}/${safeSlug}.${ext}`; + const namespace = safeIdentifier || "uploads"; + return `u/${safeNpub}/${namespace}/${safeSlug}.${ext}`; } export function buildPublicUrl(baseUrl, key) { diff --git a/js/services/attachmentService.js b/js/services/attachmentService.js index 267d60c6..ba4adb0f 100644 --- a/js/services/attachmentService.js +++ b/js/services/attachmentService.js @@ -356,7 +356,8 @@ export async function uploadAttachment({ } const prepared = await prepareAttachmentUpload({ file, encrypt }); - const key = typeof buildKey === "function" ? buildKey(pubkey, file) : ""; + const key = + typeof buildKey === "function" ? buildKey(pubkey, file, prepared.sha256) : ""; if (!key) { throw new Error("Unable to derive storage key for attachment."); } diff --git a/js/services/r2Service.js b/js/services/r2Service.js index c17500c6..b21c4792 100644 --- a/js/services/r2Service.js +++ b/js/services/r2Service.js @@ -844,6 +844,11 @@ class R2Service { this.updateCloudflareProgress(0); this.setCloudflareUploading(true); + const normalizedInfoHash = normalizeInfoHash(infoHash); + const keyIdentifier = isValidInfoHash(normalizedInfoHash) + ? normalizedInfoHash + : ""; + let bucketResult = null; try { bucketResult = await this.ensureBucketConfigForNpub(npub, { @@ -880,7 +885,7 @@ class R2Service { this.setCloudflareUploadStatus(statusMessage, "info"); // Use forced keys if provided, otherwise generate them - const key = forcedVideoKey || buildR2Key(npub, file); + const key = forcedVideoKey || buildR2Key(npub, file, keyIdentifier); const publicUrl = forcedVideoUrl || buildPublicUrl(bucketEntry.publicBaseUrl, key); const buildTorrentKey = () => { @@ -978,7 +983,6 @@ class R2Service { // - `xs` (eXact Source): The URL to the .torrent file on R2. This allows // clients to fetch metadata (file structure, piece hashes) instantly via HTTP // instead of waiting to fetch it from the DHT (which can take minutes). - const normalizedInfoHash = normalizeInfoHash(infoHash); const hasValidInfoHash = isValidInfoHash(normalizedInfoHash); let generatedMagnet = ""; let generatedWs = ""; diff --git a/js/services/s3UploadService.js b/js/services/s3UploadService.js index b438e396..965f6c34 100644 --- a/js/services/s3UploadService.js +++ b/js/services/s3UploadService.js @@ -215,6 +215,11 @@ class S3UploadService { try { await ensureS3SdkLoaded(); + const normalizedInfoHash = normalizeInfoHash(infoHash); + const keyIdentifier = isValidInfoHash(normalizedInfoHash) + ? normalizedInfoHash + : ""; + const s3 = makeS3Client({ endpoint: normalized.endpoint, region: normalized.region, @@ -223,7 +228,7 @@ class S3UploadService { forcePathStyle: Boolean(normalized.forcePathStyle), }); - const key = forcedVideoKey || buildR2Key(npub, file); + const key = forcedVideoKey || buildR2Key(npub, file, keyIdentifier); const publicUrl = forcedVideoUrl || buildS3ObjectUrl({ diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index 3680a591..e16bc2fd 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -6,7 +6,10 @@ import { getVideoNoteErrorMessage, normalizeVideoNotePayload, } from "../../services/videoNotePayload.js"; -import { createTorrentMetadata } from "../../utils/torrentHash.js"; +import { + calculateTorrentInfoHash, + createTorrentMetadata, +} from "../../utils/torrentHash.js"; import { sanitizeBucketName } from "../../storage/r2-mgmt.js"; import { buildR2Key, buildPublicUrl } from "../../r2.js"; import { buildS3ObjectUrl } from "../../services/s3Service.js"; @@ -524,7 +527,8 @@ export class UploadModal { if (this.videoUploadId !== currentUploadId) return; // 2. Determine Keys - const videoKey = buildR2Key(npub, file); + const identifier = await this.resolveUploadIdentifier(file); + const videoKey = buildR2Key(npub, file, identifier); const baseDomain = bucketEntry.publicBaseUrl; // Note: buildPublicUrl works for both if the base is clean @@ -1088,6 +1092,19 @@ export class UploadModal { }; } + async resolveUploadIdentifier(file) { + try { + const infoHash = await calculateTorrentInfoHash(file); + const normalized = normalizeInfoHash(infoHash); + if (isValidInfoHash(normalized)) { + return normalized; + } + } catch (hashErr) { + userLogger.warn("Failed to precompute info hash for storage key:", hashErr); + } + return ""; + } + async handleExternalFlow(metadata) { metadata.url = this.inputs.url?.value?.trim() || ""; metadata.magnet = this.inputs.magnet?.value?.trim() || ""; diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index 040bab22..fe57b464 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -8914,7 +8914,9 @@ export class ProfileModalController { return; } - const key = buildR2Key(pubkey, file); + const keyIdentifier = + type === "picture" ? "profile-picture" : "profile-banner"; + const key = buildR2Key(pubkey, file, keyIdentifier); await r2Service.uploadFile({ file, ...credentials,