mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 17:46:06 +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.
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
// The size a client declares before a direct upload starts.
|
|
//
|
|
// Shared because the R2 and Bunny paths have to agree on it: both hand the
|
|
// number to the storage quota before a single byte moves, so a value one of them
|
|
// would accept and the other would not is a hole in whichever is laxer.
|
|
|
|
export type DeclaredUploadSize = { sizeBytes: bigint } | { error: string };
|
|
|
|
/**
|
|
* A byte count in the unit a person reading a limit expects.
|
|
*
|
|
* Whole gigabytes where the number is whole, which most ceilings we ship are,
|
|
* and one decimal otherwise so a share of a small quota is not rounded into a
|
|
* lie. Lives here rather than next to the quota because the upload routes need
|
|
* it too, and this module imports nothing.
|
|
*/
|
|
export function formatSizeLimit(bytes: bigint): string {
|
|
const gigabytes = Number(bytes) / 1024 ** 3;
|
|
if (gigabytes < 1) return `${Math.round(Number(bytes) / 1024 ** 2)} MB`;
|
|
return `${Number.isInteger(gigabytes) ? gigabytes : gigabytes.toFixed(1)} GB`;
|
|
}
|
|
|
|
/** The refusal, with the ceiling in it, so the client knows what would fit. */
|
|
export function uploadTooLargeMessage(maxBytes: bigint): string {
|
|
return `File exceeds the maximum allowed upload size (${formatSizeLimit(maxBytes)})`;
|
|
}
|
|
|
|
/**
|
|
* Reads a declared upload size, refusing anything that is not a whole positive
|
|
* number of bytes within the ceiling this account is held to.
|
|
*
|
|
* The number is the client's word and is treated as such. Overstating it only
|
|
* spends the caller's own quota, and understating it is caught where the bytes
|
|
* land: R2 compares the object against the declaration and deletes it on a
|
|
* mismatch, and Bunny's own storage reporting replaces the estimate once the
|
|
* upload settles. What is not tolerated is an absent or nonsense value, which is
|
|
* what asking for zero bytes effectively was.
|
|
*/
|
|
export function parseDeclaredUploadSize(raw: unknown, maxBytes: bigint): DeclaredUploadSize {
|
|
if (typeof raw !== 'number' && typeof raw !== 'string' && typeof raw !== 'bigint') {
|
|
return { error: 'sizeBytes must be a positive integer' };
|
|
}
|
|
|
|
let sizeBytes: bigint;
|
|
try {
|
|
sizeBytes = BigInt(raw);
|
|
} catch {
|
|
return { error: 'sizeBytes must be a positive integer' };
|
|
}
|
|
|
|
if (sizeBytes <= BigInt(0)) {
|
|
return { error: 'sizeBytes must be a positive integer' };
|
|
}
|
|
|
|
if (sizeBytes > maxBytes) {
|
|
return { error: uploadTooLargeMessage(maxBytes) };
|
|
}
|
|
|
|
return { sizeBytes };
|
|
}
|