Files
gitea-downloader/gitea.sh
2025-07-27 20:21:31 +08:00

339 lines
10 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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