Initial commit: Add interactive Gitea download script
This commit is contained in:
3
.cursorindexingignore
Normal file
3
.cursorindexingignore
Normal 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
2
.specstory/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# SpecStory explanation file
|
||||||
|
/.what-is-this.md
|
||||||
1253
.specstory/history/2025-07-27_09-31Z-自动化下载gitea私有仓库文件.md
Normal file
1253
.specstory/history/2025-07-27_09-31Z-自动化下载gitea私有仓库文件.md
Normal file
File diff suppressed because it is too large
Load Diff
321
gitea.sh
Normal file
321
gitea.sh
Normal 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
|
||||||
Reference in New Issue
Block a user