| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- // croc_service.go — Croc 命令行传输工具的 Wails 服务。
- //
- // croc 是一款点对点加密文件传输 CLI:发送端执行 `croc send <文件>` 会生成
- // 一个传输码(如 mug-aids-amend),接收端执行 `croc <传输码>` 即可接收。
- //
- // 本服务自动在系统 PATH 中查找 croc(exec.LookPath),找到才允许前端展示
- // 「Croc 传送」工具卡片;发送/接收共用的密码保存在设置中(AppSettings.Croc)。
- // 设置了密码时,发送改用 `croc send --code <密码>`,接收端用同一密码作为
- // 传输码,双方只要密码一致即可完成传输。
- // 注意:croc 的 `--pass` 是中继服务器密码(公共中继固定为 pass123),不能用它
- // 承载用户密码,否则会连不上公共中继。
- package main
- import (
- "bufio"
- "bytes"
- "context"
- "errors"
- "fmt"
- "io"
- "os/exec"
- "regexp"
- "strings"
- "sync"
- "time"
- )
- // CrocConfig 是 Croc 传送的配置(发送/接收共用密码)。
- type CrocConfig struct {
- Password string `json:"password"` // 发送/接收共用密码:作为 croc 传输码(--code),空=使用随机传输码
- Dir string `json:"dir"` // 接收保存目录,空=当前工作目录
- }
- // CrocInfo 描述本机 croc 的可用性,用于前端决定是否展示工具卡片。
- type CrocInfo struct {
- Available bool `json:"available"` // PATH 中是否找到 croc
- Path string `json:"path"` // croc 可执行文件路径
- Version string `json:"version"` // 版本信息(croc --version 输出)
- }
- // CrocService 是 Croc 传送功能的 Wails 服务。
- type CrocService struct {
- mu sync.Mutex
- sendCancel context.CancelFunc
- recvCancel context.CancelFunc
- }
- // crocAnsiRe 匹配终端颜色控制符,输出到界面前先剥离。
- var crocAnsiRe = regexp.MustCompile("\x1b\\[[0-9;]*[a-zA-Z]")
- // NewCrocService 创建 Croc 服务实例。
- func NewCrocService() *CrocService {
- return &CrocService{}
- }
- // Available 检测 PATH 中是否存在 croc,并返回其路径与版本。
- func (s *CrocService) Available() CrocInfo {
- path, err := exec.LookPath("croc")
- if err != nil || path == "" {
- return CrocInfo{}
- }
- info := CrocInfo{Available: true, Path: path}
- ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
- defer cancel()
- cmd := exec.CommandContext(ctx, path, "--version")
- hideProcessWindow(cmd)
- if out, err := cmd.Output(); err == nil {
- info.Version = strings.TrimSpace(string(out))
- }
- return info
- }
- // GetConfig 读取当前 Croc 配置(每次从设置文件读取,保证与设置页保存结果一致)。
- func (s *CrocService) GetConfig() CrocConfig {
- return loadSettings().Croc
- }
- // StartSend 开始发送文件:croc --yes [--code 密码] send <文件...>。
- // 设置了密码时用密码作为传输码(双方一致即可),否则由 croc 生成随机传输码。
- // 日志通过 croc:log 事件推送。
- func (s *CrocService) StartSend(files []string) error {
- if len(files) == 0 {
- return errors.New("请先选择要发送的文件")
- }
- path, err := exec.LookPath("croc")
- if err != nil {
- return errors.New("未找到 croc,请先安装并加入系统 PATH")
- }
- s.mu.Lock()
- if s.sendCancel != nil {
- s.mu.Unlock()
- return errors.New("已有发送任务正在进行")
- }
- ctx, cancel := context.WithCancel(context.Background())
- s.sendCancel = cancel
- args := append(s.baseArgs(), "send")
- if pw := s.password(); pw != "" {
- if len(pw) < 6 {
- s.sendCancel = nil
- cancel()
- s.mu.Unlock()
- return errors.New("Croc 密码至少 6 个字符(croc 对传输码长度的限制)")
- }
- args = append(args, "--code", pw) // 密码即传输码:发送/接收共用
- }
- args = append(args, files...)
- cmd := exec.CommandContext(ctx, path, args...)
- hideProcessWindow(cmd)
- if err := s.streamProcess(cmd, "send"); err != nil {
- s.sendCancel = nil
- cancel()
- s.mu.Unlock()
- return fmt.Errorf("启动发送失败:%w", err)
- }
- s.mu.Unlock()
- return nil
- }
- // StartReceive 开始接收:croc --yes [--out 目录] <传输码>。
- // 传输码为空但设置中配置了密码时,自动把密码当作传输码,便于双方只用同一密码。
- func (s *CrocService) StartReceive(code, dir string) error {
- code = strings.TrimSpace(code)
- if code == "" {
- code = s.password() // 密码即传输码:接收端可只配置密码
- }
- if code == "" {
- return errors.New("请输入对方发来的传输码(或在设置中配置 Croc 传送密码)")
- }
- if code == s.password() && len(code) < 6 {
- return errors.New("Croc 密码至少 6 个字符(croc 对传输码长度的限制)")
- }
- path, err := exec.LookPath("croc")
- if err != nil {
- return errors.New("未找到 croc,请先安装并加入系统 PATH")
- }
- s.mu.Lock()
- if s.recvCancel != nil {
- s.mu.Unlock()
- return errors.New("已有接收任务正在进行")
- }
- ctx, cancel := context.WithCancel(context.Background())
- s.recvCancel = cancel
- args := s.baseArgs()
- if dir = strings.TrimSpace(dir); dir != "" {
- args = append(args, "--out", dir)
- }
- args = append(args, code)
- cmd := exec.CommandContext(ctx, path, args...)
- hideProcessWindow(cmd)
- if err := s.streamProcess(cmd, "recv"); err != nil {
- s.recvCancel = nil
- cancel()
- s.mu.Unlock()
- return fmt.Errorf("启动接收失败:%w", err)
- }
- s.mu.Unlock()
- return nil
- }
- // StopSend 终止发送任务(若在运行)。
- func (s *CrocService) StopSend() {
- s.mu.Lock()
- cancel := s.sendCancel
- s.mu.Unlock()
- if cancel != nil {
- cancel()
- }
- }
- // StopReceive 终止接收任务(若在运行)。
- func (s *CrocService) StopReceive() {
- s.mu.Lock()
- cancel := s.recvCancel
- s.mu.Unlock()
- if cancel != nil {
- cancel()
- }
- }
- // baseArgs 返回发送/接收共用的命令行参数:自动确认所有提示。
- func (s *CrocService) baseArgs() []string {
- return []string{"--yes"}
- }
- // password 返回设置中保存的发送/接收共用密码(已去空格)。
- func (s *CrocService) password() string {
- return strings.TrimSpace(s.GetConfig().Password)
- }
- // streamProcess 启动进程并把 stdout/stderr 逐行推送给前端(croc:log)。
- // 调用方需持有 s.mu。
- func (s *CrocService) streamProcess(cmd *exec.Cmd, kind string) error {
- stdout, err := cmd.StdoutPipe()
- if err != nil {
- return err
- }
- stderr, err := cmd.StderrPipe()
- if err != nil {
- return err
- }
- if err := cmd.Start(); err != nil {
- return err
- }
- emit := func(raw string) {
- line := strings.TrimSpace(crocAnsiRe.ReplaceAllString(raw, ""))
- if line == "" {
- return
- }
- app.Event.Emit("croc:log", map[string]string{"kind": kind, "line": line})
- }
- go scanCrocStream(stdout, emit)
- go scanCrocStream(stderr, emit)
- go func() {
- waitErr := cmd.Wait()
- exitCode := 0
- if waitErr != nil {
- var ee *exec.ExitError
- if errors.As(waitErr, &ee) {
- exitCode = ee.ExitCode()
- } else {
- exitCode = -1
- }
- }
- s.mu.Lock()
- if kind == "send" {
- s.sendCancel = nil
- } else {
- s.recvCancel = nil
- }
- s.mu.Unlock()
- app.Event.Emit("croc:exit", map[string]any{"kind": kind, "code": exitCode})
- }()
- return nil
- }
- // scanCrocStream 按 \r 或 \n 切分输出(croc 的进度用 \r 刷新),逐段回调。
- func scanCrocStream(r io.Reader, emit func(string)) {
- scanner := bufio.NewScanner(r)
- scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
- scanner.Split(func(data []byte, atEOF bool) (int, []byte, error) {
- if atEOF && len(data) == 0 {
- return 0, nil, nil
- }
- if i := bytes.IndexAny(data, "\r\n"); i >= 0 {
- return i + 1, data[:i], nil
- }
- if atEOF {
- return len(data), data, nil
- }
- return 0, nil, nil
- })
- for scanner.Scan() {
- emit(scanner.Text())
- }
- }
|