mirror of
https://github.com/yusufipk/OpenFrame.git
synced 2026-09-11 09:36:08 +00:00
feat: implement storage quota management for uploads
- Added storage quota enforcement for audio and image uploads in the respective routes. - Introduced reservation system to manage concurrent uploads and prevent quota overages. - Enhanced comment creation to account for audio and image attachment sizes against user quotas. - Created new UploadReservation model to track in-flight upload reservations. - Backfilled existing video assets with size information from R2. - Added progress component for UI feedback during uploads. - Updated API responses to include reservation IDs for better quota management. - Adjusted error handling to return appropriate storage limit exceeded messages.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Bell, Send, Mail, CheckCircle2, AlertCircle, Loader2, Globe, CreditCard } from 'lucide-react';
|
||||
import { Bell, Send, Mail, CheckCircle2, AlertCircle, Loader2, Globe, CreditCard, HardDrive } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
@@ -9,6 +9,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -61,6 +62,20 @@ interface BillingOverview {
|
||||
};
|
||||
}
|
||||
|
||||
interface StorageInfo {
|
||||
usedBytes: string;
|
||||
limitBytes: string;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
function formatBytes(bytesStr: string): string {
|
||||
const bytes = Number(bytesStr);
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
||||
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
|
||||
}
|
||||
|
||||
function ToggleButton({
|
||||
enabled,
|
||||
onToggle,
|
||||
@@ -124,6 +139,8 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo
|
||||
const [billing, setBilling] = useState<BillingOverview | null>(null);
|
||||
const [billingLoading, setBillingLoading] = useState(true);
|
||||
const [billingAction, setBillingAction] = useState<'checkout' | 'portal' | null>(null);
|
||||
const [storageInfo, setStorageInfo] = useState<StorageInfo | null>(null);
|
||||
const [storageLoading, setStorageLoading] = useState(true);
|
||||
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
||||
|
||||
// Form state for Telegram chat ID (separate from saved settings for editing)
|
||||
@@ -136,9 +153,10 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo
|
||||
useEffect(() => {
|
||||
async function fetchSettings() {
|
||||
try {
|
||||
const [settingsRes, billingRes] = await Promise.all([
|
||||
const [settingsRes, billingRes, storageRes] = await Promise.all([
|
||||
fetch('/api/settings/notifications'),
|
||||
fetch('/api/billing'),
|
||||
fetch('/api/settings/storage'),
|
||||
]);
|
||||
|
||||
if (settingsRes.ok) {
|
||||
@@ -151,11 +169,17 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo
|
||||
const data = await billingRes.json();
|
||||
setBilling(data.data);
|
||||
}
|
||||
|
||||
if (storageRes.ok) {
|
||||
const data = await storageRes.json();
|
||||
setStorageInfo(data.data);
|
||||
}
|
||||
} catch {
|
||||
console.error('Failed to fetch settings');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setBillingLoading(false);
|
||||
setStorageLoading(false);
|
||||
}
|
||||
}
|
||||
fetchSettings();
|
||||
@@ -448,6 +472,62 @@ export default function SettingsPage({ billingOnly = false }: { billingOnly?: bo
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{billing?.subscription.hasBillingAccess && (
|
||||
<Card className="mb-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<HardDrive className="h-5 w-5" />
|
||||
Storage
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Combined usage across video files and media attachments (200 GB limit)
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{storageLoading || !storageInfo ? (
|
||||
<div className="space-y-2">
|
||||
<Skeleton className="h-4 w-48" />
|
||||
<Skeleton className="h-2 w-full rounded-full" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="text-muted-foreground">
|
||||
{formatBytes(storageInfo.usedBytes)} used of {formatBytes(storageInfo.limitBytes)}
|
||||
</span>
|
||||
<span
|
||||
className={
|
||||
storageInfo.percentage >= 90
|
||||
? 'text-destructive font-medium'
|
||||
: storageInfo.percentage >= 75
|
||||
? 'text-amber-600 dark:text-amber-400 font-medium'
|
||||
: 'text-muted-foreground'
|
||||
}
|
||||
>
|
||||
{storageInfo.percentage < 0.1 ? '<0.1%' : `${storageInfo.percentage.toFixed(1)}%`}
|
||||
</span>
|
||||
</div>
|
||||
<Progress
|
||||
value={storageInfo.percentage}
|
||||
className={
|
||||
storageInfo.percentage >= 90
|
||||
? '[&>div]:bg-destructive'
|
||||
: storageInfo.percentage >= 75
|
||||
? '[&>div]:bg-amber-500'
|
||||
: ''
|
||||
}
|
||||
/>
|
||||
{storageInfo.percentage >= 90 && (
|
||||
<p className="text-xs text-destructive">
|
||||
Storage is almost full. Delete unused files or contact support.
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!billingOnly && (
|
||||
<>
|
||||
{/* Event Subscriptions */}
|
||||
|
||||
Reference in New Issue
Block a user