Jodit Connector Application (Python)
Python/FastAPI implementation of the Jodit File Browser and Uploader connector.
Links:
- Jodit Editor - The WYSIWYG HTML editor
- Complete Documentation - Full documentation and API reference
- jodit-nodejs - Node.js implementation
- jodit-php - Original PHP implementation
Technology Stack
- Python 3.14+ with strict typing (mypy
--strict) - FastAPI / Starlette for the HTTP API
- Pydantic 2 for the configuration and API schemas
- Pillow for image processing and thumbnails
- httpx for SSRF-safe remote downloads
- WeasyPrint (PDF), html-for-docx (DOCX), boto3 (S3), paramiko (SFTP) as optional extras
- pytest + Testcontainers for testing (MinIO, vsftpd, OpenSSH, Apache, rclone)
- uv, Ruff and MkDocs Material for tooling and docs
Installation
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 pangoon macOS,libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz-subset0on Debian/Ubuntu)[docx](generateDocx)[s3](S3 storage)[sftp](SFTP storage; FTP needs no extra)[all]installs them all. Without an extra the connector still works and the action that needs it answers501naming what to install.
For development: uv (make sync installs everything).
Quick start
make sync # install dependencies
make run # http://localhost:8081/ping
make menu # interactive list of commands
Try it with Jodit
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.
Usage
# 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)
S3 and S3-compatible storage
{
"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).
FTP, SFTP and WebDAV servers
{
"sources": {
"site": {
"title": "Website files",
"baseurl": "https://www.example.com/uploads/",
"storageAdapter": "sftp",
"sftp": {
"host": "files.example.com",
"username": "editor",
"privateKeyFile": "/run/secrets/editor_ed25519",
"hostKey": "ssh-ed25519 AAAA...",
"directory": "/var/www/uploads"
}
}
}
}
storageAdapter: "ftp" with an ftp block works the same way (tls: true for FTPS), and so does "webdav" with a webdav block (url, username, password) for Apache, nginx, Nextcloud and other WebDAV servers. The SFTP host key is always checked (hostKey, knownHostsFile or the system known_hosts); get it with ssh-keyscan. Writes are atomic (a temporary file renamed over the target), and connections are pooled and reopened when they drop.
Multi-tenant sources
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",
)
Documentation
Complete Documentation - Full documentation with guides and API reference
Quick Links:
- Getting Started - Installation and quick start
- Installation & Setup - Extras, environment variables, configuration sources
- Authentication - Cookie, JWT and session authentication
- Access Control - ACL rules and permissions
- Configuration - All configuration options
- FastAPI Integration - Mounting, prefixes, several instances
- AWS S3 & S3-compatible - Built-in S3 adapter, MinIO, R2, Yandex
- FTP & SFTP - Files on FTP, FTPS and SFTP servers
- WebDAV - Files on WebDAV servers (Apache, nginx, Nextcloud)
- Storage Adapters - Custom adapters, registering by name
- Dynamic Sources - Multi-tenant: resolve sources per request
- Documents - PDF and DOCX generation
- Docker Deployment - Docker guide
- API Endpoints - Every action with parameters and answers
- API Reference (Swagger) - Interactive OpenAPI reference
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.
Key Features
- Full file management - browse, upload (also from a URL), download, rename, move, copy, delete
- Folder operations - create, rename, move, copy, delete, tree view
- Image processing - resize, crop, save from the image editor, thumbnails
- Document generation - PDF and DOCX from HTML (optional extras)
- Access control - rules by role, path and extension; static, computed or loaded at runtime
- Authentication - a per-request callback: cookies, JWT, sessions
- Security - SSRF-safe remote downloads, confinement to the source root (symlinks included), POST-only mode, CORS allowlist
- FastAPI integration - standalone app or router, several isolated instances in one application
- Storage - local filesystem, AWS S3 / S3-compatible, FTP / FTPS, SFTP and WebDAV out of the box, custom adapters registered by name
- Multi-tenant - sources resolved per request, one instance for many tenants
- OpenAPI - OpenAPI 3.1 and Swagger UI generated from the schemas
- Typed - mypy
--strict, shipspy.typed - Testing - pytest suite with 100% coverage
- Docker - multi-stage, non-root,
linux/amd64+linux/arm64image
Implemented Functions
- files - get list of files
- folders - get folder tree
- permissions - get permissions
- fileUpload - upload files
- fileUploadRemote - upload file from remote URL
- fileRemove - remove files
- fileMove - move files and folders
- fileCopy - copy files
- fileRename - rename files
- fileDownload - download file
- getLocalFileByUrl - resolve local file by URL
- folderCreate - create folders
- folderRemove - remove folders
- folderMove - move folders
- folderCopy - copy folders
- folderRename - rename folders
- imageResize - resize images
- imageCrop - crop images
- imageSave - save an image edited in the browser
- imageLoad - read an image as a data URL
- generatePdf - generate PDF documents from HTML
- generateDocx - generate DOCX documents from HTML
- ping - health check
Examples
Runnable 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 |
Development
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.
Docker
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.
Dev Containers
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.
License
Release files for jodit-python 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| jodit_python-0.2.0.tar.gz | 108.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| jodit_python-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 262.4 kB
Release files / jodit_python-0.2.0.tar.gz
| Download URL | jodit_python-0.2.0.tar.gz |
|---|---|
| Size | 108.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
bf7c03edf1397f234f12839a7b353190aa062050fbda31ff783635f72490f983
|
|
BLAKE2b-256 checksum How to use checksums |
b7b31f41283475c444fdb0d1d01348e43b8732636bacdddb2af73fda87193f9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / jodit_python-0.2.0-py3-none-any.whl
| Download URL | jodit_python-0.2.0-py3-none-any.whl |
|---|---|
| Size | 154.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
85c07e6c36c7fe28cd0246e03e136ac9f6c4c7d21f44f56258d2dd12fdfa1b5d
|
|
BLAKE2b-256 checksum How to use checksums |
2f392482ec83c401caefcdbbd89332e8e22477bd3f7ff0124dcce2067f57ed3b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|