From b23fea96839a67731c34e9e8338f333f6038d4b4 Mon Sep 17 00:00:00 2001 From: okxlin <61420215+okxlin@users.noreply.github.com> Date: Wed, 15 Jul 2026 08:07:56 +0800 Subject: [PATCH] Add Directus app --- apps/directus/12.1.1/.env.sample | 7 + apps/directus/12.1.1/data.yml | 102 ++++++++++++++ apps/directus/12.1.1/data/.gitkeep | 0 apps/directus/12.1.1/docker-compose.yml | 35 +++++ apps/directus/12.1.1/scripts/init.sh | 163 ++++++++++++++++++++++ apps/directus/12.1.1/scripts/uninstall.sh | 8 ++ apps/directus/12.1.1/scripts/upgrade.sh | 5 + apps/directus/README.md | 69 +++++++++ apps/directus/data.yml | 32 +++++ apps/directus/latest/.env.sample | 7 + apps/directus/latest/data.yml | 102 ++++++++++++++ apps/directus/latest/data/.gitkeep | 0 apps/directus/latest/docker-compose.yml | 35 +++++ apps/directus/latest/scripts/init.sh | 163 ++++++++++++++++++++++ apps/directus/latest/scripts/uninstall.sh | 8 ++ apps/directus/latest/scripts/upgrade.sh | 5 + apps/directus/logo.png | Bin 0 -> 2489 bytes 17 files changed, 741 insertions(+) create mode 100644 apps/directus/12.1.1/.env.sample create mode 100644 apps/directus/12.1.1/data.yml create mode 100644 apps/directus/12.1.1/data/.gitkeep create mode 100644 apps/directus/12.1.1/docker-compose.yml create mode 100755 apps/directus/12.1.1/scripts/init.sh create mode 100755 apps/directus/12.1.1/scripts/uninstall.sh create mode 100755 apps/directus/12.1.1/scripts/upgrade.sh create mode 100644 apps/directus/README.md create mode 100644 apps/directus/data.yml create mode 100644 apps/directus/latest/.env.sample create mode 100644 apps/directus/latest/data.yml create mode 100644 apps/directus/latest/data/.gitkeep create mode 100644 apps/directus/latest/docker-compose.yml create mode 100755 apps/directus/latest/scripts/init.sh create mode 100755 apps/directus/latest/scripts/uninstall.sh create mode 100755 apps/directus/latest/scripts/upgrade.sh create mode 100644 apps/directus/logo.png diff --git a/apps/directus/12.1.1/.env.sample b/apps/directus/12.1.1/.env.sample new file mode 100644 index 000000000..4aa55b35c --- /dev/null +++ b/apps/directus/12.1.1/.env.sample @@ -0,0 +1,7 @@ +PANEL_APP_PORT_HTTP=8055 +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=replace-with-a-strong-password +DIRECTUS_SECRET=replace-with-a-long-random-secret +PUBLIC_URL=/ +APP_DATA_DIR=./data +CONTAINER_NAME=directus diff --git a/apps/directus/12.1.1/data.yml b/apps/directus/12.1.1/data.yml new file mode 100644 index 000000000..5d03a9fd4 --- /dev/null +++ b/apps/directus/12.1.1/data.yml @@ -0,0 +1,102 @@ +additionalProperties: + formFields: + - default: 8055 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: Порт HTTP + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: admin@example.com + edit: true + envKey: ADMIN_EMAIL + labelEn: Administrator Email + labelZh: 管理员邮箱 + label: + en: Administrator Email + zh: 管理员邮箱 + zh-Hant: 管理員電子郵件 + ja: 管理者メールアドレス + ko: 관리자 이메일 + ru: Электронная почта администратора + ms: E-mel Pentadbir + pt-br: E-mail do administrador + required: true + type: text + - default: "" + edit: true + envKey: ADMIN_PASSWORD + labelEn: Administrator Password + labelZh: 管理员密码 + label: + en: Administrator Password + zh: 管理员密码 + zh-Hant: 管理員密碼 + ja: 管理者パスワード + ko: 관리자 비밀번호 + ru: Пароль администратора + ms: Kata Laluan Pentadbir + pt-br: Senha do administrador + required: true + rule: paramComplexity + type: password + - default: "" + edit: true + envKey: DIRECTUS_SECRET + labelEn: Instance Secret + labelZh: 实例密钥 + label: + en: Instance Secret + zh: 实例密钥 + zh-Hant: 執行個體密鑰 + ja: インスタンスシークレット + ko: 인스턴스 비밀 키 + ru: Секрет экземпляра + ms: Rahsia Instans + pt-br: Segredo da instância + random: true + required: true + rule: paramComplexity + type: password + - default: / + edit: true + envKey: PUBLIC_URL + labelEn: Public URL + labelZh: 公共访问地址 + label: + en: Public URL + zh: 公共访问地址 + zh-Hant: 公開存取位址 + ja: 公開 URL + ko: 공개 URL + ru: Публичный URL + ms: URL Awam + pt-br: URL pública + required: false + type: text + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori Data + pt-br: Diretório de dados + required: true + type: text diff --git a/apps/directus/12.1.1/data/.gitkeep b/apps/directus/12.1.1/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/directus/12.1.1/docker-compose.yml b/apps/directus/12.1.1/docker-compose.yml new file mode 100644 index 000000000..c74be3aeb --- /dev/null +++ b/apps/directus/12.1.1/docker-compose.yml @@ -0,0 +1,35 @@ +services: + directus: + image: "directus/directus:12.1.1" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + networks: + - 1panel-network + ports: + - "${PANEL_APP_PORT_HTTP}:8055" + environment: + - SECRET=${DIRECTUS_SECRET} + - DB_CLIENT=sqlite3 + - DB_FILENAME=/directus/database/data.db + - ADMIN_EMAIL=${ADMIN_EMAIL} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + - PUBLIC_URL=${PUBLIC_URL} + - WEBSOCKETS_ENABLED=true + - TELEMETRY=false + - PROJECT_OWNER_ENABLED=false + volumes: + - "${APP_DATA_DIR}/database:/directus/database" + - "${APP_DATA_DIR}/uploads:/directus/uploads" + - "${APP_DATA_DIR}/extensions:/directus/extensions" + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:8055/server/ping || exit 1"] + interval: 10s + timeout: 5s + start_period: 30s + retries: 10 + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/directus/12.1.1/scripts/init.sh b/apps/directus/12.1.1/scripts/init.sh new file mode 100755 index 000000000..60e73676b --- /dev/null +++ b/apps/directus/12.1.1/scripts/init.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +strip_matching_quotes() { + local value="$1" + + if [[ ${#value} -ge 2 ]]; then + if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + fi + printf '%s\n' "$value" +} + +read_env_value() { + local key="$1" + local value="" + + if [[ -f "$ENV_FILE" ]]; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)" + fi + strip_matching_quotes "$value" +} + +path_is_dotenv_safe() { + local value="$1" + + case "$value" in + *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) return 1 ;; + *) return 0 ;; + esac +} + +secret_is_acceptable() { + local value="$1" + + [[ ${#value} -ge 32 ]] || return 1 + case "$value" in + *[!A-Za-z0-9._~!@%+=:,-]*) return 1 ;; + *) return 0 ;; + esac +} + +generate_secret() { + local value + + value="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')" + [[ "$value" =~ ^[0-9a-f]{64}$ ]] || fail "Unable to generate DIRECTUS_SECRET" + printf '%s\n' "$value" +} + +write_env_value() { + local key="$1" + local value="$2" + local line + local found=0 + local tmp_file + + tmp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")" + chmod 600 -- "$tmp_file" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" == "${key}="* ]]; then + if [[ $found -eq 0 ]]; then + printf '%s=%s\n' "$key" "$value" >>"$tmp_file" + found=1 + fi + else + printf '%s\n' "$line" >>"$tmp_file" + fi + done <"$ENV_FILE" + if [[ $found -eq 0 ]]; then + printf '%s=%s\n' "$key" "$value" >>"$tmp_file" + fi + mv -f -- "$tmp_file" "$ENV_FILE" +} + +persist_secret() { + local value="$1" + local tmp_file + + tmp_file="$(mktemp "${APP_DATA_DIR_ABS}/.directus-secret.tmp.XXXXXX")" + chmod 600 -- "$tmp_file" + printf '%s\n' "$value" >"$tmp_file" + mv -f -- "$tmp_file" "$SECRET_FILE" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + +if [[ ${APP_DATA_DIR+x} ]]; then + APP_DATA_DIR_RAW="$APP_DATA_DIR" +else + APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" +fi +APP_DATA_DIR_RAW="$(strip_matching_quotes "${APP_DATA_DIR_RAW:-./data}")" + +[[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" +path_is_dotenv_safe "$APP_DATA_DIR_RAW" || fail "APP_DATA_DIR contains unsupported dotenv characters" + +case "$APP_DATA_DIR_RAW" in + /*) + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; + *) + APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" + case "$APP_DATA_DIR_ABS" in + "${ROOT_DIR}" | "${ROOT_DIR}"/*) ;; + *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;; + esac + ;; +esac + +[[ "$APP_DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root" +[[ ! -L "$APP_DATA_DIR_ABS" ]] || fail "APP_DATA_DIR must not be a symbolic link" +if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi +mkdir -p -- "$APP_DATA_DIR_ABS" + +for directory in database uploads extensions; do + target="${APP_DATA_DIR_ABS}/${directory}" + [[ ! -L "$target" ]] || fail "$target must not be a symbolic link" + if [[ -e "$target" && ! -d "$target" ]]; then + fail "$target must be a directory" + fi + mkdir -p -- "$target" + chown 1000:1000 -- "$target" + chmod 700 -- "$target" +done + +SECRET_FILE="${APP_DATA_DIR_ABS}/.directus-secret" +[[ ! -L "$SECRET_FILE" ]] || fail "$SECRET_FILE must not be a symbolic link" + +if [[ -e "$SECRET_FILE" ]]; then + [[ -f "$SECRET_FILE" ]] || fail "$SECRET_FILE must be a regular file" + DIRECTUS_SECRET_FINAL="$(<"$SECRET_FILE")" + secret_is_acceptable "$DIRECTUS_SECRET_FINAL" || fail "$SECRET_FILE contains an invalid secret" +else + if [[ ${DIRECTUS_SECRET+x} ]]; then + DIRECTUS_SECRET_CANDIDATE="$DIRECTUS_SECRET" + else + DIRECTUS_SECRET_CANDIDATE="$(read_env_value DIRECTUS_SECRET)" + fi + if secret_is_acceptable "$DIRECTUS_SECRET_CANDIDATE"; then + DIRECTUS_SECRET_FINAL="$DIRECTUS_SECRET_CANDIDATE" + else + DIRECTUS_SECRET_FINAL="$(generate_secret)" + fi + persist_secret "$DIRECTUS_SECRET_FINAL" +fi + +chmod 600 -- "$SECRET_FILE" +write_env_value DIRECTUS_SECRET "$DIRECTUS_SECRET_FINAL" diff --git a/apps/directus/12.1.1/scripts/uninstall.sh b/apps/directus/12.1.1/scripts/uninstall.sh new file mode 100755 index 000000000..5a3f6bd6b --- /dev/null +++ b/apps/directus/12.1.1/scripts/uninstall.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v docker-compose >/dev/null 2>&1; then + docker-compose down --volumes --remove-orphans +else + docker compose down --volumes --remove-orphans +fi diff --git a/apps/directus/12.1.1/scripts/upgrade.sh b/apps/directus/12.1.1/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/directus/12.1.1/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec bash "${SCRIPT_DIR}/init.sh" diff --git a/apps/directus/README.md b/apps/directus/README.md new file mode 100644 index 000000000..b2ab8d3e2 --- /dev/null +++ b/apps/directus/README.md @@ -0,0 +1,69 @@ +# Directus + +## 产品介绍 + +Directus 是一个无头数据平台,可连接数据库并自动提供 REST、GraphQL、WebSocket API 以及可视化数据管理界面。本应用使用 Directus 官方镜像,以单个 Directus service 和 SQLite 数据库运行。 + +## 主要功能 + +- 连接 SQLite 数据库并自动提供 REST、GraphQL 和 WebSocket API +- 使用 Directus Studio 管理数据模型、内容、文件、权限和自动化流程 +- 保存本地上传文件并加载自定义扩展 + +## 访问说明 + +- 默认容器端口为 `8055`,安装表单可修改主机端口。 +- 安装时会使用表单中的管理员邮箱和强密码创建初始管理员;管理员密码由用户输入,并由 1Panel 复杂度规则校验。 +- `SECRET` 用于签名访问令牌。安装脚本会把不足 32 字符的面板随机值替换为 64 位十六进制密钥,并以 `0600` 权限持久化到数据目录下的 `.directus-secret`。升级和迁移时必须连同该文件保留同一个值,否则已有会话和令牌可能失效。 +- `PUBLIC_URL` 可留空,此时 Directus 使用默认相对地址 `/`。如需邮件链接、OAuth、SSO 或许可证激活,请填写实际可访问的绝对 URL,例如 `https://directus.example.com`。 + +## 部署拓扑 + +- 默认包只有一个 Directus service,不依赖 1Panel 数据库、Redis 或网站 Runtime。 +- 数据库使用 SQLite,数据库文件为 `/directus/database/data.db`。 +- Directus 官方 Quickstart 提供了相同的单服务 SQLite 拓扑,但明确说明其示例不是 production-ready。本包补充了随机密钥、初始管理员、持久化、健康检查并关闭默认遥测,仍定位于单节点和中小规模自托管。 +- 较大生产环境或横向扩展应使用 PostgreSQL、MySQL 或 MariaDB;Redis 对缓存是推荐项,对多副本横向扩展是必需项。这些依赖适合后续通过 1Panel service selector 联动,但不属于当前默认包。不要仅修改现有 SQLite 实例的环境变量来切换数据库。 + +## 数据与升级 + +安装表单选择的数据目录包含以下持久化子目录: + +- `database` → `/directus/database` +- `uploads` → `/directus/uploads` +- `extensions` → `/directus/extensions` +- `.directus-secret` → 升级时用于恢复令牌签名密钥的隐藏文件 + +容器以 UID/GID `1000:1000` 运行,安装脚本会为三个数据子目录设置对应所有者并收紧为 `0700`。升级前请备份完整数据目录(包括 `.directus-secret`),并阅读目标版本的 breaking changes。Directus 会在启动时自动执行数据库迁移;迁移完成前不要中断容器。生产环境建议使用固定版本,`latest` 仅适合明确接受浮动更新的场景。 + +## 许可证与遥测 + +- Directus v12 使用 source-available 的 `MSCL-1.0-GPL`(Monospace Sustainable Core License 1.0),不是标准 OSI 开源许可证。 +- 许可证禁止将软件用于与许可方商业 Directus 产品竞争的 **Competing Use**;使用者必须自行确认用途符合上游许可条款。 +- 未配置许可证时实例运行在免费 `core tier`。更高限制或附加能力需要许可证;符合条件的组织可申请 Open Innovation Grant。 +- 本包设置 `TELEMETRY=false` 和 `PROJECT_OWNER_ENABLED=false`。某些许可证权益可能要求遥测保持启用,上游授权规则优先。 +- 若曾激活许可证,删除实例或清除数据库前应先在 Directus 中停用许可证,避免遗留占用激活名额。 + +## Introduction + +Directus is a headless data platform that provides REST, GraphQL, and WebSocket APIs plus a data studio on top of a database. This package uses the official image in a single-service SQLite topology for single-node and small-to-medium self-hosted deployments. + +The package persists `/directus/database`, `/directus/uploads`, and `/directus/extensions`, creates the initial administrator from installation fields, persists a generated token-signing secret in `.directus-secret`, and probes `/server/ping`. Larger or horizontally scaled deployments should use an external PostgreSQL/MySQL/MariaDB service and Redis where required. + +Directus 12 is source-available under `MSCL-1.0-GPL`. Review the upstream license, including the Competing Use restriction and core-tier entitlements, before deployment. + +## Features + +- REST, GraphQL, and WebSocket APIs generated from the persisted SQLite database +- Directus Studio for schema, content, file, permission, and flow management +- Persistent local uploads and extension loading + +## 参考资料 + +- Docker Compose Quickstart: +- 自托管部署: +- 数据库配置: +- 升级说明: +- 许可证说明: +- 许可证全文: +- 源码仓库: +- 官方镜像: diff --git a/apps/directus/data.yml b/apps/directus/data.yml new file mode 100644 index 000000000..46cc91d3c --- /dev/null +++ b/apps/directus/data.yml @@ -0,0 +1,32 @@ +name: Directus +tags: + - Tool +title: 连接数据库并即时生成 API 与数据管理界面的无头数据平台 +description: 连接数据库并即时生成 API 与数据管理界面的无头数据平台 +additionalProperties: + key: directus + name: Directus + tags: + - Tool + shortDescZh: 连接数据库并即时生成 API 与数据管理界面的无头数据平台 + shortDescEn: A headless data platform that connects to databases and instantly provides APIs and a data studio + description: + en: A headless data platform that connects to databases and instantly provides APIs and a data studio + zh: 连接数据库并即时生成 API 与数据管理界面的无头数据平台 + zh-Hant: 連接資料庫並即時產生 API 與資料管理介面的無頭資料平台 + ja: データベースに接続し、API とデータ管理画面を即座に提供するヘッドレスデータプラットフォーム + ko: 데이터베이스에 연결해 API와 데이터 관리 화면을 즉시 제공하는 헤드리스 데이터 플랫폼 + ru: Headless-платформа данных, которая подключается к базам данных и сразу предоставляет API и интерфейс управления + ms: Platform data tanpa kepala yang bersambung ke pangkalan data serta menyediakan API dan studio data dengan segera + pt-br: Plataforma de dados headless que se conecta a bancos de dados e fornece APIs e um estúdio de dados instantaneamente + type: tool + crossVersionUpdate: true + limit: 0 + recommend: 0 + memoryRequired: 512 + website: https://directus.io/ + github: https://github.com/directus/directus + document: https://directus.io/docs/ + architectures: + - amd64 + - arm64 diff --git a/apps/directus/latest/.env.sample b/apps/directus/latest/.env.sample new file mode 100644 index 000000000..4aa55b35c --- /dev/null +++ b/apps/directus/latest/.env.sample @@ -0,0 +1,7 @@ +PANEL_APP_PORT_HTTP=8055 +ADMIN_EMAIL=admin@example.com +ADMIN_PASSWORD=replace-with-a-strong-password +DIRECTUS_SECRET=replace-with-a-long-random-secret +PUBLIC_URL=/ +APP_DATA_DIR=./data +CONTAINER_NAME=directus diff --git a/apps/directus/latest/data.yml b/apps/directus/latest/data.yml new file mode 100644 index 000000000..5d03a9fd4 --- /dev/null +++ b/apps/directus/latest/data.yml @@ -0,0 +1,102 @@ +additionalProperties: + formFields: + - default: 8055 + edit: true + envKey: PANEL_APP_PORT_HTTP + labelEn: HTTP Port + labelZh: HTTP 端口 + label: + en: HTTP Port + zh: HTTP 端口 + zh-Hant: HTTP 連接埠 + ja: HTTP ポート + ko: HTTP 포트 + ru: Порт HTTP + ms: Port HTTP + pt-br: Porta HTTP + required: true + rule: paramPort + type: number + - default: admin@example.com + edit: true + envKey: ADMIN_EMAIL + labelEn: Administrator Email + labelZh: 管理员邮箱 + label: + en: Administrator Email + zh: 管理员邮箱 + zh-Hant: 管理員電子郵件 + ja: 管理者メールアドレス + ko: 관리자 이메일 + ru: Электронная почта администратора + ms: E-mel Pentadbir + pt-br: E-mail do administrador + required: true + type: text + - default: "" + edit: true + envKey: ADMIN_PASSWORD + labelEn: Administrator Password + labelZh: 管理员密码 + label: + en: Administrator Password + zh: 管理员密码 + zh-Hant: 管理員密碼 + ja: 管理者パスワード + ko: 관리자 비밀번호 + ru: Пароль администратора + ms: Kata Laluan Pentadbir + pt-br: Senha do administrador + required: true + rule: paramComplexity + type: password + - default: "" + edit: true + envKey: DIRECTUS_SECRET + labelEn: Instance Secret + labelZh: 实例密钥 + label: + en: Instance Secret + zh: 实例密钥 + zh-Hant: 執行個體密鑰 + ja: インスタンスシークレット + ko: 인스턴스 비밀 키 + ru: Секрет экземпляра + ms: Rahsia Instans + pt-br: Segredo da instância + random: true + required: true + rule: paramComplexity + type: password + - default: / + edit: true + envKey: PUBLIC_URL + labelEn: Public URL + labelZh: 公共访问地址 + label: + en: Public URL + zh: 公共访问地址 + zh-Hant: 公開存取位址 + ja: 公開 URL + ko: 공개 URL + ru: Публичный URL + ms: URL Awam + pt-br: URL pública + required: false + type: text + - default: ./data + edit: true + envKey: APP_DATA_DIR + labelEn: Data Directory + labelZh: 数据目录 + label: + en: Data Directory + zh: 数据目录 + zh-Hant: 資料目錄 + ja: データディレクトリ + ko: 데이터 디렉터리 + ru: Каталог данных + ms: Direktori Data + pt-br: Diretório de dados + required: true + type: text diff --git a/apps/directus/latest/data/.gitkeep b/apps/directus/latest/data/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/directus/latest/docker-compose.yml b/apps/directus/latest/docker-compose.yml new file mode 100644 index 000000000..228ec0dad --- /dev/null +++ b/apps/directus/latest/docker-compose.yml @@ -0,0 +1,35 @@ +services: + directus: + image: "directus/directus:latest" + container_name: ${CONTAINER_NAME} + restart: unless-stopped + networks: + - 1panel-network + ports: + - "${PANEL_APP_PORT_HTTP}:8055" + environment: + - SECRET=${DIRECTUS_SECRET} + - DB_CLIENT=sqlite3 + - DB_FILENAME=/directus/database/data.db + - ADMIN_EMAIL=${ADMIN_EMAIL} + - ADMIN_PASSWORD=${ADMIN_PASSWORD} + - PUBLIC_URL=${PUBLIC_URL} + - WEBSOCKETS_ENABLED=true + - TELEMETRY=false + - PROJECT_OWNER_ENABLED=false + volumes: + - "${APP_DATA_DIR}/database:/directus/database" + - "${APP_DATA_DIR}/uploads:/directus/uploads" + - "${APP_DATA_DIR}/extensions:/directus/extensions" + healthcheck: + test: ["CMD-SHELL", "wget --spider -q http://127.0.0.1:8055/server/ping || exit 1"] + interval: 10s + timeout: 5s + start_period: 30s + retries: 10 + labels: + createdBy: "Apps" + +networks: + 1panel-network: + external: true diff --git a/apps/directus/latest/scripts/init.sh b/apps/directus/latest/scripts/init.sh new file mode 100755 index 000000000..60e73676b --- /dev/null +++ b/apps/directus/latest/scripts/init.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)" +ENV_FILE="${ENV_FILE:-${ROOT_DIR}/.env}" + +fail() { + printf '%s\n' "$1" >&2 + exit 1 +} + +strip_matching_quotes() { + local value="$1" + + if [[ ${#value} -ge 2 ]]; then + if [[ "${value:0:1}" == '"' && "${value: -1}" == '"' ]]; then + value="${value:1:${#value}-2}" + elif [[ "${value:0:1}" == "'" && "${value: -1}" == "'" ]]; then + value="${value:1:${#value}-2}" + fi + fi + printf '%s\n' "$value" +} + +read_env_value() { + local key="$1" + local value="" + + if [[ -f "$ENV_FILE" ]]; then + value="$(grep -E "^${key}=" "$ENV_FILE" | tail -n 1 | cut -d '=' -f 2- || true)" + fi + strip_matching_quotes "$value" +} + +path_is_dotenv_safe() { + local value="$1" + + case "$value" in + *$'\n'* | *$'\r'* | *\\* | *'$'* | *'#'* | *'"'* | *"'"*) return 1 ;; + *) return 0 ;; + esac +} + +secret_is_acceptable() { + local value="$1" + + [[ ${#value} -ge 32 ]] || return 1 + case "$value" in + *[!A-Za-z0-9._~!@%+=:,-]*) return 1 ;; + *) return 0 ;; + esac +} + +generate_secret() { + local value + + value="$(od -An -N32 -tx1 /dev/urandom | tr -d ' \n')" + [[ "$value" =~ ^[0-9a-f]{64}$ ]] || fail "Unable to generate DIRECTUS_SECRET" + printf '%s\n' "$value" +} + +write_env_value() { + local key="$1" + local value="$2" + local line + local found=0 + local tmp_file + + tmp_file="$(mktemp "${ENV_FILE}.tmp.XXXXXX")" + chmod 600 -- "$tmp_file" + while IFS= read -r line || [[ -n "$line" ]]; do + if [[ "$line" == "${key}="* ]]; then + if [[ $found -eq 0 ]]; then + printf '%s=%s\n' "$key" "$value" >>"$tmp_file" + found=1 + fi + else + printf '%s\n' "$line" >>"$tmp_file" + fi + done <"$ENV_FILE" + if [[ $found -eq 0 ]]; then + printf '%s=%s\n' "$key" "$value" >>"$tmp_file" + fi + mv -f -- "$tmp_file" "$ENV_FILE" +} + +persist_secret() { + local value="$1" + local tmp_file + + tmp_file="$(mktemp "${APP_DATA_DIR_ABS}/.directus-secret.tmp.XXXXXX")" + chmod 600 -- "$tmp_file" + printf '%s\n' "$value" >"$tmp_file" + mv -f -- "$tmp_file" "$SECRET_FILE" +} + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found" +[[ ! -L "$ENV_FILE" ]] || fail "$ENV_FILE must not be a symbolic link" + +if [[ ${APP_DATA_DIR+x} ]]; then + APP_DATA_DIR_RAW="$APP_DATA_DIR" +else + APP_DATA_DIR_RAW="$(read_env_value APP_DATA_DIR)" +fi +APP_DATA_DIR_RAW="$(strip_matching_quotes "${APP_DATA_DIR_RAW:-./data}")" + +[[ -n "$APP_DATA_DIR_RAW" ]] || fail "APP_DATA_DIR must not be empty" +path_is_dotenv_safe "$APP_DATA_DIR_RAW" || fail "APP_DATA_DIR contains unsupported dotenv characters" + +case "$APP_DATA_DIR_RAW" in + /*) + APP_DATA_DIR_ABS="$(realpath -m -- "$APP_DATA_DIR_RAW")" + ;; + *) + APP_DATA_DIR_ABS="$(realpath -m -- "${ROOT_DIR}/${APP_DATA_DIR_RAW#./}")" + case "$APP_DATA_DIR_ABS" in + "${ROOT_DIR}" | "${ROOT_DIR}"/*) ;; + *) fail "Relative APP_DATA_DIR must stay inside the application directory" ;; + esac + ;; +esac + +[[ "$APP_DATA_DIR_ABS" != "/" ]] || fail "APP_DATA_DIR must not be the filesystem root" +[[ ! -L "$APP_DATA_DIR_ABS" ]] || fail "APP_DATA_DIR must not be a symbolic link" +if [[ -e "$APP_DATA_DIR_ABS" && ! -d "$APP_DATA_DIR_ABS" ]]; then + fail "APP_DATA_DIR must be a directory" +fi +mkdir -p -- "$APP_DATA_DIR_ABS" + +for directory in database uploads extensions; do + target="${APP_DATA_DIR_ABS}/${directory}" + [[ ! -L "$target" ]] || fail "$target must not be a symbolic link" + if [[ -e "$target" && ! -d "$target" ]]; then + fail "$target must be a directory" + fi + mkdir -p -- "$target" + chown 1000:1000 -- "$target" + chmod 700 -- "$target" +done + +SECRET_FILE="${APP_DATA_DIR_ABS}/.directus-secret" +[[ ! -L "$SECRET_FILE" ]] || fail "$SECRET_FILE must not be a symbolic link" + +if [[ -e "$SECRET_FILE" ]]; then + [[ -f "$SECRET_FILE" ]] || fail "$SECRET_FILE must be a regular file" + DIRECTUS_SECRET_FINAL="$(<"$SECRET_FILE")" + secret_is_acceptable "$DIRECTUS_SECRET_FINAL" || fail "$SECRET_FILE contains an invalid secret" +else + if [[ ${DIRECTUS_SECRET+x} ]]; then + DIRECTUS_SECRET_CANDIDATE="$DIRECTUS_SECRET" + else + DIRECTUS_SECRET_CANDIDATE="$(read_env_value DIRECTUS_SECRET)" + fi + if secret_is_acceptable "$DIRECTUS_SECRET_CANDIDATE"; then + DIRECTUS_SECRET_FINAL="$DIRECTUS_SECRET_CANDIDATE" + else + DIRECTUS_SECRET_FINAL="$(generate_secret)" + fi + persist_secret "$DIRECTUS_SECRET_FINAL" +fi + +chmod 600 -- "$SECRET_FILE" +write_env_value DIRECTUS_SECRET "$DIRECTUS_SECRET_FINAL" diff --git a/apps/directus/latest/scripts/uninstall.sh b/apps/directus/latest/scripts/uninstall.sh new file mode 100755 index 000000000..5a3f6bd6b --- /dev/null +++ b/apps/directus/latest/scripts/uninstall.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail + +if command -v docker-compose >/dev/null 2>&1; then + docker-compose down --volumes --remove-orphans +else + docker compose down --volumes --remove-orphans +fi diff --git a/apps/directus/latest/scripts/upgrade.sh b/apps/directus/latest/scripts/upgrade.sh new file mode 100755 index 000000000..593b31667 --- /dev/null +++ b/apps/directus/latest/scripts/upgrade.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)" +exec bash "${SCRIPT_DIR}/init.sh" diff --git a/apps/directus/logo.png b/apps/directus/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..3737031f357ec6d53389c20ac24eae527156b4f0 GIT binary patch literal 2489 zcma*p`9Bkk0|)TAO>UJDN{-cX#qzjv@5v@|J<7479CPNLIp&OtjL6jrJ*=EV?mJ?+ zZ$?9oImS@V7{1T*`u+pY51-HH_4yCpFU(Bu@NkK80RR9VxRIg7KU?*0IR7aCh+wk` z001N5hI&@P({y~Oj}S?m)Vf;rC-@Rf8vCnKGFsOy!7_C|g*mQ{pQ|)ai$|K<1p(F_ zN!A{DE$PM5!7Yx08)WIH>KZT?CtCUO7nZ^vwTW6ei34P>^}J%?hF&to0Jxz4;?oxEg)rloC7^N|BW3>NeBT)h)` zPl$0U$9@pWJFy{v^7kuq=YE4_AQ%f5+%i!V9&2Pvu-SsUPmpCd7l~=KZJ9XZ>|dv{ zPb)mE664Mw=1ITv`%EO1pdMlsSn6(N?}~pM_fT4?P1hh5+`cE>r9awjht;OcdITYG z3Ezo4xQT#QUJC(1E$|SZAqLnce=7D+1djCV{Rl8}ymx$$my(i$bQ%R+ z*-NuaETY(#;p-?DDBV<35K^KJOi$&$w^GPqB4LNT%uv2crG`yuGDVJfx@SO`%=TLw z%9wWqS&Ro&iN9661{A~ga;4COC7p;DpR5)4D}>%-DYNtW#cBtOevnZC%$J;wE}g$p z2O?eIB+E$r<-&(|!o-Z;X#}Cd5{%6tJ2Tg%#Q5+M9Y37(eC&mQ;Du!q4WgXE)Qcg;5&47)p2lEK@a*LR}EUltFCF9ITjTtu}-0m6Z-|Mvx+k zoHMC!omraW$KU15S{qf1N%v<)P1`-1tHUg=+hq_ZDId;%S-CZGi$|r;EQ3_;JbN2a zsQWU(ziJ?gO1*$AZCTdKoyN&Hxrq|UyLm);>4Ib`trW-H+28^Ve~~ek$lhg zz{ftIxwix(XOI{r*>@6pKDGpCZc*TG&BrLj!F`rDZZ}p;>@} zDW}W3eT-o=m--5xAMXD6RamE8c9m~`(A4(T$O1)V#x^>Ea0Y>4WMBjCyltmT(IsZW z*9FbWX=c~-XodFI#J~$xH;n;5j?gbAM_jES`flfQ?T>t7z|yZ<{5%e6 z5Wk0R10Y*RdBi9lof`Nof4&?9MB)^pOm9JrmC|&gP)9sNTTDpL1PAL6Hc+vb~T3*dT#j|}zg4}X1L+vyc8-+J_Np_vDNlz0L4W@uTC?w=h0Vh{^Q35Msu+*k1;>L8*P30km$W?$ zyP{g|F_fi(8&v6lbnXN=EsJcIp$EC6)3EAi;dMzDs;9D<$>=`~XGeS~-fZ6u(Ptv3 zvdeuN6UL@xzg!G`nVVNW&TK+rIHEX{rZ-sp5_o+xEoBF=8euSVX~1j~PP+tH&tAlo zE%OfH{>Lo=pQdM;3@L%GIxXjd_icqnfF9up0R$WxjO%jFdI^-h(CT2@SlBqX`}T^! z`DL|G8QM-OZa^Y}`a0xlAyc-ekrmpB=P6@<6X|6=xQ3*j2nMbNjYYCG=>`Vs!;s{K$;fNAhB& zHuZ~^e6z22jk5{wt5^}qw%(WxVkZn@rzEnagTbud-7kq~IV1$7%7v-c2?KeW11x`l z?d@zWu$%UueA98wY}as1dDZpa*wPgW_9$H#&t61*{SFt<4O?dcpB^Z@Na`{?({`_*<*jS4-c07=#PkN@=j?xtN zcP~+Ze{S}A(=)t6E9q)7)XR)Oe{CjmR(*WfqMrL*CH{GwS07cU5prpFfg^^p59ymZ zNSmuH@d&S}4Fp{^C~Mu{cfr?)oTi&_DmH2w^J*rltTzrGMKD6{-M`5~YPub2n0ItC zzGp#xP4KWjdgViU9IfDy_?CmW)wf-|1)8}QF>`^!f7_;*p<$Jb z9PP{@{q?RLDC@S+jct_YPr5EdWAY5-m}AOY zB2lu%6yZI&{cFPLnBuPs7t;3kF(_U+H2HdyV@Ki<)veFdOj3`9uFIWR zH+8eFloiwq9A4jcP{2*E6#Qg_-|uJT