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 000000000..3737031f3 Binary files /dev/null and b/apps/directus/logo.png differ