2 Commits

Author SHA1 Message Date
putyy
a247c708f6 perf: unified response 2025-04-29 11:31:38 +08:00
putyy
85781a150a perf: optimization download、Proxy settings, add batch export 2025-04-29 11:15:13 +08:00
14 changed files with 365 additions and 188 deletions

View File

@@ -166,30 +166,28 @@ func (a *App) installCert() {
} }
} }
func (a *App) OpenSystemProxy() bool { func (a *App) OpenSystemProxy() error {
if a.IsProxy { if a.IsProxy {
return true return nil
} }
err := systemOnce.setProxy() err := systemOnce.setProxy()
if err == nil { if err == nil {
a.IsProxy = true a.IsProxy = true
return true return nil
} }
DialogErr("设置失败:" + err.Error()) return err
return false
} }
func (a *App) UnsetSystemProxy() bool { func (a *App) UnsetSystemProxy() error {
if !a.IsProxy { if !a.IsProxy {
return true return nil
} }
err := systemOnce.unsetProxy() err := systemOnce.unsetProxy()
if err == nil { if err == nil {
a.IsProxy = false a.IsProxy = false
return true return nil
} }
DialogErr("设置失败:" + err.Error()) return err
return false
} }
func (a *App) isInstall() bool { func (a *App) isInstall() bool {

View File

@@ -12,7 +12,7 @@ import (
"sync" "sync"
) )
type ProgressCallback func(totalDownloaded float64) type ProgressCallback func(totalDownloaded float64, totalSize float64)
type DownloadTask struct { type DownloadTask struct {
taskID int taskID int
@@ -53,16 +53,15 @@ func (fd *FileDownloader) buildClient() *http.Client {
if fd.ProxyUrl != nil { if fd.ProxyUrl != nil {
transport.Proxy = http.ProxyURL(fd.ProxyUrl) transport.Proxy = http.ProxyURL(fd.ProxyUrl)
} }
// Cookie handle
return &http.Client{ return &http.Client{
Transport: transport, Transport: transport,
} }
} }
func (fd *FileDownloader) setHeaders(request *http.Request) { func (fd *FileDownloader) setHeaders(request *http.Request) {
for key, values := range fd.Headers { for key, value := range fd.Headers {
if strings.Contains(globalConfig.UseHeaders, key) { if strings.Contains(globalConfig.UseHeaders, key) {
request.Header.Set(key, values) request.Header.Set(key, value)
} }
} }
} }
@@ -85,50 +84,42 @@ func (fd *FileDownloader) init() error {
request, err := http.NewRequest("HEAD", fd.Url, nil) request, err := http.NewRequest("HEAD", fd.Url, nil)
if err != nil { if err != nil {
return fmt.Errorf("create request failed") return fmt.Errorf("create HEAD request failed: %w", err)
} }
if _, ok := fd.Headers["User-Agent"]; !ok { if _, ok := fd.Headers["User-Agent"]; !ok {
fd.Headers["User-Agent"] = globalConfig.UserAgent fd.Headers["User-Agent"] = globalConfig.UserAgent
} }
if _, ok := fd.Headers["Referer"]; !ok { if _, ok := fd.Headers["Referer"]; !ok {
fd.Headers["Referer"] = fd.Referer fd.Headers["Referer"] = fd.Referer
} }
fd.setHeaders(request) fd.setHeaders(request)
resp, err := fd.buildClient().Do(request) resp, err := fd.buildClient().Do(request)
if err != nil { if err != nil {
return fmt.Errorf("request failed" + err.Error()) return fmt.Errorf("HEAD request failed: %w", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
fd.TotalSize = resp.ContentLength fd.TotalSize = resp.ContentLength
if fd.TotalSize <= 0 { if fd.TotalSize <= 0 {
return fmt.Errorf("request init failed: size 0") return fmt.Errorf("invalid file size")
} }
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > 10*1024*1024 {
if resp.Header.Get("Accept-Ranges") == "bytes" && fd.TotalSize > 10485760 {
fd.IsMultiPart = true fd.IsMultiPart = true
} }
fd.FileName = filepath.Clean(fd.FileName)
dir := filepath.Dir(fd.FileName) dir := filepath.Dir(fd.FileName)
if err := os.MkdirAll(dir, os.ModePerm); err != nil { if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return err return err
} }
fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR|os.O_CREATE, 0644) fd.File, err = os.OpenFile(fd.FileName, os.O_RDWR|os.O_CREATE, 0644)
if err != nil { if err != nil {
return fmt.Errorf("文件初始化失败: %w", err) return fmt.Errorf("file open failed: %w", err)
} }
if err := fd.File.Truncate(fd.TotalSize); err != nil {
if err = fd.File.Truncate(fd.TotalSize); err != nil {
fd.File.Close() fd.File.Close()
return fmt.Errorf("文件大小设置失败: %w", err) return fmt.Errorf("file truncate failed: %w", err)
} }
return nil return nil
} }
@@ -139,97 +130,100 @@ func (fd *FileDownloader) createDownloadTasks() {
fd.totalTasks = int(fd.TotalSize) fd.totalTasks = int(fd.TotalSize)
} }
eachSize := fd.TotalSize / int64(fd.totalTasks) eachSize := fd.TotalSize / int64(fd.totalTasks)
for i := 0; i < fd.totalTasks; i++ { for i := 0; i < fd.totalTasks; i++ {
start := eachSize * int64(i)
end := eachSize*int64(i+1) - 1
if i == fd.totalTasks-1 {
end = fd.TotalSize - 1
}
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{ fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{
taskID: i, taskID: i,
rangeStart: eachSize * int64(i), rangeStart: start,
rangeEnd: eachSize*int64(i+1) - 1, rangeEnd: end,
downloadedSize: 0,
isCompleted: false,
}) })
} }
fd.DownloadTaskList[len(fd.DownloadTaskList)-1].rangeEnd = fd.TotalSize - 1
} else { } else {
fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{ fd.DownloadTaskList = append(fd.DownloadTaskList, &DownloadTask{taskID: 0})
taskID: 0,
rangeStart: 0,
rangeEnd: 0,
downloadedSize: 0,
isCompleted: false,
})
} }
} }
func (fd *FileDownloader) startDownload() { func (fd *FileDownloader) startDownload() {
waitGroup := &sync.WaitGroup{} wg := &sync.WaitGroup{}
progressChan := make(chan int64) progressChan := make(chan int64)
for _, task := range fd.DownloadTaskList { for _, task := range fd.DownloadTaskList {
go fd.startDownloadTask(waitGroup, progressChan, task) wg.Add(1)
waitGroup.Add(1) go fd.startDownloadTask(wg, progressChan, task)
} }
go func() { go func() {
waitGroup.Wait() wg.Wait()
close(progressChan) close(progressChan)
}() }()
if fd.progressCallback != nil { if fd.progressCallback != nil {
totalDownloaded := int64(0) totalDownloaded := int64(0)
for progress := range progressChan { for p := range progressChan {
totalDownloaded += progress totalDownloaded += p
fd.progressCallback(float64(totalDownloaded) * 100 / float64(fd.TotalSize)) fd.progressCallback(float64(totalDownloaded), float64(fd.TotalSize))
} }
} }
} }
func (fd *FileDownloader) startDownloadTask(waitGroup *sync.WaitGroup, progressChan chan int64, task *DownloadTask) { func (fd *FileDownloader) startDownloadTask(wg *sync.WaitGroup, progressChan chan int64, task *DownloadTask) {
defer waitGroup.Done() defer wg.Done()
request, err := http.NewRequest("GET", fd.Url, nil) request, err := http.NewRequest("GET", fd.Url, nil)
if err != nil { if err != nil {
globalLogger.Error().Stack().Err(err).Msgf("任务%d创建请求出错", task.taskID) globalLogger.Error().Stack().Err(err).Msgf("任务%d创建请求出错", task.taskID)
return return
} }
fd.setHeaders(request) fd.setHeaders(request)
if fd.IsMultiPart { if fd.IsMultiPart {
request.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", task.rangeStart, task.rangeEnd)) rangeHeader := fmt.Sprintf("bytes=%d-%d", task.rangeStart, task.rangeEnd)
request.Header.Set("Range", rangeHeader)
} }
resp, err := fd.buildClient().Do(request) client := fd.buildClient()
resp, err := client.Do(request)
if err != nil { if err != nil {
log.Printf("任务%d发送下载请求出错%s", task.taskID, err) log.Printf("任务%d发送下载请求出错%s", task.taskID, err)
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
buf := make([]byte, 8192) buf := make([]byte, 8192)
for { for {
n, err := resp.Body.Read(buf) n, err := resp.Body.Read(buf)
if n > 0 { if n > 0 {
_, err := fd.File.WriteAt(buf[:n], task.rangeStart+task.downloadedSize) remain := task.rangeEnd - (task.rangeStart + task.downloadedSize) + 1
if err != nil { n64 := int64(n)
log.Printf("任务%d写入文件时出现错误位置:%d, err: %s\n", task.taskID, task.rangeStart+task.downloadedSize, err) if n64 > remain {
n = int(remain)
}
_, writeErr := fd.File.WriteAt(buf[:n], task.rangeStart+task.downloadedSize)
if writeErr != nil {
log.Printf("任务%d写入文件时出现错误位置:%d, err: %s\n", task.taskID, task.rangeStart+task.downloadedSize, writeErr)
return return
} }
downSize := int64(n) task.downloadedSize += n64
task.downloadedSize += downSize progressChan <- n64
progressChan <- downSize
if task.rangeStart+task.downloadedSize-1 >= task.rangeEnd {
task.isCompleted = true
break
}
} }
if err != nil { if err != nil {
if err == io.EOF { if err == io.EOF {
task.isCompleted = true task.isCompleted = true
break
} }
log.Printf("任务%d读取响应错误%s", task.taskID, err) break
return
} }
} }
} }
func (fd *FileDownloader) Start() error { func (fd *FileDownloader) Start() error {
err := fd.init() if err := fd.init(); err != nil {
if err != nil {
return err return err
} }
fd.createDownloadTasks() fd.createDownloadTasks()

View File

@@ -10,11 +10,15 @@ import (
"net" "net"
"net/http" "net/http"
"net/url" "net/url"
"os"
"os/exec" "os/exec"
"path/filepath"
sysRuntime "runtime" sysRuntime "runtime"
"strings" "strings"
) )
type respData map[string]interface{}
type ResponseData struct { type ResponseData struct {
Code int `json:"code"` Code int `json:"code"`
Message string `json:"message"` Message string `json:"message"`
@@ -118,20 +122,54 @@ func (h *HttpServer) writeJson(w http.ResponseWriter, data ResponseData) {
} }
} }
func (h *HttpServer) error(w http.ResponseWriter, args ...interface{}) {
message := "ok"
var data interface{}
if len(args) > 0 {
message = args[0].(string)
}
if len(args) > 1 {
data = args[1]
}
h.writeJson(w, ResponseData{
Code: 0,
Message: message,
Data: data,
})
}
func (h *HttpServer) success(w http.ResponseWriter, args ...interface{}) {
message := "ok"
var data interface{}
if len(args) > 0 {
data = args[0]
}
if len(args) > 1 {
message = args[1].(string)
}
h.writeJson(w, ResponseData{
Code: 1,
Message: message,
Data: data,
})
}
func (h *HttpServer) openDirectoryDialog(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) openDirectoryDialog(w http.ResponseWriter, r *http.Request) {
folder, err := runtime.OpenDirectoryDialog(appOnce.ctx, runtime.OpenDialogOptions{ folder, err := runtime.OpenDirectoryDialog(appOnce.ctx, runtime.OpenDialogOptions{
DefaultDirectory: "", DefaultDirectory: "",
Title: "Select a folder", Title: "Select a folder",
}) })
if err != nil { if err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()}) h.error(w, err.Error())
return return
} }
h.writeJson(w, ResponseData{ h.success(w, respData{
Code: 1, "folder": folder,
Data: map[string]interface{}{
"folder": folder,
},
}) })
} }
@@ -146,14 +184,11 @@ func (h *HttpServer) openFileDialog(w http.ResponseWriter, r *http.Request) {
Title: "Select a file", Title: "Select a file",
}) })
if err != nil { if err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()}) h.error(w, err.Error())
return return
} }
h.writeJson(w, ResponseData{ h.success(w, respData{
Code: 1, "file": filePath,
Data: map[string]interface{}{
"file": filePath,
},
}) })
} }
@@ -188,77 +223,87 @@ func (h *HttpServer) openFolder(w http.ResponseWriter, r *http.Request) {
cmd = exec.Command("pcmanfm", filePath) cmd = exec.Command("pcmanfm", filePath)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
globalLogger.err(err) globalLogger.err(err)
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()}) h.error(w, err.Error())
return return
} }
} }
} }
} }
default: default:
h.writeJson(w, ResponseData{Code: 0, Message: "unsupported platform"}) h.error(w, "unsupported platform")
return return
} }
err = cmd.Start() err = cmd.Start()
if err != nil { if err != nil {
globalLogger.err(err) globalLogger.err(err)
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()}) h.error(w, err.Error())
return return
} }
h.writeJson(w, ResponseData{Code: 1}) h.success(w)
}
func (h *HttpServer) setSystemPassword(w http.ResponseWriter, r *http.Request) {
var data struct {
Password string `json:"password"`
}
err := json.NewDecoder(r.Body).Decode(&data)
if err != nil {
h.error(w, err.Error())
return
}
systemOnce.SetPassword(data.Password)
h.success(w)
} }
func (h *HttpServer) openSystemProxy(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) openSystemProxy(w http.ResponseWriter, r *http.Request) {
appOnce.OpenSystemProxy() err := appOnce.OpenSystemProxy()
h.writeJson(w, ResponseData{ if err != nil {
Code: 1, h.error(w, err.Error(), respData{
Data: map[string]bool{
"isProxy": appOnce.IsProxy, "isProxy": appOnce.IsProxy,
}, })
return
}
h.success(w, respData{
"isProxy": appOnce.IsProxy,
}) })
} }
func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) unsetSystemProxy(w http.ResponseWriter, r *http.Request) {
appOnce.UnsetSystemProxy() err := appOnce.UnsetSystemProxy()
h.writeJson(w, ResponseData{ if err != nil {
Code: 1, h.error(w, err.Error(), respData{
Data: map[string]bool{
"isProxy": appOnce.IsProxy, "isProxy": appOnce.IsProxy,
}, })
return
}
h.success(w, respData{
"isProxy": appOnce.IsProxy,
}) })
} }
func (h *HttpServer) isProxy(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) isProxy(w http.ResponseWriter, r *http.Request) {
h.writeJson(w, ResponseData{ h.success(w, respData{
Code: 1, "isProxy": appOnce.IsProxy,
Data: map[string]interface{}{
"isProxy": appOnce.IsProxy,
},
}) })
} }
func (h *HttpServer) appInfo(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) appInfo(w http.ResponseWriter, r *http.Request) {
h.writeJson(w, ResponseData{ h.success(w, appOnce)
Code: 1,
Data: appOnce,
})
} }
func (h *HttpServer) getConfig(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) getConfig(w http.ResponseWriter, r *http.Request) {
h.writeJson(w, ResponseData{ h.success(w, globalConfig)
Code: 1,
Data: globalConfig,
})
} }
func (h *HttpServer) setConfig(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) setConfig(w http.ResponseWriter, r *http.Request) {
var data Config var data Config
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()}) h.error(w, err.Error())
return return
} }
globalConfig.setConfig(data) globalConfig.setConfig(data)
h.writeJson(w, ResponseData{Code: 1}) h.success(w)
} }
func (h *HttpServer) setType(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) setType(w http.ResponseWriter, r *http.Request) {
@@ -274,12 +319,12 @@ func (h *HttpServer) setType(w http.ResponseWriter, r *http.Request) {
} }
} }
h.writeJson(w, ResponseData{Code: 1}) h.success(w)
} }
func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) clear(w http.ResponseWriter, r *http.Request) {
resourceOnce.clear() resourceOnce.clear()
h.writeJson(w, ResponseData{Code: 1}) h.success(w)
} }
func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
@@ -290,7 +335,7 @@ func (h *HttpServer) delete(w http.ResponseWriter, r *http.Request) {
if err == nil && data.Sign != "" { if err == nil && data.Sign != "" {
resourceOnce.delete(data.Sign) resourceOnce.delete(data.Sign)
} }
h.writeJson(w, ResponseData{Code: 1}) h.success(w)
} }
func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
@@ -299,11 +344,11 @@ func (h *HttpServer) download(w http.ResponseWriter, r *http.Request) {
DecodeStr string `json:"decodeStr"` DecodeStr string `json:"decodeStr"`
} }
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()}) h.error(w, err.Error())
return return
} }
resourceOnce.download(data.MediaInfo, data.DecodeStr) resourceOnce.download(data.MediaInfo, data.DecodeStr)
h.writeJson(w, ResponseData{Code: 1}) h.success(w)
} }
func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) { func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
@@ -313,18 +358,35 @@ func (h *HttpServer) wxFileDecode(w http.ResponseWriter, r *http.Request) {
DecodeStr string `json:"decodeStr"` DecodeStr string `json:"decodeStr"`
} }
if err := json.NewDecoder(r.Body).Decode(&data); err != nil { if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()}) h.error(w, err.Error())
return return
} }
savePath, err := resourceOnce.wxFileDecode(data.MediaInfo, data.Filename, data.DecodeStr) savePath, err := resourceOnce.wxFileDecode(data.MediaInfo, data.Filename, data.DecodeStr)
if err != nil { if err != nil {
h.writeJson(w, ResponseData{Code: 0, Message: err.Error()}) h.error(w, err.Error())
return return
} }
h.writeJson(w, ResponseData{ h.success(w, respData{
Code: 1, "save_path": savePath,
Data: map[string]string{ })
"save_path": savePath, }
},
func (h *HttpServer) batchImport(w http.ResponseWriter, r *http.Request) {
var data struct {
Content string `json:"content"`
}
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
h.error(w, err.Error())
return
}
fileName := filepath.Join(globalConfig.SaveDirectory, "res-downloader-"+GetCurrentDateTimeFormatted()+".txt")
// 0644 是文件权限:-rw-r--r--
err := os.WriteFile(fileName, []byte(data.Content), 0644)
if err != nil {
h.error(w, err.Error())
return
}
h.success(w, respData{
"file_name": fileName,
}) })
} }

View File

@@ -26,6 +26,8 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
switch r.URL.Path { switch r.URL.Path {
case "/api/preview": case "/api/preview":
httpServerOnce.preview(w, r) httpServerOnce.preview(w, r)
case "/api/set-system-password":
httpServerOnce.setSystemPassword(w, r)
case "/api/proxy-open": case "/api/proxy-open":
httpServerOnce.openSystemProxy(w, r) httpServerOnce.openSystemProxy(w, r)
case "/api/proxy-unset": case "/api/proxy-unset":
@@ -54,6 +56,8 @@ func HandleApi(w http.ResponseWriter, r *http.Request) bool {
httpServerOnce.download(w, r) httpServerOnce.download(w, r)
case "/api/wx-file-decode": case "/api/wx-file-decode":
httpServerOnce.wxFileDecode(w, r) httpServerOnce.wxFileDecode(w, r)
case "/api/batch-import":
httpServerOnce.batchImport(w, r)
} }
return true return true
} }

View File

@@ -147,8 +147,8 @@ func (r *Resource) download(mediaInfo MediaInfo, decodeStr string) {
headers, _ := r.parseHeaders(mediaInfo) headers, _ := r.parseHeaders(mediaInfo)
downloader := NewFileDownloader(rawUrl, mediaInfo.SavePath, globalConfig.TaskNumber, headers) downloader := NewFileDownloader(rawUrl, mediaInfo.SavePath, globalConfig.TaskNumber, headers)
downloader.progressCallback = func(totalDownloaded float64) { downloader.progressCallback = func(totalDownloaded, totalSize float64) {
r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded))+"%", DownloadStatusRunning) r.progressEventsEmit(mediaInfo, strconv.Itoa(int(totalDownloaded*100/totalSize))+"%", DownloadStatusRunning)
} }
err := downloader.Start() err := downloader.Start()
if err != nil { if err != nil {

View File

@@ -7,6 +7,7 @@ import (
type SystemSetup struct { type SystemSetup struct {
CertFile string CertFile string
Password string
} }
func initSystem() *SystemSetup { func initSystem() *SystemSetup {
@@ -33,3 +34,7 @@ func (s *SystemSetup) initCert() ([]byte, error) {
return nil, err return nil, err
} }
} }
func (s *SystemSetup) SetPassword(password string) {
s.Password = password
}

View File

@@ -9,15 +9,30 @@ import (
"strings" "strings"
) )
func (s *SystemSetup) runCommand(args []string) ([]byte, error) {
if len(args) == 0 {
return nil, fmt.Errorf("no command provided")
}
var cmd *exec.Cmd
if s.Password != "" {
cmd = exec.Command("sudo", append([]string{"-S"}, args...)...)
cmd.Stdin = bytes.NewReader([]byte(s.Password + "\n"))
} else {
cmd = exec.Command(args[0], args[1:]...)
}
output, err := cmd.CombinedOutput()
return output, err
}
func (s *SystemSetup) getNetworkServices() ([]string, error) { func (s *SystemSetup) getNetworkServices() ([]string, error) {
cmd := exec.Command("networksetup", "-listallnetworkservices") output, err := s.runCommand([]string{"networksetup", "-listallnetworkservices"})
output, err := cmd.Output()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to execute command: %v", err) return nil, fmt.Errorf("failed to execute command: %v", err)
} }
services := strings.Split(string(output), "\n") services := strings.Split(string(output), "\n")
var activeServices []string var activeServices []string
for _, service := range services { for _, service := range services {
service = strings.TrimSpace(service) service = strings.TrimSpace(service)
@@ -25,15 +40,12 @@ func (s *SystemSetup) getNetworkServices() ([]string, error) {
continue continue
} }
// 检查服务是否活动 infoOutput, err := s.runCommand([]string{"networksetup", "-getinfo", service})
infoCmd := exec.Command("networksetup", "-getinfo", service)
infoOutput, err := infoCmd.Output()
if err != nil { if err != nil {
fmt.Printf("failed to get info for service %s: %v\n", service, err) fmt.Printf("failed to get info for service %s: %v\n", service, err)
continue continue
} }
// 如果输出中包含 "IP address:",说明服务是活动的
if strings.Contains(string(infoOutput), "IP address:") { if strings.Contains(string(infoOutput), "IP address:") {
activeServices = append(activeServices, service) activeServices = append(activeServices, service)
} }
@@ -53,16 +65,19 @@ func (s *SystemSetup) setProxy() error {
} }
is := false is := false
errs := ""
for _, serviceName := range services { for _, serviceName := range services {
if err := exec.Command("networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port).Run(); err != nil { cmds := [][]string{
fmt.Println(err) {"networksetup", "-setwebproxy", serviceName, "127.0.0.1", globalConfig.Port},
} else { {"networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port},
is = true
} }
if err := exec.Command("networksetup", "-setsecurewebproxy", serviceName, "127.0.0.1", globalConfig.Port).Run(); err != nil { for _, args := range cmds {
fmt.Println(err) if output, err := s.runCommand(args); err != nil {
} else { errs = errs + "\n output" + string(output) + "err:" + err.Error()
is = true fmt.Println("setProxy:", output, " err:", err.Error())
} else {
is = true
}
} }
} }
@@ -70,7 +85,7 @@ func (s *SystemSetup) setProxy() error {
return nil return nil
} }
return fmt.Errorf("failed to set proxy for any active network service") return fmt.Errorf("failed to set proxy for any active network service, errs: %s", errs)
} }
func (s *SystemSetup) unsetProxy() error { func (s *SystemSetup) unsetProxy() error {
@@ -80,16 +95,19 @@ func (s *SystemSetup) unsetProxy() error {
} }
is := false is := false
errs := ""
for _, serviceName := range services { for _, serviceName := range services {
if err := exec.Command("networksetup", "-setwebproxystate", serviceName, "off").Run(); err != nil { cmds := [][]string{
fmt.Println(err) {"networksetup", "-setwebproxystate", serviceName, "off"},
} else { {"networksetup", "-setsecurewebproxystate", serviceName, "off"},
is = true
} }
if err := exec.Command("networksetup", "-setsecurewebproxystate", serviceName, "off").Run(); err != nil { for _, args := range cmds {
fmt.Println(err) if output, err := s.runCommand(args); err != nil {
} else { errs = errs + "\n output" + string(output) + "err:" + err.Error()
is = true fmt.Println("unsetProxy:", output, " err:", err.Error())
} else {
is = true
}
} }
} }
@@ -97,7 +115,7 @@ func (s *SystemSetup) unsetProxy() error {
return nil return nil
} }
return fmt.Errorf("failed to set proxy for any active network service") return fmt.Errorf("failed to unset proxy for any active network service, errs: %s", errs)
} }
func (s *SystemSetup) installCert() (string, error) { func (s *SystemSetup) installCert() (string, error) {
@@ -112,10 +130,11 @@ func (s *SystemSetup) installCert() (string, error) {
return string(passwordOutput), err return string(passwordOutput), err
} }
password := bytes.TrimSpace(passwordOutput) password := strings.TrimSpace(string(passwordOutput))
cmd := exec.Command("sudo", "-S", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile) s.SetPassword(password)
cmd.Stdin = bytes.NewReader(append(password, '\n')) cmd := exec.Command("sudo", "-S", "security", "add-trusted-cert", "-d", "-r", "trustRoot", "-k", "/Library/Keychains/System.keychain", s.CertFile)
cmd.Stdin = bytes.NewReader([]byte(password + "\n"))
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
return string(output), err return string(output), err

View File

@@ -37,6 +37,7 @@ declare module 'vue' {
NSpace: typeof import('naive-ui')['NSpace'] NSpace: typeof import('naive-ui')['NSpace']
NSwitch: typeof import('naive-ui')['NSwitch'] NSwitch: typeof import('naive-ui')['NSwitch']
NTooltip: typeof import('naive-ui')['NTooltip'] NTooltip: typeof import('naive-ui')['NTooltip']
Password: typeof import('./src/components/Password.vue')['default']
Preview: typeof import('./src/components/Preview.vue')['default'] Preview: typeof import('./src/components/Preview.vue')['default']
ResAction: typeof import('./src/components/ResAction.vue')['default'] ResAction: typeof import('./src/components/ResAction.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']

View File

@@ -1,6 +1,13 @@
import request from '@/api/request' import request from '@/api/request'
export default { export default {
setSystemPassword(data: object) {
return request({
url: 'api/set-system-password',
method: 'post',
data: data
})
},
openSystemProxy() { openSystemProxy() {
return request({ return request({
url: 'api/proxy-open', url: 'api/proxy-open',
@@ -93,4 +100,11 @@ export default {
data: data data: data
}) })
}, },
batchImport(data: object) {
return request({
url: 'api/batch-import',
method: 'post',
data: data
})
},
} }

View File

@@ -0,0 +1,46 @@
<template>
<n-modal
:show="showModal"
:on-update:show="changeShow"
style="--wails-draggable:no-drag"
preset="dialog"
title="管理员授权"
content=""
:show-icon="false"
:mask-closable="false"
class="rounded-lg"
>
<n-form>
<div class="text-red-500 text-base">
本操作需要管理员授权仅对本次运行期间有效
</div>
<n-form-item path="password" label="">
<n-input
v-model:value="password"
type="password"
placeholder="请输入你的电脑密码"
class="w-full"
/>
</n-form-item>
</n-form>
<template #action>
<div class="flex justify-end gap-4">
<n-button @click="emits('update:showModal', false)">取消操作</n-button>
<n-button type="primary" @click="emits('submit', password)">确认操作</n-button>
</div>
</template>
</n-modal>
</template>
<script setup lang="ts">
import {ref, computed} from 'vue'
import {NModal, NForm, NFormItem, NInput, NButton} from 'naive-ui'
const props = defineProps({
showModal: Boolean,
})
const password = ref("")
const emits = defineEmits(["update:showModal", "submit"])
const changeShow = (value: boolean) => emits("update:showModal", value)
</script>

View File

@@ -12,7 +12,7 @@
<NButton v-if="row.DecodeKey" type="warning" :tertiary="true" size="small" @click="action('decode')"> <NButton v-if="row.DecodeKey" type="warning" :tertiary="true" size="small" @click="action('decode')">
视频解密 视频解密
</NButton> </NButton>
<NButton v-if="isDebug" type="info" :tertiary="true" size="small" @click="action('json')"> <NButton type="info" :tertiary="true" size="small" @click="action('json')">
复制数据 复制数据
</NButton> </NButton>
<NButton type="error" :tertiary="true" size="small" @click="action('delete')"> <NButton type="error" :tertiary="true" size="small" @click="action('delete')">
@@ -22,8 +22,6 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {inject} from "vue"
const props = defineProps<{ const props = defineProps<{
row: any, row: any,
index: number, index: number,
@@ -31,8 +29,6 @@ const props = defineProps<{
const emits = defineEmits(["action"]) const emits = defineEmits(["action"])
const isDebug = inject('isDebug')
const action = (type: string) => { const action = (type: string) => {
emits('action', props.row, props.index, type) emits('action', props.row, props.index, type)
} }

View File

@@ -57,4 +57,10 @@ export namespace appType {
type: string type: string
event: any event: any
} }
interface Res<T = any> {
code: number;
message: string;
data: T; // T will be the specific type of your data
}
} }

View File

@@ -1,13 +1,14 @@
<template> <template>
<div class="flex flex-col px-5 py-5"> <div class="flex flex-col px-5 py-5">
<div class="pb-2 z-40" @click="triggerEvent"> <div class="pb-2 z-40">
<NSpace> <NSpace>
<NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">关闭代理</NButton> <NButton v-if="isProxy" secondary type="primary" @click.stop="close" style="--wails-draggable:no-drag">关闭代理</NButton>
<NButton v-else tertiary type="tertiary" @click.stop="open" style="--wails-draggable:no-drag">开启代理</NButton> <NButton v-else tertiary type="tertiary" @click.stop="open" style="--wails-draggable:no-drag">开启代理</NButton>
<NButton tertiary type="info" @click.stop="batchDown" style="--wails-draggable:no-drag">批量下载</NButton>
<NButton tertiary type="error" @click.stop="clear" style="--wails-draggable:no-drag">清空列表</NButton> <NButton tertiary type="error" @click.stop="clear" style="--wails-draggable:no-drag">清空列表</NButton>
<NSelect style="min-width: 100px;--wails-draggable:no-drag" placeholder="拦截类型" v-model:value="resourcesType" multiple clearable :max-tag-count="3" :options="options"></NSelect> <NSelect style="min-width: 100px;--wails-draggable:no-drag" placeholder="拦截类型" v-model:value="resourcesType" multiple clearable :max-tag-count="3" :options="options"></NSelect>
<NButton v-if="isDebug" tertiary type="info" @click.stop="showImport=true" style="--wails-draggable:no-drag">导入数据</NButton> <NButton tertiary type="info" @click.stop="batchDown" style="--wails-draggable:no-drag">批量下载</NButton>
<NButton tertiary type="info" @click.stop="batchImport" style="--wails-draggable:no-drag">批量导出</NButton>
<NButton tertiary type="info" @click.stop="showImport=true" style="--wails-draggable:no-drag">批量导入</NButton>
</NSpace> </NSpace>
</div> </div>
<div class="flex-1"> <div class="flex-1">
@@ -27,6 +28,7 @@
<Preview v-model:showModal="showPreviewRow" :previewRow="previewRow"/> <Preview v-model:showModal="showPreviewRow" :previewRow="previewRow"/>
<ShowLoading :loadingText="loadingText" :isLoading="loading"/> <ShowLoading :loadingText="loadingText" :isLoading="loading"/>
<ImportJson v-model:showModal="showImport" @submit="handleImport"/> <ImportJson v-model:showModal="showImport" @submit="handleImport"/>
<Password v-model:showModal="showPassword" @submit="handlePassword"/>
</div> </div>
</template> </template>
@@ -47,6 +49,7 @@ import ResAction from "@/components/ResAction.vue"
import ImportJson from "@/components/ImportJson.vue" import ImportJson from "@/components/ImportJson.vue"
import {useEventStore} from "@/stores/event" import {useEventStore} from "@/stores/event"
import {BrowserOpenURL, ClipboardSetText} from "../../wailsjs/runtime" import {BrowserOpenURL, ClipboardSetText} from "../../wailsjs/runtime"
import Password from "@/components/Password.vue"
const eventStore = useEventStore() const eventStore = useEventStore()
const isProxy = computed(() => { const isProxy = computed(() => {
@@ -219,12 +222,8 @@ const showPreviewRow = ref(false)
const previewRow = ref<appType.MediaInfo>() const previewRow = ref<appType.MediaInfo>()
const loading = ref(false) const loading = ref(false)
const loadingText = ref("") const loadingText = ref("")
const isDebug = ref(false)
const showImport = ref(false) const showImport = ref(false)
let clickCount = 0 const showPassword = ref(false)
let clickTimeout: any = null
provide('isDebug', isDebug);
onMounted(() => { onMounted(() => {
const temp = localStorage.getItem("resources-type") const temp = localStorage.getItem("resources-type")
@@ -361,6 +360,35 @@ const batchDown = async () => {
} }
} }
const batchImport = ()=>{
if (checkedRowKeysValue.value.length <= 0) {
window?.$message?.error('请选择需要导出的数据')
return
}
if (!store.globalConfig.SaveDirectory) {
window?.$message?.error("请设置保存目录")
return
}
loadingText.value = "导出中"
loading.value = true
let jsonData = []
for (let i = 0; i < data.value.length; i++) {
jsonData.push(encodeURIComponent(JSON.stringify(data.value[i])))
}
appApi.batchImport({content: jsonData.join("\n")}).then((res: appType.Res) => {
loading.value = false
if (res.code === 0) {
window?.$message?.error(res.message)
return
}
window?.$message?.success("导出成功")
window?.$message?.info("文件路径" + res.data?.file_name, {
duration: 5000
})
})
}
const uint8ArrayToBase64 = (bytes: any) => { const uint8ArrayToBase64 = (bytes: any) => {
let binary = ''; let binary = '';
const len = bytes.byteLength; const len = bytes.byteLength;
@@ -390,14 +418,14 @@ const download = (row: appType.MediaInfo, index: number) => {
loading.value = true loading.value = true
downIndex.value = index downIndex.value = index
if (row.DecodeKey) { if (row.DecodeKey) {
appApi.download({...row, decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))}).then((res: any) => { appApi.download({...row, decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))}).then((res: appType.Res) => {
if (res.code === 0) { if (res.code === 0) {
loading.value = false loading.value = false
window?.$message?.error(res.message) window?.$message?.error(res.message)
} }
}) })
} else { } else {
appApi.download({...row, decodeStr: ""}).then((res: any) => { appApi.download({...row, decodeStr: ""}).then((res: appType.Res) => {
if (res.code === 0) { if (res.code === 0) {
loading.value = false loading.value = false
window?.$message?.error(res.message) window?.$message?.error(res.message)
@@ -407,13 +435,25 @@ const download = (row: appType.MediaInfo, index: number) => {
} }
const open = () => { const open = () => {
appApi.openSystemProxy().then((res: any) => { appApi.openSystemProxy().then((res: appType.Res) => {
if (res.code === 0 ){
if (store.envInfo.platform === "darwin") {
showPassword.value = true
return
}
window?.$message?.error(res.message)
return
}
store.updateProxyStatus(res.data) store.updateProxyStatus(res.data)
}) })
} }
const close = () => { const close = () => {
appApi.unsetSystemProxy().then((res: any) => { appApi.unsetSystemProxy().then((res: appType.Res) => {
if (res.code === 0 ){
window?.$message?.error(res.message)
return
}
store.updateProxyStatus(res.data) store.updateProxyStatus(res.data)
}) })
} }
@@ -429,7 +469,7 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
window?.$message?.error("无法解密") window?.$message?.error("无法解密")
return return
} }
appApi.openFileDialog().then((res: any) => { appApi.openFileDialog().then((res: appType.Res) => {
if (res.code === 0) { if (res.code === 0) {
window?.$message?.error(res.message) window?.$message?.error(res.message)
return return
@@ -441,7 +481,7 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
...row, ...row,
filename: res.data.file, filename: res.data.file,
decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey)) decodeStr: uint8ArrayToBase64(getDecryptionArray(row.DecodeKey))
}).then((res: any) => { }).then((res: appType.Res) => {
loading.value = false loading.value = false
if (res.code === 0) { if (res.code === 0) {
window?.$message?.error(res.message) window?.$message?.error(res.message)
@@ -456,24 +496,6 @@ const decodeWxFile = (row: appType.MediaInfo, index: number) => {
}) })
} }
const triggerEvent = ()=>{
if(isDebug.value) {
return
}
clickCount++
if (clickCount === 5) {
// 连续点击5次开启debug
isDebug.value = true
clickCount = 0
} else {
clearTimeout(clickTimeout);
clickTimeout = setTimeout(() => {
clickCount = 0
}, 1000)
}
}
const handleImport = (content: string)=>{ const handleImport = (content: string)=>{
content.split("\n").forEach((line, index) => { content.split("\n").forEach((line, index) => {
try { try {
@@ -491,4 +513,14 @@ const handleImport = (content: string)=>{
localStorage.setItem("resources-data", JSON.stringify(data.value)) localStorage.setItem("resources-data", JSON.stringify(data.value))
showImport.value = false showImport.value = false
} }
const handlePassword = (password: string)=>{
appApi.setSystemPassword({password: password}).then((res: appType.Res)=>{
if (res.code === 0) {
window?.$message?.error(res.message)
return
}
open()
})
}
</script> </script>

View File

@@ -13,7 +13,7 @@
"info": { "info": {
"companyName": "res-downloader", "companyName": "res-downloader",
"productName": "res-downloader", "productName": "res-downloader",
"productVersion": "3.0.5", "productVersion": "3.0.6",
"copyright": "Copyright © 2023", "copyright": "Copyright © 2023",
"comments": "This is a high-value high-performance and diverse resource downloader called res-downloader." "comments": "This is a high-value high-performance and diverse resource downloader called res-downloader."
} }