A QGIS plugin repository server: it maintains a package catalog
(plugins.xml / plugins.json) populated by uploading archives over HTTP.
The XML catalog is compatible with the QGIS Desktop plugin manager: the
.../plugins.xml URL can be added directly as a repository in QGIS.
Features:
- Automatic extraction of metadata (
metadata.txt) from the uploaded archive; - Catalog updates safe from concurrent access;
- Plugin filtering by QGIS version, tag, and status (experimental, deprecated, server plugin);
- Catalog available as XML (QGIS Desktop) and JSON;
- HTTP cache handling (
ETag/Last-Modified); - Optional TLS and client certificate authentication (mTLS).
Important: the server does not serve the archives themselves. It writes the files to the
plugins.repositorydirectory and publishes download URLs built fromplugins.download_url. A static file server (nginx, apache, …) serving that same directory must respond ondownload_url.
From source (Rust 2024 edition required):
cargo build --release
# the binary is in ./target/release/yapt-serverDocker image:
make -C .docker buildThis project is in active development, some features may be missing or incomplete.
The server implements no application-level authentication: the POST
and DELETE routes are open. In production, it must be placed behind a
reverse proxy providing authentication, or use client certificate
authentication (server.tls_client_ca_file) which, when enabled, requires
a valid certificate for every connection.
At the moment, the server use a flat file for storing plugin informations. This will change in the near future by implementing support for different database formats.
It means that, at the moment, your shouldn't use that server for hosting thousands of plugins.
mkdir -p /pub
cat > server.toml <<EOF
[server]
listen = "127.0.0.1:8070"
[plugins]
repository = "/pub"
download_url = "https://packages.example.org/pub/"
EOF
yapt-server serve -C server.tomlUpload a plugin, then read the catalog:
curl -X POST -F "file=@myplugin.1.0.0.zip" http://localhost:8070/plugins/
curl "http://localhost:8070/plugins.xml?qgis=3.40"In QGIS: Plugins → Manage and Install Plugins → Settings → Add, with
the URL http://localhost:8070/plugins.xml?qgis=3.40.
yapt-server <COMMAND>
| Command | Description |
|---|---|
serve [-C FILE] |
Start the server |
config [-C FILE] |
Print the effective configuration as JSON |
createdb --url URL [--root DIR] [--output FILE] |
(Re)build the catalog from the archives present on disk |
Without -C/--conf, the configuration is built solely from default values
and environment variables.
createdb scans the <root>/*/*.zip archives (the parent directory name
gives the plugin slug) and writes the catalog to --output
(./plugins.json by default):
yapt-server createdb --root /pub --output /pub/plugins.json \
--url https://packages.example.org/pub/[server]
# Listening interface
listen = "127.0.0.1:8070"
enable_tls = false
# Required if enable_tls is set
tls_cert_file = "<path>"
tls_key_file = "<path>"
# Optional: enable client certificate authentication (mTLS)
tls_client_ca_file = "<path>"
# Defaults to the number of available physical cpus
num_workers = 4
[plugins]
# Path of the directory where archives are stored.
# A relative path is resolved against the configuration file's
# directory. The directory must exist.
repository = "/pub"
# Base url used for downloading plugins
download_url = "https://packages.3liz.org/pub/"
# Optional: temporary directory used during upload
# (defaults to the platform temporary directory)
tempdir = "/tmp"
# Optional: maximum upload size, in bytes
# (defaults to actix-multipart's default limit)
upload_limit = 104857600
[logging]
level = "info" # error | warn | info | debug | traceAll keys are optional; unknown keys are rejected at startup.
yapt-server config -C server.toml lets you check the effective
configuration.
Environment variables must have the form
CONF_SECTION[__SECTION]__KEY and override the configuration file.
Examples:
CONF_SERVER__LISTEN=0.0.0.0:9876
CONF_PLUGINS__REPOSITORY=/pub
CONF_PLUGINS__DOWNLOAD_URL=https://packages.3liz.org/pub/
CONF_LOGGING__LEVEL=debugTwo additional variables control logging:
QGIS_PLUGIN_SERVER_LOG: global log filter, inenv_loggerformat (e.g.warn,yapt_server=debug). It enables logging from external libraries; the server's own modules remain forced to the level set bylogging.level. When this variable is set, the module name is added to each log line.SERVER_LOG_STYLE: controls colorization (auto,always,never).
Routes ending with
/require it:/pluginsreturns404,/plugins/is the valid route.
| Method | Route | Description |
|---|---|---|
GET, HEAD |
/plugins.xml |
XML catalog (QGIS Desktop) |
GET, HEAD |
/plugins.json |
JSON catalog |
GET, HEAD |
/plugins/ |
JSON catalog (alias) |
GET, HEAD |
/plugins/<slug>/plugins.xml |
XML catalog for a single plugin |
GET, HEAD |
/plugins/<slug>/plugins.json |
JSON catalog for a single plugin |
GET, HEAD |
/plugins/<slug>/ |
JSON catalog for a single plugin |
Query parameters:
| Parameter | Value | Default | Description |
|---|---|---|---|
qgis |
version | — | Required. Target QGIS version (3.40, 3.40.2, …). Only compatible plugins (qgisMinimumVersion/qgisMaximumVersion) are returned |
pre |
true/false |
false |
Include experimental versions |
deprecated |
true/false |
false |
Include deprecated plugins |
server |
true/false |
false |
Return only server plugins |
all |
true/false |
false |
Return all versions, not only the latest ones (JSON only) |
tags |
text | — | Fuzzy search on plugin name and tags |
Booleans must be written explicitly: ?pre=true. ?pre alone or ?pre=1
returns 400.
Without all=true, the catalog contains at most two versions per plugin:
the latest stable version and, if pre=true, the latest experimental
version — this is the behavior expected by QGIS Desktop.
A missing or invalid qgis parameter returns 400 Bad Request.
Examples:
# XML catalog for QGIS 3.40, experimental versions included
curl "http://localhost:8070/plugins.xml?qgis=3.40&pre=true"
# Server plugins only, as JSON
curl "http://localhost:8070/plugins.json?qgis=3.40&server=true"
# All versions of a plugin
curl "http://localhost:8070/plugins/lizmap-server/plugins.json?qgis=3.40&all=true&pre=true"JSON response:
{ "plugins": [ { "name": "Lizmap server", "version": "2.16.0", "slug": "lizmap-server", "downloadUrl": "..." } ] }Responses carry the ETag (weak) and Last-Modified headers, computed from
the catalog's last modification date. A request with an up-to-date
If-None-Match receives 304 Not Modified. HEAD requests can be used to
check catalog freshness without downloading it; on a /plugins/<slug>/…
route, HEAD returns 404 if the plugin is unknown.
POST /plugins/[?pre=true|false]
multipart/form-data body:
| Field | Required | Description |
|---|---|---|
file |
yes | The plugin .zip archive |
sig |
no | Archive signature |
checksum |
no | Checksum file |
checksum_sig |
no | Checksum signature |
Optional X-Upload-Agent header: its value is stored in the catalog's
uploadedBy field.
The pre parameter forces the plugin's experimental status and overrides
the experimental field of metadata.txt; if absent, the metadata is used
as is.
The server:
- extracts
metadata.txtfrom the archive and derives the slug from it (slugify(name), e.g.Lizmap server→lizmap-server); - writes the archive (and accompanying files) to
<repository>/<slug>/<uploaded-file-name>; - records the version in the catalog and rewrites
<repository>/plugins.json.
Response 200 OK with a Location header containing the public download
URL (<download_url>/<slug>/<file_name>).
An existing stable version cannot be overwritten (400 Bad Request,
"You cannot overwrite a stable plugin version"); an experimental version
can be replaced.
curl -i -X POST \
-H "X-Upload-Agent: gitlab-ci" \
-F "file=@lizmap-server.2.16.0.zip" \
-F "checksum=@lizmap-server.2.16.0.zip.sha256" \
"https://plugins.example.org/plugins/"DELETE /plugins/<slug>/?version=<requirement>
version is a semver requirement
(=0.1.0, <1.0.0, >=1.0, <2.0, …). Beware: version=0.1.0 is
interpreted as ^0.1.0 and therefore deletes all 0.1.x versions; use
=0.1.0 for an exact deletion.
The matching archives are removed from disk and the catalog is rewritten. The response lists the versions actually removed:
curl -X DELETE "https://plugins.example.org/plugins/lizmap-server/?version=%3D2.16.0"
{"removed":[["lizmap-server","2.16.0"]]}An invalid requirement returns 400; a requirement that matches nothing
returns 200 with an empty list.
<repository>/
├── plugins.json # catalog, reloaded at startup
├── lizmap-server/
│ ├── lizmap-server.2.16.0.zip
│ └── lizmap-server.2.16.0.zip.sha256
└── edigeo-processing/
└── edigeo-processing.0.1.0.zip
At startup, the server loads <repository>/plugins.json if it exists. If
the catalog is lost or out of sync with the files on disk, it can be
rebuilt with createdb (see above).
The archive must contain a single root directory with a metadata.txt file
([general] section).
Required fields: name, description, version, qgisMinimumVersion,
homepage, author, repository, commitSha1.
Optional fields used: qgisMaximumVersion, icon, tracker, tags,
experimental, deprecated, server.
An invalid archive or incomplete metadata results in a 400 Bad Request.
Non-semver versions are accepted (a "best effort" conversion is applied:
v2.4.0, 0.6-beta3, 23.2a, …), but they cannot be targeted by DELETE
version requirements.
The image listens on port 9876 and runs yapt-server serve without a
configuration file: configuration is therefore done through environment
variables.
docker run --rm \
-p 9876:9876 \
-v /srv/plugins:/pub \
-e CONF_PLUGINS__REPOSITORY=/pub \
-e CONF_PLUGINS__DOWNLOAD_URL=https://packages.example.org/pub/ \
qgis-plugin-servercargo test
cargo build --release
make package # binary tar.gz archive
make wheel # Python wheel (maturin)The xml feature (enabled by default) provides the plugins.xml routes;
it can be disabled with cargo build --no-default-features.