Files
opsx-vpn/backend/internal/binmgr/binmgr.go
T
opsx-tools 021d012478 feat: 全协议一键安装 + 国内GitHub镜像加速 + 中英文i18n国际化
后端:
- binmgr.go: 新增 detectChina() 国内/海外自动判断(ipinfo.io)
- binmgr.go: 国内自动走 gh-proxy.com 镜像加速 GitHub 下载
- binmgr.go: 新增 InstallViaPackageManager() 包管理器一键安装
- binmgr.go: 新增 GetDeploySpec()/ProtocolAlias() 导出函数
- api.go: installBinary 支持全部7个协议(FRP/WireGuard/OpenVPN/IPSec/IKEv2/L2TP/StrongSwan)
- api.go: 策略: GitHub下载失败 → 自动降级包管理器安装

前端:
- 安装 vue-i18n@9, 支持中英文切换
- 新增 locales/zh-CN.json + en-US.json (548个翻译key, 17个模块)
- 新增 i18n.js 配置, localStorage 持久化语言偏好
- Layout.vue 右上角语言切换下拉菜单
- 所有17个Vue文件中文文本转为 $t() 调用
- Element Plus 组件语言跟随切换
- TunnelWizard: 所有协议显示「一键安装」按钮(带loading)
- 前端构建零错误
2026-07-23 03:01:46 +08:00

724 lines
21 KiB
Go
Raw 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.
// Package binmgr 提供协议二进制的下载部署与安装引导能力。
// 参考 opsx-proxy 的 xray_install.go 模式:
// - FRP: 从 GitHub Releases 下载预编译二进制到 data/bin/
// - WireGuard/OpenVPN/StrongSwan: 检测系统包管理器,返回安装命令
//
// 国内加速策略(自动判断):
// - GitHub API / Release 下载:先直连,失败后依次尝试 ghfast.top / gh-proxy.com 等代理
// - 系统包安装(yum/dnf):EPEL 源默认替换为阿里云镜像,可通过 EPEL_MIRROR 环境变量覆盖
// - 所有镜像可通过 GITHUB_MIRROR 环境变量指定单个代理(跳过自动探测)
package binmgr
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"time"
)
// BinDir 二进制存放目录(data/bin/)
func BinDir() string {
return filepath.Join(dataDir(), "bin")
}
// dataDir 获取数据目录(复用 service.BaseDir 逻辑)
func dataDir() string {
if v := os.Getenv("VPN_DATA_DIR"); v != "" {
return v
}
if exe, err := os.Executable(); err == nil {
return filepath.Join(filepath.Dir(exe), "data")
}
return "data"
}
// ---- 国内 GitHub 加速镜像 ----
// chinaGitHubMirror 国内首选镜像(经实测 gh-proxy.com 14MB 仅需 2.6 秒)。
// 海外直连 GitHub 无需代理。
var chinaGitHubMirror = "https://gh-proxy.com"
// fallbackGitHubMirrors 备选镜像(gh-proxy.com 不可用时依次尝试)。
var fallbackGitHubMirrors = []string{
"https://ghfast.top",
"https://mirror.ghproxy.com",
"https://github.moeyy.xyz",
}
// cachedRegion 缓存区域检测结果,避免每次请求都查询。
var cachedRegion = struct {
checked bool
isChina bool
}{}
// detectChina 启动时通过 ipinfo.io 判断服务器是否在中国大陆。
// 结果缓存 24 小时,查询失败默认当作国内处理(安全降级)。
func detectChina() bool {
if cachedRegion.checked {
return cachedRegion.isChina
}
client := &http.Client{Timeout: 5 * time.Second}
resp, err := client.Get("https://ipinfo.io/json")
if err != nil {
cachedRegion.checked = true
cachedRegion.isChina = true // 查询失败默认国内(走镜像)
return true
}
defer resp.Body.Close()
var info struct {
Country string `json:"country"`
}
if err := json.NewDecoder(resp.Body).Decode(&info); err != nil {
cachedRegion.checked = true
cachedRegion.isChina = true
return true
}
cachedRegion.checked = true
cachedRegion.isChina = (info.Country == "CN")
return cachedRegion.isChina
}
// getGitHubMirror 返回用户指定的镜像,或空字符串(表示直连)。
// 用户可通过 GITHUB_MIRROR 环境变量强制指定(跳过自动探测)。
func getGitHubMirror() string {
if m := os.Getenv("GITHUB_MIRROR"); m != "" {
return strings.TrimRight(m, "/")
}
return "" // 空表示由 getMirrorURLs 自动判断
}
// getMirrorURLs 返回一组候选 URL:根据国内/海外自动选择策略。
// githubPath 为 github.com 之后的路径,如 "fatedier/frp/releases/download/v0.61.1/xxx.tar.gz"。
//
// 国内:gh-proxy.com(首选)→ 直连(兜底)→ 备选镜像
// 海外:直连 GitHub → gh-proxy.com(兜底)
func getMirrorURLs(githubPath string) []string {
directURL := "https://github.com/" + githubPath
proxyURL := chinaGitHubMirror + "/https://github.com/" + githubPath
// 用户显式指定镜像
if m := getGitHubMirror(); m != "" {
return []string{m + "/https://github.com/" + githubPath, directURL}
}
if detectChina() {
// 国内:镜像优先,直连兜底
urls := []string{proxyURL, directURL}
for _, m := range fallbackGitHubMirrors {
urls = append(urls, m+"/https://github.com/"+githubPath)
}
return urls
}
// 海外:直连优先,镜像兜底
return []string{directURL, proxyURL}
}
// getAPIURLs 返回 GitHub API 的候选 URL 列表(同样遵循国内/海外策略)。
func getAPIURLs(apiPath string) []string {
directURL := "https://api.github.com/" + apiPath
proxyURL := chinaGitHubMirror + "/https://api.github.com/" + apiPath
if m := getGitHubMirror(); m != "" {
return []string{m + "/https://api.github.com/" + apiPath, directURL}
}
if detectChina() {
return []string{proxyURL, directURL}
}
return []string{directURL, proxyURL}
}
// tryURLs 依次请求候选 URL,返回第一个成功的结果。
// 用于 API 请求(返回 body)。
func tryURLs(urls []string) ([]byte, error) {
var lastErr error
for _, u := range urls {
req, _ := http.NewRequest("GET", u, nil)
req.Header.Set("Accept", "application/vnd.github+json")
req.Header.Set("User-Agent", "opsx-vpn")
resp, err := httpClient.Do(req)
if err != nil {
lastErr = err
continue
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
lastErr = err
continue
}
if resp.StatusCode != 200 {
lastErr = fmt.Errorf("%s 返回 %d", u, resp.StatusCode)
continue
}
return body, nil
}
return nil, fmt.Errorf("所有源均失败,最后错误: %w", lastErr)
}
// ---- GitHub Release 下载(FRP / OpenVPN 通用)----
type githubRelease struct {
TagName string `json:"tag_name"`
Assets []struct {
Name string `json:"name"`
BrowserDownloadURL string `json:"browser_download_url"`
} `json:"assets"`
}
var httpClient = &http.Client{Timeout: 5 * time.Minute}
// deploySpec 定义可平台下载部署的协议规格
type deploySpec struct {
Repo string // GitHub 仓库(owner/repo)
BinNames []string // 要提取的二进制名(如 frps/frpc)
AssetHint string // 匹配 asset 的关键词(用于从 release assets 中选对)
WindowsExt string // Windows 下的二进制扩展名
}
// 可下载部署的协议列表
var deploySpecs = map[string]deploySpec{
"frp": {
Repo: "fatedier/frp",
BinNames: []string{"frps", "frpc"},
AssetHint: "", // 匹配逻辑:tag + goos + goarch
},
"openvpn": {
Repo: "OpenVPN/openvpn",
BinNames: []string{"openvpn"},
AssetHint: "", // openvpn release 匹配逻辑略不同,见下方
},
}
// GetDeploySpec 返回协议的 GitHub 下载规格(供 API 判断是否支持下载)
func GetDeploySpec(name string) (deploySpec, bool) {
spec, ok := deploySpecs[name]
return spec, ok
}
// DownloadBinary 通用下载部署:从 GitHub Releases 下载协议二进制到 data/bin/
func DownloadBinary(name string) (string, error) {
spec, ok := deploySpecs[name]
if !ok {
return "", fmt.Errorf("协议 %s 不支持平台下载部署", name)
}
// 1. 获取最新 release(国内自动走镜像)
release, err := fetchLatestRelease(spec.Repo)
if err != nil {
return "", fmt.Errorf("获取 %s 版本失败: %w", name, err)
}
// 2. 匹配当前平台的 asset
assetURL, assetFile := matchAsset(name, spec, release)
if assetURL == "" {
return "", fmt.Errorf("未找到匹配 %s/%s 的 %s 发布包", runtime.GOOS, runtime.GOARCH, name)
}
// 3. 将 asset URL 转为镜像候选列表(国内自动加速)
assetURLs := rewriteAssetURLs(assetURL)
// 4. 下载 + 解压 + 提取
if err := os.MkdirAll(BinDir(), 0o755); err != nil {
return "", err
}
tmpDir, err := os.MkdirTemp("", name+"-dl-*")
if err != nil {
return "", err
}
defer os.RemoveAll(tmpDir)
archivePath := filepath.Join(tmpDir, assetFile)
if err := downloadFileWithMirror(assetURLs, archivePath); err != nil {
return "", fmt.Errorf("下载失败: %w", err)
}
extractDir := filepath.Join(tmpDir, "extracted")
if strings.HasSuffix(assetFile, ".zip") {
err = unzipTo(archivePath, extractDir)
} else if strings.HasSuffix(assetFile, ".tar.gz") || strings.HasSuffix(assetFile, ".tgz") {
err = untargzTo(archivePath, extractDir)
} else {
// 单文件直接复制
err = os.MkdirAll(extractDir, 0o755)
if err == nil {
err = copyFile(archivePath, filepath.Join(extractDir, assetFile))
}
}
if err != nil {
return "", fmt.Errorf("解压失败: %w", err)
}
// 5. 提取二进制到 data/bin/
version := release.TagName
for _, binName := range spec.BinNames {
// 尝试原名 + .exe
candidates := []string{binName}
if runtime.GOOS == "windows" {
candidates = []string{binName + ".exe", binName}
}
for _, cand := range candidates {
src := findFile(extractDir, cand)
if src != "" {
dstName := binName
if runtime.GOOS == "windows" {
dstName += ".exe"
}
dst := filepath.Join(BinDir(), dstName)
if err := copyFile(src, dst); err != nil {
return "", fmt.Errorf("复制 %s 失败: %w", binName, err)
}
os.Chmod(dst, 0o755)
break
}
}
}
// 6. 写版本标记
os.WriteFile(filepath.Join(BinDir(), name+".version"), []byte(version), 0o644)
return version, nil
}
// rewriteAssetURLs 将 GitHub release asset 原始 URL 转为镜像候选列表。
// 输入如 https://github.com/fatedier/frp/releases/download/v0.61.1/frp_0.61.1_linux_amd64.tar.gz
func rewriteAssetURLs(original string) []string {
// 提取 github.com 之后的路径
if idx := strings.Index(original, "github.com/"); idx >= 0 {
path := original[idx+len("github.com/"):]
return getMirrorURLs(path)
}
// 非 GitHub URL,直接返回
return []string{original}
}
// matchAsset 根据协议名和当前平台从 release assets 中匹配下载 URL
func matchAsset(name string, spec deploySpec, release *githubRelease) (url, file string) {
goos := runtime.GOOS
arch := runtime.GOARCH
// GOARCH 别名
if arch == "amd64" {
arch = "amd64"
}
// 构建匹配关键词
var keywords []string
switch name {
case "frp":
// frp 发布包格式:frp_x.x.x_linux_amd64.tar.gz
keywords = []string{goos + "_" + arch, goos + "-" + arch}
case "openvpn":
// OpenVPN 官方 release 主要是源码 tarball,预编译二进制在社区构建
// 匹配 openvpn-windows-installers 或社区构建的 linux 包
if goos == "windows" {
keywords = []string{"windows", "win", ".exe", ".msi"}
} else {
// Linux 预编译:尝试匹配社区构建或退回源码包
keywords = []string{goos, arch}
}
}
// 遍历 assets 找最佳匹配
for _, a := range release.Assets {
lower := strings.ToLower(a.Name)
// 排除明显不匹配的(如 .asc 签名、.src 源码)
if strings.HasSuffix(lower, ".asc") || strings.HasSuffix(lower, ".sig") {
continue
}
for _, kw := range keywords {
if strings.Contains(lower, kw) {
return a.BrowserDownloadURL, a.Name
}
}
}
return "", ""
}
// ---- 包管理器检测与安装引导 ----
// DetectPackageManager 检测系统包管理器
func DetectPackageManager() string {
for _, pm := range []string{"apt-get", "yum", "dnf", "apk", "pacman"} {
if _, err := exec.LookPath(pm); err == nil {
return pm
}
}
return ""
}
// getEPELMirror 返回国内 EPEL 镜像地址。
// 优先级:EPEL_MIRROR 环境变量 > 阿里云默认 > 官方。
func getEPELMirror() string {
if m := os.Getenv("EPEL_MIRROR"); m != "" {
return strings.TrimRight(m, "/")
}
// 默认阿里云 EPEL 镜像(国内最稳定)
return "https://mirrors.aliyun.com/epel"
}
// InstallHint 返回协议在各包管理器下的安装命令。
// 同时返回当前系统检测到的包管理器,供前端高亮推荐。
// 国内策略:yum/dnf 的 EPEL 源默认指向阿里云镜像。
func InstallHint(protocol string) map[string]interface{} {
detectedPM := DetectPackageManager()
epelMirror := getEPELMirror()
// 嵌套 map: 协议 → 包管理器 → 安装命令
// yum/dnf 使用国内 EPEL 镜像加速
hints := map[string]map[string]string{
"wireguard": {
"apt-get": "apt-get install -y wireguard-tools",
"yum": fmt.Sprintf("yum install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; yum install -y wireguard-tools", epelMirror),
"dnf": fmt.Sprintf("dnf install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; dnf install -y wireguard-tools", epelMirror),
"apk": "apk add wireguard-tools",
"pacman": "pacman -S --noconfirm wireguard-tools",
},
"openvpn": {
"apt-get": "apt-get install -y openvpn",
"yum": fmt.Sprintf("yum install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; yum install -y openvpn", epelMirror),
"dnf": fmt.Sprintf("dnf install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; dnf install -y openvpn", epelMirror),
"apk": "apk add openvpn",
"pacman": "pacman -S --noconfirm openvpn",
},
"strongswan": {
"apt-get": "apt-get install -y strongswan strongswan-swanctl",
"yum": fmt.Sprintf("yum install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; yum install -y strongswan", epelMirror),
"dnf": fmt.Sprintf("dnf install -y epel-release && sed -i 's|^metalink=|#metalink=|g; s|^#baseurl=https://download.example/pub/epel|baseurl=%s|g' /etc/yum.repos.d/epel*.repo 2>/dev/null; dnf install -y strongswan", epelMirror),
"apk": "apk add strongswan",
"pacman": "pacman -S --noconfirm strongswan",
},
}
// L2TP 额外需要 xl2tpd
if protocol == "l2tp" || protocol == "strongswan" {
// 追加 xl2tpd 包(L2TP 协议必需)
for pm, cmd := range hints["strongswan"] {
if pm == "apt-get" {
hints["strongswan"][pm] = cmd + " xl2tpd"
} else {
hints["strongswan"][pm] = strings.Replace(cmd, "strongswan\"", "strongswan xl2tpd\"", 1)
if !strings.Contains(cmd, "xl2tpd") {
hints["strongswan"][pm] = cmd + " && " + map[string]string{
"yum": "yum install -y xl2tpd", "dnf": "dnf install -y xl2tpd",
"apk": "apk add xl2tpd", "pacman": "pacman -S --noconfirm xl2tpd",
}[pm]
}
}
}
}
// IPSec / IKEv2 / L2TP 都依赖 strongswan
displayName := protocol
aliasMap := map[string]string{
"ipsec": "strongswan", "ikev2": "strongswan", "l2tp": "strongswan",
}
if real, ok := aliasMap[protocol]; ok {
protocol = real
}
protoHints, ok := hints[protocol]
if !ok {
return map[string]interface{}{"command": "", "pm": detectedPM, "all": map[string]string{}, "detected": false}
}
// 当前系统推荐的命令
recommendedCmd := protoHints[detectedPM]
if recommendedCmd == "" {
recommendedCmd = protoHints["apt-get"] // 无包管理器时兜底 apt
}
return map[string]interface{}{
"command": recommendedCmd, // 当前系统推荐的命令
"pm": detectedPM, // 检测到的包管理器(空=未检测到)
"detected": detectedPM != "",
"protocol": displayName,
"all_commands": protoHints, // 所有平台的命令,供前端展示
"epel_mirror": epelMirror, // 当前使用的 EPEL 镜像
}
}
// ---- 一键包管理器安装 ----
// InstallViaPackageManager 通过系统包管理器一键安装协议依赖。
// 返回安装输出和检测到的二进制路径。
func InstallViaPackageManager(protocol string) (output string, err error) {
pm := DetectPackageManager()
if pm == "" {
return "", fmt.Errorf("未检测到系统包管理器(apt/yum/dnf/apk/pacman)")
}
// 获取安装命令
hint := InstallHint(protocol)
cmd, _ := hint["command"].(string)
if cmd == "" {
return "", fmt.Errorf("协议 %s 无可用安装命令", protocol)
}
// 执行安装(bash -c 包含管道等)
log.Printf("[binmgr] 执行安装命令: %s", cmd)
out, err := exec.Command("bash", "-c", cmd).CombinedOutput()
output = string(out)
if err != nil {
return output, fmt.Errorf("安装失败: %w\n%s", err, output)
}
// 安装后重新检测
st := CheckBinary(ProtocolAlias(protocol))
if !st.Installed {
return output, fmt.Errorf("安装命令执行成功但未检测到二进制,可能需要手动配置")
}
return output, nil
}
// ProtocolAlias 返回协议的实际二进制检测名(导出供 API 使用)
func ProtocolAlias(protocol string) string {
aliasMap := map[string]string{
"ipsec": "strongswan", "ikev2": "strongswan", "l2tp": "strongswan",
}
if real, ok := aliasMap[protocol]; ok {
return real
}
return protocol
}
// ---- 辅助函数 ----
// BinaryStatus 二进制安装状态
type BinaryStatus struct {
Name string `json:"name"`
Installed bool `json:"installed"`
Path string `json:"path"`
Source string `json:"source"` // managed / system / undeployed
Version string `json:"version"`
CanDeploy bool `json:"can_deploy"` // 是否支持平台下载(仅 frp)
InstallCmd string `json:"install_cmd"` // 包管理器安装命令(非 frp)
}
// protocolBins 各协议需要检测的二进制名
var protocolBins = map[string][]string{
"frp": {"frps", "frpc"},
"wireguard": {"wg", "wg-quick"},
"openvpn": {"openvpn"},
"strongswan": {"swanctl", "ipsec"},
}
// CheckBinary 检查单个协议的二进制状态
func CheckBinary(name string) BinaryStatus {
st := BinaryStatus{Name: name}
_, st.CanDeploy = deploySpecs[name] // frp/openvpn 支持平台下载
bins := protocolBins[name]
if len(bins) == 0 {
return st
}
// 1. 检查 data/bin/(平台下载部署的)
for _, binName := range bins {
managed := filepath.Join(BinDir(), binName)
if _, err := os.Stat(managed); err == nil {
st.Installed = true
st.Path = managed
st.Source = "managed"
if v, err := os.ReadFile(filepath.Join(BinDir(), name+".version")); err == nil {
st.Version = strings.TrimSpace(string(v))
}
return st
}
}
// 2. 检查系统 PATH
for _, binName := range bins {
if p, err := exec.LookPath(binName); err == nil {
st.Installed = true
st.Path = p
st.Source = "system"
return st
}
}
// 3. 未安装:返回安装命令(供系统包协议用)
hint := InstallHint(name)
if cmd, ok := hint["command"].(string); ok {
st.InstallCmd = cmd
}
return st
}
// CheckAll 检查所有协议二进制状态
func CheckAll() []BinaryStatus {
return []BinaryStatus{
CheckBinary("frp"),
CheckBinary("wireguard"),
CheckBinary("openvpn"),
CheckBinary("strongswan"),
}
}
// ---- 下载与解压工具 ----
// fetchLatestRelease 获取 GitHub 最新 release,国内自动走镜像加速。
func fetchLatestRelease(repo string) (*githubRelease, error) {
apiPath := fmt.Sprintf("repos/%s/releases/latest", repo)
urls := getAPIURLs(apiPath)
body, err := tryURLs(urls)
if err != nil {
return nil, err
}
var release githubRelease
if err := json.Unmarshal(body, &release); err != nil {
return nil, err
}
return &release, nil
}
// downloadFileWithMirror 依次尝试候选 URL 下载文件到 dest。
func downloadFileWithMirror(urls []string, dest string) error {
var lastErr error
for _, u := range urls {
resp, err := httpClient.Get(u)
if err != nil {
lastErr = err
continue
}
if resp.StatusCode != 200 {
resp.Body.Close()
lastErr = fmt.Errorf("%s 返回 %d", u, resp.StatusCode)
continue
}
out, err := os.Create(dest)
if err != nil {
resp.Body.Close()
lastErr = err
continue
}
_, err = io.Copy(out, resp.Body)
out.Close()
resp.Body.Close()
if err != nil {
lastErr = err
continue
}
return nil // 成功
}
return fmt.Errorf("所有下载源均失败,最后错误: %w", lastErr)
}
// downloadFile 单 URL 下载(保留向后兼容)。
func downloadFile(url, dest string) error {
return downloadFileWithMirror([]string{url}, dest)
}
func unzipTo(zipPath, dest string) error {
r, err := zip.OpenReader(zipPath)
if err != nil {
return err
}
defer r.Close()
if err := os.MkdirAll(dest, 0o755); err != nil {
return err
}
for _, f := range r.File {
path := filepath.Join(dest, f.Name)
if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
continue // 防路径穿越
}
if f.FileInfo().IsDir() {
os.MkdirAll(path, 0o755)
continue
}
os.MkdirAll(filepath.Dir(path), 0o755)
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
if err != nil {
return err
}
rc, err := f.Open()
if err != nil {
out.Close()
return err
}
io.Copy(out, rc)
out.Close()
rc.Close()
}
return nil
}
func untargzTo(targzPath, dest string) error {
f, err := os.Open(targzPath)
if err != nil {
return err
}
defer f.Close()
gz, err := gzip.NewReader(f)
if err != nil {
return err
}
defer gz.Close()
tr := tar.NewReader(gz)
if err := os.MkdirAll(dest, 0o755); err != nil {
return err
}
for {
hdr, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
path := filepath.Join(dest, hdr.Name)
if !strings.HasPrefix(path, filepath.Clean(dest)+string(os.PathSeparator)) {
continue
}
switch hdr.Typeflag {
case tar.TypeDir:
os.MkdirAll(path, 0o755)
case tar.TypeReg:
os.MkdirAll(filepath.Dir(path), 0o755)
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, os.FileMode(hdr.Mode))
if err != nil {
return err
}
io.Copy(out, tr)
out.Close()
}
}
return nil
}
func findFile(dir, name string) string {
var found string
filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if !info.IsDir() && info.Name() == name {
found = path
}
return nil
})
return found
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}