Initial commit: Add interactive Gitea download script

This commit is contained in:
2025-07-27 17:47:43 +08:00
commit b497011047
4 changed files with 1579 additions and 0 deletions

3
.cursorindexingignore Normal file
View File

@ -0,0 +1,3 @@
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
.specstory/**

2
.specstory/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# SpecStory explanation file
/.what-is-this.md

File diff suppressed because it is too large Load Diff

321
gitea.sh Normal file
View File

@ -0,0 +1,321 @@
#!/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"
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
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
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 键选择。")
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_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