mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
refactor(upload): extract shared MediaUploader (step 1/3 edit-upload parity)
Extract UploadModal's storage+torrent upload core into a reusable js/ui/components/mediaUploader.js (resolveActiveConnection, uploadThumbnail, uploadVideo incl. infohash/.torrent/magnet) so the Edit modal can reuse it without duplicating R2/S3 + torrent logic (and without blowing EditModal's file-size budget). UploadModal now delegates its thumbnail/video upload core to MediaUploader, keeping its own progress/state/zombie-guard wrappers — behavior preserved. UploadModal: 1319 -> 1191 lines. Build + full lint + unit suite green. NOTE: the upload flow can't be unit-tested (browser + S3/R2 deps); this is a behavior-preserving extraction pending manual re-test of a normal upload before Edit upload is layered on top (steps 2-3). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
f218ec32fa
commit
7bbf0f08b1
2 files changed
+342
-191
No files matched your search
+38
-191
@@ -1,6 +1,7 @@
|
||||
// components/UploadModal.js
|
||||
import { createModalAccessibility } from "./modalAccessibility.js";
|
||||
import { Nip71FormManager } from "./nip71FormManager.js";
|
||||
import { MediaUploader } from "./mediaUploader.js";
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
import {
|
||||
getVideoNoteErrorMessage,
|
||||
@@ -73,6 +74,15 @@ export class UploadModal {
|
||||
typeof getCurrentPubkey === "function" ? getCurrentPubkey : null;
|
||||
this.safeEncodeNpub =
|
||||
typeof safeEncodeNpub === "function" ? safeEncodeNpub : () => "";
|
||||
// Shared storage+torrent upload core (also used by the Edit modal).
|
||||
this.mediaUploader = new MediaUploader({
|
||||
r2Service: this.r2Service,
|
||||
s3Service: this.s3Service,
|
||||
storageService: this.storageService,
|
||||
getCurrentPubkey: () =>
|
||||
this.getCurrentPubkey ? this.getCurrentPubkey() : null,
|
||||
safeEncodeNpub: (pubkey) => this.safeEncodeNpub(pubkey),
|
||||
});
|
||||
this.eventTarget =
|
||||
eventTarget instanceof EventTarget ? eventTarget : new EventTarget();
|
||||
this.container = container || null;
|
||||
@@ -524,159 +534,36 @@ export class UploadModal {
|
||||
this.updateVideoProgress(0, "Preparing upload...");
|
||||
|
||||
try {
|
||||
// 1. Compute Identifier (Info Hash / Fingerprint) before generating keys
|
||||
const identifier = await this.resolveUploadIdentifier(file);
|
||||
|
||||
if (this.videoUploadId !== currentUploadId) return; // Zombie guard
|
||||
|
||||
// 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"
|
||||
? this.r2Service
|
||||
: this.s3Service;
|
||||
|
||||
const { settings, bucketEntry } = await service.prepareUpload(npub, { credentials: this.activeCredentials });
|
||||
|
||||
if (this.videoUploadId !== currentUploadId) return; // Zombie guard
|
||||
|
||||
// 3. Determine Keys
|
||||
const videoKey = buildR2Key(npub, file, identifier);
|
||||
const baseDomain = bucketEntry.publicBaseUrl;
|
||||
|
||||
// Note: buildPublicUrl works for both if the base is clean
|
||||
// For S3, buildS3ObjectUrl might be safer if we have complex paths, but prepareUpload standardizes on publicBaseUrl
|
||||
let videoPublicUrl = "";
|
||||
if (this.activeProvider === PROVIDERS.R2 || this.activeProvider === "cloudflare_r2") {
|
||||
videoPublicUrl = buildPublicUrl(baseDomain, videoKey);
|
||||
} else {
|
||||
videoPublicUrl = buildS3ObjectUrl({
|
||||
publicBaseUrl: baseDomain,
|
||||
key: videoKey,
|
||||
// For S3 we might need forcePathStyle if publicBaseUrl isn't set, but prepareUpload enforces it.
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Start Video Upload
|
||||
this.updateVideoProgress(0, "Uploading video...");
|
||||
devLogger.debug("UploadModal uploadFile settings presence", {
|
||||
hasAccountId: Boolean(settings.accountId),
|
||||
hasEndpoint: Boolean(settings.endpoint),
|
||||
hasProvider: Boolean(settings.provider),
|
||||
hasRegion: Boolean(settings.region),
|
||||
hasAccessKeyId: Boolean(settings.accessKeyId),
|
||||
hasSecretAccessKey: Boolean(settings.secretAccessKey),
|
||||
hasForcePathStyle: settings.forcePathStyle !== undefined,
|
||||
});
|
||||
const uploadPromise = service.uploadFile({
|
||||
file,
|
||||
bucket: bucketEntry.bucket,
|
||||
key: videoKey,
|
||||
// Spread settings carefully
|
||||
accountId: settings.accountId,
|
||||
endpoint: settings.endpoint,
|
||||
provider: settings.provider || this.activeProvider || "cloudflare_r2",
|
||||
region: settings.region,
|
||||
accessKeyId: settings.accessKeyId,
|
||||
secretAccessKey: settings.secretAccessKey,
|
||||
forcePathStyle: settings.forcePathStyle,
|
||||
createBucketIfMissing: true,
|
||||
onProgress: (fraction) => {
|
||||
const result = await this.mediaUploader.uploadVideo(file, {
|
||||
provider: this.activeProvider,
|
||||
credentials: this.activeCredentials,
|
||||
onProgress: ({ fraction, label }) => {
|
||||
if (this.videoUploadId === currentUploadId) {
|
||||
this.updateVideoProgress(fraction);
|
||||
this.updateVideoProgress(fraction, label || undefined);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// 4. Calculate Info Hash (Parallel if possible, but JS single thread limits this.
|
||||
// Since createTorrentMetadata reads the file, it might contend with upload read.
|
||||
// Let's do it concurrently and hope the browser handles file I/O well.
|
||||
this.updateVideoProgress(null, "Uploading & Calculating Hash...");
|
||||
|
||||
const torrentPromise = this.generateTorrentMetadata({ file, videoPublicUrl });
|
||||
|
||||
const [uploadResult, torrentResult] = await Promise.all([uploadPromise, torrentPromise]);
|
||||
|
||||
if (this.videoUploadId !== currentUploadId) return; // Zombie guard
|
||||
|
||||
// Video Complete
|
||||
this.videoUploadState.status = 'complete';
|
||||
this.videoUploadState.url = videoPublicUrl;
|
||||
this.videoUploadState.key = videoKey;
|
||||
const storagePrefix = buildStoragePrefixFromKey({
|
||||
publicBaseUrl: baseDomain,
|
||||
key: videoKey,
|
||||
});
|
||||
const providerLabel =
|
||||
this.activeProvider === PROVIDERS.R2 || this.activeProvider === "cloudflare_r2"
|
||||
? "r2"
|
||||
: "s3";
|
||||
this.videoUploadState.storagePointer = buildStoragePointerValue({
|
||||
provider: providerLabel,
|
||||
prefix: storagePrefix,
|
||||
});
|
||||
if (this.results.videoUrl) this.results.videoUrl.value = videoPublicUrl;
|
||||
this.updateVideoProgress(1, "Video uploaded.");
|
||||
|
||||
// 5. Handle Torrent Result & Upload .torrent file
|
||||
if (torrentResult.hasValidInfoHash && torrentResult.torrentFile) {
|
||||
const baseKey = videoKey.replace(/\.[^/.]+$/, "");
|
||||
const torrentKey = (baseKey && baseKey !== videoKey) ? `${baseKey}.torrent` : `${videoKey}.torrent`;
|
||||
|
||||
let torrentPublicUrl = "";
|
||||
if (this.activeProvider === PROVIDERS.R2 || this.activeProvider === "cloudflare_r2") {
|
||||
torrentPublicUrl = buildPublicUrl(baseDomain, torrentKey);
|
||||
} else {
|
||||
torrentPublicUrl = buildS3ObjectUrl({ publicBaseUrl: baseDomain, key: torrentKey });
|
||||
}
|
||||
|
||||
this.updateVideoProgress(1, "Uploading torrent metadata...");
|
||||
|
||||
devLogger.debug("UploadModal uploadFile settings presence", {
|
||||
hasAccountId: Boolean(settings.accountId),
|
||||
hasEndpoint: Boolean(settings.endpoint),
|
||||
hasProvider: Boolean(settings.provider),
|
||||
hasRegion: Boolean(settings.region),
|
||||
hasAccessKeyId: Boolean(settings.accessKeyId),
|
||||
hasSecretAccessKey: Boolean(settings.secretAccessKey),
|
||||
hasForcePathStyle: settings.forcePathStyle !== undefined,
|
||||
});
|
||||
await service.uploadFile({
|
||||
file: torrentResult.torrentFile,
|
||||
bucket: bucketEntry.bucket,
|
||||
key: torrentKey,
|
||||
accountId: settings.accountId,
|
||||
endpoint: settings.endpoint,
|
||||
provider: settings.provider || this.activeProvider || "cloudflare_r2",
|
||||
region: settings.region,
|
||||
accessKeyId: settings.accessKeyId,
|
||||
secretAccessKey: settings.secretAccessKey,
|
||||
forcePathStyle: settings.forcePathStyle,
|
||||
createBucketIfMissing: true,
|
||||
});
|
||||
|
||||
if (this.videoUploadId !== currentUploadId) return; // Zombie guard
|
||||
this.videoUploadState.url = result.url;
|
||||
this.videoUploadState.key = result.key;
|
||||
this.videoUploadState.storagePointer = result.storagePointer;
|
||||
if (this.results.videoUrl) this.results.videoUrl.value = result.url;
|
||||
|
||||
if (result.hasValidInfoHash) {
|
||||
this.torrentState.status = 'complete';
|
||||
this.torrentState.infoHash = torrentResult.infoHash;
|
||||
this.torrentState.url = torrentPublicUrl;
|
||||
this.torrentState.file = torrentResult.torrentFile;
|
||||
this.torrentState.infoHash = result.infoHash;
|
||||
this.torrentState.url = result.torrentUrl;
|
||||
this.torrentState.file = result.torrentFile;
|
||||
this.torrentState.magnet = result.magnet;
|
||||
|
||||
// Construct Magnet
|
||||
const encodedDn = encodeURIComponent(file.name);
|
||||
const encodedWs = encodeURIComponent(videoPublicUrl);
|
||||
const encodedXs = encodeURIComponent(torrentPublicUrl);
|
||||
const magnet = `magnet:?xt=urn:btih:${torrentResult.infoHash}&dn=${encodedDn}&ws=${encodedWs}&xs=${encodedXs}`;
|
||||
|
||||
this.torrentState.magnet = magnet;
|
||||
|
||||
if (this.results.magnet) this.results.magnet.value = magnet;
|
||||
if (this.results.torrentUrl) this.results.torrentUrl.value = torrentPublicUrl;
|
||||
|
||||
this.updateVideoProgress(1, "Ready to publish!");
|
||||
if (this.results.magnet) this.results.magnet.value = result.magnet;
|
||||
if (this.results.torrentUrl) this.results.torrentUrl.value = result.torrentUrl;
|
||||
} else {
|
||||
this.torrentState.status = 'skipped'; // Failed hash or invalid
|
||||
this.updateVideoProgress(1, "Upload complete (No torrent fallback).");
|
||||
if (this.results.magnet) this.results.magnet.value = "Not available (Info Hash failed)";
|
||||
if (this.results.torrentUrl) this.results.torrentUrl.value = "Not available";
|
||||
}
|
||||
@@ -725,58 +612,18 @@ export class UploadModal {
|
||||
this.updateThumbnailProgress(0, "Uploading thumbnail...");
|
||||
|
||||
try {
|
||||
const pubkey = this.getCurrentPubkey();
|
||||
const npub = this.safeEncodeNpub(pubkey);
|
||||
const service = this.activeProvider === PROVIDERS.R2 || this.activeProvider === "cloudflare_r2"
|
||||
? this.r2Service
|
||||
: this.s3Service;
|
||||
|
||||
const { settings, bucketEntry } = await service.prepareUpload(npub, { credentials: this.activeCredentials });
|
||||
|
||||
if (this.thumbnailUploadId !== currentUploadId) return;
|
||||
|
||||
// Derive Key (randomish or based on file)
|
||||
// We don't have the video key here easily if video isn't selected yet.
|
||||
// Use a standalone key structure: npub/thumbnails/timestamp-name
|
||||
const timestamp = Date.now();
|
||||
const cleanName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_");
|
||||
const key = `${npub}/thumbnails/${timestamp}-${cleanName}`;
|
||||
|
||||
const baseDomain = bucketEntry.publicBaseUrl;
|
||||
let publicUrl = "";
|
||||
if (this.activeProvider === PROVIDERS.R2 || this.activeProvider === "cloudflare_r2") {
|
||||
publicUrl = buildPublicUrl(baseDomain, key);
|
||||
} else {
|
||||
publicUrl = buildS3ObjectUrl({ publicBaseUrl: baseDomain, key });
|
||||
}
|
||||
|
||||
devLogger.debug("UploadModal uploadFile settings presence", {
|
||||
hasAccountId: Boolean(settings.accountId),
|
||||
hasEndpoint: Boolean(settings.endpoint),
|
||||
hasProvider: Boolean(settings.provider),
|
||||
hasRegion: Boolean(settings.region),
|
||||
hasAccessKeyId: Boolean(settings.accessKeyId),
|
||||
hasSecretAccessKey: Boolean(settings.secretAccessKey),
|
||||
hasForcePathStyle: settings.forcePathStyle !== undefined,
|
||||
});
|
||||
await service.uploadFile({
|
||||
const { url: publicUrl } = await this.mediaUploader.uploadThumbnail(
|
||||
file,
|
||||
bucket: bucketEntry.bucket,
|
||||
key,
|
||||
accountId: settings.accountId,
|
||||
endpoint: settings.endpoint,
|
||||
provider: settings.provider || this.activeProvider || "cloudflare_r2",
|
||||
region: settings.region,
|
||||
accessKeyId: settings.accessKeyId,
|
||||
secretAccessKey: settings.secretAccessKey,
|
||||
forcePathStyle: settings.forcePathStyle,
|
||||
createBucketIfMissing: true,
|
||||
onProgress: (fraction) => {
|
||||
if (this.thumbnailUploadId === currentUploadId) {
|
||||
this.updateThumbnailProgress(fraction);
|
||||
}
|
||||
}
|
||||
});
|
||||
{
|
||||
provider: this.activeProvider,
|
||||
credentials: this.activeCredentials,
|
||||
onProgress: (fraction) => {
|
||||
if (this.thumbnailUploadId === currentUploadId) {
|
||||
this.updateThumbnailProgress(fraction);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
if (this.thumbnailUploadId !== currentUploadId) return;
|
||||
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
// js/ui/components/mediaUploader.js
|
||||
//
|
||||
// Shared storage-upload core extracted from UploadModal so the Upload and Edit
|
||||
// modals can upload thumbnails and videos without duplicating the R2/S3 +
|
||||
// torrent-metadata logic. This module owns ONLY the storage/torrent work
|
||||
// (resolve active connection, upload file, build keys/URLs, compute infohash,
|
||||
// build the .torrent + magnet). UI concerns (progress rendering, zombie-request
|
||||
// guards, form state) stay in the calling modal and are driven via onProgress.
|
||||
|
||||
import { devLogger, userLogger } from "../../utils/logger.js";
|
||||
import {
|
||||
calculateTorrentInfoHash,
|
||||
createTorrentMetadata,
|
||||
} from "../../utils/torrentHash.js";
|
||||
import { buildR2Key, buildPublicUrl } from "../../r2.js";
|
||||
import { buildS3ObjectUrl } from "../../services/s3Service.js";
|
||||
import { PROVIDERS } from "../../services/storageService.js";
|
||||
import {
|
||||
buildStoragePointerValue,
|
||||
buildStoragePrefixFromKey,
|
||||
} from "../../utils/storagePointer.js";
|
||||
|
||||
const INFO_HASH_PATTERN = /^[a-f0-9]{40}$/;
|
||||
|
||||
function normalizeInfoHash(value) {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
}
|
||||
|
||||
function isValidInfoHash(value) {
|
||||
return INFO_HASH_PATTERN.test(value);
|
||||
}
|
||||
|
||||
function isR2Provider(provider) {
|
||||
return provider === PROVIDERS.R2 || provider === "cloudflare_r2";
|
||||
}
|
||||
|
||||
export class MediaUploader {
|
||||
constructor({
|
||||
r2Service,
|
||||
s3Service,
|
||||
storageService,
|
||||
getCurrentPubkey,
|
||||
safeEncodeNpub,
|
||||
} = {}) {
|
||||
this.r2Service = r2Service || null;
|
||||
this.s3Service = s3Service || null;
|
||||
this.storageService = storageService || null;
|
||||
this.getCurrentPubkey =
|
||||
typeof getCurrentPubkey === "function" ? getCurrentPubkey : null;
|
||||
this.safeEncodeNpub =
|
||||
typeof safeEncodeNpub === "function" ? safeEncodeNpub : () => "";
|
||||
}
|
||||
|
||||
serviceFor(provider) {
|
||||
return isR2Provider(provider) ? this.r2Service : this.s3Service;
|
||||
}
|
||||
|
||||
publicUrlFor(provider, baseDomain, key) {
|
||||
return isR2Provider(provider)
|
||||
? buildPublicUrl(baseDomain, key)
|
||||
: buildS3ObjectUrl({ publicBaseUrl: baseDomain, key });
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the active storage connection for the current pubkey. Mirrors the
|
||||
* data portion of UploadModal.loadFromStorage (no UI). Returns:
|
||||
* { pubkey, npub, configured, unlocked, provider, credentials }
|
||||
* credentials is only populated when the connection is unlocked.
|
||||
*/
|
||||
async resolveActiveConnection() {
|
||||
const pubkey = this.getCurrentPubkey ? this.getCurrentPubkey() : null;
|
||||
const base = {
|
||||
pubkey: pubkey || null,
|
||||
npub: pubkey ? this.safeEncodeNpub(pubkey) : "",
|
||||
configured: false,
|
||||
unlocked: false,
|
||||
provider: null,
|
||||
credentials: null,
|
||||
};
|
||||
|
||||
if (!pubkey || !this.storageService) {
|
||||
return base;
|
||||
}
|
||||
|
||||
base.unlocked = this.storageService.isUnlocked(pubkey);
|
||||
|
||||
const connections = await this.storageService.listConnections(pubkey);
|
||||
const defaultConn = connections.find((c) => c.meta?.defaultForUploads);
|
||||
const targetConn = defaultConn || connections[0];
|
||||
if (!targetConn) {
|
||||
return base;
|
||||
}
|
||||
|
||||
base.configured = true;
|
||||
base.provider = targetConn.provider;
|
||||
|
||||
if (base.unlocked) {
|
||||
const details = await this.storageService.getConnection(
|
||||
pubkey,
|
||||
targetConn.id,
|
||||
);
|
||||
base.credentials = details || null;
|
||||
}
|
||||
|
||||
return base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a standalone thumbnail file. Returns { url, key }.
|
||||
*/
|
||||
async uploadThumbnail(file, { provider, credentials, onProgress } = {}) {
|
||||
const pubkey = this.getCurrentPubkey();
|
||||
const npub = this.safeEncodeNpub(pubkey);
|
||||
const service = this.serviceFor(provider);
|
||||
|
||||
const { settings, bucketEntry } = await service.prepareUpload(npub, {
|
||||
credentials,
|
||||
});
|
||||
|
||||
const timestamp = Date.now();
|
||||
const cleanName = file.name.replace(/[^a-zA-Z0-9.-]/g, "_");
|
||||
const key = `${npub}/thumbnails/${timestamp}-${cleanName}`;
|
||||
const baseDomain = bucketEntry.publicBaseUrl;
|
||||
const url = this.publicUrlFor(provider, baseDomain, key);
|
||||
|
||||
await service.uploadFile({
|
||||
file,
|
||||
bucket: bucketEntry.bucket,
|
||||
key,
|
||||
accountId: settings.accountId,
|
||||
endpoint: settings.endpoint,
|
||||
provider: settings.provider || provider || "cloudflare_r2",
|
||||
region: settings.region,
|
||||
accessKeyId: settings.accessKeyId,
|
||||
secretAccessKey: settings.secretAccessKey,
|
||||
forcePathStyle: settings.forcePathStyle,
|
||||
createBucketIfMissing: true,
|
||||
onProgress: (fraction) => {
|
||||
if (typeof onProgress === "function") onProgress(fraction);
|
||||
},
|
||||
});
|
||||
|
||||
return { url, key };
|
||||
}
|
||||
|
||||
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 generateTorrentMetadata({ file, videoPublicUrl } = {}) {
|
||||
let infoHash = "";
|
||||
let torrentFile = null;
|
||||
|
||||
try {
|
||||
const urlList = videoPublicUrl ? [videoPublicUrl] : [];
|
||||
const torrentMetadata = await createTorrentMetadata(file, urlList);
|
||||
|
||||
infoHash = torrentMetadata?.infoHash || "";
|
||||
if (torrentMetadata?.torrentFile) {
|
||||
const baseName = file.name.replace(/\.[^/.]+$/, "") || file.name;
|
||||
torrentFile = new File(
|
||||
[torrentMetadata.torrentFile],
|
||||
`${baseName}.torrent`,
|
||||
{ type: "application/x-bittorrent" },
|
||||
);
|
||||
}
|
||||
} catch (hashErr) {
|
||||
userLogger.warn("Failed to calculate info hash:", hashErr);
|
||||
}
|
||||
|
||||
const normalizedInfoHash = normalizeInfoHash(infoHash);
|
||||
return {
|
||||
infoHash: normalizedInfoHash,
|
||||
torrentFile,
|
||||
hasValidInfoHash: isValidInfoHash(normalizedInfoHash),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a video file (and its generated .torrent). Returns:
|
||||
* { url, key, storagePointer, infoHash, magnet, torrentUrl, torrentFile,
|
||||
* hasValidInfoHash }
|
||||
* onProgress is called as ({ fraction, label }) so the caller can render its
|
||||
* own progress UI matching the original UploadModal labels.
|
||||
*/
|
||||
async uploadVideo(file, { provider, credentials, onProgress } = {}) {
|
||||
const emit = (fraction, label) => {
|
||||
if (typeof onProgress === "function") onProgress({ fraction, label });
|
||||
};
|
||||
|
||||
const identifier = await this.resolveUploadIdentifier(file);
|
||||
|
||||
const pubkey = this.getCurrentPubkey();
|
||||
const npub = this.safeEncodeNpub(pubkey);
|
||||
const service = this.serviceFor(provider);
|
||||
|
||||
const { settings, bucketEntry } = await service.prepareUpload(npub, {
|
||||
credentials,
|
||||
});
|
||||
|
||||
const videoKey = buildR2Key(npub, file, identifier);
|
||||
const baseDomain = bucketEntry.publicBaseUrl;
|
||||
const videoPublicUrl = this.publicUrlFor(provider, baseDomain, videoKey);
|
||||
|
||||
emit(0, "Uploading video...");
|
||||
const uploadPromise = service.uploadFile({
|
||||
file,
|
||||
bucket: bucketEntry.bucket,
|
||||
key: videoKey,
|
||||
accountId: settings.accountId,
|
||||
endpoint: settings.endpoint,
|
||||
provider: settings.provider || provider || "cloudflare_r2",
|
||||
region: settings.region,
|
||||
accessKeyId: settings.accessKeyId,
|
||||
secretAccessKey: settings.secretAccessKey,
|
||||
forcePathStyle: settings.forcePathStyle,
|
||||
createBucketIfMissing: true,
|
||||
onProgress: (fraction) => emit(fraction, null),
|
||||
});
|
||||
|
||||
emit(null, "Uploading & Calculating Hash...");
|
||||
const torrentPromise = this.generateTorrentMetadata({
|
||||
file,
|
||||
videoPublicUrl,
|
||||
});
|
||||
|
||||
const [, torrentResult] = await Promise.all([uploadPromise, torrentPromise]);
|
||||
|
||||
const storagePrefix = buildStoragePrefixFromKey({
|
||||
publicBaseUrl: baseDomain,
|
||||
key: videoKey,
|
||||
});
|
||||
const providerLabel = isR2Provider(provider) ? "r2" : "s3";
|
||||
const storagePointer = buildStoragePointerValue({
|
||||
provider: providerLabel,
|
||||
prefix: storagePrefix,
|
||||
});
|
||||
|
||||
emit(1, "Video uploaded.");
|
||||
|
||||
const result = {
|
||||
url: videoPublicUrl,
|
||||
key: videoKey,
|
||||
storagePointer,
|
||||
infoHash: "",
|
||||
magnet: "",
|
||||
torrentUrl: "",
|
||||
torrentFile: null,
|
||||
hasValidInfoHash: false,
|
||||
};
|
||||
|
||||
if (torrentResult.hasValidInfoHash && torrentResult.torrentFile) {
|
||||
const baseKey = videoKey.replace(/\.[^/.]+$/, "");
|
||||
const torrentKey =
|
||||
baseKey && baseKey !== videoKey
|
||||
? `${baseKey}.torrent`
|
||||
: `${videoKey}.torrent`;
|
||||
const torrentPublicUrl = this.publicUrlFor(provider, baseDomain, torrentKey);
|
||||
|
||||
emit(1, "Uploading torrent metadata...");
|
||||
await service.uploadFile({
|
||||
file: torrentResult.torrentFile,
|
||||
bucket: bucketEntry.bucket,
|
||||
key: torrentKey,
|
||||
accountId: settings.accountId,
|
||||
endpoint: settings.endpoint,
|
||||
provider: settings.provider || provider || "cloudflare_r2",
|
||||
region: settings.region,
|
||||
accessKeyId: settings.accessKeyId,
|
||||
secretAccessKey: settings.secretAccessKey,
|
||||
forcePathStyle: settings.forcePathStyle,
|
||||
createBucketIfMissing: true,
|
||||
});
|
||||
|
||||
const magnet = `magnet:?xt=urn:btih:${torrentResult.infoHash}&dn=${encodeURIComponent(
|
||||
file.name,
|
||||
)}&ws=${encodeURIComponent(videoPublicUrl)}&xs=${encodeURIComponent(
|
||||
torrentPublicUrl,
|
||||
)}`;
|
||||
|
||||
result.infoHash = torrentResult.infoHash;
|
||||
result.magnet = magnet;
|
||||
result.torrentUrl = torrentPublicUrl;
|
||||
result.torrentFile = torrentResult.torrentFile;
|
||||
result.hasValidInfoHash = true;
|
||||
emit(1, "Ready to publish!");
|
||||
} else {
|
||||
emit(1, "Upload complete (No torrent fallback).");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
export { isR2Provider, normalizeInfoHash, isValidInfoHash };
|
||||
Reference in new issue
Block a user