platform: modularize api/gui, add docs-tests-web foundation, and refresh root config
This commit is contained in:
199
selective-vpn-api/app/transportcfg/singbox_helpers.go
Normal file
199
selective-vpn-api/app/transportcfg/singbox_helpers.go
Normal file
@@ -0,0 +1,199 @@
|
||||
package transportcfg
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConfigDiff struct {
|
||||
Added int
|
||||
Removed int
|
||||
Changed int
|
||||
}
|
||||
|
||||
func SingBoxTypedProtocolSupported(proto string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(proto)) {
|
||||
case "vless", "trojan", "shadowsocks", "wireguard", "hysteria2", "tuic":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func ParsePort(v any) (int, bool) {
|
||||
switch x := v.(type) {
|
||||
case int:
|
||||
return x, true
|
||||
case int64:
|
||||
return int(x), true
|
||||
case float64:
|
||||
return int(x), true
|
||||
case string:
|
||||
s := strings.TrimSpace(x)
|
||||
if s == "" {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return n, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
func DigestJSONMap(config map[string]any) string {
|
||||
if config == nil {
|
||||
return ""
|
||||
}
|
||||
b, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
h := sha256.Sum256(b)
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
func DiffConfigMaps(prev, next map[string]any) (ConfigDiff, bool) {
|
||||
diff := ConfigDiff{}
|
||||
changed := false
|
||||
if prev == nil {
|
||||
diff.Added = len(next)
|
||||
return diff, diff.Added > 0
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for k, pv := range prev {
|
||||
seen[k] = struct{}{}
|
||||
nv, ok := next[k]
|
||||
if !ok {
|
||||
diff.Removed++
|
||||
changed = true
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(pv, nv) {
|
||||
diff.Changed++
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
for k := range next {
|
||||
if _, ok := seen[k]; ok {
|
||||
continue
|
||||
}
|
||||
diff.Added++
|
||||
changed = true
|
||||
}
|
||||
return diff, changed
|
||||
}
|
||||
|
||||
func ProfileConfigPath(rootDir, profileID string, sanitizeID func(string) string) string {
|
||||
id := profileID
|
||||
if sanitizeID != nil {
|
||||
id = sanitizeID(id)
|
||||
}
|
||||
if strings.TrimSpace(id) == "" {
|
||||
id = "profile"
|
||||
}
|
||||
return filepath.Join(strings.TrimSpace(rootDir), id+".json")
|
||||
}
|
||||
|
||||
func WriteJSONConfigFile(path string, config map[string]any) error {
|
||||
data, err := json.MarshalIndent(config, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, append(data, '\n'), 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
func ReadJSONMapFile(path string) map[string]any {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(data, &out); err != nil {
|
||||
return nil
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func ReadFileOptional(path string) ([]byte, bool, error) {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, false, nil
|
||||
}
|
||||
return nil, false, err
|
||||
}
|
||||
return data, true, nil
|
||||
}
|
||||
|
||||
func RestoreFileOptional(path string, data []byte, exists bool) error {
|
||||
if !exists {
|
||||
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := path + ".tmp"
|
||||
if err := os.WriteFile(tmp, data, 0o644); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, path)
|
||||
}
|
||||
|
||||
func SanitizeHistoryStamp(ts string, now time.Time) string {
|
||||
s := strings.TrimSpace(ts)
|
||||
if s == "" {
|
||||
s = now.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
repl := strings.NewReplacer(":", "", "-", "", "T", "_", "Z", "", ".", "")
|
||||
out := repl.Replace(s)
|
||||
out = strings.Trim(out, "_")
|
||||
if out == "" {
|
||||
out = strconv.FormatInt(now.UTC().UnixNano(), 10)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func JoinMessages(messages []string) string {
|
||||
if len(messages) == 0 {
|
||||
return ""
|
||||
}
|
||||
parts := make([]string, 0, len(messages))
|
||||
for _, msg := range messages {
|
||||
v := strings.TrimSpace(msg)
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, v)
|
||||
}
|
||||
return strings.Join(parts, "; ")
|
||||
}
|
||||
|
||||
func SortRecordsDescByAt[T any](items []T, extractAt func(T) string) {
|
||||
if len(items) < 2 || extractAt == nil {
|
||||
return
|
||||
}
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return extractAt(items[i]) > extractAt(items[j])
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user