diff --git a/backend/open_webui/models/files.py b/backend/open_webui/models/files.py index 7fcc62558b..7f29fc5b7d 100644 --- a/backend/open_webui/models/files.py +++ b/backend/open_webui/models/files.py @@ -201,6 +201,18 @@ class FilesTable: result = await db.execute(select(File)) return [FileModel.model_validate(file) for file in result.scalars().all()] + async def count_files_by_user_id( + self, + user_id: str | None = None, + db: AsyncSession | None = None, + ) -> int: + async with get_async_db_context(db) as db: + stmt = select(func.count(File.id)) + if user_id: + stmt = stmt.filter_by(user_id=user_id) + result = await db.execute(stmt) + return result.scalar() or 0 + async def check_access_by_user_id(self, id, user_id, permission='write', db: AsyncSession | None = None) -> bool: file = await self.get_file_by_id(id, db=db) if not file: diff --git a/backend/open_webui/routers/files.py b/backend/open_webui/routers/files.py index c7080bd17c..cf3be4a08a 100644 --- a/backend/open_webui/routers/files.py +++ b/backend/open_webui/routers/files.py @@ -463,6 +463,20 @@ async def search_files( return files +############################ +# Count Files +############################ + + +@router.get('/count', response_model=int) +async def count_files( + user=Depends(get_verified_user), + db: AsyncSession = Depends(get_async_session), +): + user_id = None if (user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL) else user.id + return await Files.count_files_by_user_id(user_id=user_id, db=db) + + ############################ # Delete All Files ############################ diff --git a/src/lib/apis/files/index.ts b/src/lib/apis/files/index.ts index 6c041452b7..a2b625edf8 100644 --- a/src/lib/apis/files/index.ts +++ b/src/lib/apis/files/index.ts @@ -214,6 +214,34 @@ export const searchFiles = async ( return res; }; +export const getFileCount = async (token: string = '') => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/files/count`, { + method: 'GET', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.json(); + }) + .catch((err) => { + error = err; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; + export const getFileById = async (token: string, id: string) => { let error = null; diff --git a/src/lib/components/layout/FilesModal.svelte b/src/lib/components/layout/FilesModal.svelte index 499ea48a03..5332f1750a 100644 --- a/src/lib/components/layout/FilesModal.svelte +++ b/src/lib/components/layout/FilesModal.svelte @@ -4,7 +4,7 @@ import type { Writable } from 'svelte/store'; import dayjs from 'dayjs'; - import { searchFiles, deleteFileById } from '$lib/apis/files'; + import { searchFiles, deleteFileById, getFileCount } from '$lib/apis/files'; import Modal from '$lib/components/common/Modal.svelte'; import Spinner from '$lib/components/common/Spinner.svelte'; import Tooltip from '$lib/components/common/Tooltip.svelte'; @@ -21,6 +21,7 @@ export let show = false; let files: any[] | null = null; + let fileCount: number | null = null; let query = ''; let orderBy = 'created_at'; let direction = 'desc'; @@ -70,6 +71,10 @@ const newFiles = await searchFiles(localStorage.token, pattern, 0, PAGE_SIZE); files = sortFiles(newFiles); allFilesLoaded = newFiles.length < PAGE_SIZE; + + if (!query) { + fileCount = await getFileCount(localStorage.token); + } } catch (error) { // Handle 404 or other errors - show empty state instead of spinner files = []; @@ -124,6 +129,7 @@ toast.success($i18n.t('File deleted successfully.')); // Remove from local array instead of re-fetching to allow rapid deletion files = files?.filter((f) => f.id !== fileId) ?? null; + if (fileCount !== null) fileCount--; } catch (error) { toast.error(`${error}`); } @@ -197,7 +203,18 @@
-
{$i18n.t('Files')}
+
+
{$i18n.t('Files')}
+ {#if query && files} +
+ {files.length} +
+ {:else if fileCount !== null} +
+ {fileCount} +
+ {/if} +