339 lines
10 KiB
Bash
339 lines
10 KiB
Bash
#!/bin/bash
|
||
#
|
||
# gitea.sh - Gitea 交互式下载器
|
||
#
|
||
# 一个用于通过交互式菜单浏览和下载 Gitea 私有仓库中文件的脚本。
|
||
#
|
||
# 依赖: curl, jq, fzf
|
||
|
||
# --- 配置 ---
|
||
|
||
# 智能判断配置文件路径
|
||
if [[ $EUID -eq 0 ]] && [[ -n "$SUDO_USER" ]]; then
|
||
# 如果是 root 用户通过 sudo 运行,则使用原始用户的 HOME 目录
|
||
# 这确保了 root 可以使用调用它的用户的配置
|
||
USER_HOME=$(getent passwd "$SUDO_USER" | cut -d: -f6)
|
||
CONFIG_FILE="${USER_HOME}/.gitea_cli_config"
|
||
else
|
||
# 否则,使用当前用户的 HOME 目录
|
||
CONFIG_FILE="${HOME}/.gitea_cli_config"
|
||
fi
|
||
|
||
# --- 辅助函数 ---
|
||
|
||
# 带颜色的输出函数
|
||
_print() {
|
||
local color_code="$1"
|
||
shift
|
||
if [ -t 1 ]; then # 检查标准输出是否为终端
|
||
printf "\e[${color_code}m%s\e[0m\n" "$@"
|
||
else
|
||
printf "%s\n" "$@"
|
||
fi
|
||
}
|
||
info() { _print "34" "$@"; } # 蓝色
|
||
success() { _print "32" "$@"; } # 绿色
|
||
error() { >&2 _print "31" "$@"; } # 红色
|
||
warning() { _print "33" "$@"; } # 黄色
|
||
|
||
# 脚本退出时调用的清理函数
|
||
cleanup() {
|
||
# 恢复光标和终端设置
|
||
tput cnorm 2>/dev/null
|
||
stty echo 2>/dev/null
|
||
return
|
||
}
|
||
trap cleanup EXIT
|
||
|
||
# 检查必要的依赖工具
|
||
check_dependencies() {
|
||
local missing_deps=()
|
||
for dep in curl jq fzf; do
|
||
if ! command -v "$dep" &> /dev/null; then
|
||
missing_deps+=("$dep")
|
||
fi
|
||
done
|
||
|
||
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||
error "错误: 必要的依赖工具未安装: ${missing_deps[*]}"
|
||
|
||
# 检查是否为 Debian/Ubuntu 环境以便提供自动安装
|
||
if ! command -v apt-get &> /dev/null; then
|
||
error "未找到 'apt-get' 包管理器。自动安装功能仅支持 Debian/Ubuntu 系统。"
|
||
warning "请为您的系统手动安装以下依赖: ${missing_deps[*]}"
|
||
exit 1
|
||
fi
|
||
|
||
# 检查 sudo 是否可用
|
||
if [[ $EUID -ne 0 ]] && ! command -v sudo &> /dev/null; then
|
||
error "检测到您不是 root 用户, 且 'sudo' 命令不可用。无法进行自动安装。"
|
||
error "请使用 root 用户运行,或安装 'sudo' 后重试。"
|
||
exit 1
|
||
fi
|
||
|
||
read -r -p "是否尝试使用 'sudo apt-get' 自动安装? [Y/n] " choice
|
||
case "$choice" in
|
||
n|N)
|
||
error "用户取消安装。请手动安装依赖后重试。"
|
||
exit 1
|
||
;;
|
||
*)
|
||
info "\n即将开始自动安装依赖..."
|
||
local SUDO_CMD=""
|
||
if [[ $EUID -ne 0 ]]; then
|
||
SUDO_CMD="sudo"
|
||
fi
|
||
|
||
info "步骤 1/2: 更新软件包列表 (apt-get update)..."
|
||
$SUDO_CMD apt-get update || {
|
||
error "\n执行 'apt-get update' 失败。请检查您的软件源配置或网络连接。"
|
||
exit 1
|
||
}
|
||
|
||
info "步骤 2/2: 安装缺失的软件包 (${missing_deps[*]})..."
|
||
$SUDO_CMD apt-get install -y "${missing_deps[@]}" || {
|
||
error "\n依赖包自动安装失败。"
|
||
error "请尝试手动执行: '$SUDO_CMD apt-get install -y ${missing_deps[*]}'"
|
||
exit 1
|
||
}
|
||
|
||
success "\n依赖已成功安装!"
|
||
info "重新验证依赖..."
|
||
# 重新验证确保所有依赖都已就位
|
||
for dep in "${missing_deps[@]}"; do
|
||
if ! command -v "$dep" &> /dev/null; then
|
||
error "严重错误: 依赖 '$dep' 在安装后仍无法找到。"
|
||
error "请手动检查安装过程。"
|
||
exit 1
|
||
fi
|
||
done
|
||
success "所有依赖均已准备就绪!"
|
||
;;
|
||
esac
|
||
fi
|
||
}
|
||
|
||
# --- 配置管理 ---
|
||
|
||
# 从文件加载配置
|
||
load_config() {
|
||
if [ -f "$CONFIG_FILE" ]; then
|
||
# shellcheck source=/dev/null
|
||
source "$CONFIG_FILE"
|
||
# Sanitize variables to remove potential carriage returns from editing on Windows
|
||
GITEA_URL=${GITEA_URL%$'\r'}
|
||
ACCESS_TOKEN=${ACCESS_TOKEN%$'\r'}
|
||
return 0
|
||
else
|
||
return 1
|
||
fi
|
||
}
|
||
|
||
# 提示用户输入配置并保存
|
||
prompt_for_config() {
|
||
info "--- Gitea 交互式下载器配置 ---"
|
||
warning "您的 Gitea 实例信息将被保存在: $CONFIG_FILE"
|
||
|
||
while true; do
|
||
read -r -p "请输入您的 Gitea 实例 URL (例如 https://git.example.com): " GITEA_URL
|
||
GITEA_URL=${GITEA_URL%$'\r'}
|
||
if [[ -n "$GITEA_URL" ]]; then break; else error "URL 不能为空。"; fi
|
||
done
|
||
|
||
GITEA_URL=${GITEA_URL%/} # 移除末尾的斜杠
|
||
|
||
while true; do
|
||
read -r -s -p "请输入您的 Gitea Access Token: " GITEA_TOKEN
|
||
GITEA_TOKEN=${GITEA_TOKEN%$'\r'}
|
||
echo
|
||
if [[ -n "$GITEA_TOKEN" ]]; then break; else error "Access Token 不能为空。"; fi
|
||
done
|
||
|
||
# 保存到配置文件
|
||
cat > "$CONFIG_FILE" << EOF
|
||
# Gitea CLI Configuration
|
||
GITEA_URL="${GITEA_URL}"
|
||
ACCESS_TOKEN="${GITEA_TOKEN}"
|
||
EOF
|
||
chmod 600 "$CONFIG_FILE" # 设置文件权限保护 Token
|
||
success "\n配置已成功保存至 $CONFIG_FILE"
|
||
}
|
||
|
||
# --- Gitea API 函数 ---
|
||
|
||
# 通用 API 请求函数
|
||
gitea_api_get() {
|
||
local endpoint="$1"
|
||
local full_url="${GITEA_URL}${endpoint}"
|
||
|
||
# 使用 --fail 使 curl 在遇到 HTTP 错误时返回非零退出码
|
||
curl --fail -s -H "Authorization: token ${ACCESS_TOKEN}" -H "Accept: application/json" "${full_url}"
|
||
}
|
||
|
||
# 获取与 Token 关联的用户名
|
||
get_gitea_user() {
|
||
gitea_api_get "/api/v1/user" | jq -r '.login'
|
||
}
|
||
|
||
# 获取认证用户的所有仓库
|
||
fetch_repos() {
|
||
# 限制为 200 个仓库,如果需要可以后续增加分页功能
|
||
gitea_api_get "/api/v1/user/repos?limit=200" | jq -r '.[].full_name'
|
||
}
|
||
|
||
# 获取指定仓库的文件列表
|
||
fetch_file_list() {
|
||
local full_repo_name="$1"
|
||
|
||
info "正在获取 '${full_repo_name}' 的默认分支信息..."
|
||
local default_branch
|
||
default_branch=$(gitea_api_get "/api/v1/repos/${full_repo_name}" | jq -r '.default_branch')
|
||
if [ -z "$default_branch" ] || [ "$default_branch" == "null" ]; then
|
||
error "无法确定 '${full_repo_name}' 的默认分支。"
|
||
return 1
|
||
fi
|
||
|
||
local commit_sha
|
||
commit_sha=$(gitea_api_get "/api/v1/repos/${full_repo_name}/branches/${default_branch}" | jq -r '.commit.id')
|
||
if [ -z "$commit_sha" ] || [ "$commit_sha" == "null" ]; then
|
||
error "无法获取分支 '${default_branch}' 的 commit SHA。"
|
||
return 1
|
||
fi
|
||
|
||
info "正在获取文件树..."
|
||
gitea_api_get "/api/v1/repos/${full_repo_name}/git/trees/${commit_sha}?recursive=1" | jq -r '.tree[] | select(.type == "blob") | .path'
|
||
}
|
||
|
||
# 下载单个文件
|
||
download_file() {
|
||
local full_repo_name="$1"
|
||
local file_path="$2"
|
||
# 使用 IFS 和 read for robust splitting, avoiding global IFS issues.
|
||
local owner
|
||
local repo_name
|
||
IFS='/' read -r owner repo_name <<< "$full_repo_name"
|
||
|
||
local output_file
|
||
output_file=$(basename "${file_path}")
|
||
# 兼容 Windows,替换路径中的 / 为 \ (虽然 curl 可能不需要)
|
||
mkdir -p "$(dirname "$file_path")"
|
||
|
||
local download_url="${GITEA_URL}/api/v1/repos/${owner}/${repo_name}/raw/${file_path}"
|
||
|
||
info "正在下载 '${file_path}'..."
|
||
curl -L --progress-bar --fail \
|
||
-H "Authorization: token ${ACCESS_TOKEN}" \
|
||
-o "${file_path}" \
|
||
"${download_url}" \
|
||
|| { error "\n下载 '${file_path}' 失败。"; return 1; }
|
||
|
||
success "成功下载至 '${file_path}'"
|
||
}
|
||
|
||
# --- 主逻辑 ---
|
||
|
||
main() {
|
||
check_dependencies
|
||
|
||
if ! load_config; then
|
||
prompt_for_config
|
||
load_config
|
||
else
|
||
local username
|
||
username=$(get_gitea_user)
|
||
info "已从 $CONFIG_FILE 加载配置"
|
||
warning "URL: $GITEA_URL"
|
||
warning "用户: $username"
|
||
read -r -p "是否使用此配置? [Y/n/reset] " choice
|
||
case "$choice" in
|
||
n|N)
|
||
info "操作取消。"
|
||
exit 0
|
||
;;
|
||
reset|RESET)
|
||
prompt_for_config
|
||
load_config
|
||
;;
|
||
*)
|
||
# 使用当前配置继续
|
||
;;
|
||
esac
|
||
fi
|
||
|
||
if [ -z "$ACCESS_TOKEN" ]; then
|
||
error "无法加载 Access Token,请检查您的配置。"
|
||
exit 1
|
||
fi
|
||
|
||
# 步骤 1: 选择仓库
|
||
info "\n正在获取您的仓库列表..."
|
||
local repos
|
||
repos=$(fetch_repos)
|
||
if [ -z "$repos" ]; then
|
||
error "无法获取仓库列表。请检查您的 Token、URL 以及 Token 是否拥有 'read:repository' 权限。"
|
||
exit 1
|
||
fi
|
||
|
||
local selected_repo
|
||
selected_repo=$(echo "$repos" | fzf --height 40% --reverse --prompt="请选择一个仓库 > " --header="使用方向键导航, Enter 键选择。")
|
||
selected_repo=${selected_repo%$'\r'} # Sanitize input from fzf
|
||
|
||
if [ -z "$selected_repo" ]; then
|
||
info "未选择仓库,操作取消。"
|
||
exit 0
|
||
fi
|
||
success "已选择仓库: $selected_repo"
|
||
|
||
# 步骤 2: 选择文件
|
||
info "\n正在获取 '${selected_repo}' 的文件列表..."
|
||
local files
|
||
files=$(fetch_file_list "$selected_repo")
|
||
|
||
if [ -z "$files" ]; then
|
||
warning "仓库 '${selected_repo}' 为空或发生错误。"
|
||
exit 0
|
||
fi
|
||
|
||
local selected_files
|
||
selected_files=$(echo "$files" | fzf --multi --height 60% --reverse --prompt="请选择要下载的文件 > " --header="使用 Tab 键选中/取消多个文件, Enter 键确认。")
|
||
|
||
if [ -z "$selected_files" ]; then
|
||
info "未选择文件,操作取消。"
|
||
exit 0
|
||
fi
|
||
|
||
# 步骤 3: 下载选中的文件
|
||
info "\n准备下载选中的文件..."
|
||
local file_count=0
|
||
local success_count=0
|
||
local failed_count=0
|
||
|
||
local OLD_IFS="$IFS"
|
||
IFS=$'\n'
|
||
for file in $selected_files; do
|
||
file=${file%$'\r'} # Sanitize each line from fzf output
|
||
((file_count++))
|
||
if download_file "$selected_repo" "$file"; then
|
||
((success_count++))
|
||
else
|
||
((failed_count++))
|
||
fi
|
||
done
|
||
IFS="$OLD_IFS"
|
||
|
||
# 最终总结
|
||
info "\n--- 下载完成 ---"
|
||
success "成功下载: ${success_count} 个文件。"
|
||
if [ "$failed_count" -gt 0 ]; then
|
||
error "下载失败: ${failed_count} 个文件。"
|
||
fi
|
||
}
|
||
|
||
# --- 脚本入口 ---
|
||
|
||
if [[ "$1" != "down" ]]; then
|
||
warning "用法: $(basename "$0") down"
|
||
exit 1
|
||
fi
|
||
|
||
main |