import { env } from "cloudflare:workers"; import { MAX_IN_LIST, chunks, placeholders } from "../db"; import { accountDb } from "../scoped-db"; import { HttpError } from "../errors"; import { numberVar } from "../limits"; import { randomId } from "../crypto"; import { recordUsageSoon } from "../metering/record"; import { planBlockError } from "../metering/quota"; import { checkLfsThreshold } from "../notify/quota"; const GIB = 1024 * 1024 * 1024; const MIB = 1024 * 1024; const HTTP_INSUFFICIENT_STORAGE = 507; const HTTP_TOO_MANY = 429; export type LfsUsage = { storageBytes: number; storageLimit: number; bandwidthBytes: number; bandwidthLimit: number; period: string; }; type UsageRow = { storage_bytes: number; bandwidth_period: string; bandwidth_bytes: number }; export function maxObjectBytes(): number { return numberVar(env.LFS_MAX_OBJECT_BYTES, 100 * MIB); } function storageLimit(): number { return numberVar(env.LFS_STORAGE_BYTES_PER_OWNER, 10 * GIB); } function bandwidthLimit(): number { return numberVar(env.LFS_BANDWIDTH_BYTES_PER_MONTH, 10 * GIB); } /** Bandwidth resets each calendar month (UTC). */ function currentPeriod(now = new Date()): string { return now.toISOString().slice(0, 7); } export async function lfsUsage(ownerId: string): Promise { const row = await accountDb(ownerId).first( "SELECT storage_bytes, bandwidth_period, bandwidth_bytes FROM lfs_usage WHERE owner_id = ?", ownerId, ); const period = currentPeriod(); return { storageBytes: row?.storage_bytes ?? 0, storageLimit: storageLimit(), bandwidthBytes: row && row.bandwidth_period === period ? row.bandwidth_bytes : 0, bandwidthLimit: bandwidthLimit(), period, }; } export async function assertStorageAvailable(ownerId: string, incomingBytes: number): Promise { const usage = await lfsUsage(ownerId); if (usage.storageBytes + incomingBytes > usage.storageLimit) { await checkLfsThreshold(ownerId, "lfs_storage", usage.storageLimit, usage.storageLimit, usage.period); throw new HttpError(HTTP_INSUFFICIENT_STORAGE, "LFS storage quota exceeded for this owner"); } const blocked = incomingBytes > 0 ? await planBlockError(ownerId, "storage.lfs", incomingBytes) : null; if (blocked) throw blocked; await checkLfsThreshold(ownerId, "lfs_storage", usage.storageBytes + incomingBytes, usage.storageLimit, usage.period); } /** Bytes an upload would add to `ownerId`'s bill: oids the account already holds (any repo) are free. */ export async function billableBytes(ownerId: string, objects: Array<{ oid: string; size: number }>): Promise { const held = new Set(); for (const group of chunks( objects.map((object) => object.oid), MAX_IN_LIST - 1, )) { const rows = await accountDb(ownerId).all<{ oid: string }>( `SELECT oid FROM lfs_account_objects WHERE owner_id = ? AND oid IN (${placeholders(group.length)})`, ownerId, ...group, ); for (const row of rows) held.add(row.oid); } return objects.filter((object) => !held.has(object.oid)).reduce((sum, object) => sum + object.size, 0); } /** Charges download bandwidth up front, rejecting the transfer when it would exceed the monthly quota. */ export async function chargeBandwidth(ownerId: string, bytes: number, repoId: string | null = null): Promise { const usage = await lfsUsage(ownerId); if (usage.bandwidthBytes + bytes > usage.bandwidthLimit) { await checkLfsThreshold(ownerId, "lfs_bandwidth", usage.bandwidthLimit, usage.bandwidthLimit, usage.period); throw new HttpError(HTTP_TOO_MANY, "LFS bandwidth quota exceeded for this owner this month"); } const blocked = await planBlockError(ownerId, "network.lfs", bytes); if (blocked) throw blocked; await accountDb(ownerId).run( `INSERT INTO lfs_usage (owner_id, bandwidth_period, bandwidth_bytes) VALUES (?, ?, ?) ON CONFLICT (owner_id) DO UPDATE SET bandwidth_bytes = CASE WHEN bandwidth_period = excluded.bandwidth_period THEN bandwidth_bytes + excluded.bandwidth_bytes ELSE excluded.bandwidth_bytes END, bandwidth_period = excluded.bandwidth_period`, ownerId, usage.period, bytes, ); // Each charge is a distinct transfer (a re-run batch call is charged again by lfs_usage too), so a fresh key // keeps the two in step. Recorded off the request path. recordUsageSoon(null, { accountId: ownerId, repoId, meter: "network.lfs", quantity: bytes, unit: "bytes", idempotencyKey: `lfs-bw:${randomId()}`, }); await checkLfsThreshold(ownerId, "lfs_bandwidth", usage.bandwidthBytes + bytes, usage.bandwidthLimit, usage.period); }