Sign inSign up

w2fb/jodit-python

By w2fb

•Updated about 3 hours ago

Jodit File Browser and Uploader connector for Python (FastAPI)

Image
0

190

w2fb/jodit-python repository overview

⁠Jodit Connector Application (Python)

PyPI Docker Pulls CI Docs Coverage License: MIT Python Ruff mypy: strict

Python/FastAPI implementation of the Jodit File Browser and Uploader connector.

⁠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) as optional extras
  • pytest + Testcontainers for testing (MinIO for S3)
  • 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 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).

⁠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).

⁠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:

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 or AWS S3 / S3-compatible 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, ships py.typed
  • Testing - pytest suite with 100% coverage
  • Docker - multi-stage, non-root, linux/amd64 + linux/arm64 image

⁠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):

ExampleShows
basic.pyStandalone connector from a JSON config
cookie_auth.pyRole from a cookie
jwt_auth.pyRole from a signed JWT (PyJWT)
session_auth.pyRole in a server-signed session with login routes
custom_svg.pyCustom thumbnail icons
multi_instance.pyTwo isolated connectors in one application
s3.pyFiles in an S3 bucket
multi_tenant.pyPer-request (tenant) sources
custom_storage.pyCustom 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

MIT⁠

Tag summary

Content type

Image

Digest

sha256:dfc532900…

Size

106.8 MB

Last updated

about 3 hours ago

docker pull w2fb/jodit-python