From ee31e4318a186ffc5fc5ff2602936de5d687c545 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Mon, 26 Jan 2026 21:43:39 -0500 Subject: [PATCH] Compute upload identifiers before building R2 keys --- js/services/r2Service.js | 29 ++++++++++++++++++++++++----- js/services/s3UploadService.js | 30 ++++++++++++++++++++++++------ js/ui/components/UploadModal.js | 10 +++++++--- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/js/services/r2Service.js b/js/services/r2Service.js index b21c4792..6ae7e1be 100644 --- a/js/services/r2Service.js +++ b/js/services/r2Service.js @@ -44,6 +44,7 @@ import { VIDEO_NOTE_ERROR_CODES, } from "./videoNotePayload.js"; import storageService from "./storageService.js"; +import { calculateTorrentInfoHash } from "../utils/torrentHash.js"; const STATUS_VARIANTS = new Set(["info", "success", "error", "warning"]); const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/; @@ -56,6 +57,26 @@ function isValidInfoHash(value) { return INFO_HASH_PATTERN.test(value); } +async function resolveUploadIdentifier({ infoHash = "", file = null } = {}) { + const normalizedInfoHash = normalizeInfoHash(infoHash); + if (isValidInfoHash(normalizedInfoHash)) { + return normalizedInfoHash; + } + if (!file) { + return ""; + } + try { + const computedHash = await calculateTorrentInfoHash(file); + const normalizedComputed = normalizeInfoHash(computedHash); + if (isValidInfoHash(normalizedComputed)) { + return normalizedComputed; + } + } catch (err) { + userLogger.warn("Failed to precompute info hash for storage key:", err); + } + return ""; +} + function buildCorsGuidance({ accountId } = {}) { const origin = typeof window !== "undefined" && window.location @@ -844,10 +865,9 @@ class R2Service { this.updateCloudflareProgress(0); this.setCloudflareUploading(true); - const normalizedInfoHash = normalizeInfoHash(infoHash); - const keyIdentifier = isValidInfoHash(normalizedInfoHash) - ? normalizedInfoHash - : ""; + const keyIdentifier = await resolveUploadIdentifier({ infoHash, file }); + const normalizedInfoHash = normalizeInfoHash(infoHash || keyIdentifier); + const hasValidInfoHash = isValidInfoHash(normalizedInfoHash); let bucketResult = null; try { @@ -983,7 +1003,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 hasValidInfoHash = isValidInfoHash(normalizedInfoHash); let generatedMagnet = ""; let generatedWs = ""; diff --git a/js/services/s3UploadService.js b/js/services/s3UploadService.js index a5c58e1e..4e950bb9 100644 --- a/js/services/s3UploadService.js +++ b/js/services/s3UploadService.js @@ -13,6 +13,7 @@ import { normalizeVideoNotePayload, VIDEO_NOTE_ERROR_CODES, } from "./videoNotePayload.js"; +import { calculateTorrentInfoHash } from "../utils/torrentHash.js"; const STATUS_VARIANTS = new Set(["info", "success", "error", "warning"]); const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/; @@ -25,6 +26,26 @@ function isValidInfoHash(value) { return INFO_HASH_PATTERN.test(value); } +async function resolveUploadIdentifier({ infoHash = "", file = null } = {}) { + const normalizedInfoHash = normalizeInfoHash(infoHash); + if (isValidInfoHash(normalizedInfoHash)) { + return normalizedInfoHash; + } + if (!file) { + return ""; + } + try { + const computedHash = await calculateTorrentInfoHash(file); + const normalizedComputed = normalizeInfoHash(computedHash); + if (isValidInfoHash(normalizedComputed)) { + return normalizedComputed; + } + } catch (err) { + userLogger.warn("Failed to precompute info hash for storage key:", err); + } + return ""; +} + class S3UploadService { constructor() { this.listeners = new Map(); @@ -215,10 +236,9 @@ class S3UploadService { try { await ensureS3SdkLoaded(); - const normalizedInfoHash = normalizeInfoHash(infoHash); - const keyIdentifier = isValidInfoHash(normalizedInfoHash) - ? normalizedInfoHash - : ""; + const keyIdentifier = await resolveUploadIdentifier({ infoHash, file }); + const normalizedInfoHash = normalizeInfoHash(infoHash || keyIdentifier); + const hasValidInfoHash = isValidInfoHash(normalizedInfoHash); const s3 = makeS3Client({ endpoint: normalized.endpoint, @@ -327,8 +347,6 @@ class S3UploadService { return false; } - // normalizedInfoHash is already calculated above - const hasValidInfoHash = isValidInfoHash(normalizedInfoHash); let generatedMagnet = ""; let generatedWs = ""; diff --git a/js/ui/components/UploadModal.js b/js/ui/components/UploadModal.js index e16bc2fd..32f4d226 100644 --- a/js/ui/components/UploadModal.js +++ b/js/ui/components/UploadModal.js @@ -515,7 +515,12 @@ export class UploadModal { this.updateVideoProgress(0, "Preparing upload..."); try { - // 1. Prepare Upload (Get Creds & Bucket) + // 1. Compute Identifier (Info Hash / Fingerprint) before generating keys + const identifier = await this.resolveUploadIdentifier(file); + + if (this.videoUploadId !== currentUploadId) return; + + // 2. Prepare Upload (Get Creds & Bucket) const pubkey = this.getCurrentPubkey(); const npub = this.safeEncodeNpub(pubkey); const service = this.activeProvider === PROVIDERS.R2 || this.activeProvider === "cloudflare_r2" @@ -526,8 +531,7 @@ export class UploadModal { if (this.videoUploadId !== currentUploadId) return; - // 2. Determine Keys - const identifier = await this.resolveUploadIdentifier(file); + // 3. Determine Keys const videoKey = buildR2Key(npub, file, identifier); const baseDomain = bucketEntry.publicBaseUrl;