This commit is contained in:
Timothy Jaeryang Baek committed 2026-08-27 15:29:58 -04:00
1 parent 3ea2648506
commit c7530495a5
2 files changed
+67 -1

No files matched your search

+19
View File
@@ -167,6 +167,25 @@ Set `OPEN_TERMINAL_FILE_BROWSER_ROOT` to:
This is a UI hint for clients. It does not restrict terminal commands or file APIs.
### External Workspace Storage
Open Terminal uses the filesystem it is mounted on. To store workspace files
externally, mount that storage into the container:
- Single-user: `/home/user`
- Multi-user: `/home`
This can be a Docker volume, Kubernetes persistent volume, NFS/Azure Files
mount, or FUSE mount such as blobfuse, s3fs, or rclone.
### Office Previews
`GET /files/view?path=...` always returns the original file by default. Clients
that want a rendered preview can add `preview=true`; when LibreOffice is
available the server can return DOCX and PPTX previews as PDF. When rendered
preview support is not available, the same endpoint falls back to returning the
original file bytes.
## Using with Open WebUI
Open Terminal integrates with [Open WebUI](https://github.com/open-webui/open-webui), giving your AI assistants the ability to run commands, manage files, and interact with a terminal right from the AI interface. Make sure to add it under **Open Terminal** in the integrations settings, not as a tool server. Adding it as an Open Terminal connection gives you a built-in file navigation sidebar where you can browse directories, upload, download, and edit files. There are two ways to connect:
+48 -1
View File
@@ -3,6 +3,8 @@ import hmac
from importlib.metadata import version as _pkg_version
import fnmatch
import json
import logging
import tempfile
import aiofiles
import aiofiles.os
@@ -32,6 +34,7 @@ from open_terminal.utils.fs import UserFS
MATCH_PAGE_SIZE = 100
MAX_CONTENT_MATCHES_PER_FILE = 3
MAX_CONTENT_SEARCH_FILE_SIZE = 1 * 1024 * 1024
log = logging.getLogger(__name__)
if MULTI_USER:
from open_terminal.utils.user_isolation import check_environment, resolve_user
@@ -169,6 +172,37 @@ def get_file_browser_root(fs: UserFS) -> dict[str, str] | None:
return {"path": root_path, "label": label}
def _preview_pdf(target: str) -> bytes | None:
libreoffice = shutil.which("soffice") or shutil.which("libreoffice")
if not libreoffice:
return None
with tempfile.TemporaryDirectory() as tmpdir:
command = [
libreoffice,
"--headless",
"--nologo",
"--nofirststartwizard",
"--nodefault",
"--nolockcheck",
"--convert-to",
"pdf",
"--outdir",
tmpdir,
target,
]
subprocess.run(command, check=True, capture_output=True, timeout=60)
pdf_path = os.path.join(tmpdir, f"{os.path.splitext(os.path.basename(target))[0]}.pdf")
if not os.path.isfile(pdf_path):
matches = [name for name in os.listdir(tmpdir) if name.lower().endswith(".pdf")]
if not matches:
return None
pdf_path = os.path.join(tmpdir, sorted(matches)[0])
with open(pdf_path, "rb") as f:
return f.read()
app = FastAPI(
title="Open Terminal",
description="A remote terminal API.",
@@ -627,6 +661,7 @@ async def display_file(
)
async def view_file(
path: str = Query(..., description="Path to the file to view."),
preview: bool = Query(False, description="Return a best-effort rendered preview when available."),
fs: UserFS = Depends(get_filesystem),
):
"""Return raw file bytes with the appropriate Content-Type.
@@ -638,6 +673,18 @@ async def view_file(
if not await fs.isfile(target):
raise HTTPException(status_code=404, detail="File not found")
if preview:
ext = os.path.splitext(target)[1].lower()
if ext in (".docx", ".pptx"):
try:
pdf = await asyncio.to_thread(_preview_pdf, target)
except Exception:
log.exception("Rendered preview failed for %s", target)
pdf = None
if pdf:
return Response(content=pdf, media_type="application/pdf")
import mimetypes
mime, _ = mimetypes.guess_type(target)
@@ -653,7 +700,7 @@ async def view_file(
)
async def serve_file(path: str, fs: UserFS = Depends(get_filesystem)):
"""Path-based alias for view_file — enables relative URL resolution in iframes."""
return await view_file(path=f"/{path}", fs=fs)
return await view_file(path=f"/{path}", preview=False, fs=fs)
@app.post(