文件夹挂载逻辑变动
This commit is contained in:
@@ -399,7 +399,8 @@ func getFrpcStatusHandler(c *gin.Context) {
|
||||
// ========== 日志 Handler ==========
|
||||
|
||||
func getFrpcLogHandler(c *gin.Context) {
|
||||
lines, err := readTailLog("./frpc.log", 200)
|
||||
// 从 ./data/frpc.log 读取日志
|
||||
lines, err := readTailLog("./data/frpc.log", 200)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"code": 0,
|
||||
|
||||
@@ -17,17 +17,7 @@ import (
|
||||
|
||||
var DB *sql.DB
|
||||
|
||||
// ============================================================
|
||||
// 数据库版本常量
|
||||
// ============================================================
|
||||
|
||||
const (
|
||||
SchemaVersion = "v2" // 当前数据库 Schema 版本
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 数据模型
|
||||
// ============================================================
|
||||
const SchemaVersion = "v2"
|
||||
|
||||
type GlobalConfig struct {
|
||||
ID int `json:"id"`
|
||||
@@ -61,10 +51,6 @@ type User struct {
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 数据库初始化
|
||||
// ============================================================
|
||||
|
||||
func InitDB() error {
|
||||
// 确保 data 目录存在
|
||||
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||
@@ -94,12 +80,7 @@ func InitDB() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 建表
|
||||
// ============================================================
|
||||
|
||||
func createTables() error {
|
||||
// 用户表
|
||||
_, err := DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -113,7 +94,6 @@ func createTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 全局配置表
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS global_config (
|
||||
id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
@@ -135,7 +115,6 @@ func createTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 隧道表
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS proxies (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -153,7 +132,6 @@ func createTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 应用配置表
|
||||
_, err = DB.Exec(`
|
||||
CREATE TABLE IF NOT EXISTS app_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
@@ -165,7 +143,6 @@ func createTables() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 初始化默认配置
|
||||
var count int
|
||||
DB.QueryRow("SELECT COUNT(*) FROM global_config").Scan(&count)
|
||||
if count == 0 {
|
||||
@@ -185,10 +162,6 @@ func createTables() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 迁移引擎
|
||||
// ============================================================
|
||||
|
||||
func getCurrentSchemaVersion() string {
|
||||
var version string
|
||||
err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'schema_version'").Scan(&version)
|
||||
@@ -272,7 +245,6 @@ func runMigrations() error {
|
||||
log.Printf("📌 当前数据库 Schema: %s, 目标版本: %s", currentVer, targetVer)
|
||||
|
||||
if currentVer == targetVer {
|
||||
// 检查数据库是否包含有效数据
|
||||
var userCount int
|
||||
err := DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&userCount)
|
||||
if err != nil || userCount == 0 {
|
||||
@@ -302,7 +274,6 @@ func runMigrations() error {
|
||||
|
||||
if currentSchema == nil || schemaVersionsEqual(currentSchema, targetSchema) {
|
||||
log.Println(" 迁移类型: 轻量复制(Schema 无变更)")
|
||||
// 验证数据库是否有效
|
||||
var userCount int
|
||||
err := DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&userCount)
|
||||
if err != nil || userCount == 0 {
|
||||
@@ -448,10 +419,6 @@ func buildInsertSQL(oldTable, newTable string, oldDef, newDef *SchemaVersionDef)
|
||||
), nil
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// JWT 密钥管理
|
||||
// ============================================================
|
||||
|
||||
func ensureJwtSecret() error {
|
||||
var value string
|
||||
err := DB.QueryRow("SELECT value FROM app_config WHERE key = 'jwt_secret'").Scan(&value)
|
||||
@@ -484,10 +451,6 @@ func GetJwtSecret() (string, error) {
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 全局配置 CRUD
|
||||
// ============================================================
|
||||
|
||||
func GetGlobalConfig() (*GlobalConfig, error) {
|
||||
var cfg GlobalConfig
|
||||
err := DB.QueryRow(`
|
||||
@@ -523,10 +486,6 @@ func UpdateGlobalConfig(cfg *GlobalConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 隧道 CRUD
|
||||
// ============================================================
|
||||
|
||||
func GetProxies() ([]Proxy, error) {
|
||||
rows, err := DB.Query(`
|
||||
SELECT id, name, type, local_ip, local_port, remote_port, enabled
|
||||
@@ -589,10 +548,6 @@ func DeleteProxy(id int) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 用户 CRUD
|
||||
// ============================================================
|
||||
|
||||
func GetUserByUsername(username string) (*User, error) {
|
||||
var u User
|
||||
err := DB.QueryRow(`
|
||||
|
||||
@@ -431,11 +431,12 @@ do_deploy() {
|
||||
|
||||
# ----- 启动新容器 -----
|
||||
print_step "启动 frpc-console 容器..."
|
||||
# 启动容器(只挂载数据目录)
|
||||
docker run -d \
|
||||
--name frpc-console \
|
||||
--restart=always \
|
||||
--network host \
|
||||
-v ${DEPLOY_DIR}:/app \
|
||||
-v ${DEPLOY_DIR}/data:/app/data \
|
||||
-e PORT=${PORT} \
|
||||
-e TZ=Asia/Shanghai \
|
||||
${IMAGE_NAME}:${IMAGE_TAG}
|
||||
@@ -449,7 +450,7 @@ do_deploy() {
|
||||
|
||||
# ----- 写入版本文件 -----
|
||||
print_step "写入版本文件..."
|
||||
echo "$TARGET_VERSION" > "${DEPLOY_DIR}/version.ini"
|
||||
echo "$TARGET_VERSION" > "${DEPLOY_DIR}/data/version.ini"
|
||||
print_success "版本写入: ${TARGET_VERSION}"
|
||||
|
||||
# ----- 检查 frpc 子进程 -----
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -24,16 +25,12 @@ var FrpcTemplateContent string
|
||||
|
||||
var (
|
||||
cachedFrpcPath string
|
||||
frpcPathMutex sync.Mutex
|
||||
frpsPathMutex sync.Mutex
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 获取 frpc 路径
|
||||
// ============================================================
|
||||
|
||||
func getFrpcPath() (string, error) {
|
||||
frpcPathMutex.Lock()
|
||||
defer frpcPathMutex.Unlock()
|
||||
frpsPathMutex.Lock()
|
||||
defer frpsPathMutex.Unlock()
|
||||
|
||||
if cachedFrpcPath != "" {
|
||||
if _, err := os.Stat(cachedFrpcPath); err == nil {
|
||||
@@ -61,14 +58,12 @@ func getFrpcPath() (string, error) {
|
||||
return "", fmt.Errorf("不支持的平台: %s/%s", runtime.GOOS, runtime.GOARCH)
|
||||
}
|
||||
|
||||
// 优先级1:本地 ./bin/
|
||||
localPath := filepath.Join(".", "bin", fileName)
|
||||
if _, err := os.Stat(localPath); err == nil {
|
||||
cachedFrpcPath = localPath
|
||||
return localPath, nil
|
||||
}
|
||||
|
||||
// 优先级2:embed 解压
|
||||
data, err := embeddedFrpc.ReadFile("bin/" + fileName)
|
||||
if err == nil {
|
||||
tmpPath := filepath.Join(os.TempDir(), "frpc")
|
||||
@@ -85,7 +80,6 @@ func getFrpcPath() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 优先级3:系统 PATH
|
||||
path, err := exec.LookPath("frpc")
|
||||
if err == nil {
|
||||
cachedFrpcPath = path
|
||||
@@ -95,10 +89,6 @@ func getFrpcPath() (string, error) {
|
||||
return "", fmt.Errorf("未找到 frpc 文件")
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 生成 frpc.toml(v2.0 支持 wireProtocol)
|
||||
// ============================================================
|
||||
|
||||
func GenerateFrpcConfig() error {
|
||||
cfg, err := GetGlobalConfig()
|
||||
if err != nil {
|
||||
@@ -117,24 +107,21 @@ func GenerateFrpcConfig() error {
|
||||
}
|
||||
}
|
||||
|
||||
// 构建模板数据
|
||||
data := struct {
|
||||
*GlobalConfig
|
||||
Proxies []Proxy
|
||||
WireProtocolLine string // v2 协议配置行(为空则不输出)
|
||||
WireProtocolLine string
|
||||
}{
|
||||
GlobalConfig: cfg,
|
||||
Proxies: activeProxies,
|
||||
}
|
||||
|
||||
// v2.0: 如果启用 v2 协议,生成配置行
|
||||
if cfg.WireProtocolV2 {
|
||||
data.WireProtocolLine = `wireProtocol = "v2"`
|
||||
} else {
|
||||
data.WireProtocolLine = "" // 不输出,使用默认 v1
|
||||
data.WireProtocolLine = ""
|
||||
}
|
||||
|
||||
// 读取模板
|
||||
var tmplContent string
|
||||
if _, err := os.Stat("frpc.tmpl"); err == nil {
|
||||
content, readErr := os.ReadFile("frpc.tmpl")
|
||||
@@ -157,19 +144,20 @@ func GenerateFrpcConfig() error {
|
||||
return fmt.Errorf("渲染模板失败: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile("./frpc.toml", buf.Bytes(), 0644); err != nil {
|
||||
// 所有文件写入 ./data/ 目录
|
||||
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||
return fmt.Errorf("创建 data 目录失败: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile("./data/frpc.toml", buf.Bytes(), 0644); err != nil {
|
||||
return fmt.Errorf("写入配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// frpc 进程管理
|
||||
// ============================================================
|
||||
|
||||
func isFrpcRunning() bool {
|
||||
pidData, err := os.ReadFile("./frpc.pid")
|
||||
pidData, err := os.ReadFile("./data/frpc.pid")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -200,7 +188,11 @@ func StartFrpc() error {
|
||||
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat("./frpc.toml"); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||
return fmt.Errorf("创建 data 目录失败: %w", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat("./data/frpc.toml"); os.IsNotExist(err) {
|
||||
if err := GenerateFrpcConfig(); err != nil {
|
||||
return fmt.Errorf("生成配置文件失败: %w", err)
|
||||
}
|
||||
@@ -210,13 +202,13 @@ func StartFrpc() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
os.Remove("./frpc.pid")
|
||||
os.Remove("./data/frpc.pid")
|
||||
|
||||
cmd := exec.Command(frpcPath, "-c", "./frpc.toml")
|
||||
cmd := exec.Command(frpcPath, "-c", "./data/frpc.toml")
|
||||
setWindowHide(cmd)
|
||||
setSysProcAttr(cmd)
|
||||
|
||||
logFile, err := os.OpenFile("./frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
logFile, err := os.OpenFile("./data/frpc.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开日志文件失败: %w", err)
|
||||
}
|
||||
@@ -227,7 +219,7 @@ func StartFrpc() error {
|
||||
return fmt.Errorf("启动 frpc 失败: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile("./frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
|
||||
if err := os.WriteFile("./data/frpc.pid", []byte(fmt.Sprintf("%d", cmd.Process.Pid)), 0644); err != nil {
|
||||
return fmt.Errorf("保存 PID 失败: %w", err)
|
||||
}
|
||||
|
||||
@@ -240,11 +232,11 @@ func StopFrpc() error {
|
||||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "not found") {
|
||||
return fmt.Errorf("停止 frpc 失败: %w", err)
|
||||
}
|
||||
os.Remove("./frpc.pid")
|
||||
os.Remove("./data/frpc.pid")
|
||||
return nil
|
||||
}
|
||||
|
||||
pidData, err := os.ReadFile("./frpc.pid")
|
||||
pidData, err := os.ReadFile("./data/frpc.pid")
|
||||
if err != nil {
|
||||
cmd := exec.Command("pkill", "-f", "frpc")
|
||||
if err := cmd.Run(); err != nil && !strings.Contains(err.Error(), "no process") {
|
||||
@@ -256,7 +248,7 @@ func StopFrpc() error {
|
||||
pid, _ := strconv.Atoi(strings.TrimSpace(string(pidData)))
|
||||
process, err := os.FindProcess(pid)
|
||||
if err != nil {
|
||||
os.Remove("./frpc.pid")
|
||||
os.Remove("./data/frpc.pid")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -264,7 +256,7 @@ func StopFrpc() error {
|
||||
return fmt.Errorf("杀死进程失败: %w", err)
|
||||
}
|
||||
|
||||
os.Remove("./frpc.pid")
|
||||
os.Remove("./data/frpc.pid")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -283,7 +275,7 @@ func ReloadFrpc() error {
|
||||
return fmt.Errorf("获取 frpc 路径失败: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(frpcPath, "reload", "-c", "./frpc.toml")
|
||||
cmd := exec.Command(frpcPath, "reload", "-c", "./data/frpc.toml")
|
||||
_, err = cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ 热加载失败 (%v),自动降级为重启 frpc", err)
|
||||
@@ -297,3 +289,77 @@ func ReloadFrpc() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readTailLog 读取文件末尾 n 行
|
||||
func readTailLog(filePath string, n int) ([]string, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileSize := info.Size()
|
||||
if fileSize == 0 {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
const chunkSize = 4096
|
||||
var lines []string
|
||||
var leftover []byte
|
||||
offset := fileSize
|
||||
|
||||
for len(lines) < n && offset > 0 {
|
||||
readSize := chunkSize
|
||||
if offset < int64(chunkSize) {
|
||||
readSize = int(offset)
|
||||
}
|
||||
offset -= int64(readSize)
|
||||
|
||||
buf := make([]byte, readSize)
|
||||
_, err := file.ReadAt(buf, offset)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data := append(buf, leftover...)
|
||||
leftover = nil
|
||||
|
||||
start := 0
|
||||
for i := len(data) - 1; i >= 0; i-- {
|
||||
if data[i] == '\n' {
|
||||
if i+1 < len(data) {
|
||||
line := string(data[i+1:])
|
||||
if line != "" {
|
||||
lines = append([]string{line}, lines...)
|
||||
if len(lines) >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
start = i
|
||||
}
|
||||
}
|
||||
|
||||
if len(lines) < n && start > 0 {
|
||||
leftover = data[:start]
|
||||
}
|
||||
}
|
||||
|
||||
if len(lines) < n && len(leftover) > 0 {
|
||||
parts := strings.Split(string(leftover), "\n")
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if parts[i] != "" {
|
||||
lines = append([]string{parts[i]}, lines...)
|
||||
if len(lines) >= n {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return lines, nil
|
||||
}
|
||||
|
||||
@@ -8,8 +8,13 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 读取 version.ini 显示版本(如果存在)
|
||||
if data, err := os.ReadFile("./version.ini"); err == nil {
|
||||
// 确保 data 目录存在
|
||||
if err := os.MkdirAll("./data", 0755); err != nil {
|
||||
log.Printf("⚠️ 创建 data 目录失败: %v", err)
|
||||
}
|
||||
|
||||
// 读取 version.ini 显示版本
|
||||
if data, err := os.ReadFile("./data/version.ini"); err == nil {
|
||||
version := strings.TrimSpace(string(data))
|
||||
log.Printf("📌 版本: %s", version)
|
||||
} else {
|
||||
@@ -43,6 +48,7 @@ func main() {
|
||||
func startWatchdog() {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for range ticker.C {
|
||||
if !isFrpcRunning() {
|
||||
log.Println("⚠️ frpc 进程已停止,自动重启...")
|
||||
|
||||
Reference in New Issue
Block a user