localsend_service.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. // localsend_service.go — LocalSend 集成的 Wails 服务。
  2. //
  3. // 管理 LocalSend v2.2 的发现(组播 UDP)与接收(HTTPS 服务器),
  4. // 提供启动/停止、本机名、接收模式(自动/手动)、接收位置等配置,
  5. // 并通过 Wails 事件向前端推送设备发现、文件接收请求与接收完成。
  6. // 纯 Go 实现,与官方 LocalSend 客户端(v2.2)完全互通。
  7. package main
  8. import (
  9. "context"
  10. "fmt"
  11. "os"
  12. "path/filepath"
  13. "sync"
  14. "time"
  15. "TengTuToolbox/localsend/crypto"
  16. "TengTuToolbox/localsend/discovery"
  17. "TengTuToolbox/localsend/logging"
  18. "TengTuToolbox/localsend/model"
  19. "TengTuToolbox/localsend/send"
  20. )
  21. // LocalSendConfig 是 LocalSend 的运行时配置。
  22. type LocalSendConfig struct {
  23. Enabled bool `json:"enabled"` // 是否已启动服务
  24. Alias string `json:"alias"` // 本机名称(广播给其他设备)
  25. ReceiveDir string `json:"receiveDir"` // 接收文件保存目录
  26. AutoAccept bool `json:"autoAccept"` // true=自动接收;false=手动确认
  27. Port int `json:"port"` // 监听端口
  28. Fingerprint string `json:"fingerprint"` // 本机证书指纹
  29. }
  30. // LocalSendService 是 LocalSend 功能的 Wails 服务。
  31. type LocalSendService struct {
  32. mu sync.Mutex
  33. running bool
  34. secCtx *crypto.StoredSecurityContext
  35. server *lsServer
  36. discSvc *discovery.Service
  37. discCtx context.CancelFunc
  38. cfg LocalSendConfig
  39. }
  40. // NewLocalSendService 创建服务实例,并加载持久化配置(本机名/接收位置/接收模式)。
  41. func NewLocalSendService() *LocalSendService {
  42. s := &LocalSendService{
  43. cfg: LocalSendConfig{
  44. Alias: "我的设备",
  45. AutoAccept: true,
  46. Port: model.DefaultPort,
  47. },
  48. }
  49. s.loadPersistedConfig()
  50. return s
  51. }
  52. // loadPersistedConfig 从主 settings.json 加载 LocalSend 配置(本机名/接收位置/接收模式)。
  53. func (s *LocalSendService) loadPersistedConfig() {
  54. s.cfg = normalizeLocalSend(loadSettings().LocalSend)
  55. }
  56. // savePersistedConfig 把 LocalSend 配置写入主 settings.json,保证重启后保留。
  57. func (s *LocalSendService) savePersistedConfig() {
  58. settings := loadSettings()
  59. settings.LocalSend = normalizeLocalSend(s.cfg)
  60. _ = saveSettings(settings)
  61. }
  62. // Start 启动 LocalSend 服务:生成/加载证书、启动接收服务器与发现服务。
  63. func (s *LocalSendService) Start() error {
  64. s.mu.Lock()
  65. defer s.mu.Unlock()
  66. if s.running {
  67. return nil
  68. }
  69. // 端口兜底:防止旧配置文件残留 port:0 或意外置 0,
  70. // 否则组播公告广播 port=0,对端会连 https://<ip>:0 而 Connection refused。
  71. if s.cfg.Port <= 0 {
  72. s.cfg.Port = model.DefaultPort
  73. }
  74. alias := s.cfg.Alias
  75. if alias == "" {
  76. alias = "我的设备"
  77. }
  78. receiveDir := s.cfg.ReceiveDir
  79. if receiveDir == "" {
  80. home, err := os.UserHomeDir()
  81. if err == nil {
  82. receiveDir = filepath.Join(home, "Downloads", "LocalSend")
  83. } else {
  84. receiveDir = os.TempDir()
  85. }
  86. }
  87. logger := logging.NewQuiet()
  88. // 1. 加载或生成自签证书(持久化到用户配置目录)
  89. secCtx, err := s.loadOrCreateSecCtx(alias, logger)
  90. if err != nil {
  91. return err
  92. }
  93. s.secCtx = secCtx
  94. // 2. 设置全局客户端证书(供 HTTP 注册 mTLS 使用)
  95. clientCertFn, err := secCtx.ClientCertificateFunc()
  96. if err != nil {
  97. return err
  98. }
  99. discovery.SetClientCertificate(clientCertFn)
  100. // 3. 启动接收服务器
  101. srv := newLSServer(s.cfg.Port, secCtx)
  102. srv.setConfig(alias, receiveDir, s.cfg.AutoAccept)
  103. srv.onFileOffer = func(offer *fileOffer) {
  104. // 手动接收:通知前端弹窗确认
  105. app.Event.Emit("localsend:fileOffer", offer)
  106. }
  107. s.server = srv
  108. go func() {
  109. if err := srv.start(); err != nil {
  110. app.Event.Emit("localsend:error", map[string]string{"message": err.Error()})
  111. }
  112. }()
  113. // 4. 启动发现服务(组播 UDP + HTTP 注册)
  114. discCtx, cancel := context.WithCancel(context.Background())
  115. s.discCtx = cancel
  116. multicast := discovery.NewMulticastDiscovery(
  117. discovery.DefaultMulticastConfig(),
  118. s.buildMulticastDto(alias, secCtx, s.cfg.Port),
  119. logger,
  120. )
  121. disc := discovery.NewService(discovery.DefaultServiceConfig(), multicast, logger)
  122. disc.AddDeviceHandler(func(device *model.Device) {
  123. s.emitDevices()
  124. })
  125. s.discSvc = disc
  126. if err := disc.Start(discCtx, s.buildMulticastDto(alias, secCtx, s.cfg.Port)); err != nil {
  127. cancel()
  128. return err
  129. }
  130. s.running = true
  131. s.cfg.Enabled = true
  132. s.cfg.ReceiveDir = receiveDir
  133. s.cfg.Fingerprint = secCtx.CertificateHash
  134. return nil
  135. }
  136. // Stop 停止 LocalSend 服务。
  137. func (s *LocalSendService) Stop() {
  138. s.mu.Lock()
  139. defer s.mu.Unlock()
  140. if !s.running {
  141. return
  142. }
  143. if s.discCtx != nil {
  144. s.discCtx()
  145. }
  146. if s.discSvc != nil {
  147. s.discSvc.Stop()
  148. }
  149. if s.server != nil {
  150. s.server.stop()
  151. }
  152. s.running = false
  153. s.cfg.Enabled = false
  154. }
  155. // SetConfig 更新本机名、接收位置、接收模式。
  156. // 若服务运行中,即时生效(服务器配置与后续公告同步更新)。
  157. func (s *LocalSendService) SetConfig(alias, receiveDir string, autoAccept bool) error {
  158. s.mu.Lock()
  159. defer s.mu.Unlock()
  160. if alias != "" {
  161. s.cfg.Alias = alias
  162. }
  163. if receiveDir != "" {
  164. s.cfg.ReceiveDir = receiveDir
  165. }
  166. s.cfg.AutoAccept = autoAccept
  167. if s.server != nil {
  168. s.server.setConfig(s.cfg.Alias, s.cfg.ReceiveDir, s.cfg.AutoAccept)
  169. }
  170. if s.discSvc != nil {
  171. // 更新本机名后重新广播公告,使对端看到新名字
  172. _ = s.discSvc
  173. }
  174. s.savePersistedConfig() // 持久化本机名/接收位置/接收模式,重启后保留
  175. return nil
  176. }
  177. // GetConfig 返回当前配置。
  178. func (s *LocalSendService) GetConfig() LocalSendConfig {
  179. s.mu.Lock()
  180. defer s.mu.Unlock()
  181. return s.cfg
  182. }
  183. // GetDevices 返回当前发现的设备列表。
  184. func (s *LocalSendService) GetDevices() []*model.Device {
  185. s.mu.Lock()
  186. defer s.mu.Unlock()
  187. if s.discSvc == nil {
  188. return nil
  189. }
  190. return s.discSvc.GetDevices()
  191. }
  192. // ConfirmTransfer 手动接收确认:accept=true 接受,false 拒绝。
  193. func (s *LocalSendService) ConfirmTransfer(sessionID string, accept bool) {
  194. s.mu.Lock()
  195. defer s.mu.Unlock()
  196. if s.server != nil {
  197. s.server.confirm(sessionID, accept)
  198. }
  199. }
  200. // SendFiles 把本地文件发送给指定的 LocalSend 设备(按 IP 定位)。
  201. // filePaths 为本地文件/目录路径列表;deviceIP 为目标设备 IP(如 192.168.1.5)。
  202. func (s *LocalSendService) SendFiles(filePaths []string, deviceIP string) error {
  203. s.mu.Lock()
  204. defer s.mu.Unlock()
  205. if len(filePaths) == 0 {
  206. return fmt.Errorf("未选择要发送的文件")
  207. }
  208. if deviceIP == "" {
  209. return fmt.Errorf("未指定目标设备")
  210. }
  211. if s.secCtx == nil {
  212. return fmt.Errorf("LocalSend 服务未启动,请先启动服务再发送文件")
  213. }
  214. // 按 IP 找到目标设备
  215. var target *model.Device
  216. if s.discSvc != nil {
  217. for _, d := range s.discSvc.GetDevices() {
  218. if d.IP == deviceIP {
  219. target = d
  220. break
  221. }
  222. }
  223. }
  224. if target == nil {
  225. return fmt.Errorf("未找到目标设备 %s,请刷新设备列表后重试", deviceIP)
  226. }
  227. cfg := &send.SendConfig{
  228. Alias: s.cfg.Alias,
  229. Port: s.cfg.Port,
  230. SecurityContext: s.secCtx,
  231. HttpsEnabled: true,
  232. Concurrency: 4,
  233. }
  234. logger := logging.NewQuiet()
  235. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
  236. defer cancel()
  237. if err := send.SendFiles(ctx, cfg, target, filePaths, logger); err != nil {
  238. return fmt.Errorf("发送文件失败: %w", err)
  239. }
  240. return nil
  241. }
  242. // loadOrCreateSecCtx 从用户配置目录加载已生成的证书,不存在则生成并保存。
  243. func (s *LocalSendService) loadOrCreateSecCtx(alias string, logger *logging.Logger) (*crypto.StoredSecurityContext, error) {
  244. dir, err := os.UserConfigDir()
  245. if err != nil {
  246. dir = os.TempDir()
  247. }
  248. dir = filepath.Join(dir, "TengTuToolbox")
  249. _ = os.MkdirAll(dir, 0o755)
  250. path := filepath.Join(dir, "localsend_cert.json")
  251. ctx, err := crypto.LoadSecurityContext(path, logger)
  252. if err == nil && ctx.Certificate != "" {
  253. return ctx, nil
  254. }
  255. ctx, err = crypto.GenerateSecurityContext(alias, logger)
  256. if err != nil {
  257. return nil, err
  258. }
  259. _ = crypto.SaveSecurityContext(ctx, path, logger)
  260. return ctx, nil
  261. }
  262. // buildMulticastDto 构造组播公告 DTO。
  263. func (s *LocalSendService) buildMulticastDto(alias string, secCtx *crypto.StoredSecurityContext, port int) model.MulticastDto {
  264. return model.MulticastDto{
  265. Alias: alias,
  266. Version: "2.2",
  267. DeviceType: model.DeviceTypeDesktop,
  268. Fingerprint: secCtx.CertificateHash,
  269. Port: port,
  270. Protocol: model.ProtocolTypeHTTPS,
  271. Download: false,
  272. Announce: true,
  273. }
  274. }
  275. // emitDevices 向前端推送设备列表事件。
  276. func (s *LocalSendService) emitDevices() {
  277. devices := s.GetDevices()
  278. if devices != nil {
  279. app.Event.Emit("localsend:devices", devices)
  280. }
  281. }