Files
Havline/internal/nginx/custom.go
T

260 lines
6.3 KiB
Go
Raw Normal View History

2026-09-23 09:40:59 +08:00
package nginx
import (
"fmt"
"os"
"path/filepath"
"sort"
"strings"
"time"
"github.com/fonu/fonu/internal/config"
)
const maxCustomBackups = 10
type BackupEntry struct {
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
}
func EnsureCustomDirs(cfg config.Config) error {
for _, dir := range []string{
cfg.NginxCustomDir(),
cfg.NginxRulesDir(),
cfg.NginxGlobalBackupsDir(),
} {
if err := os.MkdirAll(dir, 0o755); err != nil {
return err
}
}
return nil
}
func ReadGlobalCustom(cfg config.Config) (string, error) {
data, err := os.ReadFile(cfg.NginxGlobalCustomPath())
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
return string(data), nil
}
func ReadRuleCustom(cfg config.Config, ruleID int64) (string, error) {
data, err := os.ReadFile(cfg.NginxRuleCustomPath(ruleID))
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
return string(data), nil
}
func ruleCustomContent(cfg config.Config, ruleID int64, opts GenerateOptions) string {
if opts.RuleCustomOverrides != nil {
if override, ok := opts.RuleCustomOverrides[ruleID]; ok {
return override
}
}
content, _ := ReadRuleCustom(cfg, ruleID)
return content
}
func globalCustomPath(cfg config.Config, opts GenerateOptions) string {
if opts.GlobalCustomPathOverride != "" {
return opts.GlobalCustomPathOverride
}
return cfg.NginxGlobalCustomPath()
}
func globalCustomShouldInclude(cfg config.Config, opts GenerateOptions) bool {
if strings.TrimSpace(opts.GlobalCustomOverride) != "" {
return true
}
if opts.GlobalCustomPathOverride != "" {
data, err := os.ReadFile(opts.GlobalCustomPathOverride)
return err == nil && strings.TrimSpace(string(data)) != ""
}
content, _ := ReadGlobalCustom(cfg)
return strings.TrimSpace(content) != ""
}
func BackupFile(srcPath, backupDir string) (string, error) {
if err := os.MkdirAll(backupDir, 0o755); err != nil {
return "", err
}
data, err := os.ReadFile(srcPath)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
name := time.Now().UTC().Format("20060102T150405.000Z") + ".conf"
dst := filepath.Join(backupDir, name)
if err := os.WriteFile(dst, data, 0o644); err != nil {
return "", err
}
if err := pruneBackups(backupDir, maxCustomBackups); err != nil {
return "", err
}
return name, nil
}
func ListBackups(backupDir string) ([]BackupEntry, error) {
if err := os.MkdirAll(backupDir, 0o755); err != nil {
return nil, err
}
entries, err := os.ReadDir(backupDir)
if err != nil {
return nil, err
}
var out []BackupEntry
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".conf") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
out = append(out, BackupEntry{Name: entry.Name(), CreatedAt: info.ModTime().UTC()})
}
sort.Slice(out, func(i, j int) bool {
return out[i].Name > out[j].Name
})
return out, nil
}
func RestoreBackup(backupDir, backupName, targetPath string) error {
src := filepath.Join(backupDir, backupName)
if _, err := os.Stat(src); err != nil {
return fmt.Errorf("备份不存在")
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0o755); err != nil {
return err
}
data, err := os.ReadFile(src)
if err != nil {
return err
}
return os.WriteFile(targetPath, data, 0o644)
}
func WriteGlobalCustom(cfg config.Config, content string) error {
if err := EnsureCustomDirs(cfg); err != nil {
return err
}
return os.WriteFile(cfg.NginxGlobalCustomPath(), []byte(content), 0o644)
}
func WriteRuleCustom(cfg config.Config, ruleID int64, content string) error {
if err := os.MkdirAll(cfg.NginxRulesDir(), 0o755); err != nil {
return err
}
return os.WriteFile(cfg.NginxRuleCustomPath(ruleID), []byte(content), 0o644)
}
func RemoveRuleCustom(cfg config.Config, ruleID int64) error {
path := cfg.NginxRuleCustomPath(ruleID)
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return err
}
backupDir := cfg.NginxRuleBackupsDir(ruleID)
_ = os.RemoveAll(backupDir)
return nil
}
func SaveGlobalCustomWithBackup(cfg config.Config, content string) (string, error) {
path := cfg.NginxGlobalCustomPath()
var backupName string
if _, err := os.Stat(path); err == nil {
name, err := BackupFile(path, cfg.NginxGlobalBackupsDir())
if err != nil {
return "", err
}
backupName = name
}
if err := WriteGlobalCustom(cfg, content); err != nil {
return "", err
}
return backupName, nil
}
func SaveRuleCustomWithBackup(cfg config.Config, ruleID int64, content string) (string, error) {
path := cfg.NginxRuleCustomPath(ruleID)
var backupName string
if _, err := os.Stat(path); err == nil {
name, err := BackupFile(path, cfg.NginxRuleBackupsDir(ruleID))
if err != nil {
return "", err
}
backupName = name
}
if err := WriteRuleCustom(cfg, ruleID, content); err != nil {
return "", err
}
return backupName, nil
}
func pruneBackups(dir string, keep int) error {
entries, err := ListBackups(dir)
if err != nil {
return err
}
for i := keep; i < len(entries); i++ {
_ = os.Remove(filepath.Join(dir, entries[i].Name))
}
return nil
}
func DefaultGlobalHTTPSnippet() string {
return `# 全局 http 块片段(自动生成)
# 上传大小限制(全局默认 50M,单条规则可在 server 块覆盖)
client_max_body_size 50m;
`
}
func GenerateGlobalFramework(cfg config.Config) string {
return fmt.Sprintf(`# 以下部分由 Fonu 自动生成,请勿在此编辑
# 主进程
worker_processes auto;
# 错误日志
error_log %s warn;
# PID 文件
pid %s;
# 事件模型
events {
worker_connections 1024;
}
http {
# MIME 类型
include %s;
default_type application/octet-stream;
# 访问日志格式与路径
log_format fonu_access '$time_iso8601 $host $server_port $request_method $request_uri $status $request_time $remote_addr $upstream_addr $request_length $bytes_sent';
access_log %s fonu_access;
# 传输优化
sendfile on;
keepalive_timeout 65;
# WebSocket 升级映射
# map $http_upgrade $connection_upgrade { ... }
# limit_req_zone / geo / real_ip 等由规则与系统设置动态生成
# 全局自定义片段插入在上述动态块之后、各 server {} 之前
}
`, absNginxPath(filepath.Join(cfg.LogsDir(), "error.log")),
absNginxPath(cfg.NginxPIDFile),
absNginxPath(cfg.NginxMimeTypes),
absNginxPath(filepath.Join(cfg.LogsDir(), "access.log")))
}