feat: create standalone native fnOS repository
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
package nginx
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/havline/havline/internal/config"
|
||||
"github.com/havline/havline/internal/fsutil"
|
||||
)
|
||||
|
||||
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 fsutil.WriteFileAtomic(targetPath, data, 0o644)
|
||||
}
|
||||
|
||||
func WriteGlobalCustom(cfg config.Config, content string) error {
|
||||
if err := EnsureCustomDirs(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return fsutil.WriteFileAtomic(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 fsutil.WriteFileAtomic(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(`# 以下部分由 Havline 自动生成,请勿在此编辑
|
||||
|
||||
# 主进程
|
||||
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 havline_access '$time_iso8601 $host $server_port $request_method $request_uri $status $request_time $remote_addr $upstream_addr $request_length $bytes_sent';
|
||||
access_log %s havline_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")))
|
||||
}
|
||||
Reference in New Issue
Block a user