fix(storage): paginate stats query and align file size formatting (#3720)

Paginate Supabase select queries in the storage stats API to avoid
the 1000 row default limit. Align StorageManager's formatFileSize
with useQuotaStats calculation so quota values display consistently.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Huang Xin
2026-04-01 22:44:15 +08:00
committed by GitHub
parent b8ddb5475e
commit f1a08565e3
2 changed files with 55 additions and 21 deletions
@@ -241,10 +241,11 @@ const StorageManager = () => {
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`;
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1e6) return `${Math.round((bytes / 1024) * 10) / 10} KB`;
const inGB = bytes > 1e9;
const value = bytes / 1024 / 1024 / (inGB ? 1024 : 1);
return `${Math.round(value * 10) / 10} ${inGB ? 'GB' : 'MB'}`;
};
const formatDate = (dateString: string): string => {
+50 -17
View File
@@ -31,20 +31,36 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
const supabase = createSupabaseAdminClient();
// Get total file count and size
const { data: totalStats, error: totalError } = await supabase
.from('files')
.select('file_size')
.eq('user_id', user.id)
.is('deleted_at', null);
// Get total file count and size (paginated to avoid Supabase 1000 row limit)
const PAGE_SIZE = 1000;
let allFileStats: { file_size: number }[] = [];
let offset = 0;
let hasMore = true;
if (totalError) {
console.error('Error querying total stats:', totalError);
return res.status(500).json({ error: 'Failed to retrieve storage statistics' });
while (hasMore) {
const { data, error } = await supabase
.from('files')
.select('file_size')
.eq('user_id', user.id)
.is('deleted_at', null)
.range(offset, offset + PAGE_SIZE - 1);
if (error) {
console.error('Error querying total stats:', error);
return res.status(500).json({ error: 'Failed to retrieve storage statistics' });
}
if (data && data.length > 0) {
allFileStats = allFileStats.concat(data);
offset += PAGE_SIZE;
hasMore = data.length === PAGE_SIZE;
} else {
hasMore = false;
}
}
const totalFiles = totalStats?.length || 0;
const totalSize = totalStats?.reduce((sum, file) => sum + (file.file_size || 0), 0) || 0;
const totalFiles = allFileStats.length;
const totalSize = allFileStats.reduce((sum, file) => sum + (file.file_size || 0), 0);
// Get storage plan data
const { usage, quota } = getStoragePlanData(token);
@@ -62,13 +78,30 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
if (bookHashError) {
console.warn('RPC function not available, using fallback aggregation:', bookHashError);
const { data: allFiles, error: filesError } = await supabase
.from('files')
.select('book_hash, file_size')
.eq('user_id', user.id)
.is('deleted_at', null);
let allFiles: { book_hash: string | null; file_size: number }[] = [];
let fallbackOffset = 0;
let fallbackHasMore = true;
if (!filesError && allFiles) {
while (fallbackHasMore) {
const { data, error: filesError } = await supabase
.from('files')
.select('book_hash, file_size')
.eq('user_id', user.id)
.is('deleted_at', null)
.range(fallbackOffset, fallbackOffset + PAGE_SIZE - 1);
if (filesError) break;
if (data && data.length > 0) {
allFiles = allFiles.concat(data);
fallbackOffset += PAGE_SIZE;
fallbackHasMore = data.length === PAGE_SIZE;
} else {
fallbackHasMore = false;
}
}
if (allFiles.length > 0) {
const grouped = new Map<string | null, { count: number; size: number }>();
allFiles.forEach((file) => {