From 48a5c01142d64e80dd413033f2d698f456e4fc4e Mon Sep 17 00:00:00 2001 From: snakem982 Date: Wed, 11 Dec 2024 16:01:36 +0800 Subject: [PATCH] feat: Support config file export feat: Support config file import feat: Windows close button minimizes the window refactor: Crawl config file export chore: upgrade dependencies --- app.go | 112 ++++++++++++- app_others.go | 111 ++++++++++++- backend/cache/cache.go | 67 ++++++++ backend/constant/constant.go | 2 + backend/meta/meta.go | 54 +++++++ backend/system/admin/isadmin.go | 28 +++- backend/system/admin/isadmin_windows.go | 30 ++++ backend/tools/zip.go | 181 ++++++++++++++++++++++ frontend/src/views/General.vue | 76 ++++++++- frontend/src/weight/FilterAndDownload.vue | 29 +++- frontend/wailsjs/go/main/App.d.ts | 26 ++-- frontend/wailsjs/go/main/App.js | 34 ++-- go.mod | 8 +- go.sum | 14 +- main.go => main_darwin.go | 41 ++--- main_linux.go | 126 +++++++++++++++ main_windows.go | 127 +++++++++++++++ 17 files changed, 992 insertions(+), 74 deletions(-) create mode 100644 backend/tools/zip.go rename main.go => main_darwin.go (81%) create mode 100644 main_linux.go create mode 100644 main_windows.go diff --git a/app.go b/app.go index fd8b5ff..b7521b1 100644 --- a/app.go +++ b/app.go @@ -5,7 +5,10 @@ package main import ( "context" "github.com/keybase/go-keychain" + C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" + "github.com/wailsapp/wails/v2/pkg/runtime" + "os" "os/exec" "pandora-box/backend/cache" "pandora-box/backend/constant" @@ -13,7 +16,8 @@ import ( isadmin "pandora-box/backend/system/admin" "pandora-box/backend/system/open" "pandora-box/backend/system/update" - "runtime" + "pandora-box/backend/tools" + "path/filepath" "strings" ) @@ -35,11 +39,7 @@ func (a *App) startup(ctx context.Context) { } func (a *App) IsMac() string { - if runtime.GOOS == "darwin" { - return "true" - } - - return "false" + return "true" } func (a *App) GetMacAcStatus() string { @@ -160,3 +160,103 @@ func (a *App) IsNeedUpdate() string { return "false" } + +func (a *App) ImportConfig() string { + selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "选择文件 Select File", + Filters: []runtime.FileFilter{ + { + DisplayName: "文件类型 Type (*.zip)", + Pattern: "*.zip", + }, + }, + }) + + if err != nil || selection == "" { + return "false" + } + + secret := cache.Get(constant.SecretKey) + + err = meta.Recovery(selection) + + if err != nil { + return err.Error() + } + + if secret != nil { + _ = cache.Put(constant.SecretKey, secret) + } + + return "true" +} + +func (a *App) ExportConfig() string { + homeDir := filepath.Dir(C.Path.HomeDir()) + desktopPath := filepath.Join(homeDir, "Downloads") + _, err := os.Stat(desktopPath) + if !os.IsNotExist(err) { + homeDir = desktopPath + } + + selection, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "选择导出位置 Select Export Directory", + DefaultDirectory: homeDir, + DefaultFilename: "Pandora-Box-Config.zip", + Filters: []runtime.FileFilter{ + { + DisplayName: "文件类型 Type (*.zip)", + Pattern: "*.zip", + }, + }, + }) + + if err != nil || selection == "" { + return "false" + } + + err = meta.Dump(selection) + + if err != nil { + return err.Error() + } + + return "true" +} + +func (a *App) SfQuit() { + runtime.Quit(a.ctx) +} + +func (a *App) ExportCrawl() string { + + homeDir := filepath.Dir(C.Path.HomeDir()) + desktopPath := filepath.Join(homeDir, "Downloads") + _, err := os.Stat(desktopPath) + if !os.IsNotExist(err) { + homeDir = desktopPath + } + + selection, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "选择导出位置 Select Export Directory", + DefaultDirectory: homeDir, + DefaultFilename: "Pandora-Box-Config.yaml", + Filters: []runtime.FileFilter{ + { + DisplayName: "文件类型 Type (*.yaml)", + Pattern: "*.yaml", + }, + }, + }) + + if err != nil || selection == "" { + return "false" + } + + err = tools.CopyFile(filepath.Join(C.Path.HomeDir(), constant.DefaultDownload), selection) + if err != nil { + return err.Error() + } + + return "true" +} diff --git a/app_others.go b/app_others.go index 706e29c..0214486 100644 --- a/app_others.go +++ b/app_others.go @@ -4,14 +4,18 @@ package main import ( "context" + C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" + "github.com/wailsapp/wails/v2/pkg/runtime" + "os" "pandora-box/backend/cache" "pandora-box/backend/constant" "pandora-box/backend/meta" isadmin "pandora-box/backend/system/admin" "pandora-box/backend/system/open" "pandora-box/backend/system/update" - "runtime" + "pandora-box/backend/tools" + "path/filepath" ) // App struct @@ -32,10 +36,6 @@ func (a *App) startup(ctx context.Context) { } func (a *App) IsMac() string { - if runtime.GOOS == "darwin" { - return "true" - } - return "false" } @@ -95,3 +95,104 @@ func (a *App) IsNeedUpdate() string { return "false" } + +func (a *App) ImportConfig() string { + selection, err := runtime.OpenFileDialog(a.ctx, runtime.OpenDialogOptions{ + Title: "选择文件 Select File", + Filters: []runtime.FileFilter{ + { + DisplayName: "文件类型 Type (*.zip)", + Pattern: "*.zip", + }, + }, + }) + + if err != nil || selection == "" { + return "false" + } + + secret := cache.Get(constant.SecretKey) + + err = meta.Recovery(selection) + + if err != nil { + return err.Error() + } + + if secret != nil { + _ = cache.Put(constant.SecretKey, secret) + } + + return "true" +} + +func (a *App) ExportConfig() string { + homeDir := filepath.Dir(C.Path.HomeDir()) + desktopPath := filepath.Join(homeDir, "Desktop") + _, err := os.Stat(desktopPath) + if !os.IsNotExist(err) { + homeDir = desktopPath + } + + selection, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "选择导出位置 Select Export Directory", + DefaultDirectory: homeDir, + DefaultFilename: "Pandora-Box-Config.zip", + Filters: []runtime.FileFilter{ + { + DisplayName: "文件类型 Type (*.zip)", + Pattern: "*.zip", + }, + }, + }) + + if err != nil || selection == "" { + return "false" + } + + err = meta.Dump(selection) + + if err != nil { + return err.Error() + } + + return "true" +} + +func (a *App) SfQuit() { + _ = cache.Put(constant.QuitSignal, []byte("1")) + runtime.Quit(a.ctx) +} + +func (a *App) ExportCrawl() string { + + homeDir := filepath.Dir(C.Path.HomeDir()) + desktopPath := filepath.Join(homeDir, "Desktop") + _, err := os.Stat(desktopPath) + if !os.IsNotExist(err) { + homeDir = desktopPath + } + + selection, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{ + Title: "选择导出位置 Select Export Directory", + DefaultDirectory: homeDir, + DefaultFilename: "Pandora-Box-Config.yaml", + Filters: []runtime.FileFilter{ + { + DisplayName: "文件类型 Type (*.yaml)", + Pattern: "*.yaml", + }, + }, + }) + + if err != nil || selection == "" { + return "false" + } + + err = tools.CopyFile(filepath.Join(C.Path.HomeDir(), constant.DefaultDownload), selection) + if err != nil { + return err.Error() + } + + return "true" +} diff --git a/backend/cache/cache.go b/backend/cache/cache.go index 22e5710..4ea11a2 100644 --- a/backend/cache/cache.go +++ b/backend/cache/cache.go @@ -1,7 +1,11 @@ package cache import ( + "fmt" "github.com/metacubex/bbolt" + "github.com/metacubex/mihomo/log" + "os" + "pandora-box/backend/constant" "strings" ) @@ -65,3 +69,66 @@ func DeleteList(m map[string]any) error { }) }) } + +func Dump(dstDBPath string) error { + _, err := os.Stat(dstDBPath) + if !os.IsNotExist(err) { + _ = os.Remove(dstDBPath) + } + + // 创建目标数据库 + dstDB, err := bbolt.Open(dstDBPath, 0600, nil) + if err != nil { + return err + } + defer dstDB.Close() + + return dstDB.Batch(func(tx *bbolt.Tx) error { + newBucket, err := tx.CreateBucketIfNotExists(BName) + if err != nil { + log.Warnln("[DumpFile] can't create bucket: %s", err.Error()) + return fmt.Errorf("create bucket: %v", err) + } + + return BDb.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(BName) + return b.ForEach(func(k, v []byte) error { + key := string(k) + if key == constant.SecretKey { + return nil + } + + if key == constant.QuitSignal { + return nil + } + + if !strings.HasPrefix(key, constant.RealIpHeader) { + return newBucket.Put(k, v) + } + + return nil + }) + }) + }) +} + +func Recovery(srcDBPath string) error { + _, err := os.Stat(srcDBPath) + if os.IsNotExist(err) { + return err + } + + // 打开源数据库 + srcDB, err := bbolt.Open(srcDBPath, 0600, nil) + if err != nil { + return err + } + defer srcDB.Close() + + return srcDB.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(BName) + return b.ForEach(func(k, v []byte) error { + return Put(string(k), v) + }) + }) +} diff --git a/backend/constant/constant.go b/backend/constant/constant.go index 8355824..a399e46 100644 --- a/backend/constant/constant.go +++ b/backend/constant/constant.go @@ -10,6 +10,8 @@ const ( PrefixGetter = "Getter_" RealIpHeader = "RealIp_" SecretKey = "SecretKey_pb" + RecoverTmp = "RecoverTmp" + QuitSignal = "QuitSignal" ) const ( diff --git a/backend/meta/meta.go b/backend/meta/meta.go index 49f0c8d..47a4f58 100644 --- a/backend/meta/meta.go +++ b/backend/meta/meta.go @@ -215,3 +215,57 @@ func StartCore(profile resolve.Profile, reload bool) { executor.ApplyConfig(NowConfig, !reload) } + +func Dump(dst string) error { + + _, err := os.Stat(dst) + if !os.IsNotExist(err) { + _ = os.Remove(dst) + } + + base := C.Path.HomeDir() + + dump := filepath.Join(base, "dump.db") + err = cache.Dump(dump) + if err != nil { + return err + } + + exclude := []string{ + "geoip.metadb", + "cache.db", + "log.log", + "Cloudflare.yaml", + ".DS_Store", + } + + err = tools.ZipDirectory(base, dst, exclude) + _ = os.Remove(dump) + if err != nil { + return err + } + + return nil +} + +func Recovery(src string) error { + // 临时目录 + tmp := filepath.Join(C.Path.HomeDir(), constant.RecoverTmp) + err := tools.Unzip(src, tmp) + if err != nil { + return err + } + srcDb := filepath.Join(tmp, "dump.db") + err = cache.Recovery(srcDb) + if err != nil { + return err + } + _ = os.Remove(srcDb) + _ = tools.CopyDirectory(tmp, C.Path.HomeDir()) + + SwitchProfile(false) + + _ = os.RemoveAll(tmp) + + return nil +} diff --git a/backend/system/admin/isadmin.go b/backend/system/admin/isadmin.go index 4e54520..1782d86 100644 --- a/backend/system/admin/isadmin.go +++ b/backend/system/admin/isadmin.go @@ -2,9 +2,35 @@ package isadmin -import "os" +import ( + "fmt" + "os" + "os/exec" + "strings" +) // Check if the program has administrative privileges. func Check() bool { return os.Getuid() == 0 } + +// KillProcessesByName 杀死所有名字为指定名称的进程 +func KillProcessesByName(name string) error { + // 使用 ps 和 grep 命令来查找进程 + cmd := exec.Command("bash", "-c", fmt.Sprintf("ps aux | grep %s | grep -v grep | awk '{print $2}'", name)) + output, err := cmd.Output() + if err != nil { + return err + } + + pids := strings.Fields(string(output)) + for _, pid := range pids { + // 使用 kill 命令来杀死进程 + killCmd := exec.Command("kill", "-9", pid) + err := killCmd.Run() + if err != nil { + return err + } + } + return nil +} diff --git a/backend/system/admin/isadmin_windows.go b/backend/system/admin/isadmin_windows.go index 44582f1..b3986f1 100644 --- a/backend/system/admin/isadmin_windows.go +++ b/backend/system/admin/isadmin_windows.go @@ -4,6 +4,8 @@ package isadmin import ( "golang.org/x/sys/windows" + "pandora-box/backend/system/proxy" + "strings" ) // Check if the program has administrative privileges. @@ -22,3 +24,31 @@ func Check() bool { return admin } + +// KillProcessesByName 杀死所有名字为指定名称的进程 +func KillProcessesByName(name string) error { + // 使用 tasklist 命令查找进程 + output, err := proxy.Command("tasklist") + if err != nil { + return err + } + + lines := strings.Split(string(output), "\n") + for _, line := range lines { + line = strings.ToLower(line) + name = strings.ToLower(name) + if strings.Contains(line, name) { + fields := strings.Fields(line) + if len(fields) > 1 { + pid := fields[1] + + // 使用 taskkill 命令来杀死进程 + _, err = proxy.Command("taskkill", "/F", "/PID", pid) + if err != nil { + return err + } + } + } + } + return nil +} diff --git a/backend/tools/zip.go b/backend/tools/zip.go new file mode 100644 index 0000000..302f42c --- /dev/null +++ b/backend/tools/zip.go @@ -0,0 +1,181 @@ +package tools + +import ( + "github.com/klauspost/compress/zip" + "github.com/metacubex/mihomo/log" + "io" + "os" + "path/filepath" + "strings" +) + +// ZipDirectory 压缩指定目录及其所有子文件夹和文件 +func ZipDirectory(sourceDir, outputZip string, exclude []string) error { + zipFile, err := os.Create(outputZip) + if err != nil { + return err + } + defer zipFile.Close() + + zipWriter := zip.NewWriter(zipFile) + defer zipWriter.Close() + + err = filepath.Walk(sourceDir, func(path string, info os.FileInfo, err error) error { + + for _, suffix := range exclude { + if strings.HasSuffix(path, suffix) { + return nil + } + } + + if err != nil { + return err + } + + header, err := zip.FileInfoHeader(info) + if err != nil { + return err + } + header.Name, _ = filepath.Rel(sourceDir, path) + if info.IsDir() { + header.Name += "/" + } else { + header.Method = zip.Deflate + } + + writer, err := zipWriter.CreateHeader(header) + if err != nil { + return err + } + if !info.IsDir() { + file, err := os.Open(path) + if err != nil { + log.Errorln("os.Open path=%s, err=%v", path, err) + return nil + } + defer file.Close() + _, err = io.Copy(writer, file) + if err != nil { + return err + } + } + return nil + }) + + return err +} + +func Unzip(src, dest string) error { + r, err := zip.OpenReader(src) + if err != nil { + return err + } + defer r.Close() + + // 创建目标目录如果不存在 + if _, err := os.Stat(dest); os.IsNotExist(err) { + os.MkdirAll(dest, 0755) + } + + // 解压每个文件 + for _, f := range r.File { + fpath := filepath.Join(dest, f.Name) + + //// 检查文件路径是否存在,避免文件泄露 + //if !strings.HasPrefix(fpath, filepath.Clean(dest)+string(os.PathSeparator)) { + // return fmt.Errorf("illegal file path: %s", fpath) + //} + + if f.FileInfo().IsDir() { + os.MkdirAll(fpath, f.Mode()) + continue + } + + if err = os.MkdirAll(filepath.Dir(fpath), f.Mode()); err != nil { + return err + } + + outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return err + } + + rc, err := f.Open() + if err != nil { + return err + } + + _, err = io.Copy(outFile, rc) + + // 关闭资源 + outFile.Close() + rc.Close() + + if err != nil { + return err + } + } + return nil +} + +// CopyFile 复制文件 +func CopyFile(src, dst string) error { + sourceFile, err := os.Open(src) + if err != nil { + return err + } + defer sourceFile.Close() + + destFile, err := os.Create(dst) + if err != nil { + return err + } + defer destFile.Close() + + _, err = io.Copy(destFile, sourceFile) + if err != nil { + return err + } + + err = destFile.Sync() + if err != nil { + return err + } + + return nil +} + +// CopyDirectory 拷贝目录及其所有子文件夹和文件 +func CopyDirectory(srcDir, dstDir string) error { + return filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + // 计算相对路径并生成目标路径 + relPath, err := filepath.Rel(srcDir, path) + if err != nil { + log.Errorln("filepath.Rel path=%s,srcDir=%s, err=%v", path, srcDir, err) + return nil + } + + dstPath := filepath.Join(dstDir, relPath) + + if info.IsDir() { + // 如果是目录,创建目录 + err = os.MkdirAll(dstPath, info.Mode()) + if err != nil { + log.Errorln("os.MkdirAll path=%s,dstPath=%s, err=%v", path, dstPath, err) + } + return nil + } else { + // 如果是文件,拷贝文件 + err = CopyFile(path, dstPath) + if err != nil { + log.Errorln("CopyFile path=%s,dstPath=%s, err=%v", path, dstPath, err) + } + + return nil + } + }) +} diff --git a/frontend/src/views/General.vue b/frontend/src/views/General.vue index 80beb5b..04dc644 100644 --- a/frontend/src/views/General.vue +++ b/frontend/src/views/General.vue @@ -4,14 +4,16 @@ import {del, get, patch, put} from "../api/http"; import {toggleDark} from "../composables"; import {BrowserOpenURL, ClipboardSetText, WindowSetDarkTheme, WindowSetLightTheme} from "../../wailsjs/runtime"; import { + ExportConfig, GetFreePort, GetMacAcStatus, GetSecret, + ImportConfig, IsAdmin, IsMac, IsNeedUpdate, OpenConfigDirectory, - SetMacAc + SetMacAc, SfQuit } from "../../wailsjs/go/main/App"; import {ElLoading, ElMessage, ElMessageBox} from "element-plus"; @@ -103,6 +105,66 @@ async function openConfig() { await OpenConfigDirectory() } +async function importConfig() { + const loading = ElLoading.service({ + lock: true, + text: '导入中Importing...', + background: 'rgba(0, 0, 0, 0.7)', + }) + const ok = await ImportConfig() + loading.close() + + switch (ok) { + case "false": + break; + case "true": + ElMessage({ + showClose: true, + message: "导入成功 Import Success", + type: 'success', + }) + break; + default: + ElMessage({ + showClose: true, + message: "导入失败 Import failed : " + ok, + type: 'error', + }) + break; + } +} + +async function exportConfig() { + + const loading = ElLoading.service({ + lock: true, + text: '导出中Exporting...', + background: 'rgba(0, 0, 0, 0.7)', + }) + + const ok = await ExportConfig() + loading.close() + + switch (ok) { + case "false": + break; + case "true": + ElMessage({ + showClose: true, + message: "导出成功 Export Success", + type: 'success', + }) + break; + default: + ElMessage({ + showClose: true, + message: "导出失败 Export failed : " + ok, + type: 'error', + }) + break; + } +} + function copyApi() { ClipboardSetText(form.clash_api) ElMessage({ @@ -152,6 +214,11 @@ async function checkUpdate() { } } +async function quit() { + await SfQuit() +} + + const pwdVisible = ref(false) const pwd = ref("") const isMac = ref(false) @@ -333,6 +400,8 @@ onBeforeMount(async () => { 打开 open + 导出 export + 导入 import {{ form.clash_api }}   @@ -342,8 +411,9 @@ onBeforeMount(async () => { {{ form.clash_secret }}   复制 copy - - 检查 check + + 检查更新 check update + 退出软件 quit software diff --git a/frontend/src/weight/FilterAndDownload.vue b/frontend/src/weight/FilterAndDownload.vue index 9eef52b..ad22216 100644 --- a/frontend/src/weight/FilterAndDownload.vue +++ b/frontend/src/weight/FilterAndDownload.vue @@ -5,8 +5,7 @@ import {CheckboxValueType, ElMessage} from 'element-plus' import {mdiShare} from "@mdi/js"; import SvgIcon from "@jamescoyle/vue-icon"; import {get, post} from "../api/http"; -import {BrowserOpenURL} from "../../wailsjs/runtime"; -import {GetFreePort} from "../../wailsjs/go/main/App"; +import {ExportCrawl} from "../../wailsjs/go/main/App"; const protocolCheckAll = ref(false) const protocolIndeterminate = ref(false) @@ -155,10 +154,28 @@ async function Generate() { async function Export() { const msg = await post("/nodeFilter", getReq(4)); - if (msg) { - const baseUrl = await GetFreePort() - BrowserOpenURL('http://' + baseUrl + '/Pandora-Box-Download') - ElMessage.success("导出配置成功,请在浏览器中查看 Export Config Successfully. Please view it in the browser.") + if (!msg) { + ElMessage.error("生成新配置失败 Generate New Config Failed") + return + } + const ok = await ExportCrawl() + switch (ok) { + case "false": + break; + case "true": + ElMessage({ + showClose: true, + message: "导出成功 Export Success", + type: 'success', + }) + break; + default: + ElMessage({ + showClose: true, + message: "导出失败 Export failed : " + ok, + type: 'error', + }) + break; } } diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 68ba523..9f68e8b 100755 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -1,20 +1,28 @@ // Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL // This file is automatically generated. DO NOT EDIT -export function GetFreePort():Promise; +export function GetFreePort(): Promise; -export function GetMacAcStatus():Promise; +export function GetMacAcStatus(): Promise; -export function GetSecret():Promise; +export function GetSecret(): Promise; -export function IsAdmin():Promise; +export function IsAdmin(): Promise; -export function IsMac():Promise; +export function IsMac(): Promise; -export function IsNeedUpdate():Promise; +export function IsNeedUpdate(): Promise; -export function IsUnifiedDelay():Promise; +export function IsUnifiedDelay(): Promise; -export function OpenConfigDirectory():Promise; +export function OpenConfigDirectory(): Promise; -export function SetMacAc(arg1:string):Promise; +export function SetMacAc(arg1: string): Promise; + +export function ImportConfig(): Promise; + +export function ExportConfig(): Promise; + +export function SfQuit(): Promise; + +export function ExportCrawl(): Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index ff9576e..6c20324 100755 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -3,37 +3,53 @@ // This file is automatically generated. DO NOT EDIT export function GetFreePort() { - return window['go']['main']['App']['GetFreePort'](); + return window['go']['main']['App']['GetFreePort'](); } export function GetMacAcStatus() { - return window['go']['main']['App']['GetMacAcStatus'](); + return window['go']['main']['App']['GetMacAcStatus'](); } export function GetSecret() { - return window['go']['main']['App']['GetSecret'](); + return window['go']['main']['App']['GetSecret'](); } export function IsAdmin() { - return window['go']['main']['App']['IsAdmin'](); + return window['go']['main']['App']['IsAdmin'](); } export function IsMac() { - return window['go']['main']['App']['IsMac'](); + return window['go']['main']['App']['IsMac'](); } export function IsNeedUpdate() { - return window['go']['main']['App']['IsNeedUpdate'](); + return window['go']['main']['App']['IsNeedUpdate'](); } export function IsUnifiedDelay() { - return window['go']['main']['App']['IsUnifiedDelay'](); + return window['go']['main']['App']['IsUnifiedDelay'](); } export function OpenConfigDirectory() { - return window['go']['main']['App']['OpenConfigDirectory'](); + return window['go']['main']['App']['OpenConfigDirectory'](); } export function SetMacAc(arg1) { - return window['go']['main']['App']['SetMacAc'](arg1); + return window['go']['main']['App']['SetMacAc'](arg1); +} + +export function ImportConfig() { + return window['go']['main']['App']['ImportConfig'](); +} + +export function ExportConfig() { + return window['go']['main']['App']['ExportConfig'](); +} + +export function SfQuit() { + return window['go']['main']['App']['SfQuit'](); +} + +export function ExportCrawl() { + return window['go']['main']['App']['ExportCrawl'](); } diff --git a/go.mod b/go.mod index a186043..3d3225c 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/go-chi/chi/v5 v5.1.0 github.com/go-chi/render v1.0.3 github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 + github.com/klauspost/compress v1.17.11 github.com/metacubex/bbolt v0.0.0-20240822011022-aed6d4850399 github.com/metacubex/mihomo v1.18.10 github.com/panjf2000/ants/v2 v2.10.0 @@ -28,7 +29,7 @@ require ( github.com/cloudflare/circl v1.5.0 // indirect github.com/coreos/go-iptables v0.8.0 // indirect github.com/dlclark/regexp2 v1.11.4 // indirect - github.com/enfein/mieru/v3 v3.8.3 // indirect + github.com/enfein/mieru/v3 v3.8.4 // indirect github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 // indirect github.com/ericlagergren/polyval v0.0.0-20230805202542-18692a1b76f9 // indirect github.com/ericlagergren/siv v0.0.0-20220507050439-0b757b3aa5f1 // indirect @@ -50,7 +51,6 @@ require ( github.com/insomniacslk/dhcp v0.0.0-20241203100832-a481575ed0ef // indirect github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect github.com/josharian/native v1.1.0 // indirect - github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/labstack/echo/v4 v4.13.0 // indirect @@ -134,14 +134,12 @@ require ( golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.8.0 // indirect golang.org/x/tools v0.28.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 // indirect - google.golang.org/grpc v1.68.1 // indirect google.golang.org/protobuf v1.35.2 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect lukechampine.com/blake3 v1.3.0 // indirect ) replace ( - github.com/metacubex/mihomo => github.com/snakem982/mihomo v1.0.21-moshen + github.com/metacubex/mihomo => github.com/snakem982/mihomo v1.0.22-moshen github.com/sagernet/sing => github.com/metacubex/sing v0.0.0-20241121030428-33b6ebc52000 ) diff --git a/go.sum b/go.sum index 702421e..876f8e5 100644 --- a/go.sum +++ b/go.sum @@ -27,8 +27,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/enfein/mieru/v3 v3.8.3 h1:s4K0hMFDg6LHltokR8/nBTVCq15XnnxPsvc1LrHwpoo= -github.com/enfein/mieru/v3 v3.8.3/go.mod h1:YtU00qjAEt54mCBQu4WZPCey6cBdB1BUtXjvrHLEUNQ= +github.com/enfein/mieru/v3 v3.8.4 h1:PmBQykuEcl8yKcQ647pg8Qbjl433CRYgUbW6VLBgGn4= +github.com/enfein/mieru/v3 v3.8.4/go.mod h1:YtU00qjAEt54mCBQu4WZPCey6cBdB1BUtXjvrHLEUNQ= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9 h1:/5RkVc9Rc81XmMyVqawCiDyrBHZbLAZgTTCqou4mwj8= github.com/ericlagergren/aegis v0.0.0-20230312195928-b4ce538b56f9/go.mod h1:hkIFzoiIPZYxdFOOLyDho59b7SrDfo+w3h+yWdlg45I= github.com/ericlagergren/polyval v0.0.0-20230805202542-18692a1b76f9 h1:NUmyvuwVoDsIFzOGFKW4zpCtQTbX2T4JpSn1jal64gM= @@ -67,8 +67,6 @@ github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5x github.com/gofrs/uuid/v5 v5.3.0 h1:m0mUMr+oVYUdxpMLgSYCZiXe7PuVPnI94+OMeVBNedk= github.com/gofrs/uuid/v5 v5.3.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -239,8 +237,8 @@ github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e h1:ur8uMsPIF github.com/sina-ghaderi/rabbitio v0.0.0-20220730151941-9ce26f4f872e/go.mod h1:+e5fBW3bpPyo+3uLo513gIUblc03egGjMM0+5GKbzK8= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/snakem982/mihomo v1.0.21-moshen h1:OuvEcW3C5TviWxZGfYHEwLEfzFiB/OANmT3PSb5WHrg= -github.com/snakem982/mihomo v1.0.21-moshen/go.mod h1:WT/src/D5I3uQbqkKt/KFGZuJ0WzB0IFICC16EVCSdk= +github.com/snakem982/mihomo v1.0.22-moshen h1:5KX6qiJhl/3KIpVuNoR5onnWSj/m1VPpUrlxlornMjQ= +github.com/snakem982/mihomo v1.0.22-moshen/go.mod h1:mtpsehV8m2GiXm1evz4jZS0EH9xpLgEiILOmY180Yfs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -345,10 +343,6 @@ golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583 h1:IfdSdTcLFy4lqUQrQJLkLt1PB+AsqVz6lwkWPzWEz10= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241206012308-a4fef0638583/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= -google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= -google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/main.go b/main_darwin.go similarity index 81% rename from main.go rename to main_darwin.go index 71f93b6..362ca4e 100644 --- a/main.go +++ b/main_darwin.go @@ -1,3 +1,5 @@ +//go:build darwin + package main import ( @@ -22,7 +24,7 @@ import ( IsAdmin "pandora-box/backend/system/admin" "pandora-box/backend/system/proxy" "pandora-box/backend/tools" - "runtime" + goruntime "runtime" "strings" "time" ) @@ -41,7 +43,7 @@ var icon []byte func main() { - if !*devFlag && runtime.GOOS == "darwin" && !IsAdmin.Check() { + if !*devFlag && !IsAdmin.Check() { status, pwd := GetAcStatus() if status == "3" { startMacInAdmin(pwd) @@ -52,7 +54,7 @@ func main() { meta.Init() log.Infoln("Pandora-Box %s %s %s with %s", - constant.PandoraVersion, runtime.GOOS, runtime.GOARCH, runtime.Version()) + constant.PandoraVersion, goruntime.GOOS, goruntime.GOARCH, goruntime.Version()) route.Register(api.Hello) route.Register(api.Version) @@ -78,33 +80,32 @@ func main() { AssetServer: &assetserver.Options{ Assets: assets, }, - OnStartup: app.startup, + HideWindowOnClose: true, + OnStartup: app.startup, OnShutdown: func(ctx context.Context) { executor.Shutdown() proxy.RemoveProxy() + _ = IsAdmin.KillProcessesByName("Pandora-Box") }, Bind: []interface{}{ app, }, } - if runtime.GOOS == "darwin" { - AppMenu := menu.NewMenu() - AppMenu.Append(menu.AppMenu()) - AppMenu.Append(menu.EditMenu()) - option.Menu = AppMenu - option.HideWindowOnClose = true - option.Mac = &mac.Options{ - TitleBar: mac.TitleBarHidden(), - About: &mac.AboutInfo{ - Title: constant.PandoraVersion, - Message: "Copyright © 2024 snakem982", - Icon: icon, - }, - } - option.CSSDragProperty = "widows" - option.CSSDragValue = "1" + AppMenu := menu.NewMenu() + AppMenu.Append(menu.AppMenu()) + AppMenu.Append(menu.EditMenu()) + option.Menu = AppMenu + option.Mac = &mac.Options{ + TitleBar: mac.TitleBarHidden(), + About: &mac.AboutInfo{ + Title: constant.PandoraVersion, + Message: "Copyright © 2024 snakem982", + Icon: icon, + }, } + option.CSSDragProperty = "widows" + option.CSSDragValue = "1" err := wails.Run(option) if err != nil { diff --git a/main_linux.go b/main_linux.go new file mode 100644 index 0000000..7436c92 --- /dev/null +++ b/main_linux.go @@ -0,0 +1,126 @@ +//go:build linux + +package main + +import ( + "context" + "embed" + "flag" + "fmt" + "github.com/metacubex/mihomo/hub/executor" + "github.com/metacubex/mihomo/hub/route" + "github.com/metacubex/mihomo/log" + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "pandora-box/backend/api" + "pandora-box/backend/cache" + "pandora-box/backend/constant" + "pandora-box/backend/meta" + IsAdmin "pandora-box/backend/system/admin" + "pandora-box/backend/system/proxy" + "pandora-box/backend/tools" + "runtime" + "time" +) + +var devFlag = flag.Bool("dev", false, "布尔类型参数") + +func init() { + flag.Parse() +} + +//go:embed all:frontend/dist +var assets embed.FS + +//go:embed build/540x540.png +var icon []byte + +func main() { + + meta.Init() + + log.Infoln("Pandora-Box %s %s %s with %s", + constant.PandoraVersion, runtime.GOOS, runtime.GOARCH, runtime.Version()) + + route.Register(api.Hello) + route.Register(api.Version) + route.Register(api.Profile) + route.Register(api.Getter) + route.Register(api.System) + route.Register(api.Ignore) + route.Register(api.MyRules) + route.Register(api.Filter) + + addr := startHttpApi() + + meta.SwitchProfile(false) + + app := NewApp(addr) + + option := &options.App{ + Title: "Pandora-Box", + Width: 1200, + Height: 780, + MinWidth: 925, + MinHeight: 675, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + OnBeforeClose: func(ctx context.Context) (prevent bool) { + + value := cache.Get(constant.QuitSignal) + if value != nil && string(value) == "1" { + _ = cache.Put(constant.QuitSignal, []byte("0")) + return false + } + + runtime.WindowMinimise(ctx) + return true + }, + OnStartup: app.startup, + OnShutdown: func(ctx context.Context) { + executor.Shutdown() + proxy.RemoveProxy() + _ = IsAdmin.KillProcessesByName("Pandora-Box") + }, + Bind: []interface{}{ + app, + }, + } + + err := wails.Run(option) + if err != nil { + log.Errorln("wails.Run Error:", err) + } +} + +func startHttpApi() (addr string) { + var secret string + value := cache.Get(constant.SecretKey) + if value != nil { + secret = string(value) + } else { + secret = tools.String(32) + _ = cache.Put(constant.SecretKey, []byte(secret)) + } + addr = route.StartByPandora(secret) + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", secret), + } + timeOut := 500 * time.Millisecond + for i := 0; i < 3; i++ { + okUrl := fmt.Sprintf("http://%s/ok", addr) + body, _, err := tools.HttpGetWithTimeout(okUrl, timeOut, false, headers) + if err == nil && string(body) == "ok" { + log.Infoln("Start Http Serve Success.Addr is %s", addr) + break + } else { + log.Errorln("Start Http Serve Error: %v.Addr is %s", err, addr) + } + + time.Sleep(timeOut) + } + + return +} diff --git a/main_windows.go b/main_windows.go new file mode 100644 index 0000000..2f05d5a --- /dev/null +++ b/main_windows.go @@ -0,0 +1,127 @@ +//go:build windows + +package main + +import ( + "context" + "embed" + "flag" + "fmt" + "github.com/metacubex/mihomo/hub/executor" + "github.com/metacubex/mihomo/hub/route" + "github.com/metacubex/mihomo/log" + "github.com/wailsapp/wails/v2" + "github.com/wailsapp/wails/v2/pkg/options" + "github.com/wailsapp/wails/v2/pkg/options/assetserver" + "github.com/wailsapp/wails/v2/pkg/runtime" + "pandora-box/backend/api" + "pandora-box/backend/cache" + "pandora-box/backend/constant" + "pandora-box/backend/meta" + IsAdmin "pandora-box/backend/system/admin" + "pandora-box/backend/system/proxy" + "pandora-box/backend/tools" + goruntime "runtime" + "time" +) + +var devFlag = flag.Bool("dev", false, "布尔类型参数") + +func init() { + flag.Parse() +} + +//go:embed all:frontend/dist +var assets embed.FS + +//go:embed build/540x540.png +var icon []byte + +func main() { + + meta.Init() + + log.Infoln("Pandora-Box %s %s %s with %s", + constant.PandoraVersion, goruntime.GOOS, goruntime.GOARCH, goruntime.Version()) + + route.Register(api.Hello) + route.Register(api.Version) + route.Register(api.Profile) + route.Register(api.Getter) + route.Register(api.System) + route.Register(api.Ignore) + route.Register(api.MyRules) + route.Register(api.Filter) + + addr := startHttpApi() + + meta.SwitchProfile(false) + + app := NewApp(addr) + + option := &options.App{ + Title: "Pandora-Box", + Width: 1200, + Height: 785, + MinWidth: 925, + MinHeight: 675, + AssetServer: &assetserver.Options{ + Assets: assets, + }, + OnBeforeClose: func(ctx context.Context) (prevent bool) { + + value := cache.Get(constant.QuitSignal) + if value != nil && string(value) == "1" { + _ = cache.Put(constant.QuitSignal, []byte("0")) + return false + } + + runtime.WindowMinimise(ctx) + return true + }, + OnStartup: app.startup, + OnShutdown: func(ctx context.Context) { + executor.Shutdown() + proxy.RemoveProxy() + _ = IsAdmin.KillProcessesByName("Pandora-Box") + }, + Bind: []interface{}{ + app, + }, + } + + err := wails.Run(option) + if err != nil { + log.Errorln("wails.Run Error:", err) + } +} + +func startHttpApi() (addr string) { + var secret string + value := cache.Get(constant.SecretKey) + if value != nil { + secret = string(value) + } else { + secret = tools.String(32) + _ = cache.Put(constant.SecretKey, []byte(secret)) + } + addr = route.StartByPandora(secret) + headers := map[string]string{ + "Authorization": fmt.Sprintf("Bearer %s", secret), + } + timeOut := 500 * time.Millisecond + for i := 0; i < 3; i++ { + okUrl := fmt.Sprintf("http://%s/ok", addr) + body, _, err := tools.HttpGetWithTimeout(okUrl, timeOut, false, headers) + if err == nil && string(body) == "ok" { + log.Infoln("Start Http Serve Success.Addr is %s", addr) + break + } else { + log.Errorln("Start Http Serve Error: %v.Addr is %s", err, addr) + } + + time.Sleep(timeOut) + } + + return +}