| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- // localsend_service.go — LocalSend 集成的 Wails 服务。
- //
- // 管理 LocalSend v2.2 的发现(组播 UDP)与接收(HTTPS 服务器),
- // 提供启动/停止、本机名、接收模式(自动/手动)、接收位置等配置,
- // 并通过 Wails 事件向前端推送设备发现、文件接收请求与接收完成。
- // 纯 Go 实现,与官方 LocalSend 客户端(v2.2)完全互通。
- package main
- import (
- "context"
- "fmt"
- "os"
- "path/filepath"
- "sync"
- "time"
- "TengTuToolbox/localsend/crypto"
- "TengTuToolbox/localsend/discovery"
- "TengTuToolbox/localsend/logging"
- "TengTuToolbox/localsend/model"
- "TengTuToolbox/localsend/send"
- )
- // LocalSendConfig 是 LocalSend 的运行时配置。
- type LocalSendConfig struct {
- Enabled bool `json:"enabled"` // 是否已启动服务
- Alias string `json:"alias"` // 本机名称(广播给其他设备)
- ReceiveDir string `json:"receiveDir"` // 接收文件保存目录
- AutoAccept bool `json:"autoAccept"` // true=自动接收;false=手动确认
- Port int `json:"port"` // 监听端口
- Fingerprint string `json:"fingerprint"` // 本机证书指纹
- }
- // LocalSendService 是 LocalSend 功能的 Wails 服务。
- type LocalSendService struct {
- mu sync.Mutex
- running bool
- secCtx *crypto.StoredSecurityContext
- server *lsServer
- discSvc *discovery.Service
- discCtx context.CancelFunc
- cfg LocalSendConfig
- }
- // NewLocalSendService 创建服务实例,并加载持久化配置(本机名/接收位置/接收模式)。
- func NewLocalSendService() *LocalSendService {
- s := &LocalSendService{
- cfg: LocalSendConfig{
- Alias: "我的设备",
- AutoAccept: true,
- Port: model.DefaultPort,
- },
- }
- s.loadPersistedConfig()
- return s
- }
- // loadPersistedConfig 从主 settings.json 加载 LocalSend 配置(本机名/接收位置/接收模式)。
- func (s *LocalSendService) loadPersistedConfig() {
- s.cfg = normalizeLocalSend(loadSettings().LocalSend)
- }
- // savePersistedConfig 把 LocalSend 配置写入主 settings.json,保证重启后保留。
- func (s *LocalSendService) savePersistedConfig() {
- settings := loadSettings()
- settings.LocalSend = normalizeLocalSend(s.cfg)
- _ = saveSettings(settings)
- }
- // Start 启动 LocalSend 服务:生成/加载证书、启动接收服务器与发现服务。
- func (s *LocalSendService) Start() error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.running {
- return nil
- }
- // 端口兜底:防止旧配置文件残留 port:0 或意外置 0,
- // 否则组播公告广播 port=0,对端会连 https://<ip>:0 而 Connection refused。
- if s.cfg.Port <= 0 {
- s.cfg.Port = model.DefaultPort
- }
- alias := s.cfg.Alias
- if alias == "" {
- alias = "我的设备"
- }
- receiveDir := s.cfg.ReceiveDir
- if receiveDir == "" {
- home, err := os.UserHomeDir()
- if err == nil {
- receiveDir = filepath.Join(home, "Downloads", "LocalSend")
- } else {
- receiveDir = os.TempDir()
- }
- }
- logger := logging.NewQuiet()
- // 1. 加载或生成自签证书(持久化到用户配置目录)
- secCtx, err := s.loadOrCreateSecCtx(alias, logger)
- if err != nil {
- return err
- }
- s.secCtx = secCtx
- // 2. 设置全局客户端证书(供 HTTP 注册 mTLS 使用)
- clientCertFn, err := secCtx.ClientCertificateFunc()
- if err != nil {
- return err
- }
- discovery.SetClientCertificate(clientCertFn)
- // 3. 启动接收服务器
- srv := newLSServer(s.cfg.Port, secCtx)
- srv.setConfig(alias, receiveDir, s.cfg.AutoAccept)
- srv.onFileOffer = func(offer *fileOffer) {
- // 手动接收:通知前端弹窗确认
- app.Event.Emit("localsend:fileOffer", offer)
- }
- s.server = srv
- go func() {
- if err := srv.start(); err != nil {
- app.Event.Emit("localsend:error", map[string]string{"message": err.Error()})
- }
- }()
- // 4. 启动发现服务(组播 UDP + HTTP 注册)
- discCtx, cancel := context.WithCancel(context.Background())
- s.discCtx = cancel
- multicast := discovery.NewMulticastDiscovery(
- discovery.DefaultMulticastConfig(),
- s.buildMulticastDto(alias, secCtx, s.cfg.Port),
- logger,
- )
- disc := discovery.NewService(discovery.DefaultServiceConfig(), multicast, logger)
- disc.AddDeviceHandler(func(device *model.Device) {
- s.emitDevices()
- })
- s.discSvc = disc
- if err := disc.Start(discCtx, s.buildMulticastDto(alias, secCtx, s.cfg.Port)); err != nil {
- cancel()
- return err
- }
- s.running = true
- s.cfg.Enabled = true
- s.cfg.ReceiveDir = receiveDir
- s.cfg.Fingerprint = secCtx.CertificateHash
- return nil
- }
- // Stop 停止 LocalSend 服务。
- func (s *LocalSendService) Stop() {
- s.mu.Lock()
- defer s.mu.Unlock()
- if !s.running {
- return
- }
- if s.discCtx != nil {
- s.discCtx()
- }
- if s.discSvc != nil {
- s.discSvc.Stop()
- }
- if s.server != nil {
- s.server.stop()
- }
- s.running = false
- s.cfg.Enabled = false
- }
- // SetConfig 更新本机名、接收位置、接收模式。
- // 若服务运行中,即时生效(服务器配置与后续公告同步更新)。
- func (s *LocalSendService) SetConfig(alias, receiveDir string, autoAccept bool) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if alias != "" {
- s.cfg.Alias = alias
- }
- if receiveDir != "" {
- s.cfg.ReceiveDir = receiveDir
- }
- s.cfg.AutoAccept = autoAccept
- if s.server != nil {
- s.server.setConfig(s.cfg.Alias, s.cfg.ReceiveDir, s.cfg.AutoAccept)
- }
- if s.discSvc != nil {
- // 更新本机名后重新广播公告,使对端看到新名字
- _ = s.discSvc
- }
- s.savePersistedConfig() // 持久化本机名/接收位置/接收模式,重启后保留
- return nil
- }
- // GetConfig 返回当前配置。
- func (s *LocalSendService) GetConfig() LocalSendConfig {
- s.mu.Lock()
- defer s.mu.Unlock()
- return s.cfg
- }
- // GetDevices 返回当前发现的设备列表。
- func (s *LocalSendService) GetDevices() []*model.Device {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.discSvc == nil {
- return nil
- }
- return s.discSvc.GetDevices()
- }
- // ConfirmTransfer 手动接收确认:accept=true 接受,false 拒绝。
- func (s *LocalSendService) ConfirmTransfer(sessionID string, accept bool) {
- s.mu.Lock()
- defer s.mu.Unlock()
- if s.server != nil {
- s.server.confirm(sessionID, accept)
- }
- }
- // SendFiles 把本地文件发送给指定的 LocalSend 设备(按 IP 定位)。
- // filePaths 为本地文件/目录路径列表;deviceIP 为目标设备 IP(如 192.168.1.5)。
- func (s *LocalSendService) SendFiles(filePaths []string, deviceIP string) error {
- s.mu.Lock()
- defer s.mu.Unlock()
- if len(filePaths) == 0 {
- return fmt.Errorf("未选择要发送的文件")
- }
- if deviceIP == "" {
- return fmt.Errorf("未指定目标设备")
- }
- if s.secCtx == nil {
- return fmt.Errorf("LocalSend 服务未启动,请先启动服务再发送文件")
- }
- // 按 IP 找到目标设备
- var target *model.Device
- if s.discSvc != nil {
- for _, d := range s.discSvc.GetDevices() {
- if d.IP == deviceIP {
- target = d
- break
- }
- }
- }
- if target == nil {
- return fmt.Errorf("未找到目标设备 %s,请刷新设备列表后重试", deviceIP)
- }
- cfg := &send.SendConfig{
- Alias: s.cfg.Alias,
- Port: s.cfg.Port,
- SecurityContext: s.secCtx,
- HttpsEnabled: true,
- Concurrency: 4,
- }
- logger := logging.NewQuiet()
- ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
- defer cancel()
- if err := send.SendFiles(ctx, cfg, target, filePaths, logger); err != nil {
- return fmt.Errorf("发送文件失败: %w", err)
- }
- return nil
- }
- // loadOrCreateSecCtx 从用户配置目录加载已生成的证书,不存在则生成并保存。
- func (s *LocalSendService) loadOrCreateSecCtx(alias string, logger *logging.Logger) (*crypto.StoredSecurityContext, error) {
- dir, err := os.UserConfigDir()
- if err != nil {
- dir = os.TempDir()
- }
- dir = filepath.Join(dir, "TengTuToolbox")
- _ = os.MkdirAll(dir, 0o755)
- path := filepath.Join(dir, "localsend_cert.json")
- ctx, err := crypto.LoadSecurityContext(path, logger)
- if err == nil && ctx.Certificate != "" {
- return ctx, nil
- }
- ctx, err = crypto.GenerateSecurityContext(alias, logger)
- if err != nil {
- return nil, err
- }
- _ = crypto.SaveSecurityContext(ctx, path, logger)
- return ctx, nil
- }
- // buildMulticastDto 构造组播公告 DTO。
- func (s *LocalSendService) buildMulticastDto(alias string, secCtx *crypto.StoredSecurityContext, port int) model.MulticastDto {
- return model.MulticastDto{
- Alias: alias,
- Version: "2.2",
- DeviceType: model.DeviceTypeDesktop,
- Fingerprint: secCtx.CertificateHash,
- Port: port,
- Protocol: model.ProtocolTypeHTTPS,
- Download: false,
- Announce: true,
- }
- }
- // emitDevices 向前端推送设备列表事件。
- func (s *LocalSendService) emitDevices() {
- devices := s.GetDevices()
- if devices != nil {
- app.Event.Emit("localsend:devices", devices)
- }
- }
|