From c1147578c073a8c7fa7e7f836149e1cdfec8f18d Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 8 Jan 2026 12:49:45 +0400 Subject: [PATCH] feat: export kb to zip --- backend/open_webui/routers/knowledge.py | 53 ++++++++++++++++++- src/lib/apis/knowledge/index.ts | 26 +++++++++ src/lib/components/icons/ArrowDownTray.svelte | 19 +++++++ src/lib/components/workspace/Knowledge.svelte | 30 ++++++++++- .../workspace/Knowledge/ItemMenu.svelte | 13 +++++ 5 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 src/lib/components/icons/ArrowDownTray.svelte diff --git a/backend/open_webui/routers/knowledge.py b/backend/open_webui/routers/knowledge.py index 5d2167c2c..c67175930 100644 --- a/backend/open_webui/routers/knowledge.py +++ b/backend/open_webui/routers/knowledge.py @@ -1,8 +1,11 @@ from typing import List, Optional from pydantic import BaseModel from fastapi import APIRouter, Depends, HTTPException, status, Request, Query +from fastapi.responses import StreamingResponse from fastapi.concurrency import run_in_threadpool import logging +import io +import zipfile from sqlalchemy.orm import Session from open_webui.internal.db import get_session @@ -25,7 +28,7 @@ from open_webui.routers.retrieval import ( from open_webui.storage.provider import Storage from open_webui.constants import ERROR_MESSAGES -from open_webui.utils.auth import get_verified_user +from open_webui.utils.auth import get_verified_user, get_admin_user from open_webui.utils.access_control import has_access, has_permission @@ -837,3 +840,51 @@ async def add_files_to_knowledge_batch( **knowledge.model_dump(), files=Knowledges.get_file_metadatas_by_id(knowledge.id, db=db), ) + + +############################ +# ExportKnowledgeById +############################ + + +@router.get("/{id}/export") +async def export_knowledge_by_id( + id: str, user=Depends(get_admin_user), db: Session = Depends(get_session) +): + """ + Export a knowledge base as a zip file containing .txt files. + Admin only. + """ + + knowledge = Knowledges.get_knowledge_by_id(id=id, db=db) + if not knowledge: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=ERROR_MESSAGES.NOT_FOUND, + ) + + files = Knowledges.get_files_by_id(id, db=db) + + # Create zip file in memory + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zf: + for file in files: + content = file.data.get("content", "") if file.data else "" + if content: + # Use original filename with .txt extension + filename = file.filename + if not filename.endswith(".txt"): + filename = f"{filename}.txt" + zf.writestr(filename, content) + + zip_buffer.seek(0) + + # Sanitize knowledge name for filename + safe_name = "".join(c if c.isalnum() or c in " -_" else "_" for c in knowledge.name) + zip_filename = f"{safe_name}.zip" + + return StreamingResponse( + zip_buffer, + media_type="application/zip", + headers={"Content-Disposition": f"attachment; filename={zip_filename}"}, + ) diff --git a/src/lib/apis/knowledge/index.ts b/src/lib/apis/knowledge/index.ts index 9656a232e..dc9dd8b88 100644 --- a/src/lib/apis/knowledge/index.ts +++ b/src/lib/apis/knowledge/index.ts @@ -485,3 +485,29 @@ export const reindexKnowledgeFiles = async (token: string) => { return res; }; + +export const exportKnowledgeById = async (token: string, id: string) => { + let error = null; + + const res = await fetch(`${WEBUI_API_BASE_URL}/knowledge/${id}/export`, { + method: 'GET', + headers: { + authorization: `Bearer ${token}` + } + }) + .then(async (res) => { + if (!res.ok) throw await res.json(); + return res.blob(); + }) + .catch((err) => { + error = err.detail; + console.error(err); + return null; + }); + + if (error) { + throw error; + } + + return res; +}; diff --git a/src/lib/components/icons/ArrowDownTray.svelte b/src/lib/components/icons/ArrowDownTray.svelte new file mode 100644 index 000000000..55620e9fe --- /dev/null +++ b/src/lib/components/icons/ArrowDownTray.svelte @@ -0,0 +1,19 @@ + + + + + diff --git a/src/lib/components/workspace/Knowledge.svelte b/src/lib/components/workspace/Knowledge.svelte index 8f8d5c1db..e93faa155 100644 --- a/src/lib/components/workspace/Knowledge.svelte +++ b/src/lib/components/workspace/Knowledge.svelte @@ -8,7 +8,11 @@ const i18n = getContext('i18n'); import { WEBUI_NAME, knowledge, user } from '$lib/stores'; - import { deleteKnowledgeById, searchKnowledgeBases } from '$lib/apis/knowledge'; + import { + deleteKnowledgeById, + searchKnowledgeBases, + exportKnowledgeById + } from '$lib/apis/knowledge'; import { goto } from '$app/navigation'; import { capitalizeFirstLetter } from '$lib/utils'; @@ -104,6 +108,25 @@ } }; + const exportHandler = async (item) => { + try { + const blob = await exportKnowledgeById(localStorage.token, item.id); + if (blob) { + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `${item.name}.zip`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + toast.success($i18n.t('Knowledge exported successfully')); + } + } catch (e) { + toast.error(`${e}`); + } + }; + onMount(async () => { viewOption = localStorage?.workspaceViewOption || ''; loaded = true; @@ -239,6 +262,11 @@
{ + exportHandler(item); + } + : null} on:delete={() => { selectedItem = item; showDeleteConfirm = true; diff --git a/src/lib/components/workspace/Knowledge/ItemMenu.svelte b/src/lib/components/workspace/Knowledge/ItemMenu.svelte index 82e243f3c..641b77472 100644 --- a/src/lib/components/workspace/Knowledge/ItemMenu.svelte +++ b/src/lib/components/workspace/Knowledge/ItemMenu.svelte @@ -18,6 +18,7 @@ const i18n = getContext('i18n'); + export let onExport: null | Function = null; export let onClose: Function = () => {}; let show = false; @@ -54,6 +55,18 @@ align="end" transition={flyAndScale} > + {#if onExport} + { + onExport(); + }} + > + +
{$i18n.t('Export')}
+
+ {/if} + {