#!/bin/bash # # gitea.sh - Gitea 交互式下载器 # # 一个用于通过交互式菜单浏览和下载 Gitea 私有仓库中文件的脚本。 # # 依赖: curl, jq, fzf # --- 配置 --- CONFIG_FILE="${HOME}/.gitea_cli_config" # --- 辅助函数 --- # 带颜色的输出函数 _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" local owner_repo=(${full_repo_name//// }) local owner=${owner_repo[0]} local repo_name=${owner_repo[1]} 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