feat: refact syncrepo using go
This commit is contained in:
+1
-1
@@ -19,7 +19,7 @@ export PATH="~/.local/share/mise/bin:~/.local/share/mise/shims:$PATH"
|
||||
```bash
|
||||
tar -xzvf baihu-linux-amd64.tar.gz
|
||||
chmod +x baihu-linux-amd64
|
||||
./baihu-linux-amd64
|
||||
./baihu-linux-amd64 server
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -72,7 +72,7 @@ clean:
|
||||
run:
|
||||
@mkdir -p bin
|
||||
$(GOBUILD) -o $(BINARY) main.go
|
||||
./$(BINARY)
|
||||
./$(BINARY) server
|
||||
|
||||
# Development run with hot reload (both frontend and backend)
|
||||
dev:
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
package reposync
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
SourceType string
|
||||
SourceURL string
|
||||
TargetPath string
|
||||
Branch string
|
||||
Path string
|
||||
SingleFile bool
|
||||
Proxy string
|
||||
ProxyURL string
|
||||
AuthToken string
|
||||
HttpProxy string
|
||||
}
|
||||
|
||||
func Run(args []string) {
|
||||
fs := flag.NewFlagSet("reposync", flag.ExitOnError)
|
||||
var cfg Config
|
||||
fs.StringVar(&cfg.SourceType, "source-type", "git", "Source type: git or url")
|
||||
fs.StringVar(&cfg.SourceURL, "source-url", "", "Source url")
|
||||
fs.StringVar(&cfg.TargetPath, "target-path", "", "Target path")
|
||||
fs.StringVar(&cfg.Branch, "branch", "", "Branch")
|
||||
fs.StringVar(&cfg.Path, "path", "", "Path for sparse checkout")
|
||||
fs.BoolVar(&cfg.SingleFile, "single-file", false, "Single file mode")
|
||||
fs.StringVar(&cfg.Proxy, "proxy", "none", "Proxy type")
|
||||
fs.StringVar(&cfg.ProxyURL, "proxy-url", "", "Custom proxy url")
|
||||
fs.StringVar(&cfg.AuthToken, "auth-token", "", "Auth token")
|
||||
fs.StringVar(&cfg.HttpProxy, "http-proxy", "", "Http proxy")
|
||||
|
||||
fs.Parse(args)
|
||||
|
||||
if cfg.SourceURL == "" || cfg.TargetPath == "" {
|
||||
fmt.Println("Error: --source-url and --target-path are required")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("Arguments: %v\n", args)
|
||||
|
||||
if cfg.SourceType == "git" {
|
||||
syncGit(cfg)
|
||||
} else {
|
||||
syncURL(cfg)
|
||||
}
|
||||
}
|
||||
|
||||
func syncGit(cfg Config) {
|
||||
env := os.Environ()
|
||||
|
||||
if isRawFileURL(cfg.SourceURL) {
|
||||
fmt.Println("Raw file URL detected, switching to URL mode")
|
||||
syncURL(cfg)
|
||||
return
|
||||
}
|
||||
|
||||
if cfg.HttpProxy != "" {
|
||||
env = append(env, "http_proxy="+cfg.HttpProxy, "https_proxy="+cfg.HttpProxy)
|
||||
}
|
||||
|
||||
repoURL := buildProxyURL(cfg.SourceURL, cfg.Proxy, cfg.ProxyURL)
|
||||
if cfg.AuthToken != "" && strings.HasPrefix(repoURL, "https://") {
|
||||
repoURL = strings.Replace(repoURL, "https://", "https://"+cfg.AuthToken+"@", 1)
|
||||
}
|
||||
|
||||
dest := cfg.TargetPath
|
||||
|
||||
if cfg.Path != "" && cfg.SingleFile {
|
||||
syncGitFile(cfg, repoURL, env)
|
||||
return
|
||||
}
|
||||
|
||||
gitDir := filepath.Join(dest, ".git")
|
||||
if isDir(dest) && !pathExists(gitDir) {
|
||||
repoName := getRepoName(cfg.SourceURL)
|
||||
dest = filepath.Join(dest, repoName)
|
||||
fmt.Printf("Appending repo name to target path: %s\n", dest)
|
||||
gitDir = filepath.Join(dest, ".git")
|
||||
}
|
||||
|
||||
if pathExists(gitDir) {
|
||||
fmt.Println("Executing git pull")
|
||||
if cfg.Branch != "" {
|
||||
runCmd([]string{"git", "checkout", cfg.Branch}, dest, env)
|
||||
}
|
||||
runCmd([]string{"git", "pull"}, dest, env)
|
||||
} else {
|
||||
fmt.Println("Executing git clone")
|
||||
parentDir := filepath.Dir(dest)
|
||||
if parentDir != "" {
|
||||
os.MkdirAll(parentDir, 0755)
|
||||
}
|
||||
|
||||
if pathExists(dest) && !isDirEmpty(dest) {
|
||||
fmt.Printf("Error: Target dir '%s' is not empty.\n", dest)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
cloneCmd := []string{"git", "clone", "--depth", "1"}
|
||||
if cfg.Branch != "" {
|
||||
cloneCmd = append(cloneCmd, "-b", cfg.Branch)
|
||||
}
|
||||
|
||||
if cfg.Path != "" {
|
||||
cloneCmd = append(cloneCmd, "--filter=blob:none", "--no-checkout", repoURL, dest)
|
||||
runCmd(cloneCmd, "", env)
|
||||
runCmd([]string{"git", "sparse-checkout", "init", "--cone"}, dest, env)
|
||||
runCmd([]string{"git", "sparse-checkout", "set", cfg.Path}, dest, env)
|
||||
runCmd([]string{"git", "checkout"}, dest, env)
|
||||
} else {
|
||||
cloneCmd = append(cloneCmd, repoURL, dest)
|
||||
runCmd(cloneCmd, "", env)
|
||||
}
|
||||
}
|
||||
fmt.Println("Sync completed")
|
||||
}
|
||||
|
||||
func syncURL(cfg Config) {
|
||||
downloadURL := buildProxyURL(cfg.SourceURL, cfg.Proxy, cfg.ProxyURL)
|
||||
dest := cfg.TargetPath
|
||||
|
||||
if isDir(dest) || strings.HasSuffix(dest, string(os.PathSeparator)) || strings.HasSuffix(dest, "/") {
|
||||
urlPath := strings.Split(cfg.SourceURL, "?")[0]
|
||||
filename := filepath.Base(urlPath)
|
||||
if filename == "" {
|
||||
filename = "downloaded_file"
|
||||
}
|
||||
dest = filepath.Join(dest, filename)
|
||||
fmt.Printf("Target file: %s\n", dest)
|
||||
}
|
||||
|
||||
downloadFile(downloadURL, dest, cfg.AuthToken)
|
||||
}
|
||||
|
||||
func syncGitFile(cfg Config, repoURL string, env []string) {
|
||||
sourceURL := cfg.SourceURL
|
||||
filePath := cfg.Path
|
||||
dest := cfg.TargetPath
|
||||
|
||||
if isDir(dest) || strings.HasSuffix(dest, string(os.PathSeparator)) || strings.HasSuffix(dest, "/") {
|
||||
filename := filepath.Base(filePath)
|
||||
dest = filepath.Join(dest, filename)
|
||||
fmt.Printf("Auto corrected path to: %s\n", dest)
|
||||
}
|
||||
|
||||
branch := cfg.Branch
|
||||
if branch == "" {
|
||||
branch = getRemoteDefaultBranch(repoURL, env)
|
||||
}
|
||||
|
||||
cleanURL := strings.TrimSuffix(cfg.SourceURL, ".git")
|
||||
rawURL := ""
|
||||
|
||||
if strings.Contains(sourceURL, "github.com") {
|
||||
base := strings.Replace(strings.TrimSuffix(cfg.SourceURL, ".git"), "github.com", "raw.githubusercontent.com", 1)
|
||||
rawURL = fmt.Sprintf("%s/%s/%s", base, branch, filePath)
|
||||
} else if strings.Contains(sourceURL, "gitlab.com") {
|
||||
rawURL = fmt.Sprintf("%s/-/raw/%s/%s", cleanURL, branch, filePath)
|
||||
} else if strings.Contains(sourceURL, "gitee.com") {
|
||||
rawURL = fmt.Sprintf("%s/raw/%s/%s", cleanURL, branch, filePath)
|
||||
} else {
|
||||
rawURL = fmt.Sprintf("%s/raw/%s/%s", cleanURL, branch, filePath)
|
||||
}
|
||||
|
||||
rawURL = buildProxyURL(rawURL, cfg.Proxy, cfg.ProxyURL)
|
||||
downloadFile(rawURL, dest, cfg.AuthToken)
|
||||
}
|
||||
|
||||
func getRemoteDefaultBranch(repoURL string, env []string) string {
|
||||
fmt.Printf("Detecting remote default branch: %s\n", repoURL)
|
||||
cmd := exec.Command("git", "ls-remote", "--symref", repoURL, "HEAD")
|
||||
cmd.Env = env
|
||||
out, err := cmd.Output()
|
||||
if err == nil {
|
||||
lines := strings.Split(string(out), "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) >= 2 && parts[0] == "ref:" && strings.Contains(parts[1], "refs/heads/") {
|
||||
branch := strings.TrimPrefix(parts[1], "refs/heads/")
|
||||
fmt.Printf("Detected branch: %s\n", branch)
|
||||
return branch
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println("Failed to detect, using 'main'")
|
||||
return "main"
|
||||
}
|
||||
|
||||
func buildProxyURL(url string, proxyType string, proxyURL string) string {
|
||||
if proxyType == "" || proxyType == "none" {
|
||||
return url
|
||||
}
|
||||
base := ""
|
||||
if proxyType == "ghproxy" {
|
||||
base = "https://gh-proxy.com/"
|
||||
} else if proxyType == "mirror" {
|
||||
base = "https://mirror.ghproxy.com/"
|
||||
} else if proxyType == "custom" && proxyURL != "" {
|
||||
base = strings.TrimSuffix(proxyURL, "/") + "/"
|
||||
}
|
||||
|
||||
if base != "" && strings.HasPrefix(url, "http") {
|
||||
return base + url
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
func downloadFile(url, dest, authToken string) {
|
||||
fmt.Printf("Downloading: %s\n", url)
|
||||
fmt.Printf("To: %s\n", dest)
|
||||
|
||||
parentDir := filepath.Dir(dest)
|
||||
if parentDir != "" {
|
||||
os.MkdirAll(parentDir, 0755)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
fmt.Printf("Download prep failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
if authToken != "" {
|
||||
req.Header.Set("Authorization", "token "+authToken)
|
||||
}
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; reposync)")
|
||||
|
||||
client := &http.Client{Timeout: 300 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Printf("Download request failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
fmt.Printf("Download failed, HTTP code: %d\n", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
out, err := os.Create(dest)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to create file: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
n, err := io.Copy(out, resp.Body)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to write data: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("File size: %d bytes\n", n)
|
||||
fmt.Println("Download completed")
|
||||
}
|
||||
|
||||
func isRawFileURL(url string) bool {
|
||||
rawPatterns := []string{
|
||||
"raw.githubusercontent.com",
|
||||
"/raw/",
|
||||
"/-/raw/",
|
||||
"/blob/",
|
||||
}
|
||||
for _, p := range rawPatterns {
|
||||
if strings.Contains(url, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getRepoName(url string) string {
|
||||
u := strings.TrimSuffix(url, "/")
|
||||
u = strings.TrimSuffix(u, ".git")
|
||||
return filepath.Base(u)
|
||||
}
|
||||
|
||||
func runCmd(args []string, dir string, env []string) {
|
||||
fmt.Printf(">> %s\n", strings.Join(args, " "))
|
||||
cmd := exec.Command(args[0], args[1:]...)
|
||||
cmd.Dir = dir
|
||||
cmd.Env = env
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
fmt.Printf("Command failed: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func isDir(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return info.IsDir()
|
||||
}
|
||||
|
||||
func pathExists(path string) bool {
|
||||
_, err := os.Stat(path)
|
||||
return err == nil || !os.IsNotExist(err)
|
||||
}
|
||||
|
||||
func isDirEmpty(path string) bool {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer f.Close()
|
||||
_, err = f.Readdirnames(1)
|
||||
return err == io.EOF
|
||||
}
|
||||
-331
@@ -1,331 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
from typing import TYPE_CHECKING, Protocol, cast
|
||||
|
||||
# 仅在类型检查时导入,避免运行时依赖
|
||||
if TYPE_CHECKING:
|
||||
from http.client import HTTPResponse
|
||||
|
||||
|
||||
class SyncArgs(Protocol):
|
||||
"""用于类型检查的参数协议,映射 argparse 的解析结果"""
|
||||
source_type: str
|
||||
source_url: str
|
||||
target_path: str
|
||||
branch: str | None
|
||||
path: str | None
|
||||
single_file: bool
|
||||
proxy: str | None
|
||||
proxy_url: str | None
|
||||
auth_token: str | None
|
||||
http_proxy: str | None
|
||||
|
||||
|
||||
def run(
|
||||
cmd: list[str],
|
||||
env: dict[str, str] | None = None,
|
||||
cwd: str | None = None,
|
||||
capture_output: bool = False
|
||||
) -> str | None:
|
||||
"""
|
||||
执行系统命令。
|
||||
|
||||
Args:
|
||||
cmd: 命令列表
|
||||
env: 环境变量字典
|
||||
cwd: 当前工作目录
|
||||
capture_output: 是否捕获输出。
|
||||
"""
|
||||
if not capture_output:
|
||||
print(">>", " ".join(cmd))
|
||||
|
||||
if capture_output:
|
||||
# 捕获模式:不打印到屏幕,返回 stdout
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
timeout=30,
|
||||
encoding="utf-8",
|
||||
errors="ignore"
|
||||
)
|
||||
else:
|
||||
# 直通模式:直接打印到屏幕
|
||||
result = subprocess.run(
|
||||
cmd,
|
||||
cwd=cwd,
|
||||
env=env,
|
||||
stdout=sys.stdout,
|
||||
stderr=sys.stderr,
|
||||
)
|
||||
|
||||
if result.returncode != 0:
|
||||
if capture_output:
|
||||
return None
|
||||
sys.exit(result.returncode)
|
||||
|
||||
if capture_output:
|
||||
return str(result.stdout).strip()
|
||||
|
||||
return str(result)
|
||||
|
||||
|
||||
def get_remote_default_branch(repo_url: str, env: dict[str, str]) -> str:
|
||||
"""检测远程仓库的默认分支名称(通常是 main 或 master)。"""
|
||||
print(f"正在检测远程仓库默认分支: {repo_url}")
|
||||
cmd = ["git", "ls-remote", "--symref", repo_url, "HEAD"]
|
||||
output = run(cmd, env=env, capture_output=True)
|
||||
|
||||
if isinstance(output, str):
|
||||
for line in output.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) >= 2 and parts[0] == "ref:" and "refs/heads/" in parts[1]:
|
||||
branch = parts[1].removeprefix("refs/heads/")
|
||||
print(f"检测到默认分支: {branch}")
|
||||
return branch
|
||||
|
||||
print("无法检测到默认分支,回退使用 'main'")
|
||||
return "main"
|
||||
|
||||
|
||||
def build_proxy_url(url: str, proxy_type: str | None, proxy_url: str | None) -> str:
|
||||
"""根据配置构建带有代理前缀的 URL。"""
|
||||
if not proxy_type or proxy_type == "none":
|
||||
return url
|
||||
|
||||
proxy_base = ""
|
||||
if proxy_type == "ghproxy":
|
||||
proxy_base = "https://gh-proxy.com/"
|
||||
elif proxy_type == "mirror":
|
||||
proxy_base = "https://mirror.ghproxy.com/"
|
||||
elif proxy_type == "custom" and proxy_url:
|
||||
proxy_base = proxy_url.rstrip("/") + "/"
|
||||
|
||||
if proxy_base and url.startswith("http"):
|
||||
return proxy_base + url
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def _download_file(url: str, dest: str, auth_token: str | None) -> None:
|
||||
"""
|
||||
内部通用下载函数,处理请求构建、Token 认证和文件写入。
|
||||
"""
|
||||
print(f"下载地址: {url}")
|
||||
print(f"目标路径: {dest}")
|
||||
|
||||
parent_dir = os.path.dirname(dest)
|
||||
if parent_dir:
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
req = urllib.request.Request(url)
|
||||
if auth_token:
|
||||
req.add_header("Authorization", f"token {auth_token}")
|
||||
req.add_header("User-Agent", "Mozilla/5.0 (compatible; sync.py)")
|
||||
|
||||
try:
|
||||
with cast("HTTPResponse", urllib.request.urlopen(req, timeout=300)) as response:
|
||||
content: bytes = response.read()
|
||||
with open(dest, "wb") as f:
|
||||
_ = f.write(content)
|
||||
|
||||
print(f"文件大小: {len(content)} 字节")
|
||||
print("下载完成")
|
||||
except urllib.error.HTTPError as e:
|
||||
print(f"下载失败, HTTP 状态码: {e.code}")
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as e:
|
||||
print(f"下载失败: {e.reason}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def sync_git_file(args: SyncArgs, repo_url: str, env: dict[str, str]) -> None:
|
||||
"""
|
||||
从 Git 仓库同步单个文件(通过构造 Raw URL 下载)。
|
||||
"""
|
||||
source_url = args.source_url
|
||||
file_path = args.path or ""
|
||||
dest = args.target_path
|
||||
|
||||
# 如果目标是目录,自动拼接文件名
|
||||
if os.path.isdir(dest) or dest.endswith(os.sep) or (os.altsep and dest.endswith(os.altsep)):
|
||||
filename = os.path.basename(file_path)
|
||||
dest = os.path.join(dest, filename)
|
||||
print(f"检测到目标路径为目录 '{args.target_path}',自动修正为: '{dest}'")
|
||||
|
||||
branch = args.branch or get_remote_default_branch(repo_url, env)
|
||||
|
||||
# 构建 raw 文件 URL
|
||||
# GitHub: https://github.com/user/repo -> https://raw.githubusercontent.com/user/repo/branch/path
|
||||
# GitLab: https://gitlab.com/user/repo -> https://gitlab.com/user/repo/-/raw/branch/path
|
||||
# Gitee: https://gitee.com/user/repo -> https://gitee.com/user/repo/raw/branch/path
|
||||
|
||||
clean_url = args.source_url.rstrip(".git")
|
||||
raw_url = ""
|
||||
|
||||
if "github.com" in source_url:
|
||||
base = args.source_url.replace("github.com", "raw.githubusercontent.com").rstrip(".git")
|
||||
raw_url = f"{base}/{branch}/{file_path}"
|
||||
elif "gitlab.com" in source_url:
|
||||
raw_url = f"{clean_url}/-/raw/{branch}/{file_path}"
|
||||
elif "gitee.com" in source_url:
|
||||
raw_url = f"{clean_url}/raw/{branch}/{file_path}"
|
||||
else:
|
||||
# 通用策略:尝试 GitHub 风格
|
||||
raw_url = f"{clean_url}/raw/{branch}/{file_path}"
|
||||
|
||||
raw_url = build_proxy_url(raw_url, args.proxy, args.proxy_url)
|
||||
|
||||
# 调用通用下载函数
|
||||
_download_file(raw_url, dest, args.auth_token)
|
||||
|
||||
|
||||
def is_raw_file_url(url: str) -> bool:
|
||||
"""判断 URL 是否已经是 Raw 文件链接。"""
|
||||
raw_patterns = [
|
||||
"raw.githubusercontent.com",
|
||||
"/raw/",
|
||||
"/-/raw/",
|
||||
"/blob/",
|
||||
]
|
||||
return any(pattern in url for pattern in raw_patterns)
|
||||
|
||||
|
||||
def get_repo_name(url: str) -> str:
|
||||
"""从 Git URL 中提取仓库名称。"""
|
||||
url_stripped = url.rstrip("/").rstrip(".git")
|
||||
return os.path.basename(url_stripped)
|
||||
|
||||
|
||||
def sync_git(args: SyncArgs) -> None:
|
||||
"""处理 Git 类型的同步逻辑(Clone, Pull 或 Sparse Checkout)。"""
|
||||
env = os.environ.copy()
|
||||
|
||||
if is_raw_file_url(args.source_url):
|
||||
print("检测到 raw 文件 URL,自动切换到 URL 下载模式")
|
||||
sync_url(args)
|
||||
return
|
||||
|
||||
if args.http_proxy:
|
||||
env["http_proxy"] = args.http_proxy
|
||||
env["https_proxy"] = args.http_proxy
|
||||
|
||||
repo_url = build_proxy_url(args.source_url, args.proxy, args.proxy_url)
|
||||
|
||||
if args.auth_token and repo_url.startswith("https://"):
|
||||
repo_url = repo_url.replace("https://", f"https://{args.auth_token}@")
|
||||
|
||||
dest = args.target_path
|
||||
|
||||
# 单文件模式:使用 Raw URL 下载
|
||||
if args.path and args.single_file:
|
||||
sync_git_file(args, repo_url, env)
|
||||
return
|
||||
|
||||
# 自动追加仓库名逻辑
|
||||
git_dir = os.path.join(dest, ".git")
|
||||
if os.path.isdir(dest) and not os.path.exists(git_dir):
|
||||
repo_name = get_repo_name(args.source_url)
|
||||
dest = os.path.join(dest, repo_name)
|
||||
print(f"目标路径自动追加仓库名: {dest}")
|
||||
git_dir = os.path.join(dest, ".git")
|
||||
|
||||
if os.path.exists(git_dir):
|
||||
print("检测到已存在仓库,执行 git pull")
|
||||
if args.branch:
|
||||
_ = run(["git", "checkout", args.branch], cwd=dest, env=env)
|
||||
_ = run(["git", "pull"], cwd=dest, env=env)
|
||||
else:
|
||||
print("执行 git clone")
|
||||
parent_dir = os.path.dirname(dest)
|
||||
if parent_dir:
|
||||
os.makedirs(parent_dir, exist_ok=True)
|
||||
|
||||
if os.path.exists(dest) and os.listdir(dest):
|
||||
print(f"错误: 目标目录 '{dest}' 已存在且不为空,无法执行 git clone")
|
||||
print("提示: 请清空目标目录或指定一个新目录")
|
||||
sys.exit(1)
|
||||
|
||||
clone_cmd = ["git", "clone", "--depth", "1"]
|
||||
|
||||
if args.branch:
|
||||
clone_cmd.extend(["-b", args.branch])
|
||||
|
||||
# 稀疏检出 (Sparse Checkout)
|
||||
if args.path:
|
||||
clone_cmd.extend(["--filter=blob:none", "--no-checkout", repo_url, dest])
|
||||
_ = run(clone_cmd, env=env)
|
||||
_ = run(["git", "sparse-checkout", "init", "--cone"], cwd=dest, env=env)
|
||||
_ = run(["git", "sparse-checkout", "set", args.path], cwd=dest, env=env)
|
||||
_ = run(["git", "checkout"], cwd=dest, env=env)
|
||||
else:
|
||||
# 普通 Clone
|
||||
clone_cmd.extend([repo_url, dest])
|
||||
_ = run(clone_cmd, env=env)
|
||||
|
||||
print("同步完成")
|
||||
|
||||
|
||||
def sync_url(args: SyncArgs) -> None:
|
||||
"""处理普通 URL 文件下载逻辑。"""
|
||||
download_url = build_proxy_url(args.source_url, args.proxy, args.proxy_url)
|
||||
print(f"下载地址: {download_url}")
|
||||
|
||||
dest = args.target_path
|
||||
|
||||
if os.path.isdir(dest) or dest.endswith("/"):
|
||||
url_path = args.source_url.split("?")[0]
|
||||
filename = os.path.basename(url_path) or "downloaded_file"
|
||||
dest = os.path.join(dest, filename)
|
||||
print(f"目标文件: {dest}")
|
||||
|
||||
# 调用通用下载函数
|
||||
_download_file(download_url, dest, args.auth_token)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="仓库/文件同步工具")
|
||||
|
||||
_ = parser.add_argument("--source-type", choices=["git", "url"], default="git",
|
||||
help="源类型: git(Git仓库) 或 url(URL下载)")
|
||||
_ = parser.add_argument("--source-url", required=True,
|
||||
help="源地址(Git仓库URL或文件URL)")
|
||||
_ = parser.add_argument("--target-path", required=True,
|
||||
help="目标路径")
|
||||
_ = parser.add_argument("--branch",
|
||||
help="Git 分支名(仅 git 类型有效)")
|
||||
_ = parser.add_argument("--path",
|
||||
help="仅拉取指定文件或目录(仅 git 类型有效)")
|
||||
_ = parser.add_argument("--single-file", action="store_true",
|
||||
help="单文件模式,直接下载指定文件而非 sparse-checkout(需配合 --path 使用)")
|
||||
_ = parser.add_argument("--proxy", choices=["none", "ghproxy", "mirror", "custom"], default="none",
|
||||
help="代理类型")
|
||||
_ = parser.add_argument("--proxy-url",
|
||||
help="自定义代理地址(仅 proxy=custom 时有效)")
|
||||
_ = parser.add_argument("--auth-token",
|
||||
help="认证 Token(用于私有仓库)")
|
||||
_ = parser.add_argument("--http-proxy",
|
||||
help="HTTP 代理(如 http://127.0.0.1:7890)")
|
||||
|
||||
# 使用 cast 将 Namespace 转换为 SyncArgs 协议,满足静态类型检查
|
||||
args = cast(SyncArgs, cast(object, parser.parse_args()))
|
||||
|
||||
print("参数:", " ".join(sys.argv[1:]))
|
||||
|
||||
if args.source_type == "git":
|
||||
sync_git(args)
|
||||
else:
|
||||
sync_url(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
+2
-4
@@ -135,14 +135,12 @@ COPY --from=backend-builder /app/baihu .
|
||||
COPY --from=backend-builder /app/configs ./configs
|
||||
COPY docker/docker-entrypoint.sh .
|
||||
|
||||
# Copy sync.py to /opt
|
||||
COPY custom/sync.py /opt/sync.py
|
||||
|
||||
|
||||
# Copy agent binaries to /opt/agent
|
||||
COPY --from=agent-builder /opt/agent /opt/agent
|
||||
|
||||
RUN chmod +x /opt/sync.py \
|
||||
&& chmod +x docker-entrypoint.sh \
|
||||
RUN chmod +x docker-entrypoint.sh \
|
||||
&& echo "set encoding=utf-8" >> /etc/vim/vimrc
|
||||
|
||||
EXPOSE 8052
|
||||
|
||||
@@ -52,4 +52,4 @@ echo "[entrypoint][Nodejs] npm: $(npm --version) at $(which npm)"
|
||||
# 启动应用
|
||||
# ============================
|
||||
cd /app
|
||||
exec ./baihu
|
||||
exec ./baihu server
|
||||
@@ -44,6 +44,7 @@ require (
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/gohugoio/hugo v0.149.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.7.6 // indirect
|
||||
@@ -66,6 +67,8 @@ require (
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.9.2 // indirect
|
||||
github.com/spf13/cobra v1.10.2 // indirect
|
||||
github.com/spf13/pflag v1.0.9 // indirect
|
||||
github.com/tdewolff/parse/v2 v2.8.3 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
|
||||
@@ -52,6 +52,7 @@ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhD
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||
github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
|
||||
github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -145,6 +146,8 @@ github.com/hairyhenderson/go-codeowners v0.7.0 h1:s0W4wF8bdsBEjTWzwzSlsatSthWtTA
|
||||
github.com/hairyhenderson/go-codeowners v0.7.0/go.mod h1:wUlNgQ3QjqC4z8DnM5nnCYVq/icpqXJyJOukKx5U8/Q=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
@@ -241,6 +244,7 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
@@ -251,6 +255,10 @@ github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||
github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo=
|
||||
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
|
||||
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -294,6 +302,7 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -819,8 +820,13 @@ func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string)
|
||||
}
|
||||
absTargetPath, _ := filepath.Abs(targetPath)
|
||||
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
exePath = "baihu" // Fallback if executable path can't be found
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"/opt/sync.py",
|
||||
"reposync",
|
||||
"--source-type", config.SourceType,
|
||||
"--source-url", config.SourceURL,
|
||||
"--target-path", absTargetPath,
|
||||
@@ -844,7 +850,7 @@ func (es *ExecutorService) BuildRepoCommand(task *models.Task) (string, string)
|
||||
args = append(args, "--auth-token", config.AuthToken)
|
||||
}
|
||||
|
||||
return "python3 " + strings.Join(args, " "), "/opt"
|
||||
return exePath + " " + strings.Join(args, " "), filepath.Dir(exePath)
|
||||
}
|
||||
|
||||
// loadEnvVars 加载环境变量
|
||||
|
||||
@@ -1,7 +1,28 @@
|
||||
package main
|
||||
|
||||
import "github.com/engigu/baihu-panel/internal/bootstrap"
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/engigu/baihu-panel/cmd/reposync"
|
||||
"github.com/engigu/baihu-panel/internal/bootstrap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
bootstrap.New().Run()
|
||||
if len(os.Args) < 2 {
|
||||
bootstrap.New().Run()
|
||||
return
|
||||
}
|
||||
|
||||
cmd := os.Args[1]
|
||||
switch cmd {
|
||||
case "server":
|
||||
bootstrap.New().Run()
|
||||
case "reposync":
|
||||
reposync.Run(os.Args[2:])
|
||||
default:
|
||||
fmt.Printf("Unknown command: %s\n", cmd)
|
||||
fmt.Println("Available commands: server, reposync")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user