diff --git a/docs/systems/admin.md b/docs/systems/admin.md index 8e4f6160..c551687c 100644 --- a/docs/systems/admin.md +++ b/docs/systems/admin.md @@ -57,7 +57,7 @@ Settings are split into two API surfaces: | discoveryEnabled | boolean | discoveryEnabled | boolean | Controls space Explore page | | gifApiKey | string? | gifApiKey | string or empty to clear | Returned masked as `****{last4}` for security | | gifEnabled | boolean? | (derived) | -- | Read-only; true when gifApiKey is non-null | -| maxUploadSizeMb | number | maxUploadSizeBytes | 1-5120 MB | Stored as bytes; converted on read/write. DB null = use env `MAX_UPLOAD_SIZE` (default 100MB) | +| maxUploadSizeMb | number | maxUploadSizeBytes | positive integer (>=1 MB), upper bound is JS safe-integer ceiling (~8 EB) — effectively unlimited | Operator-set; no application cap. Stored as bytes; converted on read/write. DB null = use env `MAX_UPLOAD_SIZE` (default 100MB) | | federationRelayEnabled | boolean | federationRelayEnabled | boolean | Default: 1 (enabled) | | federationRelayTtlDays | number | federationRelayTtlDays | integer 1-365 | Default: 30 days | | autoAcceptPeering | boolean | autoAcceptPeering | boolean | Default: true. When false, `peer/accept` rejects unsolicited requests (403 PEERING_REQUIRES_APPROVAL); only requests where a local `pending` record already exists are accepted | diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 4782afdc..2105840a 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -65,7 +65,7 @@ async function main(): Promise { await app.register(multipart, { limits: { - fileSize: 5 * 1024 * 1024 * 1024, // 5GB hard ceiling — actual limit enforced per-request from DB + fileSize: Number.MAX_SAFE_INTEGER, // No global cap — actual limit enforced per-request from DB }, }); diff --git a/packages/server/src/routes/settings.ts b/packages/server/src/routes/settings.ts index 7bd6722f..74e0caa7 100644 --- a/packages/server/src/routes/settings.ts +++ b/packages/server/src/routes/settings.ts @@ -233,10 +233,11 @@ export async function settingsRoutes(app: FastifyInstance): Promise { if (body.maxUploadSizeMb !== undefined) { const mb = Number(body.maxUploadSizeMb); - if (isNaN(mb) || mb < 1 || mb > 5120) { - return reply.code(400).send({ error: 'maxUploadSizeMb must be between 1 and 5120', statusCode: 400 }); + const MAX_MB = Math.floor(Number.MAX_SAFE_INTEGER / (1024 * 1024)); + if (!Number.isFinite(mb) || !Number.isInteger(mb) || mb < 1 || mb > MAX_MB) { + return reply.code(400).send({ error: `maxUploadSizeMb must be a positive integer (1 - ${MAX_MB})`, statusCode: 400 }); } - updateData.maxUploadSizeBytes = Math.round(mb * 1024 * 1024); + updateData.maxUploadSizeBytes = mb * 1024 * 1024; } if (body.federationRelayEnabled !== undefined) { diff --git a/packages/web/src/components/modals/instanceSettingsPanels/StoragePanel.tsx b/packages/web/src/components/modals/instanceSettingsPanels/StoragePanel.tsx index 2fc414cc..c0d758d5 100644 --- a/packages/web/src/components/modals/instanceSettingsPanels/StoragePanel.tsx +++ b/packages/web/src/components/modals/instanceSettingsPanels/StoragePanel.tsx @@ -12,6 +12,22 @@ function formatBytes(bytes: number): string { return `${value < 10 ? value.toFixed(2) : value < 100 ? value.toFixed(1) : Math.round(value)} ${units[i]}`; } +type UploadUnit = 'MB' | 'GB'; + +// Render a number with up to 3 decimals, trimming trailing zeros. 1.5 → "1.5", 5 → "5", 0.098 → "0.098". +function formatUnitValue(n: number): string { + return Number(n.toFixed(3)).toString(); +} + +function parseDisplayMb(input: string, unit: UploadUnit): number | null { + const trimmed = input.trim(); + if (trimmed === '') return null; + const n = Number(trimmed); + if (!Number.isFinite(n) || n <= 0) return null; + const mb = unit === 'GB' ? Math.round(n * 1024) : Math.round(n); + return Number.isInteger(mb) && mb >= 1 ? mb : null; +} + export function StoragePanel() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); @@ -24,9 +40,9 @@ export function StoragePanel() { // Upload limit state const instanceSettings = useSettingsStore((s) => s.instanceSettings); const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings); - const [uploadLimitMb, setUploadLimitMb] = useState(100); + const [uploadUnit, setUploadUnit] = useState('MB'); + const [uploadLimitInput, setUploadLimitInput] = useState('100'); const [uploadLimitSaving, setUploadLimitSaving] = useState(false); - const [uploadLimitDirty, setUploadLimitDirty] = useState(false); // Media retention state const [mediaAgeDays, setMediaAgeDays] = useState(90); @@ -52,17 +68,34 @@ export function StoragePanel() { }, [fetchStats]); useEffect(() => { - if (instanceSettings?.maxUploadSizeMb) { - setUploadLimitMb(instanceSettings.maxUploadSizeMb); + const mb = instanceSettings?.maxUploadSizeMb; + if (typeof mb === 'number' && mb > 0) { + const unit: UploadUnit = mb >= 1024 ? 'GB' : 'MB'; + setUploadUnit(unit); + setUploadLimitInput(unit === 'GB' ? formatUnitValue(mb / 1024) : String(mb)); } }, [instanceSettings]); + const parsedUploadMb = parseDisplayMb(uploadLimitInput, uploadUnit); + const uploadLimitDirty = parsedUploadMb !== null && parsedUploadMb !== instanceSettings?.maxUploadSizeMb; + + const handleUploadUnitChange = (next: UploadUnit) => { + if (next === uploadUnit) return; + const n = Number(uploadLimitInput.trim()); + if (Number.isFinite(n) && n > 0) { + setUploadLimitInput(next === 'GB' ? formatUnitValue(n / 1024) : String(Math.round(n * 1024))); + } + setUploadUnit(next); + }; + const handleUploadLimitSave = async () => { + if (parsedUploadMb === null) return; + const mb = parsedUploadMb; setUploadLimitSaving(true); try { - await updateInstanceSettings({ maxUploadSizeMb: uploadLimitMb }); - setUploadLimitDirty(false); - addToast(`Upload limit set to ${uploadLimitMb} MB`, 'success'); + await updateInstanceSettings({ maxUploadSizeMb: mb }); + const display = mb >= 1024 ? `${formatUnitValue(mb / 1024)} GB` : `${mb} MB`; + addToast(`Upload limit set to ${display}`, 'success'); } catch { addToast('Failed to update upload limit', 'warning'); } finally { @@ -199,16 +232,31 @@ export function StoragePanel() { { setUploadLimitMb(Number(e.target.value)); setUploadLimitDirty(true); }} - className="input-standard w-20 px-2 py-1 text-sm text-center" + min={uploadUnit === 'GB' ? 0.001 : 1} + step={uploadUnit === 'GB' ? 0.5 : 1} + value={uploadLimitInput} + onChange={(e) => setUploadLimitInput(e.target.value)} + className="input-standard w-24 px-2 py-1 text-sm text-center" /> - MB +
+ {(['MB', 'GB'] as const).map((u) => ( + + ))} +