647 lines
16 KiB
Go
647 lines
16 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
//go:embed static/*
|
|
var staticFS embed.FS
|
|
|
|
func SetupRouter() *gin.Engine {
|
|
r := gin.Default()
|
|
|
|
// 从 embed 读取前端静态文件
|
|
staticSubFS, _ := fs.Sub(staticFS, "static")
|
|
r.StaticFS("/static", http.FS(staticSubFS))
|
|
|
|
// 根路由
|
|
r.GET("/", func(c *gin.Context) {
|
|
content, err := staticFS.ReadFile("static/index.html")
|
|
if err != nil {
|
|
c.String(500, "加载前端页面失败")
|
|
return
|
|
}
|
|
c.Data(http.StatusOK, "text/html; charset=utf-8", content)
|
|
})
|
|
|
|
// 健康检查
|
|
r.GET("/ping", func(c *gin.Context) {
|
|
c.String(200, "frpc-console 后端已启动 🎉")
|
|
})
|
|
|
|
api := r.Group("/api")
|
|
{
|
|
// ---- 公开路由(不需要认证) ----
|
|
api.GET("/check/users", checkUsersHandler)
|
|
api.POST("/register", registerHandler)
|
|
api.POST("/login", loginHandler)
|
|
api.GET("/ping", pingHandler) // ← 移到这里
|
|
|
|
// ---- 需要认证的路由 ----
|
|
auth := api.Group("/")
|
|
auth.Use(AuthMiddleware())
|
|
{
|
|
auth.GET("/config", getConfigHandler)
|
|
auth.PUT("/config", updateConfigHandler)
|
|
|
|
auth.GET("/proxies", getProxiesHandler)
|
|
auth.GET("/proxy/:id", getProxyHandler)
|
|
auth.POST("/proxy", createProxyHandler)
|
|
auth.PUT("/proxy/:id", updateProxyHandler)
|
|
auth.DELETE("/proxy/:id", deleteProxyHandler)
|
|
|
|
auth.POST("/frpc/reload", reloadFrpcHandler)
|
|
auth.POST("/frpc/start", startFrpcHandler)
|
|
auth.POST("/frpc/stop", stopFrpcHandler)
|
|
auth.GET("/frpc/status", getFrpcStatusHandler)
|
|
auth.GET("/frpc/log", getFrpcLogHandler)
|
|
|
|
auth.POST("/import/toml", importTomlHandler)
|
|
auth.GET("/export/toml", ExportTomlHandler)
|
|
|
|
auth.PUT("/user/password", changePasswordHandler)
|
|
}
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// ========== 所有 Handler ==========
|
|
|
|
func checkUsersHandler(c *gin.Context) {
|
|
count, err := CountUsers()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": 0,
|
|
"data": gin.H{"hasUsers": count > 0, "count": count},
|
|
})
|
|
}
|
|
|
|
func registerHandler(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
|
return
|
|
}
|
|
|
|
count, err := CountUsers()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询用户失败"})
|
|
return
|
|
}
|
|
if count > 0 {
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 1, "msg": "已存在管理员账户,请登录"})
|
|
return
|
|
}
|
|
|
|
if len(req.Username) < 5 {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "用户名至少 5 位"})
|
|
return
|
|
}
|
|
|
|
if !ValidatePassword(req.Password) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"})
|
|
return
|
|
}
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"})
|
|
return
|
|
}
|
|
|
|
if err := CreateUser(req.Username, string(hash)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建用户失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
token, err := GenerateJWT(req.Username)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": 0,
|
|
"msg": "注册成功",
|
|
"data": gin.H{"token": token},
|
|
})
|
|
}
|
|
|
|
// Ping 延迟检测
|
|
func pingHandler(c *gin.Context) {
|
|
target := c.Query("target")
|
|
if target == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "缺少 target 参数"})
|
|
return
|
|
}
|
|
|
|
// 获取全局配置(获取服务端端口)
|
|
cfg, err := GetGlobalConfig()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"})
|
|
return
|
|
}
|
|
|
|
port := cfg.ServerPort
|
|
address := net.JoinHostPort(target, strconv.Itoa(port))
|
|
|
|
start := time.Now()
|
|
conn, err := net.DialTimeout("tcp", address, 5*time.Second)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{"code": 1, "msg": "ping 失败", "latency": -1})
|
|
return
|
|
}
|
|
conn.Close()
|
|
|
|
latency := time.Since(start).Milliseconds()
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "latency": latency})
|
|
}
|
|
|
|
func loginHandler(c *gin.Context) {
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
|
return
|
|
}
|
|
|
|
user, err := GetUserByUsername(req.Username)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
|
|
return
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "用户名或密码错误"})
|
|
return
|
|
}
|
|
|
|
token, err := GenerateJWT(user.Username)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成Token失败"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": 0,
|
|
"msg": "登录成功",
|
|
"data": gin.H{"token": token},
|
|
})
|
|
}
|
|
|
|
func changePasswordHandler(c *gin.Context) {
|
|
var req struct {
|
|
OldPassword string `json:"oldPassword"`
|
|
NewPassword string `json:"newPassword"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
|
return
|
|
}
|
|
|
|
username, exists := c.Get("username")
|
|
if !exists {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"code": 1, "msg": "未登录"})
|
|
return
|
|
}
|
|
|
|
user, err := GetUserByUsername(username.(string))
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "用户不存在"})
|
|
return
|
|
}
|
|
|
|
if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.OldPassword)); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "当前密码错误"})
|
|
return
|
|
}
|
|
|
|
if !ValidatePassword(req.NewPassword) {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "密码至少 8 位,需包含大小写字母、数字和特殊字符"})
|
|
return
|
|
}
|
|
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "密码加密失败"})
|
|
return
|
|
}
|
|
|
|
if err := UpdatePassword(username.(string), string(hash)); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新密码失败"})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "密码修改成功"})
|
|
}
|
|
|
|
func getConfigHandler(c *gin.Context) {
|
|
cfg, err := GetGlobalConfig()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": cfg})
|
|
}
|
|
|
|
func updateConfigHandler(c *gin.Context) {
|
|
var cfg GlobalConfig
|
|
if err := c.ShouldBindJSON(&cfg); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
|
return
|
|
}
|
|
cfg.TcpMux = true
|
|
|
|
if err := UpdateGlobalConfig(&cfg); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := GenerateFrpcConfig(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置文件失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := ReloadFrpc(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "配置更新成功"})
|
|
}
|
|
|
|
func getProxiesHandler(c *gin.Context) {
|
|
proxies, err := GetProxies()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道列表失败"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": proxies})
|
|
}
|
|
|
|
func getProxyHandler(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
|
return
|
|
}
|
|
p, err := GetProxy(id)
|
|
if err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"code": 1, "msg": "隧道不存在"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": p})
|
|
}
|
|
|
|
func createProxyHandler(c *gin.Context) {
|
|
var p Proxy
|
|
if err := c.ShouldBindJSON(&p); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
|
return
|
|
}
|
|
p.Enabled = true
|
|
|
|
if err := CreateProxy(&p); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "创建隧道失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := generateAndReload(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道创建成功", "data": gin.H{"id": p.ID}})
|
|
}
|
|
|
|
func updateProxyHandler(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
|
return
|
|
}
|
|
var p Proxy
|
|
if err := c.ShouldBindJSON(&p); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请求参数错误"})
|
|
return
|
|
}
|
|
p.ID = id
|
|
|
|
if err := UpdateProxy(&p); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新隧道失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := generateAndReload(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道更新成功"})
|
|
}
|
|
|
|
func deleteProxyHandler(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "无效的ID"})
|
|
return
|
|
}
|
|
|
|
if err := DeleteProxy(id); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "删除隧道失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := generateAndReload(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "配置生效失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "隧道删除成功"})
|
|
}
|
|
|
|
func reloadFrpcHandler(c *gin.Context) {
|
|
if err := GenerateFrpcConfig(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
|
return
|
|
}
|
|
if err := ReloadFrpc(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "热加载失败: " + err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "热加载成功"})
|
|
}
|
|
|
|
func startFrpcHandler(c *gin.Context) {
|
|
if err := StartFrpc(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "启动失败: " + err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 启动成功"})
|
|
}
|
|
|
|
func stopFrpcHandler(c *gin.Context) {
|
|
if err := StopFrpc(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "停止失败: " + err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "msg": "frpc 已停止"})
|
|
}
|
|
|
|
func getFrpcStatusHandler(c *gin.Context) {
|
|
running, err := GetFrpcStatus()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "查询状态失败"})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{"code": 0, "data": gin.H{"running": running}})
|
|
}
|
|
|
|
func importTomlHandler(c *gin.Context) {
|
|
file, err := c.FormFile("file")
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "请选择文件"})
|
|
return
|
|
}
|
|
|
|
f, err := file.Open()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取文件失败"})
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
buf := new(bytes.Buffer)
|
|
if _, err := buf.ReadFrom(f); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取文件失败"})
|
|
return
|
|
}
|
|
|
|
parsed, err := ParseToml(buf.String())
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 1, "msg": "解析 TOML 失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
cfg := parsed.ToGlobalConfig()
|
|
cfg.TcpMux = true
|
|
if err := UpdateGlobalConfig(cfg); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "更新配置失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if _, err := DB.Exec("DELETE FROM proxies"); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "清空隧道失败"})
|
|
return
|
|
}
|
|
|
|
proxies := parsed.ToProxies()
|
|
for _, p := range proxies {
|
|
if err := CreateProxy(&p); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "导入隧道失败: " + err.Error()})
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := GenerateFrpcConfig(); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "生成配置失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if err := ReloadFrpc(); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": 0,
|
|
"msg": fmt.Sprintf("导入成功!共 %d 条隧道,但热加载失败: %s", len(proxies), err.Error()),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": 0,
|
|
"msg": fmt.Sprintf("导入成功!共 %d 条隧道", len(proxies)),
|
|
})
|
|
}
|
|
|
|
func ExportTomlHandler(c *gin.Context) {
|
|
cfg, err := GetGlobalConfig()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取配置失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
proxies, err := GetProxies()
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "读取隧道失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
var activeProxies []Proxy
|
|
for _, p := range proxies {
|
|
if p.Enabled {
|
|
activeProxies = append(activeProxies, p)
|
|
}
|
|
}
|
|
|
|
data := struct {
|
|
*GlobalConfig
|
|
Proxies []Proxy
|
|
}{
|
|
GlobalConfig: cfg,
|
|
Proxies: activeProxies,
|
|
}
|
|
|
|
tmpl, err := template.New("frpc").Parse(FrpcTemplateContent)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "解析模板失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := tmpl.Execute(&buf, data); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 2, "msg": "渲染模板失败: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
c.Header("Content-Type", "text/plain; charset=utf-8")
|
|
c.Header("Content-Disposition", "attachment; filename=frpc.toml")
|
|
c.String(http.StatusOK, buf.String())
|
|
}
|
|
|
|
// ========== 日志读取 ==========
|
|
|
|
func getFrpcLogHandler(c *gin.Context) {
|
|
// 读取 ./frpc.log,最多返回 200 行(最新的 200 行)
|
|
lines, err := readTailLog("./frpc.log", 200)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": 0,
|
|
"data": gin.H{
|
|
"lines": []string{},
|
|
"total": 0,
|
|
"error": err.Error(),
|
|
},
|
|
})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"code": 0,
|
|
"data": gin.H{
|
|
"lines": lines,
|
|
"total": len(lines),
|
|
},
|
|
})
|
|
}
|
|
|
|
// 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 {
|
|
// 从 leftover 中提取行
|
|
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
|
|
}
|
|
|
|
func generateAndReload() error {
|
|
if err := GenerateFrpcConfig(); err != nil {
|
|
return err
|
|
}
|
|
return ReloadFrpc()
|
|
}
|