mirror of
https://github.com/arch3rPro/1Panel-Appstore.git
synced 2026-06-11 00:59:40 +08:00
1073 lines
27 KiB
Bash
Executable File
1073 lines
27 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
|
||
# =============================================================================
|
||
# 1Panel App Builder - 主生成脚本
|
||
# 功能:从 GitHub 项目、docker-compose、docker run 命令生成 1Panel APP 配置
|
||
# 用法:./generate-app.sh <输入源> [输出目录]
|
||
# =============================================================================
|
||
|
||
set -euo pipefail
|
||
|
||
# 颜色定义
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# 默认配置
|
||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
TEMPLATE_DIR="${SCRIPT_DIR}/../templates"
|
||
OUTPUT_BASE="./apps"
|
||
ICON_MODE="auto"
|
||
ICON_URL=""
|
||
ICON_CACHE_DIR="${SCRIPT_DIR}/../.cache/icons"
|
||
USER_APP_KEY=""
|
||
USER_APP_NAME=""
|
||
USER_VERSION=""
|
||
USER_SERVICE=""
|
||
FORCE=false
|
||
DRY_RUN=false
|
||
CHECK_DEPS_ONLY=false
|
||
POSITIONAL=()
|
||
SERVICE_VOLUMES=()
|
||
SERVICE_ENV=()
|
||
SERVICE_ENV_FILES=()
|
||
DOCKER_RUN_CONTENT=""
|
||
GITHUB_API_BASE_URL="${GITHUB_API_BASE_URL:-https://api.github.com/repos}"
|
||
GITHUB_RAW_BASE_URL="${GITHUB_RAW_BASE_URL:-https://raw.githubusercontent.com}"
|
||
|
||
# 打印帮助
|
||
print_help() {
|
||
cat << EOF
|
||
${BLUE}1Panel App Builder${NC} - 快速生成 1Panel 应用配置
|
||
|
||
${YELLOW}用法:${NC}
|
||
$0 [选项] <输入源> [输出目录]
|
||
|
||
${YELLOW}输入源支持:${NC}
|
||
1. GitHub 项目链接
|
||
例如: https://github.com/alist-org/alist
|
||
|
||
2. docker-compose.yml 文件链接
|
||
例如: https://raw.githubusercontent.com/alist-org/alist/master/docker-compose.yml
|
||
|
||
3. docker run 命令
|
||
例如: "docker run -d --name=alist -p 5244:5244 xhofe/alist:v3.45.0"
|
||
|
||
4. 本地文件路径
|
||
例如: ./myapp/docker-compose.yml
|
||
|
||
${YELLOW}输出:${NC}
|
||
在指定目录(默认 ./apps)下生成:
|
||
<app-name>/<version>/
|
||
├── data.yml # 应用元数据
|
||
├── docker-compose.yml # 编排文件
|
||
├── logo.png # 应用图标
|
||
├── README.md # 中文简介
|
||
└── README_en.md # 英文简介
|
||
|
||
${YELLOW}示例:${NC}
|
||
$0 https://github.com/alist-org/alist
|
||
$0 ./my-docker-compose.yml ./my-apps
|
||
$0 "docker run -d --name nginx -p 80:80 nginx:latest"
|
||
$0 --app-key demo-app --name DemoApp --version 1.2.3 --icon-mode skip ./docker-compose.yml
|
||
|
||
${YELLOW}选项:${NC}
|
||
--output <目录> 输出目录(默认: ./apps)
|
||
--app-key <key> 显式指定应用目录名
|
||
--name <名称> 显式指定应用显示名
|
||
--service <服务名> 从 compose 中选择主服务
|
||
--version <版本> 显式指定具体版本目录和镜像 tag
|
||
--icon-mode <模式> auto|required|skip|cache-only
|
||
--icon-url <URL> 使用指定图标 URL
|
||
--force 允许覆盖已有输出目录
|
||
--dry-run 只解析和打印结果,不写文件
|
||
--check-deps 只检查脚本依赖
|
||
EOF
|
||
}
|
||
|
||
# 日志函数
|
||
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
||
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
||
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
||
|
||
normalize_app_key() {
|
||
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--' | tr -cd 'a-z0-9.-'
|
||
}
|
||
|
||
check_dependencies() {
|
||
local missing=()
|
||
local dep
|
||
|
||
for dep in curl jq yq python3; do
|
||
if ! command -v "$dep" >/dev/null 2>&1; then
|
||
missing+=("$dep")
|
||
fi
|
||
done
|
||
|
||
if ((${#missing[@]} > 0)); then
|
||
log_error "缺少依赖: ${missing[*]}"
|
||
log_info "请先安装缺失工具后再运行 generate-app.sh"
|
||
return 1
|
||
fi
|
||
|
||
return 0
|
||
}
|
||
|
||
parse_args() {
|
||
POSITIONAL=()
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--output|-o)
|
||
OUTPUT_BASE="${2:-}"
|
||
shift 2
|
||
;;
|
||
--app-key)
|
||
USER_APP_KEY="${2:-}"
|
||
shift 2
|
||
;;
|
||
--name)
|
||
USER_APP_NAME="${2:-}"
|
||
shift 2
|
||
;;
|
||
--service)
|
||
USER_SERVICE="${2:-}"
|
||
shift 2
|
||
;;
|
||
--version)
|
||
USER_VERSION="${2:-}"
|
||
shift 2
|
||
;;
|
||
--icon-mode)
|
||
ICON_MODE="${2:-}"
|
||
shift 2
|
||
;;
|
||
--icon-url)
|
||
ICON_URL="${2:-}"
|
||
shift 2
|
||
;;
|
||
--force)
|
||
FORCE=true
|
||
shift
|
||
;;
|
||
--dry-run)
|
||
DRY_RUN=true
|
||
shift
|
||
;;
|
||
--check-deps)
|
||
CHECK_DEPS_ONLY=true
|
||
shift
|
||
;;
|
||
-h|--help)
|
||
print_help
|
||
exit 0
|
||
;;
|
||
--)
|
||
shift
|
||
POSITIONAL+=("$@")
|
||
break
|
||
;;
|
||
-*)
|
||
log_error "未知选项: $1"
|
||
exit 2
|
||
;;
|
||
*)
|
||
POSITIONAL+=("$1")
|
||
shift
|
||
;;
|
||
esac
|
||
done
|
||
|
||
case "$ICON_MODE" in
|
||
auto|required|skip|cache-only) ;;
|
||
*)
|
||
log_error "无效图标模式: $ICON_MODE"
|
||
exit 2
|
||
;;
|
||
esac
|
||
|
||
if [[ ${#POSITIONAL[@]} -gt 1 ]]; then
|
||
OUTPUT_BASE="${POSITIONAL[1]}"
|
||
fi
|
||
}
|
||
|
||
# 解析镜像引用(支持 registry/namespace/repo:tag)
|
||
parse_image_ref() {
|
||
local image_ref="$1"
|
||
local image_no_tag tag
|
||
|
||
# 去掉 digest
|
||
image_ref="${image_ref%%@*}"
|
||
|
||
if [[ "$image_ref" =~ ^(.+):([^/:]+)$ ]]; then
|
||
image_no_tag="${BASH_REMATCH[1]}"
|
||
tag="${BASH_REMATCH[2]}"
|
||
else
|
||
image_no_tag="$image_ref"
|
||
tag="latest"
|
||
fi
|
||
|
||
local first_segment="${image_no_tag%%/*}"
|
||
if [[ "$image_no_tag" == */* ]] && ([[ "$first_segment" == *.* ]] || [[ "$first_segment" == *:* ]] || [[ "$first_segment" == "localhost" ]]); then
|
||
REGISTRY="$first_segment"
|
||
IMAGE_PATH="${image_no_tag#*/}"
|
||
else
|
||
REGISTRY="docker.io"
|
||
IMAGE_PATH="$image_no_tag"
|
||
fi
|
||
|
||
if [[ "$IMAGE_PATH" == */* ]]; then
|
||
NAMESPACE="${IMAGE_PATH%%/*}"
|
||
REPO_PATH="${IMAGE_PATH#*/}"
|
||
else
|
||
NAMESPACE="library"
|
||
REPO_PATH="$IMAGE_PATH"
|
||
fi
|
||
|
||
IMAGE_BASE="$image_no_tag"
|
||
TAG="$tag"
|
||
}
|
||
|
||
# 获取 Docker Hub 最新版本号
|
||
get_latest_tag_docker_hub() {
|
||
local namespace="$1"
|
||
local repo_path="$2"
|
||
local url="https://hub.docker.com/v2/repositories/${namespace}/${repo_path}/tags?page_size=100&ordering=last_updated"
|
||
local tags
|
||
tags=$(curl -fsSL --connect-timeout 5 --max-time 20 "$url" | jq -r '.results[].name' 2>/dev/null || true)
|
||
|
||
local semver
|
||
semver=$(echo "$tags" | grep -E '^[vV]?[0-9]+\.[0-9]+(\.[0-9]+)?$' | sort -V | tail -n1 || true)
|
||
if [[ -n "$semver" ]]; then
|
||
echo "$semver"
|
||
return 0
|
||
fi
|
||
|
||
local nonlatest
|
||
nonlatest=$(echo "$tags" | grep -v '^latest$' | head -n1 || true)
|
||
echo "${nonlatest:-latest}"
|
||
}
|
||
|
||
# 获取 GHCR 最新版本号
|
||
get_latest_tag_ghcr() {
|
||
local namespace="$1"
|
||
local repo_path="$2"
|
||
local token tags
|
||
|
||
token=$(curl -fsSL --connect-timeout 5 --max-time 20 "https://ghcr.io/token?service=ghcr.io&scope=repository:${namespace}/${repo_path}:pull" | jq -r '.token' 2>/dev/null || true)
|
||
if [[ -z "$token" || "$token" == "null" ]]; then
|
||
echo "latest"
|
||
return 0
|
||
fi
|
||
|
||
tags=$(curl -fsSL --connect-timeout 5 --max-time 20 -H "Authorization: Bearer ${token}" "https://ghcr.io/v2/${namespace}/${repo_path}/tags/list" | jq -r '.tags[]' 2>/dev/null || true)
|
||
|
||
local semver
|
||
semver=$(echo "$tags" | grep -E '^[vV]?[0-9]+\.[0-9]+(\.[0-9]+)?$' | sort -V | tail -n1 || true)
|
||
if [[ -n "$semver" ]]; then
|
||
echo "$semver"
|
||
return 0
|
||
fi
|
||
|
||
local nonlatest
|
||
nonlatest=$(echo "$tags" | grep -v '^latest$' | sort -V | tail -n1 || true)
|
||
echo "${nonlatest:-latest}"
|
||
}
|
||
|
||
# 统一获取最新版本号
|
||
get_latest_tag() {
|
||
if [[ "$REGISTRY" == "ghcr.io" ]]; then
|
||
get_latest_tag_ghcr "$NAMESPACE" "$REPO_PATH"
|
||
return 0
|
||
fi
|
||
|
||
# 默认走 Docker Hub
|
||
get_latest_tag_docker_hub "$NAMESPACE" "$REPO_PATH"
|
||
}
|
||
|
||
# 解析端口映射条目为 host:container
|
||
parse_port_entry() {
|
||
local entry="$1"
|
||
entry="${entry%\"}"
|
||
entry="${entry#\"}"
|
||
if [[ "$entry" == */* ]]; then
|
||
entry="${entry%/*}"
|
||
fi
|
||
IFS=':' read -r -a parts <<< "$entry"
|
||
local host_port container_port
|
||
if [[ ${#parts[@]} -eq 1 ]]; then
|
||
host_port="${parts[0]}"
|
||
container_port="${parts[0]}"
|
||
elif [[ ${#parts[@]} -eq 2 ]]; then
|
||
host_port="${parts[0]}"
|
||
container_port="${parts[1]}"
|
||
else
|
||
host_port="${parts[-2]}"
|
||
container_port="${parts[-1]}"
|
||
fi
|
||
echo "${host_port}:${container_port}"
|
||
}
|
||
|
||
# 根据端口号选择 1Panel 常用 envKey
|
||
map_port_envkey() {
|
||
local port="$1"
|
||
local index="$2"
|
||
case "$port" in
|
||
80|8080|3000|5173|8000|5000)
|
||
echo "PANEL_APP_PORT_HTTP"
|
||
;;
|
||
443|8443)
|
||
echo "PANEL_APP_PORT_HTTPS"
|
||
;;
|
||
22)
|
||
echo "PANEL_APP_PORT_SSH"
|
||
;;
|
||
3306|5432|27017|6379)
|
||
echo "PANEL_APP_PORT_DB"
|
||
;;
|
||
9000|9001|8081|7001)
|
||
echo "PANEL_APP_PORT_API"
|
||
;;
|
||
*)
|
||
if [[ "$index" -eq 0 ]]; then
|
||
echo "PANEL_APP_PORT_HTTP"
|
||
else
|
||
echo "PANEL_APP_PORT_API"
|
||
fi
|
||
;;
|
||
esac
|
||
}
|
||
|
||
# envKey 对应标签
|
||
labels_for_envkey() {
|
||
local envkey="$1"
|
||
case "$envkey" in
|
||
PANEL_APP_PORT_HTTP) echo "Web Port|Web端口" ;;
|
||
PANEL_APP_PORT_HTTPS) echo "HTTPS Port|HTTPS端口" ;;
|
||
PANEL_APP_PORT_API) echo "API Port|API端口" ;;
|
||
PANEL_APP_PORT_ADMIN) echo "Admin Port|管理端口" ;;
|
||
PANEL_APP_PORT_PROXY) echo "Proxy Port|代理端口" ;;
|
||
PANEL_APP_PORT_DB) echo "DB Port|数据库端口" ;;
|
||
PANEL_APP_PORT_SSH) echo "SSH Port|SSH端口" ;;
|
||
PANEL_APP_PORT_S3) echo "S3 Port|S3端口" ;;
|
||
PANEL_APP_PORT_PROXY_HTTP) echo "Proxy HTTP Port|代理HTTP端口" ;;
|
||
PANEL_APP_PORT_PROXY_HTTPS) echo "Proxy HTTPS Port|代理HTTPS端口" ;;
|
||
PANEL_APP_PORT_SYNC) echo "Sync Port|同步端口" ;;
|
||
*) echo "Port|端口" ;;
|
||
esac
|
||
}
|
||
# 检测输入类型
|
||
detect_input_type() {
|
||
local input="$1"
|
||
|
||
# GitHub 项目链接
|
||
if [[ "$input" =~ ^https?://github\.com/[^/]+/[^/]+ ]]; then
|
||
echo "github"
|
||
return 0
|
||
fi
|
||
|
||
# docker-compose 文件链接
|
||
if [[ "$input" =~ ^https?://.*\.ya?ml$ ]] || [[ "$input" =~ docker-compose ]]; then
|
||
echo "compose_url"
|
||
return 0
|
||
fi
|
||
|
||
# docker run 命令
|
||
if [[ "$input" =~ ^docker\ run ]] || [[ "$input" =~ ^docker\ run$ ]]; then
|
||
echo "docker_run"
|
||
return 0
|
||
fi
|
||
|
||
# 本地文件
|
||
if [[ -f "$input" ]]; then
|
||
if [[ "$input" =~ \.ya?ml$ ]]; then
|
||
echo "compose_file"
|
||
return 0
|
||
fi
|
||
fi
|
||
|
||
echo "unknown"
|
||
}
|
||
|
||
# 从 GitHub 项目提取信息
|
||
extract_from_github() {
|
||
local github_url="$1"
|
||
local api_url
|
||
|
||
log_info "正在从 GitHub 项目提取信息: $github_url"
|
||
|
||
local owner_repo
|
||
owner_repo=$(echo "$github_url" | sed -E 's|https?://github.com/||; s|/$||; s|\.git$||')
|
||
api_url="${GITHUB_API_BASE_URL%/}/${owner_repo}"
|
||
|
||
# 获取项目信息
|
||
local repo_info
|
||
repo_info=$(curl -fsSL --connect-timeout 5 --max-time 20 "$api_url" 2>/dev/null || echo "{}")
|
||
|
||
# 提取字段
|
||
APP_NAME=$(echo "$repo_info" | jq -r '.name // empty')
|
||
APP_KEY=$(normalize_app_key "$APP_NAME")
|
||
DESCRIPTION=$(echo "$repo_info" | jq -r '.description // empty')
|
||
GITHUB="$github_url"
|
||
WEBSITE=$(echo "$repo_info" | jq -r '.homepage // empty')
|
||
if [[ -z "$WEBSITE" ]]; then
|
||
WEBSITE="$github_url"
|
||
fi
|
||
DEFAULT_BRANCH=$(echo "$repo_info" | jq -r '.default_branch // "main"')
|
||
|
||
COMPOSE_CONTENT=$(discover_github_compose "$owner_repo" "$DEFAULT_BRANCH")
|
||
if [[ -z "${COMPOSE_CONTENT:-}" ]]; then
|
||
DOCKER_RUN_CONTENT=$(discover_github_docker_run "$owner_repo" "$DEFAULT_BRANCH")
|
||
fi
|
||
if [[ -z "${COMPOSE_CONTENT:-}" && -z "${DOCKER_RUN_CONTENT:-}" ]]; then
|
||
log_error "未在 GitHub 项目中发现常见 docker-compose 文件或 README docker run 命令"
|
||
exit 1
|
||
fi
|
||
|
||
log_info "应用名称: $APP_NAME"
|
||
log_info "描述: $DESCRIPTION"
|
||
}
|
||
|
||
discover_github_compose() {
|
||
local owner_repo="$1"
|
||
local default_branch="$2"
|
||
local branches=("$default_branch")
|
||
local branch path url content
|
||
local paths=(
|
||
"docker-compose.yml"
|
||
"docker-compose.yaml"
|
||
"compose.yml"
|
||
"compose.yaml"
|
||
"deploy/docker-compose.yml"
|
||
"deploy/docker-compose.yaml"
|
||
"docker/docker-compose.yml"
|
||
"docker/docker-compose.yaml"
|
||
)
|
||
|
||
if [[ "$default_branch" != "main" ]]; then
|
||
branches+=("main")
|
||
fi
|
||
if [[ "$default_branch" != "master" ]]; then
|
||
branches+=("master")
|
||
fi
|
||
|
||
for branch in "${branches[@]}"; do
|
||
for path in "${paths[@]}"; do
|
||
url="${GITHUB_RAW_BASE_URL%/}/${owner_repo}/${branch}/${path}"
|
||
content=$(curl -fsSL --connect-timeout 5 --max-time 20 "$url" 2>/dev/null || true)
|
||
if [[ -n "$content" ]] && echo "$content" | yq -e '.services' >/dev/null 2>&1; then
|
||
log_info "发现 compose 文件: ${path} (${branch})" >&2
|
||
printf '%s\n' "$content"
|
||
return 0
|
||
fi
|
||
done
|
||
done
|
||
|
||
return 0
|
||
}
|
||
|
||
discover_github_docker_run() {
|
||
local owner_repo="$1"
|
||
local default_branch="$2"
|
||
local branches=("$default_branch")
|
||
local branch path url content line
|
||
local paths=(
|
||
"README.md"
|
||
"README_en.md"
|
||
"docs/README.md"
|
||
)
|
||
|
||
if [[ "$default_branch" != "main" ]]; then
|
||
branches+=("main")
|
||
fi
|
||
if [[ "$default_branch" != "master" ]]; then
|
||
branches+=("master")
|
||
fi
|
||
|
||
for branch in "${branches[@]}"; do
|
||
for path in "${paths[@]}"; do
|
||
url="${GITHUB_RAW_BASE_URL%/}/${owner_repo}/${branch}/${path}"
|
||
content=$(curl -fsSL --connect-timeout 5 --max-time 20 "$url" 2>/dev/null || true)
|
||
if [[ -z "$content" ]]; then
|
||
continue
|
||
fi
|
||
line=$(printf '%s\n' "$content" | sed -n 's/^[[:space:]]*//; /docker run /{s/^`*//; s/`*$//; p; q;}')
|
||
if [[ -n "$line" ]]; then
|
||
log_info "发现 README docker run 命令: ${path} (${branch})" >&2
|
||
printf '%s\n' "$line"
|
||
return 0
|
||
fi
|
||
done
|
||
done
|
||
|
||
return 0
|
||
}
|
||
|
||
# 从 docker-compose 提取信息
|
||
extract_from_compose() {
|
||
local compose_content="$1"
|
||
|
||
log_info "正在解析 docker-compose 内容"
|
||
|
||
# 提取服务名
|
||
if [[ -n "$USER_SERVICE" ]]; then
|
||
SERVICE_NAME="$USER_SERVICE"
|
||
if ! echo "$compose_content" | yq -e ".services[\"${SERVICE_NAME}\"]" >/dev/null 2>&1; then
|
||
log_error "compose 中不存在指定服务: $SERVICE_NAME"
|
||
exit 1
|
||
fi
|
||
else
|
||
SERVICE_NAME=$(echo "$compose_content" | yq -r '.services | keys | .[0]' 2>/dev/null || echo "")
|
||
fi
|
||
|
||
# 提取镜像
|
||
local image
|
||
image=$(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].image" 2>/dev/null || echo "")
|
||
if [[ -z "$image" || "$image" == "null" ]]; then
|
||
log_error "服务 ${SERVICE_NAME} 缺少 image,无法生成应用版本"
|
||
exit 1
|
||
fi
|
||
parse_image_ref "$image"
|
||
|
||
# 提取端口
|
||
PORT_ENTRIES=()
|
||
while IFS= read -r line; do
|
||
[[ -z "$line" || "$line" == "null" ]] && continue
|
||
PORT_ENTRIES+=("$line")
|
||
done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].ports[]" 2>/dev/null || true)
|
||
|
||
if [[ ${#PORT_ENTRIES[@]} -eq 0 ]]; then
|
||
PORT_ENTRIES=("8080")
|
||
fi
|
||
|
||
SERVICE_VOLUMES=()
|
||
while IFS= read -r line; do
|
||
[[ -z "$line" || "$line" == "null" ]] && continue
|
||
SERVICE_VOLUMES+=("$line")
|
||
done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].volumes[]?" 2>/dev/null || true)
|
||
|
||
SERVICE_ENV=()
|
||
while IFS= read -r line; do
|
||
[[ -z "$line" || "$line" == "null" ]] && continue
|
||
SERVICE_ENV+=("$line")
|
||
done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].environment[]?" 2>/dev/null || true)
|
||
|
||
SERVICE_ENV_FILES=()
|
||
while IFS= read -r line; do
|
||
[[ -z "$line" || "$line" == "null" ]] && continue
|
||
SERVICE_ENV_FILES+=("$line")
|
||
done < <(echo "$compose_content" | yq -r ".services[\"${SERVICE_NAME}\"].env_file[]?" 2>/dev/null || true)
|
||
|
||
log_info "服务名: $SERVICE_NAME"
|
||
log_info "镜像: $IMAGE_BASE:$TAG"
|
||
log_info "端口数量: ${#PORT_ENTRIES[@]}"
|
||
}
|
||
|
||
# 从 docker run 命令提取信息
|
||
extract_from_docker_run() {
|
||
local cmd="$1"
|
||
|
||
log_info "正在解析 docker run 命令"
|
||
|
||
local args=()
|
||
while IFS= read -r line; do
|
||
args+=("$line")
|
||
done < <(python3 - "$cmd" <<'PY'
|
||
import shlex
|
||
import sys
|
||
|
||
for item in shlex.split(sys.argv[1]):
|
||
print(item)
|
||
PY
|
||
)
|
||
|
||
local i=0
|
||
if [[ "${args[0]:-}" == "docker" && "${args[1]:-}" == "run" ]]; then
|
||
i=2
|
||
fi
|
||
|
||
SERVICE_NAME="app"
|
||
PORT_ENTRIES=()
|
||
SERVICE_VOLUMES=()
|
||
SERVICE_ENV=()
|
||
SERVICE_ENV_FILES=()
|
||
|
||
local image_ref=""
|
||
while ((i < ${#args[@]})); do
|
||
local arg="${args[$i]}"
|
||
case "$arg" in
|
||
--name=*)
|
||
SERVICE_NAME="${arg#--name=}"
|
||
;;
|
||
--name)
|
||
((i+=1))
|
||
SERVICE_NAME="${args[$i]:-app}"
|
||
;;
|
||
-p|--publish)
|
||
((i+=1))
|
||
PORT_ENTRIES+=("${args[$i]:-}")
|
||
;;
|
||
-p=*|--publish=*)
|
||
PORT_ENTRIES+=("${arg#*=}")
|
||
;;
|
||
-v|--volume)
|
||
((i+=1))
|
||
SERVICE_VOLUMES+=("${args[$i]:-}")
|
||
;;
|
||
-v=*|--volume=*)
|
||
SERVICE_VOLUMES+=("${arg#*=}")
|
||
;;
|
||
-e|--env)
|
||
((i+=1))
|
||
SERVICE_ENV+=("${args[$i]:-}")
|
||
;;
|
||
-e=*|--env=*)
|
||
SERVICE_ENV+=("${arg#*=}")
|
||
;;
|
||
--env-file)
|
||
((i+=1))
|
||
SERVICE_ENV_FILES+=("${args[$i]:-}")
|
||
;;
|
||
--env-file=*)
|
||
SERVICE_ENV_FILES+=("${arg#--env-file=}")
|
||
;;
|
||
--)
|
||
((i+=1))
|
||
image_ref="${args[$i]:-}"
|
||
break
|
||
;;
|
||
-d|--detach|--rm|--init|-i|-t|-it|-ti|--privileged)
|
||
;;
|
||
--restart|--network|--hostname|--user|--workdir|--entrypoint|--add-host|--dns)
|
||
((i+=1))
|
||
;;
|
||
--restart=*|--network=*|--hostname=*|--user=*|--workdir=*|--entrypoint=*|--add-host=*|--dns=*)
|
||
;;
|
||
-*)
|
||
if [[ "$arg" != *=* && "${args[$((i + 1))]:-}" != -* && "${args[$((i + 1))]:-}" != "" ]]; then
|
||
((i+=1))
|
||
fi
|
||
;;
|
||
*)
|
||
image_ref="$arg"
|
||
break
|
||
;;
|
||
esac
|
||
((i+=1))
|
||
done
|
||
|
||
if [[ -z "$image_ref" ]]; then
|
||
log_error "未能从 docker run 命令中解析出镜像"
|
||
exit 1
|
||
fi
|
||
|
||
parse_image_ref "$image_ref"
|
||
|
||
if [[ ${#PORT_ENTRIES[@]} -eq 0 ]]; then
|
||
PORT_ENTRIES=("8080")
|
||
fi
|
||
|
||
APP_NAME="$SERVICE_NAME"
|
||
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||
|
||
log_info "应用名称: $APP_NAME"
|
||
log_info "镜像: $IMAGE_BASE:$TAG"
|
||
log_info "端口数量: ${#PORT_ENTRIES[@]}"
|
||
}
|
||
|
||
# 生成 data.yml
|
||
generate_data_yml() {
|
||
local output_file="$1"
|
||
|
||
log_info "生成 data.yml: $output_file"
|
||
|
||
cat > "$output_file" << EOF
|
||
name: ${APP_NAME:-MyApp}
|
||
tags:
|
||
- 实用工具
|
||
- 容器
|
||
title: ${APP_NAME:-MyApp} - 容器应用
|
||
description: ${DESCRIPTION:-自动生成的应用配置}
|
||
additionalProperties:
|
||
key: ${APP_KEY:-myapp}
|
||
name: ${APP_NAME:-MyApp}
|
||
tags:
|
||
- Tool
|
||
- Container
|
||
shortDescZh: ${DESCRIPTION:-自动生成的应用配置}
|
||
shortDescEn: ${DESCRIPTION:-Auto-generated application configuration}
|
||
description:
|
||
en: ${DESCRIPTION:-Auto-generated application configuration}
|
||
ja: ${DESCRIPTION:-Auto-generated application configuration}
|
||
ms: ${DESCRIPTION:-Auto-generated application configuration}
|
||
pt-br: ${DESCRIPTION:-Auto-generated application configuration}
|
||
ru: ${DESCRIPTION:-Auto-generated application configuration}
|
||
ko: ${DESCRIPTION:-Auto-generated application configuration}
|
||
zh-Hant: ${DESCRIPTION:-Auto-generated application configuration}
|
||
zh: ${DESCRIPTION:-自动生成的应用配置}
|
||
type: website
|
||
crossVersionUpdate: true
|
||
limit: 0
|
||
recommend: 50
|
||
website: ${WEBSITE:-https://example.com}
|
||
github: ${GITHUB:-https://github.com}
|
||
document: ${GITHUB:-https://github.com}
|
||
architectures:
|
||
- amd64
|
||
- arm64
|
||
EOF
|
||
}
|
||
|
||
# 生成 version/data.yml(参数定义)
|
||
generate_version_data_yml() {
|
||
local output_file="$1"
|
||
|
||
log_info "生成版本 data.yml: $output_file"
|
||
|
||
cat > "$output_file" << EOF
|
||
additionalProperties:
|
||
formFields:
|
||
EOF
|
||
|
||
local i
|
||
for ((i=0; i<${#CONTAINER_PORTS[@]}; i++)); do
|
||
local envkey="${PORT_ENV_KEYS[$i]}"
|
||
local default_port="${HOST_PORTS[$i]}"
|
||
local labels
|
||
labels=$(labels_for_envkey "$envkey")
|
||
local label_en="${labels%%|*}"
|
||
local label_zh="${labels##*|}"
|
||
cat >> "$output_file" << EOF
|
||
- default: ${default_port}
|
||
edit: true
|
||
envKey: ${envkey}
|
||
labelEn: ${label_en}
|
||
labelZh: ${label_zh}
|
||
required: true
|
||
rule: paramPort
|
||
type: number
|
||
label:
|
||
en: ${label_en}
|
||
zh: ${label_zh}
|
||
EOF
|
||
done
|
||
}
|
||
|
||
# 生成 docker-compose.yml
|
||
generate_docker_compose() {
|
||
local output_file="$1"
|
||
local image_tag="$2"
|
||
|
||
log_info "生成 docker-compose.yml: $output_file"
|
||
|
||
cat > "$output_file" << EOF
|
||
services:
|
||
${SERVICE_NAME:-app}:
|
||
container_name: \${CONTAINER_NAME}
|
||
restart: always
|
||
networks:
|
||
- 1panel-network
|
||
ports:
|
||
EOF
|
||
|
||
local i
|
||
for ((i=0; i<${#CONTAINER_PORTS[@]}; i++)); do
|
||
cat >> "$output_file" << EOF
|
||
- "\${${PORT_ENV_KEYS[$i]}}:${CONTAINER_PORTS[$i]}"
|
||
EOF
|
||
done
|
||
|
||
cat >> "$output_file" << EOF
|
||
volumes:
|
||
EOF
|
||
|
||
if ((${#SERVICE_VOLUMES[@]} > 0)); then
|
||
local volume
|
||
for volume in "${SERVICE_VOLUMES[@]}"; do
|
||
cat >> "$output_file" << EOF
|
||
- ${volume}
|
||
EOF
|
||
done
|
||
else
|
||
cat >> "$output_file" << EOF
|
||
- ./data/data:/app/data
|
||
EOF
|
||
fi
|
||
|
||
cat >> "$output_file" << EOF
|
||
environment:
|
||
EOF
|
||
|
||
if ((${#SERVICE_ENV[@]} > 0)); then
|
||
local env
|
||
for env in "${SERVICE_ENV[@]}"; do
|
||
cat >> "$output_file" << EOF
|
||
- ${env}
|
||
EOF
|
||
done
|
||
else
|
||
cat >> "$output_file" << EOF
|
||
- PUID=0
|
||
- PGID=0
|
||
- UMASK=022
|
||
EOF
|
||
fi
|
||
|
||
if ((${#SERVICE_ENV_FILES[@]} > 0)); then
|
||
cat >> "$output_file" << EOF
|
||
env_file:
|
||
EOF
|
||
local env_file
|
||
for env_file in "${SERVICE_ENV_FILES[@]}"; do
|
||
cat >> "$output_file" << EOF
|
||
- ${env_file}
|
||
EOF
|
||
done
|
||
fi
|
||
|
||
cat >> "$output_file" << EOF
|
||
image: ${IMAGE_BASE:-app}:${image_tag}
|
||
labels:
|
||
createdBy: "Apps"
|
||
networks:
|
||
1panel-network:
|
||
external: true
|
||
EOF
|
||
}
|
||
|
||
# 生成 README
|
||
generate_readme() {
|
||
local output_dir="$1"
|
||
|
||
log_info "生成 README 文件"
|
||
|
||
cat > "${output_dir}/README.md" << EOF
|
||
# ${APP_NAME:-MyApp}
|
||
|
||
## 简介
|
||
|
||
${DESCRIPTION:-这是一个自动生成的应用配置。}
|
||
|
||
## 使用说明
|
||
|
||
1. 在 1Panel 应用商店中安装此应用
|
||
2. 配置相关参数
|
||
3. 启动应用
|
||
|
||
## 更多信息
|
||
|
||
- 官网: ${WEBSITE:-https://example.com}
|
||
- GitHub: ${GITHUB:-https://github.com}
|
||
- 文档: ${GITHUB:-https://github.com}
|
||
EOF
|
||
|
||
cat > "${output_dir}/README_en.md" << EOF
|
||
# ${APP_NAME:-MyApp}
|
||
|
||
## Introduction
|
||
|
||
${DESCRIPTION:-This is an auto-generated application configuration.}
|
||
|
||
## Usage
|
||
|
||
1. Install this app from 1Panel App Store
|
||
2. Configure parameters
|
||
3. Start the application
|
||
|
||
## More Information
|
||
|
||
- Website: ${WEBSITE:-https://example.com}
|
||
- GitHub: ${GITHUB:-https://github.com}
|
||
- Documentation: ${GITHUB:-https://github.com}
|
||
EOF
|
||
}
|
||
|
||
# 下载图标
|
||
download_icon() {
|
||
local app_name="$1"
|
||
local output_file="$2"
|
||
local args=(--mode "$ICON_MODE" --cache-dir "$ICON_CACHE_DIR")
|
||
|
||
if [[ -n "$ICON_URL" ]]; then
|
||
args+=(--url "$ICON_URL")
|
||
fi
|
||
|
||
if bash "${SCRIPT_DIR}/download-icon.sh" "${args[@]}" "$app_name" "$output_file"; then
|
||
return 0
|
||
fi
|
||
|
||
if [[ "$ICON_MODE" == "required" ]]; then
|
||
log_error "图标为 required 模式,但未能获取图标"
|
||
return 1
|
||
fi
|
||
|
||
log_warn "未找到图标,请手动补充 logo.png"
|
||
return 0
|
||
}
|
||
|
||
# 主函数
|
||
main() {
|
||
parse_args "$@"
|
||
|
||
if [[ "$CHECK_DEPS_ONLY" == "true" ]]; then
|
||
check_dependencies
|
||
exit $?
|
||
fi
|
||
check_dependencies
|
||
|
||
local input="${POSITIONAL[0]:-}"
|
||
local input_type
|
||
|
||
# 参数检查
|
||
if [[ -z "$input" ]] || [[ "$input" == "-h" ]] || [[ "$input" == "--help" ]]; then
|
||
print_help
|
||
exit 0
|
||
fi
|
||
|
||
log_info "开始处理: $input"
|
||
|
||
# 检测输入类型
|
||
input_type=$(detect_input_type "$input")
|
||
log_info "检测到输入类型: $input_type"
|
||
|
||
# 根据类型提取信息
|
||
case "$input_type" in
|
||
github)
|
||
extract_from_github "$input"
|
||
if [[ -n "$COMPOSE_CONTENT" ]]; then
|
||
extract_from_compose "$COMPOSE_CONTENT"
|
||
elif [[ -n "$DOCKER_RUN_CONTENT" ]]; then
|
||
extract_from_docker_run "$DOCKER_RUN_CONTENT"
|
||
fi
|
||
;;
|
||
compose_url)
|
||
COMPOSE_CONTENT=$(curl -fsSL --connect-timeout 5 --max-time 20 "$input" 2>/dev/null)
|
||
extract_from_compose "$COMPOSE_CONTENT"
|
||
APP_NAME="$SERVICE_NAME"
|
||
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||
;;
|
||
compose_file)
|
||
COMPOSE_CONTENT=$(cat "$input")
|
||
extract_from_compose "$COMPOSE_CONTENT"
|
||
APP_NAME="$SERVICE_NAME"
|
||
APP_KEY=$(normalize_app_key "$SERVICE_NAME")
|
||
;;
|
||
docker_run)
|
||
extract_from_docker_run "$input"
|
||
;;
|
||
*)
|
||
log_error "无法识别的输入类型: $input"
|
||
exit 1
|
||
;;
|
||
esac
|
||
|
||
if [[ -n "$USER_APP_NAME" ]]; then
|
||
APP_NAME="$USER_APP_NAME"
|
||
fi
|
||
if [[ -n "$USER_APP_KEY" ]]; then
|
||
APP_KEY=$(normalize_app_key "$USER_APP_KEY")
|
||
fi
|
||
if [[ -z "${APP_KEY:-}" ]]; then
|
||
APP_KEY=$(normalize_app_key "${APP_NAME:-app}")
|
||
fi
|
||
|
||
# 生成端口映射数组
|
||
HOST_PORTS=()
|
||
CONTAINER_PORTS=()
|
||
PORT_ENV_KEYS=()
|
||
PORT_ENV_KEYS_USED=()
|
||
local i
|
||
for ((i=0; i<${#PORT_ENTRIES[@]}; i++)); do
|
||
local mapping
|
||
mapping=$(parse_port_entry "${PORT_ENTRIES[$i]}")
|
||
local host_port="${mapping%%:*}"
|
||
local container_port="${mapping##*:}"
|
||
HOST_PORTS+=("$host_port")
|
||
CONTAINER_PORTS+=("$container_port")
|
||
local envkey
|
||
envkey=$(map_port_envkey "$container_port" "$i")
|
||
# 保证 envKey 唯一
|
||
if [[ " ${PORT_ENV_KEYS_USED[*]} " == *" ${envkey} "* ]]; then
|
||
envkey="${envkey}_${i}"
|
||
fi
|
||
PORT_ENV_KEYS+=("$envkey")
|
||
PORT_ENV_KEYS_USED+=("$envkey")
|
||
done
|
||
|
||
# 获取最新版本号
|
||
if [[ -n "$USER_VERSION" ]]; then
|
||
VERSION_TAG="$USER_VERSION"
|
||
else
|
||
VERSION_TAG=$(get_latest_tag)
|
||
if [[ -z "$VERSION_TAG" ]]; then
|
||
VERSION_TAG="$TAG"
|
||
fi
|
||
fi
|
||
if [[ -z "$VERSION_TAG" ]]; then
|
||
VERSION_TAG="latest"
|
||
fi
|
||
|
||
if [[ "$DRY_RUN" == "true" ]]; then
|
||
echo "app_key=${APP_KEY}"
|
||
echo "app_name=${APP_NAME:-}"
|
||
echo "service=${SERVICE_NAME:-}"
|
||
echo "image=${IMAGE_BASE:-}:${VERSION_TAG}"
|
||
echo "output=${OUTPUT_BASE}/${APP_KEY}"
|
||
exit 0
|
||
fi
|
||
|
||
# 创建输出目录(latest + 具体版本)
|
||
local output_dir_latest="${OUTPUT_BASE}/${APP_KEY}/latest"
|
||
if [[ -e "${OUTPUT_BASE}/${APP_KEY}" && "$FORCE" != "true" ]]; then
|
||
log_error "输出目录已存在: ${OUTPUT_BASE}/${APP_KEY}(如需覆盖请使用 --force)"
|
||
exit 1
|
||
fi
|
||
mkdir -p "$output_dir_latest"
|
||
|
||
local output_dir_version=""
|
||
if [[ "$VERSION_TAG" != "latest" ]]; then
|
||
output_dir_version="${OUTPUT_BASE}/${APP_KEY}/${VERSION_TAG}"
|
||
mkdir -p "$output_dir_version"
|
||
else
|
||
log_warn "未能获取具体版本号,version 目录将使用 latest(请手动修正)"
|
||
fi
|
||
|
||
log_info "输出目录: $output_dir_latest"
|
||
if [[ -n "$output_dir_version" ]]; then
|
||
log_info "输出目录: $output_dir_version"
|
||
fi
|
||
|
||
# 生成 latest 版本文件
|
||
generate_version_data_yml "${output_dir_latest}/data.yml"
|
||
generate_docker_compose "${output_dir_latest}/docker-compose.yml" "latest"
|
||
|
||
# 生成具体版本文件
|
||
if [[ -n "$output_dir_version" ]]; then
|
||
generate_version_data_yml "${output_dir_version}/data.yml"
|
||
generate_docker_compose "${output_dir_version}/docker-compose.yml" "$VERSION_TAG"
|
||
fi
|
||
|
||
generate_readme "${OUTPUT_BASE}/${APP_KEY}"
|
||
download_icon "$APP_NAME" "${OUTPUT_BASE}/${APP_KEY}/logo.png"
|
||
|
||
# 生成上级目录的 data.yml
|
||
generate_data_yml "${OUTPUT_BASE}/${APP_KEY}/data.yml"
|
||
# 输出结果
|
||
echo ""
|
||
log_info "${GREEN}✓ 生成完成!${NC}"
|
||
echo ""
|
||
echo "目录结构:"
|
||
tree "${OUTPUT_BASE}/${APP_KEY}" 2>/dev/null || find "${OUTPUT_BASE}/${APP_KEY}" -type f
|
||
echo ""
|
||
log_info "下一步:"
|
||
echo " 1. 检查生成的配置文件"
|
||
echo " 2. 补充完善应用描述和标签"
|
||
echo " 3. 替换 logo.png 为合适的应用图标"
|
||
echo " 4. 测试 docker-compose.yml"
|
||
echo " 5. 提交到应用商店仓库"
|
||
}
|
||
|
||
# 执行主函数
|
||
main "$@"
|