Jodit File Browser and Uploader connector for Python (FastAPI)
190
Python/FastAPI implementation of the Jodit File Browser and Uploader connector.
--strict)pip install "jodit-python[all]"
# or run the Docker image
docker run --rm -p 8081:8081 -v $(pwd)/files:/app/files w2fb/jodit-python
Python 3.14+. Optional features are extras:
[pdf] (generatePdf, needs the Pango system library: brew install pango on macOS, libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0 on Debian/Ubuntu)[docx] (generateDocx)[s3] (S3 storage)[all] installs them all. Without an extra the connector still works and the action that needs it answers 501 naming what to install.For development: uv (make sync installs everything).
make sync # install dependencies
make run # http://localhost:8081/ping
make menu # interactive list of commands
make demo # opens http://localhost:8080/demo/ (NO_BROWSER=1 to skip)
demo/index.html is the Jodit PRO file browser (from a CDN) talking to the connector configured by demo/config.json (CORS on, files in ./files, served by the same static server as the page). Jodit PRO needs no license key on localhost; on other hosts it shows a "Trial version" notice.
# main.py
from starlette.requests import Request
from jcpy import create_app
async def check_authentication(request: Request) -> str:
token = request.headers.get("authorization")
return "admin" if token == "Bearer secret" else "guest"
app = create_app("config.json", check_authentication=check_authentication)
uv run uvicorn main:app --port 8081
config.json overrides only what differs from the built-in defaults (camelCase keys):
{
"onlyPOST": true,
"sources": {
"uploads": {
"title": "Uploads",
"root": "/var/www/uploads",
"baseurl": "https://example.com/uploads/"
}
}
}
Without an explicit path the configuration is read from the CONFIG environment variable (JSON text) or the file named by CONFIG_FILE.
Access rules live in accessControl (the last matching rule wins, unlisted actions are allowed):
{
"defaultRole": "guest",
"accessControl": [
{ "role": "guest", "FILE_UPLOAD": false, "FILE_REMOVE": false },
{ "role": "admin", "path": "/private", "FILES": true }
]
}
They can also be loaded per check from code, e.g. from a database:
from jcpy import AccessControlRule, create_app
async def load_rules() -> list[AccessControlRule]:
rows = await db.fetch_rules()
return [AccessControlRule.model_validate(row) for row in rows]
app = create_app("config.json", access_control=load_rules)
{
"sources": {
"media": {
"title": "Media",
"baseurl": "https://my-bucket.s3.eu-central-1.amazonaws.com/media/",
"storageAdapter": "s3",
"s3": {"bucket": "my-bucket", "region": "eu-central-1", "prefix": "media"}
}
}
}
Without credentials the AWS default chain is used (environment, profile, instance role). MinIO, Cloudflare R2, Yandex Object Storage and others work through endpoint (plus forcePathStyle: true where needed). Other backends implement jcpy.StorageAdapter and are registered with register_storage_adapter("name", factory).
from starlette.requests import Request
from jcpy import ResolvedSources, create_app
async def resolve_sources(request: Request) -> ResolvedSources | None:
tenant = await find_tenant(request.headers.get("x-tenant-id"))
if tenant is None:
return None # static "sources" apply
return ResolvedSources(
id=f"{tenant.id}:{tenant.updated_at}",
sources={"files": tenant.source_settings},
)
app = create_app("config.json", resolve_sources=resolve_sources)
The resolver runs on every request, before authentication; the built sources are cached by id (dynamicSourcesCache: 200 tenants, 60 s by default). Use "sources": {} for an instance that only serves tenants.
Several independent instances can live in one application:
from fastapi import FastAPI
from jcpy import create_router
app = FastAPI()
app.include_router(create_router("public.json"), prefix="/public")
app.include_router(
create_router("admin.json", check_authentication=admin_auth),
prefix="/admin",
)
Complete Documentation - Full documentation with guides and API reference
Quick Links:
OpenAPI Specification:
The site is built from docs/ (make docs serves it locally, make docs-build builds it). The OpenAPI 3.1 document is generated from Pydantic models (make openapi); CI fails when it is stale. The connector itself does not serve /docs or /openapi.json: every path is an action name.
--strict, ships py.typedlinux/amd64 + linux/arm64 imageRunnable programs in examples/ (start them from the repository root, e.g. uv run python examples/basic.py):
| Example | Shows |
|---|---|
basic.py | Standalone connector from a JSON config |
cookie_auth.py | Role from a cookie |
jwt_auth.py | Role from a signed JWT (PyJWT) |
session_auth.py | Role in a server-signed session with login routes |
custom_svg.py | Custom thumbnail icons |
multi_instance.py | Two isolated connectors in one application |
s3.py | Files in an S3 bucket |
multi_tenant.py | Per-request (tenant) sources |
custom_storage.py | Custom storage adapter registered by name |
make check # lint, format, mypy --strict, OpenAPI and docs checks, tests
All caches (uv, ruff, mypy, pytest, coverage, bytecode) live in .cache/. The Makefile and the dev container set PYTHONPYCACHEPREFIX=.cache/pycache; export it yourself when running uv run ... directly.
make dev-up # dev container with hot reload on http://localhost:8081
make dev-shell # shell inside it (make check works there too)
make dev-down
make prod-up # production image (non-root, tini, healthcheck)
make prod-down
PORT overrides the published host port, e.g. PORT=9000 make prod-up. Files served by the production container live in ./files.
Open the folder in VS Code (or any IDE supporting Dev Containers) and choose Reopen in Container. The container is built from the dev stage of the Dockerfile; the virtualenv lives in a named volume, so it does not clash with a local .venv. Start the server with make dev.
Content type
Image
Digest
sha256:dfc532900…
Size
106.8 MB
Last updated
about 3 hours ago
docker pull w2fb/jodit-python