mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
The per-file ceiling was a flat 5 GiB from the environment, which is both too small for a paying account with 200 GB of storage and unaware of what an upload actually costs. The provider derives its own renditions from the file (1080p, 720p and down) and bills them to the same account, so a file allowed to fill the quota exactly is over it by the time it finishes processing. The ceiling is now 80% of whatever limit the account is held to: 160 GB on the plan, 2.4 GB on a cardless trial, and it moves on its own when either number changes. OPENFRAME_MAX_VIDEO_UPLOAD_BYTES keeps working as an absolute cap for a host that wants one, where the lower of the two applies, and an instance running without billing has no quota to divide and falls back to the flat 5 GiB. The refusal now names the ceiling, which the old one left the client to guess. Finalize re-checks only the host cap. Re-deriving the account's ceiling there would delete a finished upload over a plan that lapsed while the bytes were in flight, and an upload larger than what was declared is already caught by the declared-size check beside it.
154 lines
5.0 KiB
TypeScript
154 lines
5.0 KiB
TypeScript
import { logError } from '@/lib/logger';
|
|
|
|
function readBooleanEnv(name: string, defaultValue: boolean): boolean {
|
|
const value = process.env[name];
|
|
if (!value) return defaultValue;
|
|
|
|
const normalized = value.trim().toLowerCase();
|
|
if (normalized === 'true') return true;
|
|
if (normalized === 'false') return false;
|
|
|
|
return defaultValue;
|
|
}
|
|
|
|
let warnedAboutConflictingUploadFlags = false;
|
|
|
|
function warnIfConflictingDirectUploadFlags(): void {
|
|
if (warnedAboutConflictingUploadFlags) return;
|
|
if (!isS3VideoUploadsFeatureEnabled() || !isBunnyUploadsFeatureEnabled()) return;
|
|
if (!hasR2Config() || !hasBunnyUploadsConfig()) return;
|
|
|
|
warnedAboutConflictingUploadFlags = true;
|
|
logError(
|
|
'OPENFRAME_ENABLE_S3_VIDEO_UPLOADS and OPENFRAME_ENABLE_BUNNY_UPLOADS are both enabled with valid config. S3 video uploads take precedence; disable Bunny uploads for self-hosted deployments.',
|
|
new Error('Conflicting direct upload feature flags')
|
|
);
|
|
}
|
|
|
|
export function isStripeFeatureEnabled() {
|
|
return readBooleanEnv('OPENFRAME_ENABLE_STRIPE', true);
|
|
}
|
|
|
|
export function hasStripeConfig() {
|
|
return Boolean(process.env.STRIPE_SECRET_KEY && process.env.STRIPE_PRICE_ID);
|
|
}
|
|
|
|
export function isStripeBillingEnabled() {
|
|
return isStripeFeatureEnabled() && hasStripeConfig();
|
|
}
|
|
|
|
export function isBunnyUploadsFeatureEnabled() {
|
|
return readBooleanEnv('OPENFRAME_ENABLE_BUNNY_UPLOADS', true);
|
|
}
|
|
|
|
export function hasBunnyUploadsConfig() {
|
|
return Boolean(
|
|
process.env.BUNNY_STREAM_API_KEY &&
|
|
(process.env.BUNNY_STREAM_LIBRARY_ID || process.env.NEXT_PUBLIC_BUNNY_STREAM_LIBRARY_ID)
|
|
);
|
|
}
|
|
|
|
export function isS3VideoUploadsFeatureEnabled() {
|
|
return readBooleanEnv('OPENFRAME_ENABLE_S3_VIDEO_UPLOADS', false);
|
|
}
|
|
|
|
export function hasR2Config() {
|
|
return Boolean(
|
|
process.env.R2_ACCESS_KEY_ID &&
|
|
process.env.R2_SECRET_ACCESS_KEY &&
|
|
process.env.R2_BUCKET_NAME &&
|
|
(process.env.R2_ENDPOINT || process.env.R2_ACCOUNT_ID)
|
|
);
|
|
}
|
|
|
|
export function isS3VideoUploadsEnabled() {
|
|
warnIfConflictingDirectUploadFlags();
|
|
return isS3VideoUploadsFeatureEnabled() && hasR2Config();
|
|
}
|
|
|
|
export function isBunnyUploadsEnabled() {
|
|
if (isS3VideoUploadsEnabled()) {
|
|
return false;
|
|
}
|
|
return isBunnyUploadsFeatureEnabled() && hasBunnyUploadsConfig();
|
|
}
|
|
|
|
export function isDirectFileUploadEnabled() {
|
|
return isS3VideoUploadsEnabled() || isBunnyUploadsEnabled();
|
|
}
|
|
|
|
/**
|
|
* The per-file ceiling for a host that has no billing, and therefore no per
|
|
* account quota to derive one from.
|
|
*/
|
|
export const DEFAULT_MAX_VIDEO_UPLOAD_BYTES =
|
|
BigInt(5) * BigInt(1024) * BigInt(1024) * BigInt(1024);
|
|
|
|
/**
|
|
* A flat per-file ceiling this host has pinned, or null when it has not.
|
|
*
|
|
* Null is the ordinary case. The ceiling that applies to a paying account is a
|
|
* share of that account's own storage quota, which one number in the
|
|
* environment cannot express (see `getMaxVideoUploadBytesForUser`). A host that
|
|
* wants an absolute cap on top of that still sets this, and the lower of the
|
|
* two wins.
|
|
*/
|
|
export function getConfiguredMaxVideoUploadBytes(): bigint | null {
|
|
const raw = process.env.OPENFRAME_MAX_VIDEO_UPLOAD_BYTES?.trim();
|
|
if (!raw) return null;
|
|
|
|
try {
|
|
const parsed = BigInt(raw);
|
|
return parsed > BigInt(0) ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function isInviteCodeRequired() {
|
|
return readBooleanEnv('OPENFRAME_REQUIRE_INVITE_CODE', true);
|
|
}
|
|
|
|
// Acquisition attribution and funnel events. Defaults to OFF, unlike the other
|
|
// product flags here, because the cost of the two mistakes is not symmetric: a
|
|
// hosted instance that forgets to switch it on shows an empty growth page and is
|
|
// noticed the same day, while a self-hosted instance that gets it silently
|
|
// switched on accumulates rows nobody asked for. Nothing is ever sent off the
|
|
// instance either way, so this is about cost, not disclosure.
|
|
export function isProductAnalyticsEnabled() {
|
|
return readBooleanEnv('OPENFRAME_ENABLE_ANALYTICS', false);
|
|
}
|
|
|
|
function parseBigIntEnv(name: string, defaultValue: bigint, minValue?: bigint): bigint {
|
|
const raw = process.env[name]?.trim();
|
|
if (!raw) return defaultValue;
|
|
|
|
try {
|
|
const parsed = BigInt(raw);
|
|
if (parsed <= BigInt(0)) return defaultValue;
|
|
if (minValue !== undefined && parsed < minValue) return minValue;
|
|
return parsed;
|
|
} catch {
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
// Files larger than this use S3 multipart upload (chunked) instead of a single PUT.
|
|
// Default 90 MiB keeps each request under the common 100 MB Cloudflare proxy/tunnel cap.
|
|
export function getR2MultipartThresholdBytes(): bigint {
|
|
return parseBigIntEnv(
|
|
'OPENFRAME_R2_MULTIPART_THRESHOLD_BYTES',
|
|
BigInt(90) * BigInt(1024) * BigInt(1024)
|
|
);
|
|
}
|
|
|
|
// Size of each multipart chunk. Clamped to the S3 minimum of 5 MiB for non-final parts.
|
|
export function getR2MultipartPartSizeBytes(): bigint {
|
|
const minPartSize = BigInt(5) * BigInt(1024) * BigInt(1024);
|
|
return parseBigIntEnv(
|
|
'OPENFRAME_R2_MULTIPART_PART_SIZE_BYTES',
|
|
BigInt(32) * BigInt(1024) * BigInt(1024),
|
|
minPartSize
|
|
);
|
|
}
|