first commit: 项目初始化
@ -0,0 +1,39 @@
|
||||
# Dependencies
|
||||
node_modules/
|
||||
|
||||
# Build output
|
||||
dist/
|
||||
src-tauri/target/
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Environment
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Tauri
|
||||
src-tauri/target/
|
||||
src-tauri/WixTools/
|
||||
src-tauri/icons/*.png
|
||||
src-tauri/icons/*.ico
|
||||
src-tauri/icons/*.icns
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
|
||||
# Certificates (generated)
|
||||
src-tauri/certs/
|
||||
@ -0,0 +1,123 @@
|
||||
# Flash Send
|
||||
|
||||
跨平台局域网文件传输与聊天应用,基于 Tauri 2 + Vue 3 + Rust 构建。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- 🔍 **局域网设备发现** - UDP 广播自动发现同一网络内的设备
|
||||
- 💬 **实时聊天** - WebSocket 实现的低延迟文本消息
|
||||
- 📁 **文件传输** - HTTPS 加密传输,支持大文件
|
||||
- 🔒 **安全通信** - TLS 加密所有通信,自签名证书
|
||||
- 💾 **历史记录** - SQLite 本地存储聊天和传输记录
|
||||
- 🎨 **现代 UI** - 扁平化设计,参考微信/LocalSend 风格
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端 (Rust)
|
||||
- **Tauri 2** - 跨平台桌面应用框架
|
||||
- **Tokio** - 异步运行时
|
||||
- **Rustls** - TLS 加密
|
||||
- **Axum** - HTTP 服务
|
||||
- **tokio-tungstenite** - WebSocket
|
||||
- **rusqlite** - SQLite 数据库
|
||||
|
||||
### 前端 (TypeScript)
|
||||
- **Vue 3** - 响应式 UI 框架
|
||||
- **Pinia** - 状态管理
|
||||
- **Vue Router** - 路由管理
|
||||
- **TailwindCSS** - 样式框架
|
||||
- **Lucide Icons** - 图标库
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
flash-send/
|
||||
├── src/ # Vue 前端源码
|
||||
│ ├── pages/ # 页面组件
|
||||
│ ├── components/ # UI 组件
|
||||
│ ├── stores/ # Pinia 状态管理
|
||||
│ ├── api/ # Tauri API 封装
|
||||
│ ├── hooks/ # Vue Hooks
|
||||
│ ├── types/ # TypeScript 类型
|
||||
│ └── router/ # 路由配置
|
||||
├── src-tauri/ # Rust 后端源码
|
||||
│ └── src/
|
||||
│ ├── discovery/ # UDP 设备发现
|
||||
│ ├── websocket/ # WebSocket 聊天
|
||||
│ ├── http/ # HTTP 文件传输
|
||||
│ ├── tls/ # TLS 加密
|
||||
│ ├── database/ # SQLite 数据库
|
||||
│ ├── models/ # 数据模型
|
||||
│ ├── commands/ # Tauri 命令
|
||||
│ └── utils/ # 工具函数
|
||||
└── public/ # 静态资源
|
||||
```
|
||||
|
||||
## 开发环境
|
||||
|
||||
### 前置要求
|
||||
- Node.js 18+
|
||||
- Rust 1.70+
|
||||
- pnpm 或 npm
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
# 安装前端依赖
|
||||
npm install
|
||||
|
||||
# Rust 依赖会在首次构建时自动安装
|
||||
```
|
||||
|
||||
### 开发模式
|
||||
|
||||
```bash
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
### 构建发布
|
||||
|
||||
```bash
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
## 通信协议
|
||||
|
||||
### UDP 设备发现 (端口 53317)
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"device": {
|
||||
"deviceId": "uuid",
|
||||
"deviceName": "设备名",
|
||||
"ip": "192.168.1.100",
|
||||
"wsPort": 53318,
|
||||
"httpPort": 53319,
|
||||
"capabilities": ["text", "image", "file"]
|
||||
},
|
||||
"packetType": "announce|query|response|goodbye"
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket 消息 (端口 53318)
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"type": "text|image|event|ack|ping|pong",
|
||||
"content": "消息内容",
|
||||
"from": "发送者设备ID",
|
||||
"to": "接收者设备ID",
|
||||
"timestamp": 1700000000,
|
||||
"status": "pending|sent|delivered|read|failed"
|
||||
}
|
||||
```
|
||||
|
||||
### HTTP 文件传输 (端口 53319)
|
||||
- `POST /upload` - 上传文件
|
||||
- `GET /file/:id` - 下载文件
|
||||
- `GET /file/:id/info` - 获取文件信息
|
||||
- `GET /health` - 健康检查
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT License
|
||||
@ -0,0 +1,537 @@
|
||||
# 后端结构
|
||||
|
||||
本文档详细介绍 Rust 后端的代码结构、模块功能和核心实现。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
src-tauri/src/
|
||||
├── commands/ # Tauri 命令(前端调用入口)
|
||||
├── discovery/ # UDP 设备发现
|
||||
├── websocket/ # WebSocket 聊天
|
||||
├── http/ # HTTP 文件传输
|
||||
├── tls/ # TLS 加密
|
||||
├── database/ # SQLite 数据库
|
||||
├── models/ # 数据模型
|
||||
├── utils/ # 工具函数
|
||||
├── state.rs # 全局状态
|
||||
├── main.rs # 程序入口
|
||||
└── lib.rs # 库入口
|
||||
```
|
||||
|
||||
## 核心模块详解
|
||||
|
||||
### 1. Commands 模块
|
||||
|
||||
Tauri 命令是前端与后端通信的桥梁。
|
||||
|
||||
#### discovery_commands.rs
|
||||
|
||||
```rust
|
||||
// 启动设备发现服务
|
||||
#[tauri::command]
|
||||
pub async fn start_discovery(app: AppHandle) -> CommandResult<()>;
|
||||
|
||||
// 停止设备发现服务
|
||||
#[tauri::command]
|
||||
pub async fn stop_discovery() -> CommandResult<()>;
|
||||
|
||||
// 获取在线设备列表
|
||||
#[tauri::command]
|
||||
pub async fn get_online_devices() -> CommandResult<Vec<DeviceInfo>>;
|
||||
|
||||
// 获取本机设备信息
|
||||
#[tauri::command]
|
||||
pub fn get_local_device() -> CommandResult<DeviceInfo>;
|
||||
```
|
||||
|
||||
#### chat_commands.rs
|
||||
|
||||
```rust
|
||||
// 启动 WebSocket 服务
|
||||
#[tauri::command]
|
||||
pub async fn start_ws_server(app: AppHandle) -> CommandResult<()>;
|
||||
|
||||
// 连接到指定设备
|
||||
#[tauri::command]
|
||||
pub async fn connect_to_device(device_id: String) -> CommandResult<()>;
|
||||
|
||||
// 发送聊天消息
|
||||
#[tauri::command]
|
||||
pub async fn send_chat_message(
|
||||
device_id: String,
|
||||
content: String,
|
||||
message_type: String,
|
||||
) -> CommandResult<ChatMessage>;
|
||||
|
||||
// 获取聊天历史
|
||||
#[tauri::command]
|
||||
pub async fn get_chat_history(
|
||||
device_id: String,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
) -> CommandResult<Vec<ChatMessage>>;
|
||||
```
|
||||
|
||||
#### file_commands.rs
|
||||
|
||||
```rust
|
||||
// 启动 HTTP 文件服务
|
||||
#[tauri::command]
|
||||
pub async fn start_http_server(app: AppHandle) -> CommandResult<()>;
|
||||
|
||||
// 选择要发送的文件
|
||||
#[tauri::command]
|
||||
pub async fn select_file(app: AppHandle) -> CommandResult<Option<FileMetadata>>;
|
||||
|
||||
// 发送文件
|
||||
#[tauri::command]
|
||||
pub async fn send_file(
|
||||
app: AppHandle,
|
||||
device_id: String,
|
||||
file_id: String,
|
||||
file_path: String,
|
||||
) -> CommandResult<FileTransfer>;
|
||||
|
||||
// 取消传输
|
||||
#[tauri::command]
|
||||
pub async fn cancel_transfer(file_id: String) -> CommandResult<()>;
|
||||
|
||||
// 获取传输历史
|
||||
#[tauri::command]
|
||||
pub async fn get_transfer_history(
|
||||
device_id: String,
|
||||
limit: Option<usize>,
|
||||
) -> CommandResult<Vec<FileTransfer>>;
|
||||
|
||||
// 打开文件位置
|
||||
#[tauri::command]
|
||||
pub async fn open_file_location(path: String) -> CommandResult<()>;
|
||||
```
|
||||
|
||||
#### config_commands.rs
|
||||
|
||||
```rust
|
||||
// 获取应用配置
|
||||
#[tauri::command]
|
||||
pub fn get_app_config() -> CommandResult<AppConfig>;
|
||||
|
||||
// 更新设备名称
|
||||
#[tauri::command]
|
||||
pub async fn update_device_name(name: String) -> CommandResult<()>;
|
||||
|
||||
// 更新下载目录
|
||||
#[tauri::command]
|
||||
pub fn update_download_dir(path: String) -> CommandResult<()>;
|
||||
```
|
||||
|
||||
### 2. Discovery 模块
|
||||
|
||||
UDP 广播设备发现服务。
|
||||
|
||||
#### service.rs
|
||||
|
||||
```rust
|
||||
pub struct DiscoveryService {
|
||||
socket: Arc<UdpSocket>,
|
||||
local_device: DeviceInfo,
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl DiscoveryService {
|
||||
// 创建服务
|
||||
pub async fn new(local_device: DeviceInfo, port: u16) -> Result<Self>;
|
||||
|
||||
// 启动广播和监听
|
||||
pub async fn start(&self, app: AppHandle);
|
||||
|
||||
// 停止服务
|
||||
pub fn stop(&self);
|
||||
|
||||
// 发送广播
|
||||
async fn broadcast(&self);
|
||||
|
||||
// 处理收到的消息
|
||||
async fn handle_message(&self, data: &[u8], addr: SocketAddr, app: &AppHandle);
|
||||
}
|
||||
```
|
||||
|
||||
#### manager.rs
|
||||
|
||||
```rust
|
||||
pub struct DeviceManager {
|
||||
devices: DashMap<String, DeviceInfo>,
|
||||
}
|
||||
|
||||
impl DeviceManager {
|
||||
// 添加/更新设备
|
||||
pub fn add_or_update(&self, device: DeviceInfo);
|
||||
|
||||
// 移除设备
|
||||
pub fn remove(&self, device_id: &str);
|
||||
|
||||
// 获取设备
|
||||
pub fn get(&self, device_id: &str) -> Option<DeviceInfo>;
|
||||
|
||||
// 获取所有设备
|
||||
pub fn get_all(&self) -> Vec<DeviceInfo>;
|
||||
|
||||
// 清理过期设备
|
||||
pub fn cleanup_stale(&self, max_age: Duration) -> Vec<String>;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. WebSocket 模块
|
||||
|
||||
WebSocket 实时聊天服务。
|
||||
|
||||
#### server.rs
|
||||
|
||||
```rust
|
||||
pub struct WsServer {
|
||||
port: u16,
|
||||
running: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl WsServer {
|
||||
// 启动服务
|
||||
pub async fn start(&self, app: AppHandle, connection_manager: ConnectionManager);
|
||||
|
||||
// 处理新连接
|
||||
async fn handle_connection(
|
||||
stream: TcpStream,
|
||||
addr: SocketAddr,
|
||||
app: AppHandle,
|
||||
connection_manager: ConnectionManager,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
#### client.rs
|
||||
|
||||
```rust
|
||||
pub struct WsClient;
|
||||
|
||||
impl WsClient {
|
||||
// 连接到设备
|
||||
pub async fn connect(
|
||||
device: &DeviceInfo,
|
||||
local_device: &DeviceInfo,
|
||||
connection_manager: &ConnectionManager,
|
||||
) -> Result<()>;
|
||||
|
||||
// 发送消息
|
||||
pub async fn send_message(
|
||||
connection_manager: &ConnectionManager,
|
||||
device_id: &str,
|
||||
message: &str,
|
||||
) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
#### connection.rs
|
||||
|
||||
```rust
|
||||
pub struct ConnectionManager {
|
||||
// device_id -> write half of WebSocket
|
||||
connections: DashMap<String, SplitSink<WebSocketStream<TcpStream>, Message>>,
|
||||
}
|
||||
|
||||
impl ConnectionManager {
|
||||
// 添加连接
|
||||
pub fn add(&self, device_id: String, sink: SplitSink<...>);
|
||||
|
||||
// 移除连接
|
||||
pub fn remove(&self, device_id: &str);
|
||||
|
||||
// 发送消息
|
||||
pub async fn send(&self, device_id: &str, message: &str) -> Result<()>;
|
||||
|
||||
// 检查连接状态
|
||||
pub fn is_connected(&self, device_id: &str) -> bool;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. HTTP 模块
|
||||
|
||||
HTTPS 文件传输服务。
|
||||
|
||||
#### server.rs
|
||||
|
||||
```rust
|
||||
pub struct HttpServer {
|
||||
port: u16,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
// 启动服务
|
||||
pub async fn start(&self, app: AppHandle, download_dir: PathBuf) -> Result<()>;
|
||||
|
||||
// 创建路由
|
||||
fn create_router(app: AppHandle, download_dir: PathBuf) -> Router;
|
||||
}
|
||||
```
|
||||
|
||||
#### handlers.rs
|
||||
|
||||
```rust
|
||||
// 文件上传处理
|
||||
pub async fn upload_handler(
|
||||
State(state): State<AppState>,
|
||||
mut multipart: Multipart,
|
||||
) -> impl IntoResponse;
|
||||
|
||||
// 文件下载处理
|
||||
pub async fn download_handler(
|
||||
Path(file_id): Path<String>,
|
||||
State(state): State<AppState>,
|
||||
) -> impl IntoResponse;
|
||||
```
|
||||
|
||||
#### client.rs
|
||||
|
||||
```rust
|
||||
pub struct HttpClient {
|
||||
app_handle: Arc<RwLock<Option<AppHandle>>>,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
// 上传文件(支持取消)
|
||||
pub async fn upload_file_with_cancel(
|
||||
&self,
|
||||
device: &DeviceInfo,
|
||||
file_path: &PathBuf,
|
||||
file_id: &str,
|
||||
from_device: &str,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<()>;
|
||||
|
||||
// 发送进度事件
|
||||
fn emit_progress(&self, file_id: &str, progress: f64, ...);
|
||||
}
|
||||
```
|
||||
|
||||
### 5. TLS 模块
|
||||
|
||||
TLS 证书生成和配置。
|
||||
|
||||
#### certificate.rs
|
||||
|
||||
```rust
|
||||
pub struct CertificateManager;
|
||||
|
||||
impl CertificateManager {
|
||||
// 加载或生成证书
|
||||
pub fn load_or_generate(data_dir: &Path) -> Result<(Vec<Certificate>, PrivateKey)>;
|
||||
|
||||
// 生成自签名证书
|
||||
fn generate_self_signed() -> Result<(Certificate, PrivateKey)>;
|
||||
}
|
||||
```
|
||||
|
||||
#### config.rs
|
||||
|
||||
```rust
|
||||
// 构建服务端 TLS 配置
|
||||
pub fn build_server_config(certs: Vec<Certificate>, key: PrivateKey) -> Result<ServerConfig>;
|
||||
|
||||
// 构建客户端 TLS 配置(跳过证书验证)
|
||||
pub fn build_client_config() -> Result<ClientConfig>;
|
||||
```
|
||||
|
||||
### 6. Database 模块
|
||||
|
||||
SQLite 数据库操作。
|
||||
|
||||
#### schema.rs
|
||||
|
||||
```rust
|
||||
// 初始化数据库表结构
|
||||
pub fn init_database(conn: &Connection) -> Result<()>;
|
||||
|
||||
// 表结构:
|
||||
// - chat_messages: 聊天消息
|
||||
// - file_transfers: 文件传输记录
|
||||
// - known_devices: 已知设备
|
||||
// - app_settings: 应用设置
|
||||
```
|
||||
|
||||
#### repository.rs
|
||||
|
||||
```rust
|
||||
pub struct Database;
|
||||
|
||||
impl Database {
|
||||
// 消息操作
|
||||
pub fn save_message(message: &ChatMessage) -> Result<()>;
|
||||
pub fn get_chat_history(device_id: &str, limit: usize, offset: usize) -> Result<Vec<ChatMessage>>;
|
||||
|
||||
// 传输操作
|
||||
pub fn save_file_transfer(transfer: &FileTransfer) -> Result<()>;
|
||||
pub fn update_transfer_progress(file_id: &str, progress: f64, ...) -> Result<()>;
|
||||
pub fn get_transfer_history(device_id: &str, ...) -> Result<Vec<FileTransfer>>;
|
||||
|
||||
// 设备操作
|
||||
pub fn save_known_device(device: &DeviceInfo) -> Result<()>;
|
||||
pub fn get_known_devices() -> Result<Vec<DeviceInfo>>;
|
||||
|
||||
// 设置操作
|
||||
pub fn get_setting(key: &str) -> Result<Option<String>>;
|
||||
pub fn set_setting(key: &str, value: &str) -> Result<()>;
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Models 模块
|
||||
|
||||
数据模型定义。
|
||||
|
||||
#### device.rs
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DeviceInfo {
|
||||
pub device_id: String, // UUID
|
||||
pub device_name: String, // 显示名称
|
||||
pub ip: String, // IP 地址
|
||||
pub ws_port: u16, // WebSocket 端口
|
||||
pub http_port: u16, // HTTP 端口
|
||||
pub last_seen: i64, // 最后在线时间
|
||||
}
|
||||
```
|
||||
|
||||
#### message.rs
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
pub id: String,
|
||||
pub from_device: String,
|
||||
pub to_device: String,
|
||||
pub content: String,
|
||||
pub message_type: MessageType,
|
||||
pub timestamp: i64,
|
||||
pub is_read: bool,
|
||||
}
|
||||
```
|
||||
|
||||
#### file_transfer.rs
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FileTransfer {
|
||||
pub file_id: String,
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub progress: f64,
|
||||
pub status: TransferStatus,
|
||||
pub mime_type: Option<String>,
|
||||
pub from_device: String,
|
||||
pub to_device: String,
|
||||
pub local_path: Option<String>,
|
||||
pub created_at: i64,
|
||||
pub completed_at: Option<i64>,
|
||||
pub transferred_bytes: u64,
|
||||
}
|
||||
|
||||
pub enum TransferStatus {
|
||||
Pending,
|
||||
Transferring,
|
||||
Completed,
|
||||
Failed,
|
||||
Cancelled,
|
||||
}
|
||||
```
|
||||
|
||||
#### events.rs
|
||||
|
||||
```rust
|
||||
// 事件名称常量
|
||||
pub mod event_names {
|
||||
pub const DEVICE_FOUND: &str = "device:found";
|
||||
pub const DEVICE_LOST: &str = "device:lost";
|
||||
pub const CHAT_MESSAGE: &str = "chat:message";
|
||||
pub const FILE_PROGRESS: &str = "file:progress";
|
||||
}
|
||||
|
||||
// 传输进度事件
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct TransferProgressEvent {
|
||||
pub file_id: String,
|
||||
pub progress: f64,
|
||||
pub transferred_bytes: u64,
|
||||
pub total_bytes: u64,
|
||||
pub status: TransferStatus,
|
||||
pub file_name: Option<String>,
|
||||
pub local_path: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
### 8. State 模块
|
||||
|
||||
全局应用状态管理。
|
||||
|
||||
```rust
|
||||
// 全局单例
|
||||
static APP_STATE: OnceCell<Arc<AppStateInner>> = OnceCell::new();
|
||||
|
||||
pub struct AppStateInner {
|
||||
pub local_device: RwLock<DeviceInfo>,
|
||||
pub device_manager: DeviceManager,
|
||||
pub discovery_service: RwLock<Option<DiscoveryService>>,
|
||||
pub ws_server: RwLock<Option<WsServer>>,
|
||||
pub ws_client: RwLock<Option<WsClient>>,
|
||||
pub http_server: RwLock<Option<HttpServer>>,
|
||||
pub http_client: HttpClient,
|
||||
pub connection_manager: ConnectionManager,
|
||||
pub app_data_dir: PathBuf,
|
||||
pub transfer_cancellation_tokens: RwLock<HashMap<String, CancellationToken>>,
|
||||
}
|
||||
|
||||
pub struct AppState;
|
||||
|
||||
impl AppState {
|
||||
pub fn init(app_data_dir: PathBuf) -> Result<()>;
|
||||
pub fn get() -> Arc<AppStateInner>;
|
||||
}
|
||||
```
|
||||
|
||||
## 错误处理
|
||||
|
||||
使用 `thiserror` 定义统一的错误类型:
|
||||
|
||||
```rust
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum AppError {
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
|
||||
#[error("WebSocket error: {0}")]
|
||||
WebSocket(String),
|
||||
|
||||
#[error("TLS error: {0}")]
|
||||
Tls(String),
|
||||
|
||||
#[error("File transfer error: {0}")]
|
||||
FileTransfer(String),
|
||||
|
||||
#[error("{0}")]
|
||||
General(String),
|
||||
}
|
||||
```
|
||||
|
||||
## 依赖关系
|
||||
|
||||
```
|
||||
main.rs
|
||||
└── lib.rs
|
||||
├── commands/
|
||||
│ ├── discovery_commands ──▶ discovery/, state
|
||||
│ ├── chat_commands ──────▶ websocket/, database/, state
|
||||
│ ├── file_commands ──────▶ http/, database/, state
|
||||
│ └── config_commands ────▶ utils/, database/
|
||||
├── state ──────────────────▶ models/, discovery/, websocket/, http/
|
||||
└── database/ ──────────────▶ models/
|
||||
```
|
||||
@ -0,0 +1,449 @@
|
||||
# 数据库设计
|
||||
|
||||
本文档介绍 Flash Send 的 SQLite 数据库设计。
|
||||
|
||||
## 概述
|
||||
|
||||
Flash Send 使用 SQLite 作为本地数据库,存储:
|
||||
- 聊天消息记录
|
||||
- 文件传输记录
|
||||
- 已知设备信息
|
||||
- 应用设置
|
||||
|
||||
**数据库文件位置**: `{app_data_dir}/flash_send.db`
|
||||
|
||||
## 表结构
|
||||
|
||||
### chat_messages
|
||||
|
||||
存储聊天消息。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
id TEXT PRIMARY KEY, -- 消息唯一 ID (UUID)
|
||||
from_device TEXT NOT NULL, -- 发送方设备 ID
|
||||
to_device TEXT NOT NULL, -- 接收方设备 ID
|
||||
content TEXT NOT NULL, -- 消息内容
|
||||
message_type TEXT NOT NULL, -- 消息类型 (text/image/file/system)
|
||||
timestamp INTEGER NOT NULL, -- 时间戳 (毫秒)
|
||||
is_read INTEGER DEFAULT 0 -- 是否已读 (0/1)
|
||||
);
|
||||
|
||||
-- 索引:按设备和时间查询
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_devices
|
||||
ON chat_messages(from_device, to_device);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_timestamp
|
||||
ON chat_messages(timestamp);
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| id | TEXT | 消息 UUID |
|
||||
| from_device | TEXT | 发送方设备 ID |
|
||||
| to_device | TEXT | 接收方设备 ID |
|
||||
| content | TEXT | 消息内容 |
|
||||
| message_type | TEXT | text/image/file/system |
|
||||
| timestamp | INTEGER | Unix 时间戳(毫秒) |
|
||||
| is_read | INTEGER | 0=未读, 1=已读 |
|
||||
|
||||
---
|
||||
|
||||
### file_transfers
|
||||
|
||||
存储文件传输记录。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS file_transfers (
|
||||
file_id TEXT PRIMARY KEY, -- 传输唯一 ID (UUID)
|
||||
name TEXT NOT NULL, -- 文件名
|
||||
size INTEGER NOT NULL, -- 文件大小 (字节)
|
||||
progress REAL DEFAULT 0, -- 传输进度 (0.0-1.0)
|
||||
status TEXT NOT NULL, -- 传输状态 (JSON)
|
||||
mime_type TEXT, -- MIME 类型
|
||||
from_device TEXT NOT NULL, -- 发送方设备 ID
|
||||
to_device TEXT NOT NULL, -- 接收方设备 ID
|
||||
local_path TEXT, -- 本地文件路径
|
||||
created_at INTEGER NOT NULL, -- 创建时间
|
||||
completed_at INTEGER, -- 完成时间
|
||||
transferred_bytes INTEGER DEFAULT 0 -- 已传输字节数
|
||||
);
|
||||
|
||||
-- 索引:按设备查询
|
||||
CREATE INDEX IF NOT EXISTS idx_transfers_devices
|
||||
ON file_transfers(from_device, to_device);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_transfers_created
|
||||
ON file_transfers(created_at);
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| file_id | TEXT | 传输 UUID |
|
||||
| name | TEXT | 文件名 |
|
||||
| size | INTEGER | 文件大小(字节) |
|
||||
| progress | REAL | 进度 0.0-1.0 |
|
||||
| status | TEXT | JSON: "pending"/"transferring"/"completed"/"failed"/"cancelled" |
|
||||
| mime_type | TEXT | MIME 类型 |
|
||||
| from_device | TEXT | 发送方设备 ID |
|
||||
| to_device | TEXT | 接收方设备 ID |
|
||||
| local_path | TEXT | 本地存储路径 |
|
||||
| created_at | INTEGER | 创建时间戳 |
|
||||
| completed_at | INTEGER | 完成时间戳 |
|
||||
| transferred_bytes | INTEGER | 已传输字节数 |
|
||||
|
||||
**status 字段格式**:
|
||||
```json
|
||||
"\"pending\""
|
||||
"\"transferring\""
|
||||
"\"completed\""
|
||||
"\"failed\""
|
||||
"\"cancelled\""
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### known_devices
|
||||
|
||||
存储已知设备信息。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS known_devices (
|
||||
device_id TEXT PRIMARY KEY, -- 设备唯一 ID
|
||||
device_name TEXT NOT NULL, -- 设备名称
|
||||
ip TEXT, -- 最后已知 IP
|
||||
ws_port INTEGER, -- WebSocket 端口
|
||||
http_port INTEGER, -- HTTP 端口
|
||||
last_seen INTEGER -- 最后在线时间
|
||||
);
|
||||
```
|
||||
|
||||
**字段说明**:
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| device_id | TEXT | 设备 UUID |
|
||||
| device_name | TEXT | 显示名称 |
|
||||
| ip | TEXT | IP 地址 |
|
||||
| ws_port | INTEGER | WebSocket 端口 |
|
||||
| http_port | INTEGER | HTTP 端口 |
|
||||
| last_seen | INTEGER | 最后在线时间戳 |
|
||||
|
||||
---
|
||||
|
||||
### app_settings
|
||||
|
||||
存储应用设置。
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS app_settings (
|
||||
key TEXT PRIMARY KEY, -- 设置键名
|
||||
value TEXT -- 设置值
|
||||
);
|
||||
```
|
||||
|
||||
**常用设置键**:
|
||||
|
||||
| 键名 | 说明 | 示例值 |
|
||||
|------|------|--------|
|
||||
| theme | 主题模式 | system/light/dark |
|
||||
| device_name | 设备名称 | My Computer |
|
||||
| download_dir | 下载目录 | C:\Downloads |
|
||||
| language | 语言 | zh-CN |
|
||||
|
||||
---
|
||||
|
||||
## 数据操作
|
||||
|
||||
### 消息操作
|
||||
|
||||
#### 保存消息
|
||||
|
||||
```rust
|
||||
pub fn save_message(message: &ChatMessage) -> Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO chat_messages
|
||||
(id, from_device, to_device, content, message_type, timestamp, is_read)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
|
||||
params![
|
||||
message.id,
|
||||
message.from_device,
|
||||
message.to_device,
|
||||
message.content,
|
||||
message.message_type.to_string(),
|
||||
message.timestamp,
|
||||
message.is_read as i32,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取聊天历史
|
||||
|
||||
```rust
|
||||
pub fn get_chat_history(
|
||||
device_id: &str,
|
||||
local_device_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Vec<ChatMessage>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT id, from_device, to_device, content, message_type, timestamp, is_read
|
||||
FROM chat_messages
|
||||
WHERE (from_device = ?1 AND to_device = ?2)
|
||||
OR (from_device = ?2 AND to_device = ?1)
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?3 OFFSET ?4"
|
||||
)?;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除聊天历史
|
||||
|
||||
```rust
|
||||
pub fn delete_chat_history(device_id: &str, local_device_id: &str) -> Result<()> {
|
||||
conn.execute(
|
||||
"DELETE FROM chat_messages
|
||||
WHERE (from_device = ?1 AND to_device = ?2)
|
||||
OR (from_device = ?2 AND to_device = ?1)",
|
||||
params![device_id, local_device_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 传输操作
|
||||
|
||||
#### 保存传输记录
|
||||
|
||||
```rust
|
||||
pub fn save_file_transfer(transfer: &FileTransfer) -> Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO file_transfers
|
||||
(file_id, name, size, progress, status, mime_type,
|
||||
from_device, to_device, local_path, created_at, completed_at, transferred_bytes)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
|
||||
ON CONFLICT(file_id) DO UPDATE SET
|
||||
progress = ?4, status = ?5, local_path = ?9,
|
||||
completed_at = ?11, transferred_bytes = ?12",
|
||||
params![...],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新传输进度
|
||||
|
||||
```rust
|
||||
pub fn update_transfer_progress(
|
||||
file_id: &str,
|
||||
progress: f64,
|
||||
transferred_bytes: u64,
|
||||
status: &str,
|
||||
) -> Result<()> {
|
||||
let completed_at = if status == "completed" || status == "failed" {
|
||||
Some(chrono::Utc::now().timestamp())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
conn.execute(
|
||||
"UPDATE file_transfers
|
||||
SET progress = ?1, transferred_bytes = ?2, status = ?3, completed_at = ?4
|
||||
WHERE file_id = ?5",
|
||||
params![progress, transferred_bytes, status, completed_at, file_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取传输历史
|
||||
|
||||
```rust
|
||||
pub fn get_transfer_history(
|
||||
device_id: &str,
|
||||
local_device_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<FileTransfer>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT file_id, name, size, progress, status, mime_type,
|
||||
from_device, to_device, local_path, created_at, completed_at, transferred_bytes
|
||||
FROM file_transfers
|
||||
WHERE (from_device = ?1 AND to_device = ?2)
|
||||
OR (from_device = ?2 AND to_device = ?1)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?3"
|
||||
)?;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 设备操作
|
||||
|
||||
#### 保存已知设备
|
||||
|
||||
```rust
|
||||
pub fn save_known_device(device: &DeviceInfo) -> Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO known_devices
|
||||
(device_id, device_name, ip, ws_port, http_port, last_seen)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(device_id) DO UPDATE SET
|
||||
device_name = ?2, ip = ?3, ws_port = ?4, http_port = ?5, last_seen = ?6",
|
||||
params![...],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 设置操作
|
||||
|
||||
#### 获取设置
|
||||
|
||||
```rust
|
||||
pub fn get_setting(key: &str) -> Result<Option<String>> {
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT value FROM app_settings WHERE key = ?1"
|
||||
)?;
|
||||
stmt.query_row(params![key], |row| row.get(0)).optional()
|
||||
}
|
||||
```
|
||||
|
||||
#### 保存设置
|
||||
|
||||
```rust
|
||||
pub fn set_setting(key: &str, value: &str) -> Result<()> {
|
||||
conn.execute(
|
||||
"INSERT INTO app_settings (key, value) VALUES (?1, ?2)
|
||||
ON CONFLICT(key) DO UPDATE SET value = ?2",
|
||||
params![key, value],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
### 迁移机制
|
||||
|
||||
使用版本号管理数据库迁移:
|
||||
|
||||
```rust
|
||||
pub fn init_database(conn: &Connection) -> Result<()> {
|
||||
// 创建版本表
|
||||
conn.execute(
|
||||
"CREATE TABLE IF NOT EXISTS schema_version (version INTEGER)",
|
||||
[],
|
||||
)?;
|
||||
|
||||
let version: i32 = conn
|
||||
.query_row("SELECT version FROM schema_version", [], |row| row.get(0))
|
||||
.unwrap_or(0);
|
||||
|
||||
// 根据版本执行迁移
|
||||
if version < 1 {
|
||||
// 创建初始表结构
|
||||
create_initial_tables(conn)?;
|
||||
update_version(conn, 1)?;
|
||||
}
|
||||
|
||||
if version < 2 {
|
||||
// 添加 transferred_bytes 字段
|
||||
add_transferred_bytes_column(conn)?;
|
||||
update_version(conn, 2)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移示例
|
||||
|
||||
添加 `transferred_bytes` 字段:
|
||||
|
||||
```rust
|
||||
fn add_transferred_bytes_column(conn: &Connection) -> Result<()> {
|
||||
// 检查字段是否存在
|
||||
let columns: Vec<String> = conn
|
||||
.prepare("PRAGMA table_info(file_transfers)")?
|
||||
.query_map([], |row| row.get::<_, String>(1))?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
if !columns.contains(&"transferred_bytes".to_string()) {
|
||||
conn.execute(
|
||||
"ALTER TABLE file_transfers ADD COLUMN transferred_bytes INTEGER DEFAULT 0",
|
||||
[],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据清理
|
||||
|
||||
### 自动清理策略
|
||||
|
||||
- **消息**: 保留最近 1000 条/设备
|
||||
- **传输记录**: 保留最近 100 条/设备
|
||||
- **离线设备**: 30 天未见自动删除
|
||||
|
||||
### 手动清理
|
||||
|
||||
```rust
|
||||
// 清理指定设备的所有数据
|
||||
pub fn cleanup_device_data(device_id: &str) -> Result<()> {
|
||||
conn.execute("DELETE FROM chat_messages WHERE from_device = ?1 OR to_device = ?1", [device_id])?;
|
||||
conn.execute("DELETE FROM file_transfers WHERE from_device = ?1 OR to_device = ?1", [device_id])?;
|
||||
conn.execute("DELETE FROM known_devices WHERE device_id = ?1", [device_id])?;
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 索引策略
|
||||
|
||||
- 按设备 ID 查询:`idx_messages_devices`, `idx_transfers_devices`
|
||||
- 按时间排序:`idx_messages_timestamp`, `idx_transfers_created`
|
||||
|
||||
### 连接池
|
||||
|
||||
使用全局单例连接,通过 `parking_lot::Mutex` 保护:
|
||||
|
||||
```rust
|
||||
static DB_CONN: OnceCell<Mutex<Connection>> = OnceCell::new();
|
||||
|
||||
pub fn conn() -> MutexGuard<'static, Connection> {
|
||||
DB_CONN.get().expect("Database not initialized").lock()
|
||||
}
|
||||
```
|
||||
|
||||
### 事务使用
|
||||
|
||||
批量操作使用事务提升性能:
|
||||
|
||||
```rust
|
||||
conn.execute("BEGIN TRANSACTION", [])?;
|
||||
for message in messages {
|
||||
save_message(&message)?;
|
||||
}
|
||||
conn.execute("COMMIT", [])?;
|
||||
```
|
||||
@ -0,0 +1,492 @@
|
||||
# 开发指南
|
||||
|
||||
本文档介绍 Flash Send 的开发环境搭建、开发流程和构建部署。
|
||||
|
||||
## 环境要求
|
||||
|
||||
### 系统要求
|
||||
|
||||
| 平台 | 最低版本 |
|
||||
|------|----------|
|
||||
| Windows | Windows 10 |
|
||||
| macOS | 10.15+ |
|
||||
| Linux | Ubuntu 18.04+ |
|
||||
|
||||
### 开发工具
|
||||
|
||||
| 工具 | 版本要求 | 用途 |
|
||||
|------|----------|------|
|
||||
| Node.js | >= 18.0 | 前端构建 |
|
||||
| Rust | >= 1.70 | 后端开发 |
|
||||
| pnpm/npm/yarn | 最新版 | 包管理 |
|
||||
|
||||
### 推荐 IDE
|
||||
|
||||
- **VS Code** + 插件:
|
||||
- Vue - Official
|
||||
- rust-analyzer
|
||||
- Tailwind CSS IntelliSense
|
||||
- Tauri
|
||||
|
||||
- **RustRover** / **WebStorm**
|
||||
|
||||
---
|
||||
|
||||
## 环境搭建
|
||||
|
||||
### 1. 克隆项目
|
||||
|
||||
```bash
|
||||
git clone https://github.com/your-repo/flash-send.git
|
||||
cd flash-send
|
||||
```
|
||||
|
||||
### 2. 安装 Node.js 依赖
|
||||
|
||||
```bash
|
||||
npm install
|
||||
# 或
|
||||
pnpm install
|
||||
# 或
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 3. 安装 Rust
|
||||
|
||||
```bash
|
||||
# Windows (使用 rustup-init.exe)
|
||||
# https://rustup.rs/
|
||||
|
||||
# macOS / Linux
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||
|
||||
# 验证安装
|
||||
rustc --version
|
||||
cargo --version
|
||||
```
|
||||
|
||||
### 4. 安装 Tauri CLI
|
||||
|
||||
```bash
|
||||
npm install -g @tauri-apps/cli
|
||||
# 或
|
||||
cargo install tauri-cli
|
||||
```
|
||||
|
||||
### 5. 系统依赖 (Linux)
|
||||
|
||||
```bash
|
||||
# Ubuntu/Debian
|
||||
sudo apt update
|
||||
sudo apt install libwebkit2gtk-4.0-dev \
|
||||
build-essential \
|
||||
curl \
|
||||
wget \
|
||||
file \
|
||||
libssl-dev \
|
||||
libgtk-3-dev \
|
||||
libayatana-appindicator3-dev \
|
||||
librsvg2-dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发命令
|
||||
|
||||
### 启动开发服务器
|
||||
|
||||
```bash
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
这会同时启动:
|
||||
- Vite 开发服务器(前端热重载)
|
||||
- Tauri 开发窗口(后端实时编译)
|
||||
|
||||
### 仅启动前端
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 类型检查
|
||||
|
||||
```bash
|
||||
# 前端 TypeScript
|
||||
npm run build # 包含 vue-tsc --noEmit
|
||||
|
||||
# 后端 Rust
|
||||
cd src-tauri
|
||||
cargo check
|
||||
```
|
||||
|
||||
### 代码格式化
|
||||
|
||||
```bash
|
||||
# 前端
|
||||
npx prettier --write src/
|
||||
|
||||
# 后端
|
||||
cd src-tauri
|
||||
cargo fmt
|
||||
```
|
||||
|
||||
### 代码检查
|
||||
|
||||
```bash
|
||||
# 后端
|
||||
cd src-tauri
|
||||
cargo clippy
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 项目结构说明
|
||||
|
||||
```
|
||||
flash-send/
|
||||
├── src/ # Vue 前端源码
|
||||
│ ├── api/ # Tauri API 封装
|
||||
│ ├── components/ # Vue 组件
|
||||
│ ├── pages/ # 页面组件
|
||||
│ ├── stores/ # Pinia 状态
|
||||
│ ├── hooks/ # 组合式函数
|
||||
│ ├── types/ # TypeScript 类型
|
||||
│ ├── router/ # Vue Router
|
||||
│ ├── styles/ # 全局样式
|
||||
│ ├── App.vue # 根组件
|
||||
│ └── main.ts # 入口文件
|
||||
│
|
||||
├── src-tauri/ # Rust 后端源码
|
||||
│ ├── src/
|
||||
│ │ ├── commands/ # Tauri 命令
|
||||
│ │ ├── discovery/ # 设备发现
|
||||
│ │ ├── websocket/ # WebSocket
|
||||
│ │ ├── http/ # HTTP 传输
|
||||
│ │ ├── tls/ # TLS 加密
|
||||
│ │ ├── database/ # SQLite
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ ├── state.rs # 全局状态
|
||||
│ │ ├── main.rs # 程序入口
|
||||
│ │ └── lib.rs # 库入口
|
||||
│ ├── Cargo.toml # Rust 依赖
|
||||
│ └── tauri.conf.json # Tauri 配置
|
||||
│
|
||||
├── document/ # 项目文档
|
||||
├── public/ # 静态资源
|
||||
├── index.html # HTML 入口
|
||||
├── package.json # Node 依赖
|
||||
├── vite.config.ts # Vite 配置
|
||||
├── tailwind.config.js # Tailwind 配置
|
||||
└── tsconfig.json # TypeScript 配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 开发流程
|
||||
|
||||
### 1. 新增 Tauri 命令
|
||||
|
||||
**步骤 a**: 在 `src-tauri/src/commands/` 创建或编辑命令文件
|
||||
|
||||
```rust
|
||||
// src-tauri/src/commands/my_commands.rs
|
||||
#[tauri::command]
|
||||
pub async fn my_command(param: String) -> CommandResult<String> {
|
||||
// 实现逻辑
|
||||
Ok(format!("Hello, {}!", param))
|
||||
}
|
||||
```
|
||||
|
||||
**步骤 b**: 在 `mod.rs` 注册命令
|
||||
|
||||
```rust
|
||||
// src-tauri/src/commands/mod.rs
|
||||
pub fn get_handlers() -> impl Fn(tauri::Builder<Wry>) -> tauri::Builder<Wry> {
|
||||
|builder| {
|
||||
builder.invoke_handler(tauri::generate_handler![
|
||||
// ... 其他命令
|
||||
my_command,
|
||||
])
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**步骤 c**: 在前端调用
|
||||
|
||||
```typescript
|
||||
// src/api/index.ts
|
||||
export const myApi = {
|
||||
myCommand: (param: string) => invoke<string>('my_command', { param }),
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 新增前端页面
|
||||
|
||||
**步骤 a**: 创建页面组件
|
||||
|
||||
```vue
|
||||
<!-- src/pages/MyPage.vue -->
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const data = ref('')
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h1>My Page</h1>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
**步骤 b**: 添加路由
|
||||
|
||||
```typescript
|
||||
// src/router/index.ts
|
||||
{
|
||||
path: '/my-page',
|
||||
name: 'myPage',
|
||||
component: () => import('../pages/MyPage.vue')
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 新增后端事件
|
||||
|
||||
**步骤 a**: 定义事件
|
||||
|
||||
```rust
|
||||
// src-tauri/src/models/events.rs
|
||||
pub mod event_names {
|
||||
pub const MY_EVENT: &str = "my:event";
|
||||
}
|
||||
```
|
||||
|
||||
**步骤 b**: 发送事件
|
||||
|
||||
```rust
|
||||
use tauri::Emitter;
|
||||
|
||||
app.emit("my:event", payload)?;
|
||||
```
|
||||
|
||||
**步骤 c**: 前端监听
|
||||
|
||||
```typescript
|
||||
import { listen } from '@tauri-apps/api/event'
|
||||
|
||||
const unlisten = await listen('my:event', (event) => {
|
||||
console.log(event.payload)
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 前端调试
|
||||
|
||||
1. **Chrome DevTools**: `Ctrl+Shift+I` 或 `F12`
|
||||
2. **Vue DevTools**: 安装浏览器扩展
|
||||
3. **Console 日志**: 使用 `console.log()`
|
||||
|
||||
### 后端调试
|
||||
|
||||
1. **日志输出**:
|
||||
```rust
|
||||
log::info!("Debug message: {:?}", value);
|
||||
log::error!("Error: {}", err);
|
||||
```
|
||||
|
||||
2. **环境变量**:
|
||||
```bash
|
||||
RUST_LOG=debug npm run tauri dev
|
||||
```
|
||||
|
||||
3. **查看后端日志**: 在终端中查看 Tauri 输出
|
||||
|
||||
### 网络调试
|
||||
|
||||
1. **WebSocket**: 使用浏览器 DevTools Network 面板
|
||||
2. **HTTP**: 使用 Postman 或 curl 测试
|
||||
3. **UDP**: 使用 Wireshark 抓包
|
||||
|
||||
---
|
||||
|
||||
## 构建发布
|
||||
|
||||
### 构建生产版本
|
||||
|
||||
```bash
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
**输出目录**: `src-tauri/target/release/bundle/`
|
||||
|
||||
### 构建产物
|
||||
|
||||
| 平台 | 格式 |
|
||||
|------|------|
|
||||
| Windows | `.msi`, `.exe` (NSIS) |
|
||||
| macOS | `.dmg`, `.app` |
|
||||
| Linux | `.deb`, `.AppImage` |
|
||||
|
||||
### 配置构建
|
||||
|
||||
编辑 `src-tauri/tauri.conf.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"productName": "Flash Send",
|
||||
"version": "1.0.0",
|
||||
"identifier": "com.flashsend.app",
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": ["icons/icon.ico", "icons/icon.png"],
|
||||
"windows": {
|
||||
"certificateThumbprint": null,
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 代码签名
|
||||
|
||||
#### Windows
|
||||
|
||||
```json
|
||||
{
|
||||
"bundle": {
|
||||
"windows": {
|
||||
"certificateThumbprint": "YOUR_CERT_THUMBPRINT",
|
||||
"digestAlgorithm": "sha256",
|
||||
"timestampUrl": "http://timestamp.digicert.com"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### macOS
|
||||
|
||||
```bash
|
||||
export APPLE_SIGNING_IDENTITY="Developer ID Application: Your Name (XXXXXXXXXX)"
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 测试
|
||||
|
||||
### 单元测试 (Rust)
|
||||
|
||||
```bash
|
||||
cd src-tauri
|
||||
cargo test
|
||||
```
|
||||
|
||||
### 集成测试
|
||||
|
||||
1. 启动两个实例(不同机器或虚拟机)
|
||||
2. 测试设备发现
|
||||
3. 测试聊天功能
|
||||
4. 测试文件传输
|
||||
|
||||
### 测试清单
|
||||
|
||||
- [ ] 设备发现正常
|
||||
- [ ] WebSocket 连接稳定
|
||||
- [ ] 消息收发正常
|
||||
- [ ] 文件传输完整
|
||||
- [ ] 取消传输有效
|
||||
- [ ] 深色模式正常
|
||||
- [ ] 数据库持久化
|
||||
- [ ] 重启后数据恢复
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 编译时出现 OpenSSL 错误
|
||||
|
||||
**A**: 安装 OpenSSL 开发库
|
||||
|
||||
```bash
|
||||
# Ubuntu
|
||||
sudo apt install libssl-dev
|
||||
|
||||
# macOS
|
||||
brew install openssl
|
||||
```
|
||||
|
||||
### Q: Windows 上编译失败
|
||||
|
||||
**A**: 安装 Visual Studio Build Tools
|
||||
|
||||
下载地址: https://visualstudio.microsoft.com/visual-cpp-build-tools/
|
||||
|
||||
### Q: 设备无法发现
|
||||
|
||||
**A**: 检查防火墙设置,确保 UDP 53317 端口开放
|
||||
|
||||
### Q: WebSocket 连接失败
|
||||
|
||||
**A**: 检查防火墙设置,确保 TCP 53318 端口开放
|
||||
|
||||
### Q: 文件传输失败
|
||||
|
||||
**A**:
|
||||
1. 检查防火墙设置,确保 TCP 53319 端口开放
|
||||
2. 检查磁盘空间是否充足
|
||||
3. 检查下载目录权限
|
||||
|
||||
---
|
||||
|
||||
## 贡献指南
|
||||
|
||||
### 提交规范
|
||||
|
||||
使用 Conventional Commits:
|
||||
|
||||
```
|
||||
feat: 添加新功能
|
||||
fix: 修复 bug
|
||||
docs: 更新文档
|
||||
style: 代码格式调整
|
||||
refactor: 代码重构
|
||||
test: 添加测试
|
||||
chore: 构建/工具变更
|
||||
```
|
||||
|
||||
### 分支策略
|
||||
|
||||
- `main`: 稳定版本
|
||||
- `develop`: 开发分支
|
||||
- `feature/*`: 功能分支
|
||||
- `fix/*`: 修复分支
|
||||
|
||||
### PR 流程
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支
|
||||
3. 提交变更
|
||||
4. 创建 Pull Request
|
||||
5. 代码审查
|
||||
6. 合并到 develop
|
||||
|
||||
---
|
||||
|
||||
## 参考资源
|
||||
|
||||
- [Tauri 官方文档](https://tauri.app/v2/guides/)
|
||||
- [Vue 3 文档](https://vuejs.org/)
|
||||
- [Rust 官方文档](https://doc.rust-lang.org/)
|
||||
- [TailwindCSS 文档](https://tailwindcss.com/docs)
|
||||
- [Pinia 文档](https://pinia.vuejs.org/)
|
||||
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/flash-send.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Flash Send - 局域网传输与聊天</title>
|
||||
</head>
|
||||
<body class="bg-surface-50">
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "flash-send",
|
||||
"version": "1.0.0",
|
||||
"description": "跨平台局域网文件传输与聊天应用",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
"pinia": "^2.1.7",
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-fs": "^2.0.0",
|
||||
"@tauri-apps/plugin-shell": "^2.0.0",
|
||||
"lucide-vue-next": "^0.344.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.0.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"typescript": "^5.4.2",
|
||||
"vite": "^5.1.6",
|
||||
"vue-tsc": "^2.0.6",
|
||||
"autoprefixer": "^10.4.18",
|
||||
"postcss": "^8.4.35",
|
||||
"tailwindcss": "^3.4.1"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<defs>
|
||||
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" style="stop-color:#0ea5e9;stop-opacity:1" />
|
||||
<stop offset="100%" style="stop-color:#0284c7;stop-opacity:1" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<rect width="100" height="100" rx="20" fill="url(#grad)"/>
|
||||
<path d="M30 25 L70 50 L30 75 Z" fill="white" opacity="0.9"/>
|
||||
<path d="M45 35 L75 50 L45 65 Z" fill="white" opacity="0.6"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 507 B |
@ -0,0 +1,12 @@
|
||||
@echo off
|
||||
REM 运行第二个 Flash Send 实例用于测试
|
||||
REM 使用不同的端口和实例名称
|
||||
|
||||
set FLASH_SEND_PORT_OFFSET=100
|
||||
set FLASH_SEND_INSTANCE=测试2
|
||||
|
||||
echo 启动 Flash Send 实例 2...
|
||||
echo 端口偏移: +100 (UDP:53417, WS:53418, HTTP:53419)
|
||||
|
||||
cd /d %~dp0..
|
||||
cargo run --manifest-path src-tauri/Cargo.toml
|
||||
@ -0,0 +1,24 @@
|
||||
# 运行第二个 Flash Send 实例用于测试
|
||||
# 使用不同的端口和实例名称
|
||||
|
||||
$env:FLASH_SEND_PORT_OFFSET = "100"
|
||||
$env:FLASH_SEND_INSTANCE = "Test2"
|
||||
|
||||
Write-Host "启动 Flash Send 实例 2..." -ForegroundColor Green
|
||||
Write-Host "UDP:53317 (共用) | WS:53418 | HTTP:53419" -ForegroundColor Cyan
|
||||
|
||||
$exePath = "$PSScriptRoot\..\src-tauri\target\release\flash-send-instance2.exe"
|
||||
|
||||
# 如果副本不存在则复制
|
||||
if (-not (Test-Path $exePath)) {
|
||||
$srcPath = "$PSScriptRoot\..\src-tauri\target\release\flash-send.exe"
|
||||
if (Test-Path $srcPath) {
|
||||
Copy-Item $srcPath $exePath
|
||||
Write-Host "已复制可执行文件" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "错误: 请先编译主程序 (cargo build --release)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
& $exePath
|
||||
@ -0,0 +1,3 @@
|
||||
# Windows 静态链接 MSVC 运行时,无需用户安装 VC++ Redistributable
|
||||
[target.x86_64-pc-windows-msvc]
|
||||
rustflags = ["-C", "target-feature=+crt-static"]
|
||||
@ -0,0 +1,86 @@
|
||||
[package]
|
||||
name = "flash-send"
|
||||
version = "1.0.0"
|
||||
description = "跨平台局域网文件传输与聊天应用"
|
||||
authors = ["Flash Send Team"]
|
||||
edition = "2021"
|
||||
rust-version = "1.70"
|
||||
|
||||
[lib]
|
||||
name = "flash_send_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
# Tauri 核心
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-fs = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
|
||||
# 异步运行时
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
# 序列化
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
# 数据库
|
||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||
|
||||
# TLS 加密
|
||||
rustls = { version = "0.21", features = ["dangerous_configuration"] }
|
||||
rustls-pemfile = "1"
|
||||
rcgen = "0.11"
|
||||
tokio-rustls = "0.24"
|
||||
|
||||
# WebSocket
|
||||
tokio-tungstenite = { version = "0.20", features = ["rustls-tls-native-roots"] }
|
||||
futures-util = "0.3"
|
||||
|
||||
# HTTP 服务
|
||||
axum = { version = "0.6", features = ["multipart", "tokio"] }
|
||||
axum-server = { version = "0.5", features = ["tls-rustls"] }
|
||||
tower = "0.4"
|
||||
tower-http = { version = "0.4", features = ["cors", "fs"] }
|
||||
hyper = "0.14"
|
||||
|
||||
# 网络工具
|
||||
local-ip-address = "0.6"
|
||||
socket2 = { version = "0.5", features = ["all"] }
|
||||
|
||||
# UUID 生成
|
||||
uuid = { version = "1", features = ["v4", "serde"] }
|
||||
|
||||
# 时间处理
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
# 日志
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
|
||||
# 错误处理
|
||||
thiserror = "1"
|
||||
anyhow = "1"
|
||||
|
||||
# 文件处理
|
||||
mime_guess = "2"
|
||||
tokio-util = { version = "0.7", features = ["io"] }
|
||||
|
||||
# 并发工具
|
||||
parking_lot = "0.12"
|
||||
dashmap = "5"
|
||||
once_cell = "1"
|
||||
|
||||
# 系统信息
|
||||
hostname = "0.4"
|
||||
dirs = "5"
|
||||
|
||||
# 时间处理 (for rcgen)
|
||||
time = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2/schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default capability for Flash Send",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"dialog:default",
|
||||
"fs:default",
|
||||
"shell:default",
|
||||
"core:event:default",
|
||||
"core:window:default"
|
||||
]
|
||||
}
|
||||
@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capability for Flash Send","local":true,"windows":["main"],"permissions":["core:default","dialog:default","fs:default","shell:default","core:event:default","core:window:default"]}}
|
||||
@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
</adaptive-icon>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 7.4 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#fff</color>
|
||||
</resources>
|
||||
|
After Width: | Height: | Size: 497 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 703 B |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.9 KiB |
|
After Width: | Height: | Size: 929 B |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 2.6 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.6 KiB |
@ -0,0 +1,269 @@
|
||||
//! 聊天相关命令
|
||||
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::models::{ChatMessage, event_names};
|
||||
use crate::state::AppState;
|
||||
use crate::utils::{CommandError, CommandResult, HEARTBEAT_INTERVAL_MS};
|
||||
use crate::websocket::{HeartbeatManager, WsClient, WsConnectionEvent, WsServer};
|
||||
|
||||
/// 启动 WebSocket 服务端
|
||||
#[tauri::command]
|
||||
pub async fn start_websocket_server(app: AppHandle) -> CommandResult<()> {
|
||||
let state = AppState::get();
|
||||
let local_device = state.local_device.read().clone();
|
||||
|
||||
// 创建并启动 WS 服务端
|
||||
let mut server = WsServer::new(
|
||||
local_device.ws_port,
|
||||
local_device.device_id.clone(),
|
||||
state.connection_manager.clone(),
|
||||
);
|
||||
|
||||
server.start().await.map_err(|e| CommandError {
|
||||
code: "WS_SERVER_ERROR".to_string(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
*state.ws_server.write() = Some(server);
|
||||
|
||||
// 创建 WS 客户端
|
||||
let client = WsClient::new(
|
||||
local_device.device_id.clone(),
|
||||
state.connection_manager.clone(),
|
||||
);
|
||||
*state.ws_client.write() = Some(client);
|
||||
|
||||
// 启动心跳管理
|
||||
HeartbeatManager::start(
|
||||
state.connection_manager.clone(),
|
||||
local_device.device_id.clone(),
|
||||
HEARTBEAT_INTERVAL_MS,
|
||||
);
|
||||
|
||||
// 监听连接事件
|
||||
let mut rx = state.connection_manager.subscribe();
|
||||
let app_handle = app.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = rx.recv().await {
|
||||
match event {
|
||||
WsConnectionEvent::Connected { device_id } => {
|
||||
let _ = app_handle.emit(event_names::WEBSOCKET_CONNECTED, serde_json::json!({
|
||||
"deviceId": device_id
|
||||
}));
|
||||
}
|
||||
WsConnectionEvent::Disconnected { device_id, reason } => {
|
||||
let _ = app_handle.emit(event_names::WEBSOCKET_DISCONNECTED, serde_json::json!({
|
||||
"deviceId": device_id,
|
||||
"reason": reason
|
||||
}));
|
||||
}
|
||||
WsConnectionEvent::MessageReceived { message } => {
|
||||
let _ = app_handle.emit(event_names::MESSAGE_RECEIVED, &message);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log::info!("WebSocket server started on port {}", local_device.ws_port);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止 WebSocket 服务端
|
||||
#[tauri::command]
|
||||
pub async fn stop_websocket_server() -> CommandResult<()> {
|
||||
let state = AppState::get();
|
||||
|
||||
// 关闭所有连接
|
||||
state.connection_manager.close_all().await;
|
||||
|
||||
// 停止服务端
|
||||
if let Some(mut server) = state.ws_server.write().take() {
|
||||
server.stop();
|
||||
}
|
||||
|
||||
*state.ws_client.write() = None;
|
||||
|
||||
log::info!("WebSocket server stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 连接到指定设备
|
||||
#[tauri::command]
|
||||
pub async fn connect_to_peer(device_id: String) -> CommandResult<()> {
|
||||
let state = AppState::get();
|
||||
|
||||
// 获取设备信息
|
||||
let device = state
|
||||
.device_manager
|
||||
.get_device(&device_id)
|
||||
.ok_or_else(|| CommandError {
|
||||
code: "DEVICE_NOT_FOUND".to_string(),
|
||||
message: format!("Device {} not found", device_id),
|
||||
})?;
|
||||
|
||||
// 检查是否已连接 (同步检查,立即释放锁)
|
||||
{
|
||||
let client_guard = state.ws_client.read();
|
||||
if let Some(client) = client_guard.as_ref() {
|
||||
if client.is_connected(&device_id) || state.connection_manager.is_connected(&device_id) {
|
||||
return Ok(());
|
||||
}
|
||||
} else if state.connection_manager.is_connected(&device_id) {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// 克隆客户端引用以便异步使用
|
||||
let ws_client = {
|
||||
let client_guard = state.ws_client.read();
|
||||
client_guard.as_ref().cloned()
|
||||
};
|
||||
|
||||
let client = ws_client.ok_or_else(|| CommandError {
|
||||
code: "WS_NOT_STARTED".to_string(),
|
||||
message: "WebSocket client not started".to_string(),
|
||||
})?;
|
||||
|
||||
client.connect(&device).await.map_err(|e| CommandError {
|
||||
code: "CONNECTION_ERROR".to_string(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
// 更新设备在线状态
|
||||
state.device_manager.set_device_online(&device_id, true);
|
||||
|
||||
log::info!("Connected to device {}", device_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 断开与指定设备的连接
|
||||
#[tauri::command]
|
||||
pub async fn disconnect_from_peer(device_id: String) -> CommandResult<()> {
|
||||
let state = AppState::get();
|
||||
|
||||
// 克隆客户端引用
|
||||
let ws_client = {
|
||||
let client_guard = state.ws_client.read();
|
||||
client_guard.as_ref().cloned()
|
||||
};
|
||||
|
||||
if let Some(client) = ws_client {
|
||||
client.disconnect(&device_id).await;
|
||||
}
|
||||
|
||||
state.device_manager.set_device_online(&device_id, false);
|
||||
|
||||
log::info!("Disconnected from device {}", device_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 发送聊天消息
|
||||
#[tauri::command]
|
||||
pub async fn send_chat_message(
|
||||
device_id: String,
|
||||
content: String,
|
||||
message_type: Option<String>,
|
||||
) -> CommandResult<ChatMessage> {
|
||||
let state = AppState::get();
|
||||
|
||||
// 获取本地设备 ID (立即释放锁)
|
||||
let local_device_id = {
|
||||
let local_device = state.local_device.read();
|
||||
local_device.device_id.clone()
|
||||
};
|
||||
|
||||
// 创建消息
|
||||
let message = match message_type.as_deref() {
|
||||
Some("image") => ChatMessage::new_image(
|
||||
local_device_id,
|
||||
device_id.clone(),
|
||||
content,
|
||||
),
|
||||
_ => ChatMessage::new_text(
|
||||
local_device_id,
|
||||
device_id.clone(),
|
||||
content,
|
||||
),
|
||||
};
|
||||
|
||||
// 保存到数据库
|
||||
Database::save_message(&message).map_err(|e| CommandError {
|
||||
code: "DB_ERROR".to_string(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
// 获取客户端 (立即释放锁)
|
||||
let ws_client = {
|
||||
let client_guard = state.ws_client.read();
|
||||
client_guard.as_ref().cloned()
|
||||
};
|
||||
|
||||
let mut sent = false;
|
||||
|
||||
// 尝试通过客户端发送(主动连接的情况)
|
||||
if let Some(client) = &ws_client {
|
||||
if client.is_connected(&device_id) {
|
||||
client.send_to(&device_id, &message).await.map_err(|e| CommandError {
|
||||
code: "SEND_ERROR".to_string(),
|
||||
message: e,
|
||||
})?;
|
||||
sent = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果客户端没有发送,尝试通过服务端连接发送(被动连接的情况)
|
||||
if !sent {
|
||||
state
|
||||
.connection_manager
|
||||
.send_to(&device_id, &message)
|
||||
.await
|
||||
.map_err(|e| CommandError {
|
||||
code: "SEND_ERROR".to_string(),
|
||||
message: e,
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
/// 获取聊天历史
|
||||
#[tauri::command]
|
||||
pub async fn get_chat_history(
|
||||
device_id: String,
|
||||
limit: Option<usize>,
|
||||
offset: Option<usize>,
|
||||
) -> CommandResult<Vec<ChatMessage>> {
|
||||
let state = AppState::get();
|
||||
let local_device = state.local_device.read();
|
||||
|
||||
let messages = Database::get_chat_history(
|
||||
&device_id,
|
||||
&local_device.device_id,
|
||||
limit.unwrap_or(50),
|
||||
offset.unwrap_or(0),
|
||||
)
|
||||
.map_err(|e| CommandError {
|
||||
code: "DB_ERROR".to_string(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// 删除聊天历史
|
||||
#[tauri::command]
|
||||
pub async fn delete_chat_history(device_id: String) -> CommandResult<usize> {
|
||||
let state = AppState::get();
|
||||
let local_device = state.local_device.read();
|
||||
|
||||
let count = Database::delete_chat_history(&device_id, &local_device.device_id)
|
||||
.map_err(|e| CommandError {
|
||||
code: "DB_ERROR".to_string(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
Ok(count)
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
//! 配置相关命令
|
||||
|
||||
use crate::models::DeviceInfo;
|
||||
use crate::state::AppState;
|
||||
use crate::utils::{AppConfig, CommandResult};
|
||||
|
||||
/// 获取应用配置
|
||||
#[tauri::command]
|
||||
pub async fn get_app_config() -> CommandResult<AppConfig> {
|
||||
Ok(AppConfig::get())
|
||||
}
|
||||
|
||||
/// 更新设备名称
|
||||
#[tauri::command]
|
||||
pub async fn update_device_name(name: String) -> CommandResult<()> {
|
||||
AppConfig::set_device_name(name.clone());
|
||||
|
||||
// 更新本机设备信息
|
||||
let state = AppState::get();
|
||||
let mut device = state.local_device.write();
|
||||
device.device_name = name;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新下载目录
|
||||
#[tauri::command]
|
||||
pub async fn update_download_dir(dir: String) -> CommandResult<()> {
|
||||
// 验证目录是否存在
|
||||
let path = std::path::PathBuf::from(&dir);
|
||||
if !path.exists() {
|
||||
std::fs::create_dir_all(&path).map_err(|e| crate::utils::CommandError {
|
||||
code: "IO_ERROR".to_string(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
}
|
||||
|
||||
AppConfig::set_download_dir(dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取本机设备信息
|
||||
#[tauri::command]
|
||||
pub async fn get_local_device_info() -> CommandResult<DeviceInfo> {
|
||||
let state = AppState::get();
|
||||
state.update_local_ip();
|
||||
|
||||
let device = state.local_device.read().clone();
|
||||
Ok(device)
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
//! 设备发现相关命令
|
||||
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::discovery::{DiscoveryEvent, DiscoveryService};
|
||||
use crate::models::{DeviceInfo, event_names};
|
||||
use crate::state::AppState;
|
||||
use crate::utils::CommandResult;
|
||||
|
||||
/// 启动设备发现服务
|
||||
#[tauri::command]
|
||||
pub async fn start_discovery_service(app: AppHandle) -> CommandResult<()> {
|
||||
let state = AppState::get();
|
||||
|
||||
// 更新本机 IP
|
||||
state.update_local_ip();
|
||||
|
||||
let local_device = state.local_device.read().clone();
|
||||
|
||||
// 创建并启动发现服务
|
||||
let mut discovery = DiscoveryService::new(local_device);
|
||||
let mut rx = discovery.subscribe();
|
||||
|
||||
discovery.start().await.map_err(|e| crate::utils::CommandError {
|
||||
code: "DISCOVERY_ERROR".to_string(),
|
||||
message: e.to_string(),
|
||||
})?;
|
||||
|
||||
// 存储服务实例
|
||||
*state.discovery_service.write() = Some(discovery);
|
||||
|
||||
// 启动设备超时检查
|
||||
state.device_manager.start_timeout_checker();
|
||||
|
||||
// 监听发现事件并转发到前端
|
||||
let app_handle = app.clone();
|
||||
let device_manager = state.device_manager.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(event) = rx.recv().await {
|
||||
match event {
|
||||
DiscoveryEvent::DeviceFound(device) => {
|
||||
device_manager.upsert_device(device.clone());
|
||||
let _ = app_handle.emit(event_names::DEVICE_FOUND, &device);
|
||||
}
|
||||
DiscoveryEvent::DeviceLost(device_id) => {
|
||||
device_manager.remove_device(&device_id);
|
||||
let _ = app_handle.emit(event_names::DEVICE_LOST, serde_json::json!({
|
||||
"deviceId": device_id
|
||||
}));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
log::info!("Discovery service started");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止设备发现服务
|
||||
#[tauri::command]
|
||||
pub async fn stop_discovery_service() -> CommandResult<()> {
|
||||
let state = AppState::get();
|
||||
|
||||
// 取出 discovery 服务,释放锁后再操作
|
||||
let discovery_opt = state.discovery_service.write().take();
|
||||
|
||||
if let Some(mut discovery) = discovery_opt {
|
||||
// 发送离线通知
|
||||
let _ = discovery.send_goodbye().await;
|
||||
discovery.stop();
|
||||
}
|
||||
|
||||
// 清空设备列表
|
||||
state.device_manager.clear();
|
||||
|
||||
log::info!("Discovery service stopped");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取已发现的设备列表
|
||||
#[tauri::command]
|
||||
pub async fn get_discovered_devices() -> CommandResult<Vec<DeviceInfo>> {
|
||||
let state = AppState::get();
|
||||
let devices = state.device_manager.get_all_devices();
|
||||
Ok(devices)
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
//! Tauri 命令模块
|
||||
//! 定义所有前端可调用的命令
|
||||
|
||||
mod discovery_commands;
|
||||
mod chat_commands;
|
||||
mod file_commands;
|
||||
mod config_commands;
|
||||
|
||||
pub use discovery_commands::*;
|
||||
pub use chat_commands::*;
|
||||
pub use file_commands::*;
|
||||
pub use config_commands::*;
|
||||
|
||||
use tauri::ipc::Invoke;
|
||||
|
||||
/// 注册所有命令
|
||||
pub fn get_handlers() -> impl Fn(Invoke) -> bool {
|
||||
tauri::generate_handler![
|
||||
// 发现服务命令
|
||||
start_discovery_service,
|
||||
stop_discovery_service,
|
||||
get_discovered_devices,
|
||||
// WebSocket 命令
|
||||
start_websocket_server,
|
||||
stop_websocket_server,
|
||||
connect_to_peer,
|
||||
disconnect_from_peer,
|
||||
// 聊天命令
|
||||
send_chat_message,
|
||||
get_chat_history,
|
||||
delete_chat_history,
|
||||
// 文件传输命令
|
||||
start_http_server,
|
||||
stop_http_server,
|
||||
select_file_to_send,
|
||||
send_file,
|
||||
cancel_transfer,
|
||||
get_transfer_history,
|
||||
open_file_location,
|
||||
// 配置命令
|
||||
get_app_config,
|
||||
update_device_name,
|
||||
update_download_dir,
|
||||
get_local_device_info,
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
//! 数据库模块
|
||||
//! 使用 SQLite 存储聊天记录和文件传输历史
|
||||
|
||||
mod schema;
|
||||
mod repository;
|
||||
|
||||
pub use schema::*;
|
||||
pub use repository::*;
|
||||
@ -0,0 +1,338 @@
|
||||
//! 数据库操作封装
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use parking_lot::Mutex;
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::models::{ChatMessage, FileTransfer, MessageStatus, MessageType, TransferStatus, DeviceInfo};
|
||||
use crate::utils::AppError;
|
||||
use super::init_database;
|
||||
|
||||
/// 全局数据库连接
|
||||
static DB_CONNECTION: OnceCell<Mutex<Connection>> = OnceCell::new();
|
||||
|
||||
/// 数据库仓库
|
||||
pub struct Database;
|
||||
|
||||
impl Database {
|
||||
/// 初始化数据库连接
|
||||
pub fn init(app_data_dir: PathBuf) -> Result<(), AppError> {
|
||||
// 确保目录存在
|
||||
std::fs::create_dir_all(&app_data_dir)?;
|
||||
|
||||
let db_path = app_data_dir.join("flash_send.db");
|
||||
let conn = Connection::open(&db_path)?;
|
||||
|
||||
// 启用外键约束
|
||||
conn.execute("PRAGMA foreign_keys = ON", [])?;
|
||||
|
||||
// 初始化表结构
|
||||
init_database(&conn)?;
|
||||
|
||||
DB_CONNECTION
|
||||
.set(Mutex::new(conn))
|
||||
.map_err(|_| AppError::Database(rusqlite::Error::InvalidPath(db_path)))?;
|
||||
|
||||
log::info!("Database initialized at {:?}", app_data_dir);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取数据库连接
|
||||
fn conn() -> &'static Mutex<Connection> {
|
||||
DB_CONNECTION.get().expect("Database not initialized")
|
||||
}
|
||||
|
||||
// ==================== 聊天消息操作 ====================
|
||||
|
||||
/// 保存聊天消息
|
||||
pub fn save_message(message: &ChatMessage) -> Result<(), AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO chat_messages (id, message_type, content, from_device, to_device, timestamp, status)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
|
||||
ON CONFLICT(id) DO UPDATE SET status = ?7
|
||||
"#,
|
||||
params![
|
||||
message.id,
|
||||
serde_json::to_string(&message.message_type)?,
|
||||
message.content,
|
||||
message.from,
|
||||
message.to,
|
||||
message.timestamp,
|
||||
serde_json::to_string(&message.status)?,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新消息状态
|
||||
pub fn update_message_status(message_id: &str, status: MessageStatus) -> Result<(), AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
conn.execute(
|
||||
"UPDATE chat_messages SET status = ?1 WHERE id = ?2",
|
||||
params![serde_json::to_string(&status)?, message_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取与指定设备的聊天历史
|
||||
pub fn get_chat_history(
|
||||
device_id: &str,
|
||||
local_device_id: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
) -> Result<Vec<ChatMessage>, AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT id, message_type, content, from_device, to_device, timestamp, status
|
||||
FROM chat_messages
|
||||
WHERE (from_device = ?1 AND to_device = ?2) OR (from_device = ?2 AND to_device = ?1)
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT ?3 OFFSET ?4
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let messages = stmt
|
||||
.query_map(params![device_id, local_device_id, limit, offset], |row| {
|
||||
let message_type_str: String = row.get(1)?;
|
||||
let status_str: String = row.get(6)?;
|
||||
|
||||
Ok(ChatMessage {
|
||||
id: row.get(0)?,
|
||||
message_type: serde_json::from_str(&message_type_str).unwrap_or(MessageType::Text),
|
||||
content: row.get(2)?,
|
||||
from: row.get(3)?,
|
||||
to: row.get(4)?,
|
||||
timestamp: row.get(5)?,
|
||||
status: serde_json::from_str(&status_str).unwrap_or(MessageStatus::Pending),
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
/// 删除与指定设备的所有聊天记录
|
||||
pub fn delete_chat_history(device_id: &str, local_device_id: &str) -> Result<usize, AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
let count = conn.execute(
|
||||
r#"
|
||||
DELETE FROM chat_messages
|
||||
WHERE (from_device = ?1 AND to_device = ?2) OR (from_device = ?2 AND to_device = ?1)
|
||||
"#,
|
||||
params![device_id, local_device_id],
|
||||
)?;
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
// ==================== 文件传输操作 ====================
|
||||
|
||||
/// 保存文件传输记录
|
||||
pub fn save_file_transfer(transfer: &FileTransfer) -> Result<(), AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO file_transfers
|
||||
(file_id, name, size, progress, status, mime_type, from_device, to_device, local_path, created_at, completed_at, transferred_bytes)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12)
|
||||
ON CONFLICT(file_id) DO UPDATE SET
|
||||
progress = ?4, status = ?5, local_path = ?9, completed_at = ?11, transferred_bytes = ?12
|
||||
"#,
|
||||
params![
|
||||
transfer.file_id,
|
||||
transfer.name,
|
||||
transfer.size as i64,
|
||||
transfer.progress,
|
||||
serde_json::to_string(&transfer.status)?,
|
||||
transfer.mime_type,
|
||||
transfer.from_device,
|
||||
transfer.to_device,
|
||||
transfer.local_path,
|
||||
transfer.created_at,
|
||||
transfer.completed_at,
|
||||
transfer.transferred_bytes as i64,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新文件传输状态
|
||||
pub fn update_transfer_status(file_id: &str, status: &str) -> Result<(), AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
let completed_at = chrono::Utc::now().timestamp();
|
||||
|
||||
conn.execute(
|
||||
"UPDATE file_transfers SET status = ?1, completed_at = ?2 WHERE file_id = ?3",
|
||||
params![format!("\"{}\"", status), completed_at, file_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 更新文件传输进度和状态
|
||||
pub fn update_transfer_progress(file_id: &str, progress: f64, transferred_bytes: u64, status: &str) -> Result<(), AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
let completed_at = if status == "completed" || status == "failed" {
|
||||
Some(chrono::Utc::now().timestamp())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
conn.execute(
|
||||
"UPDATE file_transfers SET progress = ?1, transferred_bytes = ?2, status = ?3, completed_at = ?4 WHERE file_id = ?5",
|
||||
params![progress, transferred_bytes as i64, format!("\"{}\"", status), completed_at, file_id],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取文件传输记录
|
||||
pub fn get_file_transfer(file_id: &str) -> Result<Option<FileTransfer>, AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT file_id, name, size, progress, status, mime_type,
|
||||
from_device, to_device, local_path, created_at, completed_at, transferred_bytes
|
||||
FROM file_transfers WHERE file_id = ?1
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let transfer = stmt
|
||||
.query_row(params![file_id], |row| {
|
||||
let status_str: String = row.get(4)?;
|
||||
Ok(FileTransfer {
|
||||
file_id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
size: row.get::<_, i64>(2)? as u64,
|
||||
progress: row.get(3)?,
|
||||
status: serde_json::from_str(&status_str).unwrap_or(TransferStatus::Pending),
|
||||
mime_type: row.get(5)?,
|
||||
from_device: row.get(6)?,
|
||||
to_device: row.get(7)?,
|
||||
local_path: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
completed_at: row.get(10)?,
|
||||
transferred_bytes: row.get::<_, i64>(11).unwrap_or(0) as u64,
|
||||
})
|
||||
})
|
||||
.optional()?;
|
||||
|
||||
Ok(transfer)
|
||||
}
|
||||
|
||||
/// 获取与指定设备的文件传输历史
|
||||
pub fn get_transfer_history(
|
||||
device_id: &str,
|
||||
local_device_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<FileTransfer>, AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
let mut stmt = conn.prepare(
|
||||
r#"
|
||||
SELECT file_id, name, size, progress, status, mime_type,
|
||||
from_device, to_device, local_path, created_at, completed_at, transferred_bytes
|
||||
FROM file_transfers
|
||||
WHERE (from_device = ?1 AND to_device = ?2) OR (from_device = ?2 AND to_device = ?1)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?3
|
||||
"#,
|
||||
)?;
|
||||
|
||||
let transfers = stmt
|
||||
.query_map(params![device_id, local_device_id, limit], |row| {
|
||||
let status_str: String = row.get(4)?;
|
||||
Ok(FileTransfer {
|
||||
file_id: row.get(0)?,
|
||||
name: row.get(1)?,
|
||||
size: row.get::<_, i64>(2)? as u64,
|
||||
progress: row.get(3)?,
|
||||
status: serde_json::from_str(&status_str).unwrap_or(TransferStatus::Pending),
|
||||
mime_type: row.get(5)?,
|
||||
from_device: row.get(6)?,
|
||||
to_device: row.get(7)?,
|
||||
local_path: row.get(8)?,
|
||||
created_at: row.get(9)?,
|
||||
completed_at: row.get(10)?,
|
||||
transferred_bytes: row.get::<_, i64>(11).unwrap_or(0) as u64,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(transfers)
|
||||
}
|
||||
|
||||
// ==================== 设备信息操作 ====================
|
||||
|
||||
/// 保存已知设备
|
||||
pub fn save_known_device(device: &DeviceInfo) -> Result<(), AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
conn.execute(
|
||||
r#"
|
||||
INSERT INTO known_devices (device_id, device_name, ip, ws_port, http_port, last_seen)
|
||||
VALUES (?1, ?2, ?3, ?4, ?5, ?6)
|
||||
ON CONFLICT(device_id) DO UPDATE SET
|
||||
device_name = ?2, ip = ?3, ws_port = ?4, http_port = ?5, last_seen = ?6
|
||||
"#,
|
||||
params![
|
||||
device.device_id,
|
||||
device.device_name,
|
||||
device.ip,
|
||||
device.ws_port,
|
||||
device.http_port,
|
||||
device.last_seen,
|
||||
],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取所有已知设备
|
||||
pub fn get_known_devices() -> Result<Vec<DeviceInfo>, AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
let mut stmt = conn.prepare(
|
||||
"SELECT device_id, device_name, ip, ws_port, http_port, last_seen FROM known_devices",
|
||||
)?;
|
||||
|
||||
let devices = stmt
|
||||
.query_map([], |row| {
|
||||
Ok(DeviceInfo {
|
||||
device_id: row.get(0)?,
|
||||
device_name: row.get(1)?,
|
||||
ip: row.get(2)?,
|
||||
ws_port: row.get(3)?,
|
||||
http_port: row.get(4)?,
|
||||
online: false,
|
||||
capabilities: vec![],
|
||||
last_seen: row.get(5)?,
|
||||
})
|
||||
})?
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
Ok(devices)
|
||||
}
|
||||
|
||||
// ==================== 配置操作 ====================
|
||||
|
||||
/// 保存配置项
|
||||
pub fn save_config(key: &str, value: &str) -> Result<(), AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
conn.execute(
|
||||
"INSERT INTO app_config (key, value) VALUES (?1, ?2) ON CONFLICT(key) DO UPDATE SET value = ?2",
|
||||
params![key, value],
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 获取配置项
|
||||
pub fn get_config(key: &str) -> Result<Option<String>, AppError> {
|
||||
let conn = Self::conn().lock();
|
||||
let value: Option<String> = conn
|
||||
.query_row(
|
||||
"SELECT value FROM app_config WHERE key = ?1",
|
||||
params![key],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.optional()?;
|
||||
Ok(value)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
//! 数据库表结构定义
|
||||
|
||||
use rusqlite::Connection;
|
||||
use crate::utils::AppError;
|
||||
|
||||
/// 初始化数据库表结构
|
||||
pub fn init_database(conn: &Connection) -> Result<(), AppError> {
|
||||
// 创建聊天消息表
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS chat_messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
message_type TEXT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
from_device TEXT NOT NULL,
|
||||
to_device TEXT NOT NULL,
|
||||
timestamp INTEGER NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 创建聊天消息索引
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_devices
|
||||
ON chat_messages(from_device, to_device)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_timestamp
|
||||
ON chat_messages(timestamp DESC)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 创建文件传输记录表
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS file_transfers (
|
||||
file_id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
size INTEGER NOT NULL,
|
||||
progress REAL NOT NULL DEFAULT 0.0,
|
||||
status TEXT NOT NULL DEFAULT 'pending',
|
||||
mime_type TEXT NOT NULL,
|
||||
from_device TEXT NOT NULL,
|
||||
to_device TEXT NOT NULL,
|
||||
local_path TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
completed_at INTEGER,
|
||||
transferred_bytes INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 迁移:为旧表添加 transferred_bytes 字段(如果不存在)
|
||||
let _ = conn.execute(
|
||||
"ALTER TABLE file_transfers ADD COLUMN transferred_bytes INTEGER NOT NULL DEFAULT 0",
|
||||
[],
|
||||
);
|
||||
|
||||
// 创建文件传输索引
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_transfers_devices
|
||||
ON file_transfers(from_device, to_device)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE INDEX IF NOT EXISTS idx_transfers_status
|
||||
ON file_transfers(status)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 创建设备信息表 (用于持久化已知设备)
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS known_devices (
|
||||
device_id TEXT PRIMARY KEY,
|
||||
device_name TEXT NOT NULL,
|
||||
ip TEXT NOT NULL,
|
||||
ws_port INTEGER NOT NULL,
|
||||
http_port INTEGER NOT NULL,
|
||||
last_seen INTEGER NOT NULL,
|
||||
created_at INTEGER NOT NULL DEFAULT (strftime('%s', 'now'))
|
||||
)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
// 创建应用配置表
|
||||
conn.execute(
|
||||
r#"
|
||||
CREATE TABLE IF NOT EXISTS app_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL
|
||||
)
|
||||
"#,
|
||||
[],
|
||||
)?;
|
||||
|
||||
log::info!("Database schema initialized successfully");
|
||||
Ok(())
|
||||
}
|
||||
@ -0,0 +1,131 @@
|
||||
//! 设备管理器
|
||||
//! 管理已发现的设备列表,处理设备超时
|
||||
|
||||
use dashmap::DashMap;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::models::DeviceInfo;
|
||||
use crate::utils::DEVICE_TIMEOUT_MS;
|
||||
|
||||
use super::DiscoveryEvent;
|
||||
|
||||
/// 设备管理器
|
||||
#[derive(Clone)]
|
||||
pub struct DeviceManager {
|
||||
/// 已发现的设备 (device_id -> DeviceInfo)
|
||||
devices: Arc<DashMap<String, DeviceInfo>>,
|
||||
/// 事件发送器
|
||||
event_tx: broadcast::Sender<DiscoveryEvent>,
|
||||
}
|
||||
|
||||
impl DeviceManager {
|
||||
/// 创建新的设备管理器
|
||||
pub fn new() -> Self {
|
||||
let (event_tx, _) = broadcast::channel(100);
|
||||
Self {
|
||||
devices: Arc::new(DashMap::new()),
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取事件订阅
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<DiscoveryEvent> {
|
||||
self.event_tx.subscribe()
|
||||
}
|
||||
|
||||
/// 添加或更新设备
|
||||
pub fn upsert_device(&self, mut device: DeviceInfo) {
|
||||
device.touch();
|
||||
let device_id = device.device_id.clone();
|
||||
let is_new = !self.devices.contains_key(&device_id);
|
||||
|
||||
self.devices.insert(device_id, device.clone());
|
||||
|
||||
if is_new {
|
||||
let _ = self.event_tx.send(DiscoveryEvent::DeviceFound(device));
|
||||
}
|
||||
}
|
||||
|
||||
/// 移除设备
|
||||
pub fn remove_device(&self, device_id: &str) {
|
||||
if self.devices.remove(device_id).is_some() {
|
||||
let _ = self.event_tx.send(DiscoveryEvent::DeviceLost(device_id.to_string()));
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取设备信息
|
||||
pub fn get_device(&self, device_id: &str) -> Option<DeviceInfo> {
|
||||
self.devices.get(device_id).map(|d| d.clone())
|
||||
}
|
||||
|
||||
/// 获取所有在线设备
|
||||
pub fn get_all_devices(&self) -> Vec<DeviceInfo> {
|
||||
self.devices.iter().map(|d| d.clone()).collect()
|
||||
}
|
||||
|
||||
/// 获取在线设备数量
|
||||
pub fn device_count(&self) -> usize {
|
||||
self.devices.len()
|
||||
}
|
||||
|
||||
/// 启动超时检查任务
|
||||
pub fn start_timeout_checker(&self) {
|
||||
let devices = self.devices.clone();
|
||||
let event_tx = self.event_tx.clone();
|
||||
let timeout = Duration::from_millis(DEVICE_TIMEOUT_MS);
|
||||
|
||||
tokio::spawn(async move {
|
||||
let check_interval = Duration::from_secs(5);
|
||||
|
||||
loop {
|
||||
tokio::time::sleep(check_interval).await;
|
||||
|
||||
let now = chrono::Utc::now().timestamp();
|
||||
let timeout_secs = timeout.as_secs() as i64;
|
||||
|
||||
let mut expired_ids = Vec::new();
|
||||
|
||||
for entry in devices.iter() {
|
||||
if now - entry.last_seen > timeout_secs {
|
||||
expired_ids.push(entry.device_id.clone());
|
||||
}
|
||||
}
|
||||
|
||||
for device_id in expired_ids {
|
||||
if devices.remove(&device_id).is_some() {
|
||||
log::info!("Device {} timed out", device_id);
|
||||
let _ = event_tx.send(DiscoveryEvent::DeviceLost(device_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// 设置设备在线状态
|
||||
pub fn set_device_online(&self, device_id: &str, online: bool) {
|
||||
if let Some(mut device) = self.devices.get_mut(device_id) {
|
||||
device.online = online;
|
||||
if online {
|
||||
device.touch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 清空所有设备
|
||||
pub fn clear(&self) {
|
||||
let device_ids: Vec<String> = self.devices.iter().map(|d| d.device_id.clone()).collect();
|
||||
self.devices.clear();
|
||||
|
||||
for device_id in device_ids {
|
||||
let _ = self.event_tx.send(DiscoveryEvent::DeviceLost(device_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DeviceManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
//! 局域网设备发现模块
|
||||
//! 使用 UDP 广播/组播实现设备自动发现
|
||||
|
||||
mod service;
|
||||
mod manager;
|
||||
|
||||
pub use service::*;
|
||||
pub use manager::*;
|
||||
@ -0,0 +1,191 @@
|
||||
//! UDP 广播服务实现
|
||||
|
||||
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use tokio::net::UdpSocket;
|
||||
use tokio::sync::broadcast;
|
||||
|
||||
use crate::models::{DeviceInfo, DiscoveryPacket, DiscoveryPacketType};
|
||||
use crate::utils::{AppError, DISCOVERY_INTERVAL_MS, DEFAULT_UDP_PORT};
|
||||
|
||||
/// 广播地址
|
||||
const BROADCAST_ADDR: Ipv4Addr = Ipv4Addr::new(255, 255, 255, 255);
|
||||
|
||||
/// 发现服务状态
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum DiscoveryEvent {
|
||||
/// 发现新设备
|
||||
DeviceFound(DeviceInfo),
|
||||
/// 设备离线
|
||||
DeviceLost(String),
|
||||
/// 服务启动
|
||||
Started,
|
||||
/// 服务停止
|
||||
Stopped,
|
||||
}
|
||||
|
||||
/// UDP 发现服务
|
||||
pub struct DiscoveryService {
|
||||
/// 本机设备信息
|
||||
local_device: DeviceInfo,
|
||||
/// UDP 端口
|
||||
port: u16,
|
||||
/// 事件发送器
|
||||
event_tx: broadcast::Sender<DiscoveryEvent>,
|
||||
/// 停止信号
|
||||
stop_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl DiscoveryService {
|
||||
/// 创建新的发现服务
|
||||
pub fn new(local_device: DeviceInfo) -> Self {
|
||||
let (event_tx, _) = broadcast::channel(100);
|
||||
Self {
|
||||
port: local_device.ws_port.saturating_sub(1).max(DEFAULT_UDP_PORT),
|
||||
local_device,
|
||||
event_tx,
|
||||
stop_tx: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取事件接收器
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<DiscoveryEvent> {
|
||||
self.event_tx.subscribe()
|
||||
}
|
||||
|
||||
/// 启动发现服务
|
||||
pub async fn start(&mut self) -> Result<(), AppError> {
|
||||
let port = self.port;
|
||||
let local_device = self.local_device.clone();
|
||||
let event_tx = self.event_tx.clone();
|
||||
|
||||
// 创建停止信号
|
||||
let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
|
||||
self.stop_tx = Some(stop_tx);
|
||||
|
||||
// 使用 socket2 创建支持端口复用的 UDP 套接字
|
||||
let bind_addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port);
|
||||
let socket2_socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?;
|
||||
socket2_socket.set_reuse_address(true)?;
|
||||
#[cfg(unix)]
|
||||
socket2_socket.set_reuse_port(true)?;
|
||||
socket2_socket.set_broadcast(true)?;
|
||||
socket2_socket.set_nonblocking(true)?;
|
||||
socket2_socket.bind(&bind_addr.into())?;
|
||||
|
||||
let std_socket: std::net::UdpSocket = socket2_socket.into();
|
||||
let socket = UdpSocket::from_std(std_socket)?;
|
||||
|
||||
log::info!("Discovery service started on port {}", port);
|
||||
let _ = event_tx.send(DiscoveryEvent::Started);
|
||||
|
||||
let socket = Arc::new(socket);
|
||||
|
||||
// 启动接收任务
|
||||
let recv_socket = socket.clone();
|
||||
let recv_event_tx = event_tx.clone();
|
||||
let local_id = local_device.device_id.clone();
|
||||
|
||||
let recv_handle = tokio::spawn(async move {
|
||||
Self::receive_loop(recv_socket, recv_event_tx, local_id).await;
|
||||
});
|
||||
|
||||
// 启动广播任务
|
||||
let broadcast_socket = socket.clone();
|
||||
let broadcast_device = local_device.clone();
|
||||
|
||||
let broadcast_handle = tokio::spawn(async move {
|
||||
Self::broadcast_loop(broadcast_socket, broadcast_device, port).await;
|
||||
});
|
||||
|
||||
// 等待停止信号
|
||||
tokio::spawn(async move {
|
||||
let _ = stop_rx.await;
|
||||
recv_handle.abort();
|
||||
broadcast_handle.abort();
|
||||
log::info!("Discovery service stopped");
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止发现服务
|
||||
pub fn stop(&mut self) {
|
||||
if let Some(stop_tx) = self.stop_tx.take() {
|
||||
let _ = stop_tx.send(());
|
||||
let _ = self.event_tx.send(DiscoveryEvent::Stopped);
|
||||
}
|
||||
}
|
||||
|
||||
/// 接收循环
|
||||
async fn receive_loop(
|
||||
socket: Arc<UdpSocket>,
|
||||
event_tx: broadcast::Sender<DiscoveryEvent>,
|
||||
local_device_id: String,
|
||||
) {
|
||||
let mut buf = [0u8; 4096];
|
||||
|
||||
loop {
|
||||
match socket.recv_from(&mut buf).await {
|
||||
Ok((len, src)) => {
|
||||
if let Ok(packet) = serde_json::from_slice::<DiscoveryPacket>(&buf[..len]) {
|
||||
// 忽略自己的广播
|
||||
if packet.device.device_id == local_device_id {
|
||||
continue;
|
||||
}
|
||||
|
||||
match packet.packet_type {
|
||||
DiscoveryPacketType::Announce | DiscoveryPacketType::Response => {
|
||||
log::debug!("Discovered device: {} at {}", packet.device.device_name, src);
|
||||
let _ = event_tx.send(DiscoveryEvent::DeviceFound(packet.device));
|
||||
}
|
||||
DiscoveryPacketType::Goodbye => {
|
||||
log::debug!("Device offline: {}", packet.device.device_id);
|
||||
let _ = event_tx.send(DiscoveryEvent::DeviceLost(packet.device.device_id));
|
||||
}
|
||||
DiscoveryPacketType::Query => {
|
||||
// 收到查询请求,不在这里处理 (由 broadcast_loop 定期广播)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("UDP receive error: {}", e);
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 广播循环
|
||||
async fn broadcast_loop(socket: Arc<UdpSocket>, device: DeviceInfo, port: u16) {
|
||||
let broadcast_addr = SocketAddr::V4(SocketAddrV4::new(BROADCAST_ADDR, port));
|
||||
let interval = Duration::from_millis(DISCOVERY_INTERVAL_MS);
|
||||
|
||||
loop {
|
||||
let packet = DiscoveryPacket::announce(device.clone());
|
||||
if let Ok(data) = serde_json::to_vec(&packet) {
|
||||
if let Err(e) = socket.send_to(&data, broadcast_addr).await {
|
||||
log::error!("Failed to broadcast: {}", e);
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(interval).await;
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送离线通知
|
||||
pub async fn send_goodbye(&self) -> Result<(), AppError> {
|
||||
let bind_addr = SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, 0);
|
||||
let socket = UdpSocket::bind(bind_addr).await?;
|
||||
socket.set_broadcast(true)?;
|
||||
|
||||
let broadcast_addr = SocketAddr::V4(SocketAddrV4::new(BROADCAST_ADDR, self.port));
|
||||
let packet = DiscoveryPacket::goodbye(self.local_device.clone());
|
||||
let data = serde_json::to_vec(&packet)?;
|
||||
|
||||
socket.send_to(&data, broadcast_addr).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,168 @@
|
||||
//! HTTP 文件传输客户端
|
||||
//! 用于向其他设备上传/下载文件
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||
use tauri::{AppHandle, Emitter};
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::models::{DeviceInfo, TransferProgressEvent, TransferStatus};
|
||||
use crate::utils::{AppError, FILE_CHUNK_SIZE};
|
||||
use crate::models::event_names;
|
||||
|
||||
/// HTTP 传输客户端
|
||||
#[derive(Clone)]
|
||||
pub struct HttpClient {
|
||||
/// Tauri AppHandle (可选,用于发送事件)
|
||||
app_handle: Arc<std::sync::RwLock<Option<AppHandle>>>,
|
||||
}
|
||||
|
||||
impl HttpClient {
|
||||
/// 创建新的客户端
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
app_handle: Arc::new(std::sync::RwLock::new(None)),
|
||||
}
|
||||
}
|
||||
|
||||
/// 设置 AppHandle
|
||||
pub fn set_app_handle(&self, handle: AppHandle) {
|
||||
if let Ok(mut guard) = self.app_handle.write() {
|
||||
*guard = Some(handle);
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传文件到指定设备(支持取消)
|
||||
pub async fn upload_file(
|
||||
&self,
|
||||
device: &DeviceInfo,
|
||||
file_path: &PathBuf,
|
||||
file_id: &str,
|
||||
from_device: &str,
|
||||
cancel_token: CancellationToken,
|
||||
) -> Result<(), AppError> {
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||
|
||||
let file_name = file_path
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string());
|
||||
|
||||
let addr = format!("{}:{}", device.ip, device.http_port);
|
||||
let stream = TcpStream::connect(&addr).await?;
|
||||
|
||||
// TLS 握手
|
||||
let connector = crate::tls::client::build_connector()?;
|
||||
let ip_addr: std::net::IpAddr = device.ip.parse()
|
||||
.map_err(|_| AppError::Tls("Invalid IP address".to_string()))?;
|
||||
let server_name = rustls::ServerName::IpAddress(ip_addr);
|
||||
|
||||
let mut tls_stream = connector.connect(server_name, stream).await
|
||||
.map_err(|e| AppError::Tls(format!("TLS error: {}", e)))?;
|
||||
|
||||
// 读取文件内容
|
||||
let mut file = File::open(file_path).await?;
|
||||
let file_size = file.metadata().await?.len();
|
||||
let mut file_content = Vec::with_capacity(file_size as usize);
|
||||
file.read_to_end(&mut file_content).await?;
|
||||
|
||||
// 构建 multipart 请求体
|
||||
let boundary = format!("----WebKitFormBoundary{}", uuid::Uuid::new_v4().to_string().replace("-", "")[..16].to_string());
|
||||
|
||||
let mut body = Vec::new();
|
||||
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
|
||||
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"file_id\"\r\n\r\n{}\r\n", file_id).as_bytes());
|
||||
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
|
||||
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"from_device\"\r\n\r\n{}\r\n", from_device).as_bytes());
|
||||
body.extend_from_slice(format!("--{}\r\n", boundary).as_bytes());
|
||||
body.extend_from_slice(format!("Content-Disposition: form-data; name=\"file\"; filename=\"{}\"\r\n", file_name).as_bytes());
|
||||
body.extend_from_slice(b"Content-Type: application/octet-stream\r\n\r\n");
|
||||
body.extend_from_slice(&file_content);
|
||||
body.extend_from_slice(format!("\r\n--{}--\r\n", boundary).as_bytes());
|
||||
|
||||
let request = format!(
|
||||
"POST /upload HTTP/1.1\r\n\
|
||||
Host: {}\r\n\
|
||||
Content-Type: multipart/form-data; boundary={}\r\n\
|
||||
Content-Length: {}\r\n\
|
||||
Connection: close\r\n\
|
||||
\r\n",
|
||||
addr,
|
||||
boundary,
|
||||
body.len()
|
||||
);
|
||||
|
||||
tls_stream.write_all(request.as_bytes()).await?;
|
||||
|
||||
// 分块发送请求体,同时检查取消状态
|
||||
let mut sent = 0usize;
|
||||
for chunk in body.chunks(FILE_CHUNK_SIZE) {
|
||||
// 检查是否已取消
|
||||
if cancel_token.is_cancelled() {
|
||||
self.emit_progress(file_id, 0.0, sent as u64, file_size, TransferStatus::Cancelled);
|
||||
if let Err(e) = crate::database::Database::update_transfer_progress(file_id, 0.0, sent as u64, "cancelled") {
|
||||
log::error!("Failed to update transfer in database: {}", e);
|
||||
}
|
||||
log::info!("File upload cancelled: {}", file_name);
|
||||
return Err(AppError::FileTransfer("Upload cancelled".to_string()));
|
||||
}
|
||||
|
||||
tls_stream.write_all(chunk).await?;
|
||||
sent += chunk.len();
|
||||
|
||||
let progress = sent as f64 / body.len() as f64;
|
||||
self.emit_progress(file_id, progress, sent as u64, file_size, TransferStatus::Transferring);
|
||||
}
|
||||
|
||||
tls_stream.flush().await?;
|
||||
|
||||
// 读取响应
|
||||
let mut reader = BufReader::new(tls_stream);
|
||||
let mut response_line = String::new();
|
||||
reader.read_line(&mut response_line).await?;
|
||||
|
||||
if response_line.contains("200") {
|
||||
self.emit_progress(file_id, 1.0, file_size, file_size, TransferStatus::Completed);
|
||||
if let Err(e) = crate::database::Database::update_transfer_progress(file_id, 1.0, file_size, "completed") {
|
||||
log::error!("Failed to update transfer in database: {}", e);
|
||||
}
|
||||
log::info!("File uploaded successfully: {}", file_name);
|
||||
Ok(())
|
||||
} else {
|
||||
self.emit_progress(file_id, 0.0, 0, file_size, TransferStatus::Failed);
|
||||
if let Err(e) = crate::database::Database::update_transfer_progress(file_id, 0.0, 0, "failed") {
|
||||
log::error!("Failed to update transfer in database: {}", e);
|
||||
}
|
||||
Err(AppError::FileTransfer(format!("Upload failed: {}", response_line)))
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送进度事件
|
||||
fn emit_progress(&self, file_id: &str, progress: f64, transferred: u64, total: u64, status: TransferStatus) {
|
||||
let event = TransferProgressEvent {
|
||||
file_id: file_id.to_string(),
|
||||
progress,
|
||||
transferred_bytes: transferred,
|
||||
total_bytes: total,
|
||||
status,
|
||||
file_name: None, // 发送方不需要文件名
|
||||
local_path: None, // 发送方不需要本地路径
|
||||
};
|
||||
|
||||
// 直接通过 AppHandle 发送事件
|
||||
if let Ok(guard) = self.app_handle.read() {
|
||||
if let Some(ref handle) = *guard {
|
||||
let _ = handle.emit(event_names::TRANSFER_PROGRESS, &event);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for HttpClient {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,285 @@
|
||||
//! HTTP 请求处理器
|
||||
|
||||
use axum::{
|
||||
body::StreamBody,
|
||||
extract::{Multipart, Path, State},
|
||||
http::{header, StatusCode},
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::models::{FileMetadata, TransferProgressEvent, TransferStatus};
|
||||
|
||||
/// 应用状态
|
||||
#[derive(Clone)]
|
||||
pub struct AppState {
|
||||
/// 待下载文件映射 (file_id -> 文件路径)
|
||||
pub files: Arc<DashMap<String, PathBuf>>,
|
||||
/// 文件元数据映射
|
||||
pub metadata: Arc<DashMap<String, FileMetadata>>,
|
||||
/// 进度事件发送器
|
||||
pub progress_tx: broadcast::Sender<TransferProgressEvent>,
|
||||
/// 下载目录
|
||||
pub download_dir: PathBuf,
|
||||
/// 本机设备 ID
|
||||
pub local_device_id: String,
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
/// 创建新的应用状态
|
||||
pub fn new(download_dir: PathBuf, local_device_id: String) -> Self {
|
||||
let (progress_tx, _) = broadcast::channel(100);
|
||||
Self {
|
||||
files: Arc::new(DashMap::new()),
|
||||
metadata: Arc::new(DashMap::new()),
|
||||
progress_tx,
|
||||
download_dir,
|
||||
local_device_id,
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册待下载文件
|
||||
pub fn register_file(&self, file_id: String, path: PathBuf, metadata: FileMetadata) {
|
||||
self.files.insert(file_id.clone(), path);
|
||||
self.metadata.insert(file_id, metadata);
|
||||
}
|
||||
|
||||
/// 移除文件注册
|
||||
pub fn unregister_file(&self, file_id: &str) {
|
||||
self.files.remove(file_id);
|
||||
self.metadata.remove(file_id);
|
||||
}
|
||||
|
||||
/// 获取进度事件订阅
|
||||
pub fn subscribe_progress(&self) -> broadcast::Receiver<TransferProgressEvent> {
|
||||
self.progress_tx.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
/// 上传文件处理
|
||||
/// POST /upload
|
||||
pub async fn upload_handler(
|
||||
State(state): State<AppState>,
|
||||
mut multipart: Multipart,
|
||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||
let mut file_id: Option<String> = None;
|
||||
let mut file_name: Option<String> = None;
|
||||
let mut _from_device: Option<String> = None;
|
||||
let mut saved_path: Option<PathBuf> = None;
|
||||
let mut total_size: u64 = 0;
|
||||
|
||||
while let Some(field) = multipart
|
||||
.next_field()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Multipart error: {}", e)))?
|
||||
{
|
||||
let name = field.name().unwrap_or("").to_string();
|
||||
|
||||
match name.as_str() {
|
||||
"file_id" => {
|
||||
file_id = Some(
|
||||
field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?,
|
||||
);
|
||||
}
|
||||
"file_name" => {
|
||||
file_name = Some(
|
||||
field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?,
|
||||
);
|
||||
}
|
||||
"from_device" => {
|
||||
_from_device = Some(
|
||||
field
|
||||
.text()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?,
|
||||
);
|
||||
}
|
||||
"file" => {
|
||||
let fname = file_name.clone().unwrap_or_else(|| {
|
||||
field
|
||||
.file_name()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| "unknown".to_string())
|
||||
});
|
||||
|
||||
// 生成保存路径
|
||||
let save_path = state.download_dir.join(&fname);
|
||||
let mut file = File::create(&save_path)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
// 读取并写入文件内容
|
||||
let data = field
|
||||
.bytes()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::BAD_REQUEST, e.to_string()))?;
|
||||
|
||||
total_size = data.len() as u64;
|
||||
file.write_all(&data)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
file.flush()
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
saved_path = Some(save_path);
|
||||
file_name = Some(fname);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
let file_id = file_id.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
|
||||
let file_name = file_name.ok_or((StatusCode::BAD_REQUEST, "Missing file name".to_string()))?;
|
||||
let saved_path =
|
||||
saved_path.ok_or((StatusCode::BAD_REQUEST, "No file uploaded".to_string()))?;
|
||||
let from_device = _from_device.unwrap_or_default();
|
||||
|
||||
// 创建并保存传输记录(接收方)
|
||||
let transfer = crate::models::FileTransfer {
|
||||
file_id: file_id.clone(),
|
||||
name: file_name.clone(),
|
||||
size: total_size,
|
||||
progress: 1.0,
|
||||
status: TransferStatus::Completed,
|
||||
mime_type: mime_guess::from_path(&saved_path).first_or_octet_stream().to_string(),
|
||||
from_device: from_device.clone(),
|
||||
to_device: state.local_device_id.clone(),
|
||||
local_path: Some(saved_path.to_string_lossy().to_string()),
|
||||
created_at: chrono::Utc::now().timestamp(),
|
||||
completed_at: Some(chrono::Utc::now().timestamp()),
|
||||
transferred_bytes: total_size,
|
||||
};
|
||||
|
||||
if let Err(e) = crate::database::Database::save_file_transfer(&transfer) {
|
||||
log::error!("Failed to save transfer record: {}", e);
|
||||
}
|
||||
|
||||
// 发送完成事件(包含文件名和路径,供前端显示)
|
||||
let progress_event = TransferProgressEvent {
|
||||
file_id: file_id.clone(),
|
||||
progress: 1.0,
|
||||
transferred_bytes: total_size,
|
||||
total_bytes: total_size,
|
||||
status: TransferStatus::Completed,
|
||||
file_name: Some(file_name.clone()),
|
||||
local_path: Some(saved_path.to_string_lossy().to_string()),
|
||||
};
|
||||
let _ = state.progress_tx.send(progress_event);
|
||||
|
||||
log::info!("File received: {} ({} bytes) from {}", file_name, total_size, from_device);
|
||||
|
||||
Ok(Json(serde_json::json!({
|
||||
"success": true,
|
||||
"fileId": file_id,
|
||||
"fileName": file_name,
|
||||
"size": total_size,
|
||||
"path": saved_path.to_string_lossy()
|
||||
})))
|
||||
}
|
||||
|
||||
/// 下载文件处理
|
||||
/// GET /file/:id
|
||||
pub async fn download_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(file_id): Path<String>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
// 查找文件路径
|
||||
let file_path = state
|
||||
.files
|
||||
.get(&file_id)
|
||||
.map(|e| e.clone())
|
||||
.ok_or((StatusCode::NOT_FOUND, "File not found".to_string()))?;
|
||||
|
||||
// 获取元数据
|
||||
let metadata = state.metadata.get(&file_id);
|
||||
|
||||
// 打开文件
|
||||
let file = File::open(&file_path)
|
||||
.await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
let file_size = file
|
||||
.metadata()
|
||||
.await
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
// 确定 MIME 类型
|
||||
let mime_type = metadata
|
||||
.as_ref()
|
||||
.map(|m| m.mime_type.clone())
|
||||
.unwrap_or_else(|| {
|
||||
mime_guess::from_path(&file_path)
|
||||
.first_or_octet_stream()
|
||||
.to_string()
|
||||
});
|
||||
|
||||
// 确定文件名
|
||||
let file_name = metadata
|
||||
.as_ref()
|
||||
.map(|m| m.name.clone())
|
||||
.unwrap_or_else(|| {
|
||||
file_path
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "download".to_string())
|
||||
});
|
||||
|
||||
// 创建流式响应
|
||||
let stream = ReaderStream::new(file);
|
||||
let body = StreamBody::new(stream);
|
||||
|
||||
let response = Response::builder()
|
||||
.status(StatusCode::OK)
|
||||
.header(header::CONTENT_TYPE, mime_type)
|
||||
.header(header::CONTENT_LENGTH, file_size)
|
||||
.header(
|
||||
header::CONTENT_DISPOSITION,
|
||||
format!("attachment; filename=\"{}\"", file_name),
|
||||
)
|
||||
.body(body)
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||
|
||||
log::info!("File download started: {}", file_name);
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
/// 获取文件元数据
|
||||
/// GET /file/:id/info
|
||||
pub async fn file_info_handler(
|
||||
State(state): State<AppState>,
|
||||
Path(file_id): Path<String>,
|
||||
) -> Result<Json<FileMetadata>, (StatusCode, String)> {
|
||||
let metadata = state
|
||||
.metadata
|
||||
.get(&file_id)
|
||||
.map(|e| e.clone())
|
||||
.ok_or((StatusCode::NOT_FOUND, "File not found".to_string()))?;
|
||||
|
||||
Ok(Json(metadata))
|
||||
}
|
||||
|
||||
/// 健康检查
|
||||
/// GET /health
|
||||
pub async fn health_handler() -> Json<serde_json::Value> {
|
||||
Json(serde_json::json!({
|
||||
"status": "ok",
|
||||
"timestamp": chrono::Utc::now().timestamp()
|
||||
}))
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
//! HTTP 文件传输模块
|
||||
//! 提供文件上传和下载服务
|
||||
|
||||
mod server;
|
||||
mod handlers;
|
||||
mod client;
|
||||
|
||||
pub use server::*;
|
||||
pub use handlers::*;
|
||||
pub use client::*;
|
||||
@ -0,0 +1,150 @@
|
||||
//! HTTP 文件服务端
|
||||
|
||||
use axum::{
|
||||
extract::DefaultBodyLimit,
|
||||
routing::{get, post},
|
||||
Router,
|
||||
};
|
||||
use axum_server::tls_rustls::RustlsConfig;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::PathBuf;
|
||||
use tower_http::cors::{Any, CorsLayer};
|
||||
|
||||
use crate::tls::CertificateManager;
|
||||
use crate::utils::AppError;
|
||||
|
||||
use super::handlers::{
|
||||
download_handler, file_info_handler, health_handler, upload_handler, AppState,
|
||||
};
|
||||
|
||||
/// HTTP 文件服务端
|
||||
pub struct HttpServer {
|
||||
/// 监听端口
|
||||
port: u16,
|
||||
/// 应用状态
|
||||
state: AppState,
|
||||
/// 停止信号
|
||||
stop_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl HttpServer {
|
||||
/// 创建新的 HTTP 服务端
|
||||
pub fn new(port: u16, download_dir: PathBuf, local_device_id: String) -> Self {
|
||||
Self {
|
||||
port,
|
||||
state: AppState::new(download_dir, local_device_id),
|
||||
stop_tx: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取应用状态
|
||||
pub fn state(&self) -> &AppState {
|
||||
&self.state
|
||||
}
|
||||
|
||||
/// 获取可克隆的状态
|
||||
pub fn state_clone(&self) -> AppState {
|
||||
self.state.clone()
|
||||
}
|
||||
|
||||
/// 启动服务
|
||||
pub async fn start(&mut self) -> Result<(), AppError> {
|
||||
// 获取 TLS 证书
|
||||
let cert_bundle = CertificateManager::get()
|
||||
.ok_or_else(|| AppError::Tls("Certificate not initialized".to_string()))?;
|
||||
|
||||
// 创建临时证书文件用于 axum-server
|
||||
let temp_dir = std::env::temp_dir().join("flash-send-certs");
|
||||
std::fs::create_dir_all(&temp_dir)?;
|
||||
|
||||
let cert_path = temp_dir.join("cert.pem");
|
||||
let key_path = temp_dir.join("key.pem");
|
||||
|
||||
std::fs::write(&cert_path, &cert_bundle.cert_pem)?;
|
||||
std::fs::write(&key_path, &cert_bundle.key_pem)?;
|
||||
|
||||
// 配置 TLS
|
||||
let tls_config = RustlsConfig::from_pem_file(&cert_path, &key_path)
|
||||
.await
|
||||
.map_err(|e| AppError::Tls(format!("Failed to load TLS config: {}", e)))?;
|
||||
|
||||
// CORS 配置
|
||||
let cors = CorsLayer::new()
|
||||
.allow_origin(Any)
|
||||
.allow_methods(Any)
|
||||
.allow_headers(Any);
|
||||
|
||||
// 创建路由
|
||||
let app = Router::new()
|
||||
.route("/health", get(health_handler))
|
||||
.route("/upload", post(upload_handler))
|
||||
.route("/file/:id", get(download_handler))
|
||||
.route("/file/:id/info", get(file_info_handler))
|
||||
.layer(cors)
|
||||
.layer(DefaultBodyLimit::max(10 * 1024 * 1024 * 1024)) // 10GB 限制
|
||||
.with_state(self.state.clone());
|
||||
|
||||
// 绑定地址
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], self.port));
|
||||
|
||||
log::info!("HTTP server started on https://0.0.0.0:{}", self.port);
|
||||
|
||||
// 创建停止信号
|
||||
let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
|
||||
self.stop_tx = Some(stop_tx);
|
||||
|
||||
// 启动服务器
|
||||
tokio::spawn(async move {
|
||||
let server = axum_server::bind_rustls(addr, tls_config)
|
||||
.serve(app.into_make_service());
|
||||
|
||||
tokio::select! {
|
||||
result = server => {
|
||||
if let Err(e) = result {
|
||||
log::error!("HTTP server error: {}", e);
|
||||
}
|
||||
}
|
||||
_ = stop_rx => {
|
||||
log::info!("HTTP server stopping");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止服务
|
||||
pub fn stop(&mut self) {
|
||||
if let Some(stop_tx) = self.stop_tx.take() {
|
||||
let _ = stop_tx.send(());
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册文件供下载
|
||||
pub fn register_file(
|
||||
&self,
|
||||
file_id: String,
|
||||
path: PathBuf,
|
||||
name: String,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
) {
|
||||
use crate::models::FileMetadata;
|
||||
|
||||
let metadata = FileMetadata {
|
||||
file_id: file_id.clone(),
|
||||
name,
|
||||
size,
|
||||
mime_type,
|
||||
thumbnail: None,
|
||||
path: Some(path.to_string_lossy().to_string()),
|
||||
};
|
||||
|
||||
self.state.register_file(file_id, path, metadata);
|
||||
}
|
||||
|
||||
/// 移除文件注册
|
||||
pub fn unregister_file(&self, file_id: &str) {
|
||||
self.state.unregister_file(file_id);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
//! Flash Send 主库
|
||||
//! 跨平台局域网文件传输与聊天应用
|
||||
|
||||
pub mod database;
|
||||
pub mod discovery;
|
||||
pub mod http;
|
||||
pub mod models;
|
||||
pub mod tls;
|
||||
pub mod utils;
|
||||
pub mod websocket;
|
||||
|
||||
mod commands;
|
||||
mod state;
|
||||
|
||||
pub use commands::*;
|
||||
pub use state::*;
|
||||
@ -0,0 +1,135 @@
|
||||
//! 设备信息数据结构
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// 设备能力枚举
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DeviceCapability {
|
||||
/// 文本消息
|
||||
Text,
|
||||
/// 图片消息
|
||||
Image,
|
||||
/// 文件传输
|
||||
File,
|
||||
}
|
||||
|
||||
/// 设备信息结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeviceInfo {
|
||||
/// 设备唯一标识符 (UUID)
|
||||
pub device_id: String,
|
||||
/// 设备名称
|
||||
pub device_name: String,
|
||||
/// IP 地址
|
||||
pub ip: String,
|
||||
/// WebSocket 端口
|
||||
pub ws_port: u16,
|
||||
/// HTTP 端口
|
||||
pub http_port: u16,
|
||||
/// 是否在线
|
||||
pub online: bool,
|
||||
/// 设备能力列表
|
||||
pub capabilities: Vec<DeviceCapability>,
|
||||
/// 最后活跃时间戳 (Unix timestamp)
|
||||
#[serde(default)]
|
||||
pub last_seen: i64,
|
||||
}
|
||||
|
||||
impl DeviceInfo {
|
||||
/// 创建新的设备信息
|
||||
pub fn new(
|
||||
device_id: String,
|
||||
device_name: String,
|
||||
ip: String,
|
||||
ws_port: u16,
|
||||
http_port: u16,
|
||||
) -> Self {
|
||||
Self {
|
||||
device_id,
|
||||
device_name,
|
||||
ip,
|
||||
ws_port,
|
||||
http_port,
|
||||
online: true,
|
||||
capabilities: vec![
|
||||
DeviceCapability::Text,
|
||||
DeviceCapability::Image,
|
||||
DeviceCapability::File,
|
||||
],
|
||||
last_seen: chrono::Utc::now().timestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取 WebSocket 地址
|
||||
pub fn ws_addr(&self) -> String {
|
||||
format!("wss://{}:{}", self.ip, self.ws_port)
|
||||
}
|
||||
|
||||
/// 获取 HTTP 地址
|
||||
pub fn http_addr(&self) -> String {
|
||||
format!("https://{}:{}", self.ip, self.http_port)
|
||||
}
|
||||
|
||||
/// 更新最后活跃时间
|
||||
pub fn touch(&mut self) {
|
||||
self.last_seen = chrono::Utc::now().timestamp();
|
||||
self.online = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// UDP 广播发现数据包
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DiscoveryPacket {
|
||||
/// 协议版本
|
||||
pub version: u8,
|
||||
/// 设备信息
|
||||
pub device: DeviceInfo,
|
||||
/// 数据包类型
|
||||
pub packet_type: DiscoveryPacketType,
|
||||
}
|
||||
|
||||
/// 发现数据包类型
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DiscoveryPacketType {
|
||||
/// 广播通告
|
||||
Announce,
|
||||
/// 查询请求
|
||||
Query,
|
||||
/// 查询响应
|
||||
Response,
|
||||
/// 离线通知
|
||||
Goodbye,
|
||||
}
|
||||
|
||||
impl DiscoveryPacket {
|
||||
/// 创建通告数据包
|
||||
pub fn announce(device: DeviceInfo) -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
device,
|
||||
packet_type: DiscoveryPacketType::Announce,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建离线数据包
|
||||
pub fn goodbye(device: DeviceInfo) -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
device,
|
||||
packet_type: DiscoveryPacketType::Goodbye,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建查询数据包
|
||||
pub fn query(device: DeviceInfo) -> Self {
|
||||
Self {
|
||||
version: 1,
|
||||
device,
|
||||
packet_type: DiscoveryPacketType::Query,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,108 @@
|
||||
//! 前后端事件定义
|
||||
//! 定义从后端发送到前端的所有事件
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use super::{DeviceInfo, ChatMessage, FileTransfer};
|
||||
|
||||
/// 事件名称常量
|
||||
pub mod event_names {
|
||||
/// 发现新设备
|
||||
pub const DEVICE_FOUND: &str = "device_found";
|
||||
/// 设备离线
|
||||
pub const DEVICE_LOST: &str = "device_lost";
|
||||
/// 收到消息
|
||||
pub const MESSAGE_RECEIVED: &str = "message_received";
|
||||
/// 收到文件
|
||||
pub const FILE_RECEIVED: &str = "file_received";
|
||||
/// 传输进度更新
|
||||
pub const TRANSFER_PROGRESS: &str = "transfer_progress";
|
||||
/// WebSocket 已连接
|
||||
pub const WEBSOCKET_CONNECTED: &str = "websocket_connected";
|
||||
/// WebSocket 已断开
|
||||
pub const WEBSOCKET_DISCONNECTED: &str = "websocket_disconnected";
|
||||
/// 消息状态更新
|
||||
pub const MESSAGE_STATUS_UPDATED: &str = "message_status_updated";
|
||||
}
|
||||
|
||||
/// 设备发现事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeviceFoundEvent {
|
||||
/// 发现的设备信息
|
||||
pub device: DeviceInfo,
|
||||
}
|
||||
|
||||
/// 设备离线事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DeviceLostEvent {
|
||||
/// 离线设备 ID
|
||||
pub device_id: String,
|
||||
}
|
||||
|
||||
/// 消息接收事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageReceivedEvent {
|
||||
/// 接收到的消息
|
||||
pub message: ChatMessage,
|
||||
}
|
||||
|
||||
/// 文件接收事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileReceivedEvent {
|
||||
/// 文件传输信息
|
||||
pub transfer: FileTransfer,
|
||||
}
|
||||
|
||||
/// 传输进度事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TransferProgressEvent {
|
||||
/// 文件 ID
|
||||
pub file_id: String,
|
||||
/// 当前进度 (0.0 - 1.0)
|
||||
pub progress: f64,
|
||||
/// 已传输字节数
|
||||
pub transferred_bytes: u64,
|
||||
/// 总字节数
|
||||
pub total_bytes: u64,
|
||||
/// 传输状态
|
||||
pub status: super::TransferStatus,
|
||||
/// 文件名(可选,用于接收方显示)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub file_name: Option<String>,
|
||||
/// 本地路径(可选,用于接收方)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub local_path: Option<String>,
|
||||
}
|
||||
|
||||
/// WebSocket 连接事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WebSocketConnectedEvent {
|
||||
/// 连接的设备 ID
|
||||
pub device_id: String,
|
||||
}
|
||||
|
||||
/// WebSocket 断开事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct WebSocketDisconnectedEvent {
|
||||
/// 断开的设备 ID
|
||||
pub device_id: String,
|
||||
/// 断开原因
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// 消息状态更新事件
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageStatusUpdatedEvent {
|
||||
/// 消息 ID
|
||||
pub message_id: String,
|
||||
/// 新状态
|
||||
pub status: super::MessageStatus,
|
||||
}
|
||||
@ -0,0 +1,167 @@
|
||||
//! 文件传输数据结构
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 文件传输状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum TransferStatus {
|
||||
/// 等待中
|
||||
Pending,
|
||||
/// 传输中
|
||||
Transferring,
|
||||
/// 已完成
|
||||
Completed,
|
||||
/// 失败
|
||||
Failed,
|
||||
/// 已取消
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
/// 文件传输信息
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileTransfer {
|
||||
/// 文件传输唯一标识符
|
||||
pub file_id: String,
|
||||
/// 文件名
|
||||
pub name: String,
|
||||
/// 文件大小 (字节)
|
||||
pub size: u64,
|
||||
/// 传输进度 (0.0 - 1.0)
|
||||
pub progress: f64,
|
||||
/// 传输状态
|
||||
pub status: TransferStatus,
|
||||
/// MIME 类型
|
||||
pub mime_type: String,
|
||||
/// 发送者设备 ID
|
||||
pub from_device: String,
|
||||
/// 接收者设备 ID
|
||||
pub to_device: String,
|
||||
/// 本地文件路径 (发送时为源路径,接收时为保存路径)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub local_path: Option<String>,
|
||||
/// 创建时间戳
|
||||
pub created_at: i64,
|
||||
/// 完成时间戳
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub completed_at: Option<i64>,
|
||||
/// 已传输字节数
|
||||
pub transferred_bytes: u64,
|
||||
}
|
||||
|
||||
impl FileTransfer {
|
||||
/// 创建新的文件传输记录 (发送)
|
||||
pub fn new_outgoing(
|
||||
name: String,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
from_device: String,
|
||||
to_device: String,
|
||||
local_path: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
file_id: Uuid::new_v4().to_string(),
|
||||
name,
|
||||
size,
|
||||
progress: 0.0,
|
||||
status: TransferStatus::Pending,
|
||||
mime_type,
|
||||
from_device,
|
||||
to_device,
|
||||
local_path: Some(local_path),
|
||||
created_at: chrono::Utc::now().timestamp(),
|
||||
completed_at: None,
|
||||
transferred_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建新的文件传输记录 (接收)
|
||||
pub fn new_incoming(
|
||||
file_id: String,
|
||||
name: String,
|
||||
size: u64,
|
||||
mime_type: String,
|
||||
from_device: String,
|
||||
to_device: String,
|
||||
) -> Self {
|
||||
Self {
|
||||
file_id,
|
||||
name,
|
||||
size,
|
||||
progress: 0.0,
|
||||
status: TransferStatus::Pending,
|
||||
mime_type,
|
||||
from_device,
|
||||
to_device,
|
||||
local_path: None,
|
||||
created_at: chrono::Utc::now().timestamp(),
|
||||
completed_at: None,
|
||||
transferred_bytes: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新传输进度
|
||||
pub fn update_progress(&mut self, transferred_bytes: u64) {
|
||||
self.transferred_bytes = transferred_bytes;
|
||||
if self.size > 0 {
|
||||
self.progress = transferred_bytes as f64 / self.size as f64;
|
||||
}
|
||||
if self.progress >= 1.0 {
|
||||
self.progress = 1.0;
|
||||
self.status = TransferStatus::Completed;
|
||||
self.completed_at = Some(chrono::Utc::now().timestamp());
|
||||
} else {
|
||||
self.status = TransferStatus::Transferring;
|
||||
}
|
||||
}
|
||||
|
||||
/// 标记失败
|
||||
pub fn mark_failed(&mut self) {
|
||||
self.status = TransferStatus::Failed;
|
||||
self.completed_at = Some(chrono::Utc::now().timestamp());
|
||||
}
|
||||
|
||||
/// 标记取消
|
||||
pub fn mark_cancelled(&mut self) {
|
||||
self.status = TransferStatus::Cancelled;
|
||||
self.completed_at = Some(chrono::Utc::now().timestamp());
|
||||
}
|
||||
}
|
||||
|
||||
/// 文件上传请求
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileUploadRequest {
|
||||
/// 文件 ID
|
||||
pub file_id: String,
|
||||
/// 文件名
|
||||
pub file_name: String,
|
||||
/// 文件大小
|
||||
pub file_size: u64,
|
||||
/// MIME 类型
|
||||
pub mime_type: String,
|
||||
/// 发送者设备 ID
|
||||
pub from_device: String,
|
||||
}
|
||||
|
||||
/// 文件元数据
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FileMetadata {
|
||||
/// 文件 ID
|
||||
pub file_id: String,
|
||||
/// 文件名
|
||||
pub name: String,
|
||||
/// 文件大小
|
||||
pub size: u64,
|
||||
/// MIME 类型
|
||||
pub mime_type: String,
|
||||
/// 缩略图 (base64, 可选)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub thumbnail: Option<String>,
|
||||
/// 文件路径
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub path: Option<String>,
|
||||
}
|
||||
@ -0,0 +1,153 @@
|
||||
//! 聊天消息数据结构
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// 消息类型枚举
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MessageType {
|
||||
/// 文本消息
|
||||
Text,
|
||||
/// 图片消息 (base64 指针)
|
||||
Image,
|
||||
/// 系统事件 (上线/离线等)
|
||||
Event,
|
||||
/// 消息确认回执
|
||||
Ack,
|
||||
/// 心跳 ping
|
||||
Ping,
|
||||
/// 心跳 pong
|
||||
Pong,
|
||||
}
|
||||
|
||||
/// 聊天消息结构
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ChatMessage {
|
||||
/// 消息唯一标识符
|
||||
pub id: String,
|
||||
/// 消息类型
|
||||
#[serde(rename = "type")]
|
||||
pub message_type: MessageType,
|
||||
/// 消息内容
|
||||
pub content: String,
|
||||
/// 发送者设备 ID
|
||||
pub from: String,
|
||||
/// 接收者设备 ID
|
||||
pub to: String,
|
||||
/// 时间戳 (Unix timestamp)
|
||||
pub timestamp: i64,
|
||||
/// 消息状态
|
||||
#[serde(default)]
|
||||
pub status: MessageStatus,
|
||||
}
|
||||
|
||||
/// 消息状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum MessageStatus {
|
||||
/// 待发送
|
||||
#[default]
|
||||
Pending,
|
||||
/// 已发送
|
||||
Sent,
|
||||
/// 已送达
|
||||
Delivered,
|
||||
/// 已读
|
||||
Read,
|
||||
/// 发送失败
|
||||
Failed,
|
||||
}
|
||||
|
||||
impl ChatMessage {
|
||||
/// 创建新的文本消息
|
||||
pub fn new_text(from: String, to: String, content: String) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
message_type: MessageType::Text,
|
||||
content,
|
||||
from,
|
||||
to,
|
||||
timestamp: chrono::Utc::now().timestamp(),
|
||||
status: MessageStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建图片消息
|
||||
pub fn new_image(from: String, to: String, image_data: String) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
message_type: MessageType::Image,
|
||||
content: image_data,
|
||||
from,
|
||||
to,
|
||||
timestamp: chrono::Utc::now().timestamp(),
|
||||
status: MessageStatus::Pending,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建事件消息
|
||||
pub fn new_event(from: String, to: String, event: &str) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
message_type: MessageType::Event,
|
||||
content: event.to_string(),
|
||||
from,
|
||||
to,
|
||||
timestamp: chrono::Utc::now().timestamp(),
|
||||
status: MessageStatus::Sent,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建消息确认
|
||||
pub fn new_ack(from: String, to: String, message_id: &str) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
message_type: MessageType::Ack,
|
||||
content: message_id.to_string(),
|
||||
from,
|
||||
to,
|
||||
timestamp: chrono::Utc::now().timestamp(),
|
||||
status: MessageStatus::Sent,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建 ping 消息
|
||||
pub fn new_ping(from: String) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
message_type: MessageType::Ping,
|
||||
content: String::new(),
|
||||
from,
|
||||
to: String::new(),
|
||||
timestamp: chrono::Utc::now().timestamp(),
|
||||
status: MessageStatus::Sent,
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建 pong 消息
|
||||
pub fn new_pong(from: String, to: String) -> Self {
|
||||
Self {
|
||||
id: Uuid::new_v4().to_string(),
|
||||
message_type: MessageType::Pong,
|
||||
content: String::new(),
|
||||
from,
|
||||
to,
|
||||
timestamp: chrono::Utc::now().timestamp(),
|
||||
status: MessageStatus::Sent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// WebSocket 连接状态
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ConnectionState {
|
||||
/// 对端设备 ID
|
||||
pub device_id: String,
|
||||
/// 是否已连接
|
||||
pub connected: bool,
|
||||
/// 最后心跳时间
|
||||
pub last_heartbeat: i64,
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
//! 数据模型模块
|
||||
//! 定义应用中使用的所有数据结构
|
||||
|
||||
mod device;
|
||||
mod message;
|
||||
mod file_transfer;
|
||||
mod events;
|
||||
|
||||
pub use device::*;
|
||||
pub use message::*;
|
||||
pub use file_transfer::*;
|
||||
pub use events::*;
|
||||
@ -0,0 +1,132 @@
|
||||
//! 证书生成与管理 (rcgen 0.11)
|
||||
|
||||
use once_cell::sync::OnceCell;
|
||||
use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, SanType};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::utils::AppError;
|
||||
|
||||
/// 证书和私钥
|
||||
pub struct CertificateBundle {
|
||||
/// PEM 格式的证书
|
||||
pub cert_pem: String,
|
||||
/// PEM 格式的私钥
|
||||
pub key_pem: String,
|
||||
}
|
||||
|
||||
/// 全局证书存储
|
||||
static CERTIFICATE: OnceCell<Arc<CertificateBundle>> = OnceCell::new();
|
||||
|
||||
impl CertificateBundle {
|
||||
/// 生成新的自签名证书
|
||||
pub fn generate(device_id: &str, ip_addresses: Vec<String>) -> Result<Self, AppError> {
|
||||
// 配置证书参数
|
||||
let mut params = CertificateParams::default();
|
||||
|
||||
// 设置证书主题
|
||||
let mut dn = DistinguishedName::new();
|
||||
dn.push(DnType::CommonName, format!("FlashSend-{}", &device_id[..8.min(device_id.len())]));
|
||||
dn.push(DnType::OrganizationName, "Flash Send");
|
||||
params.distinguished_name = dn;
|
||||
|
||||
// 设置 SAN (Subject Alternative Names)
|
||||
let mut san_list = vec![
|
||||
SanType::DnsName("localhost".to_string()),
|
||||
];
|
||||
|
||||
// 添加 IP 地址
|
||||
for ip in ip_addresses {
|
||||
if let Ok(ip_addr) = ip.parse() {
|
||||
san_list.push(SanType::IpAddress(ip_addr));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加本地回环地址
|
||||
san_list.push(SanType::IpAddress("127.0.0.1".parse().unwrap()));
|
||||
params.subject_alt_names = san_list;
|
||||
|
||||
// 生成证书
|
||||
let cert = Certificate::from_params(params)
|
||||
.map_err(|e| AppError::Tls(format!("Failed to generate certificate: {}", e)))?;
|
||||
|
||||
Ok(Self {
|
||||
cert_pem: cert.serialize_pem()
|
||||
.map_err(|e| AppError::Tls(format!("Failed to serialize cert: {}", e)))?,
|
||||
key_pem: cert.serialize_private_key_pem(),
|
||||
})
|
||||
}
|
||||
|
||||
/// 从文件加载证书
|
||||
pub fn load_from_files(cert_path: &PathBuf, key_path: &PathBuf) -> Result<Self, AppError> {
|
||||
let cert_pem = std::fs::read_to_string(cert_path)?;
|
||||
let key_pem = std::fs::read_to_string(key_path)?;
|
||||
Ok(Self { cert_pem, key_pem })
|
||||
}
|
||||
|
||||
/// 保存证书到文件
|
||||
pub fn save_to_files(&self, cert_path: &PathBuf, key_path: &PathBuf) -> Result<(), AppError> {
|
||||
std::fs::write(cert_path, &self.cert_pem)?;
|
||||
std::fs::write(key_path, &self.key_pem)?;
|
||||
log::info!("Certificate saved to {:?}", cert_path);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// 证书管理器
|
||||
pub struct CertificateManager;
|
||||
|
||||
impl CertificateManager {
|
||||
/// 初始化证书 (加载已有或生成新证书)
|
||||
pub fn init(
|
||||
app_data_dir: &PathBuf,
|
||||
device_id: &str,
|
||||
ip_addresses: Vec<String>,
|
||||
) -> Result<Arc<CertificateBundle>, AppError> {
|
||||
let cert_dir = app_data_dir.join("certs");
|
||||
std::fs::create_dir_all(&cert_dir)?;
|
||||
|
||||
let cert_path = cert_dir.join("cert.pem");
|
||||
let key_path = cert_dir.join("key.pem");
|
||||
|
||||
let bundle = if cert_path.exists() && key_path.exists() {
|
||||
log::info!("Loading existing certificate");
|
||||
CertificateBundle::load_from_files(&cert_path, &key_path)?
|
||||
} else {
|
||||
log::info!("Generating new self-signed certificate");
|
||||
let bundle = CertificateBundle::generate(device_id, ip_addresses)?;
|
||||
bundle.save_to_files(&cert_path, &key_path)?;
|
||||
bundle
|
||||
};
|
||||
|
||||
let arc_bundle = Arc::new(bundle);
|
||||
CERTIFICATE
|
||||
.set(arc_bundle.clone())
|
||||
.map_err(|_| AppError::Tls("Certificate already initialized".to_string()))?;
|
||||
|
||||
Ok(arc_bundle)
|
||||
}
|
||||
|
||||
/// 获取当前证书
|
||||
pub fn get() -> Option<Arc<CertificateBundle>> {
|
||||
CERTIFICATE.get().cloned()
|
||||
}
|
||||
|
||||
/// 强制重新生成证书
|
||||
pub fn regenerate(
|
||||
app_data_dir: &PathBuf,
|
||||
device_id: &str,
|
||||
ip_addresses: Vec<String>,
|
||||
) -> Result<CertificateBundle, AppError> {
|
||||
let cert_dir = app_data_dir.join("certs");
|
||||
std::fs::create_dir_all(&cert_dir)?;
|
||||
|
||||
let cert_path = cert_dir.join("cert.pem");
|
||||
let key_path = cert_dir.join("key.pem");
|
||||
|
||||
let bundle = CertificateBundle::generate(device_id, ip_addresses)?;
|
||||
bundle.save_to_files(&cert_path, &key_path)?;
|
||||
|
||||
Ok(bundle)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,119 @@
|
||||
//! TLS 配置构建器 (rustls 0.21)
|
||||
|
||||
use rustls::{Certificate, PrivateKey, ServerConfig};
|
||||
use rustls_pemfile::{certs, pkcs8_private_keys, rsa_private_keys};
|
||||
use std::io::BufReader;
|
||||
use std::sync::Arc;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
|
||||
use super::CertificateBundle;
|
||||
use crate::utils::AppError;
|
||||
|
||||
/// TLS 配置构建器
|
||||
pub struct TlsConfigBuilder;
|
||||
|
||||
impl TlsConfigBuilder {
|
||||
/// 从证书包构建服务端 TLS 配置
|
||||
pub fn build_server_config(bundle: &CertificateBundle) -> Result<Arc<ServerConfig>, AppError> {
|
||||
// 解析证书
|
||||
let cert_chain = Self::parse_certificates(&bundle.cert_pem)?;
|
||||
|
||||
// 解析私钥
|
||||
let private_key = Self::parse_private_key(&bundle.key_pem)?;
|
||||
|
||||
// 构建 ServerConfig
|
||||
let config = ServerConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_no_client_auth()
|
||||
.with_single_cert(cert_chain, private_key)
|
||||
.map_err(|e| AppError::Tls(format!("Failed to build TLS config: {}", e)))?;
|
||||
|
||||
Ok(Arc::new(config))
|
||||
}
|
||||
|
||||
/// 创建 TLS Acceptor (用于接受 TLS 连接)
|
||||
pub fn build_acceptor(bundle: &CertificateBundle) -> Result<TlsAcceptor, AppError> {
|
||||
let config = Self::build_server_config(bundle)?;
|
||||
Ok(TlsAcceptor::from(config))
|
||||
}
|
||||
|
||||
/// 解析 PEM 格式的证书链
|
||||
fn parse_certificates(pem: &str) -> Result<Vec<Certificate>, AppError> {
|
||||
let mut reader = BufReader::new(pem.as_bytes());
|
||||
let certs_raw = certs(&mut reader)
|
||||
.map_err(|e| AppError::Tls(format!("Failed to parse certificate: {}", e)))?;
|
||||
|
||||
if certs_raw.is_empty() {
|
||||
return Err(AppError::Tls("No certificates found in PEM".to_string()));
|
||||
}
|
||||
|
||||
Ok(certs_raw.into_iter().map(Certificate).collect())
|
||||
}
|
||||
|
||||
/// 解析 PEM 格式的私钥
|
||||
fn parse_private_key(pem: &str) -> Result<PrivateKey, AppError> {
|
||||
let mut reader = BufReader::new(pem.as_bytes());
|
||||
|
||||
// 尝试 PKCS8 格式
|
||||
let keys = pkcs8_private_keys(&mut reader)
|
||||
.map_err(|e| AppError::Tls(format!("Failed to parse private key: {}", e)))?;
|
||||
|
||||
if let Some(key) = keys.into_iter().next() {
|
||||
return Ok(PrivateKey(key));
|
||||
}
|
||||
|
||||
// 尝试 RSA 格式
|
||||
let mut reader = BufReader::new(pem.as_bytes());
|
||||
let keys = rsa_private_keys(&mut reader)
|
||||
.map_err(|e| AppError::Tls(format!("Failed to parse RSA key: {}", e)))?;
|
||||
|
||||
keys.into_iter()
|
||||
.next()
|
||||
.map(PrivateKey)
|
||||
.ok_or_else(|| AppError::Tls("No private key found in PEM".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
/// 创建跳过证书验证的客户端配置 (用于自签名证书)
|
||||
pub mod client {
|
||||
use rustls::client::{ServerCertVerified, ServerCertVerifier};
|
||||
use rustls::{Certificate, ClientConfig, ServerName};
|
||||
use std::sync::Arc;
|
||||
use std::time::SystemTime;
|
||||
use tokio_rustls::TlsConnector;
|
||||
|
||||
use crate::utils::AppError;
|
||||
|
||||
/// 跳过验证的证书验证器 (仅用于自签名证书场景)
|
||||
struct SkipServerVerification;
|
||||
|
||||
impl ServerCertVerifier for SkipServerVerification {
|
||||
fn verify_server_cert(
|
||||
&self,
|
||||
_end_entity: &Certificate,
|
||||
_intermediates: &[Certificate],
|
||||
_server_name: &ServerName,
|
||||
_scts: &mut dyn Iterator<Item = &[u8]>,
|
||||
_ocsp_response: &[u8],
|
||||
_now: SystemTime,
|
||||
) -> Result<ServerCertVerified, rustls::Error> {
|
||||
Ok(ServerCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
|
||||
/// 构建客户端 TLS 配置 (跳过证书验证)
|
||||
pub fn build_client_config() -> Result<Arc<ClientConfig>, AppError> {
|
||||
let config = ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
.with_custom_certificate_verifier(Arc::new(SkipServerVerification))
|
||||
.with_no_client_auth();
|
||||
|
||||
Ok(Arc::new(config))
|
||||
}
|
||||
|
||||
/// 创建 TLS Connector
|
||||
pub fn build_connector() -> Result<TlsConnector, AppError> {
|
||||
let config = build_client_config()?;
|
||||
Ok(TlsConnector::from(config))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,8 @@
|
||||
//! TLS 加密模块
|
||||
//! 使用 rustls + rcgen 实现自签名证书管理
|
||||
|
||||
mod certificate;
|
||||
mod config;
|
||||
|
||||
pub use certificate::*;
|
||||
pub use config::*;
|
||||
@ -0,0 +1,105 @@
|
||||
//! 错误处理模块
|
||||
|
||||
use serde::Serialize;
|
||||
use thiserror::Error;
|
||||
|
||||
/// 应用级错误类型
|
||||
#[derive(Debug, Error)]
|
||||
pub enum AppError {
|
||||
/// IO 错误
|
||||
#[error("IO error: {0}")]
|
||||
Io(#[from] std::io::Error),
|
||||
|
||||
/// 数据库错误
|
||||
#[error("Database error: {0}")]
|
||||
Database(#[from] rusqlite::Error),
|
||||
|
||||
/// 序列化错误
|
||||
#[error("Serialization error: {0}")]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
|
||||
/// TLS 错误
|
||||
#[error("TLS error: {0}")]
|
||||
Tls(String),
|
||||
|
||||
/// WebSocket 错误
|
||||
#[error("WebSocket error: {0}")]
|
||||
WebSocket(String),
|
||||
|
||||
/// 网络错误
|
||||
#[error("Network error: {0}")]
|
||||
Network(String),
|
||||
|
||||
/// 文件传输错误
|
||||
#[error("File transfer error: {0}")]
|
||||
FileTransfer(String),
|
||||
|
||||
/// 设备未找到
|
||||
#[error("Device not found: {0}")]
|
||||
DeviceNotFound(String),
|
||||
|
||||
/// 连接错误
|
||||
#[error("Connection error: {0}")]
|
||||
Connection(String),
|
||||
|
||||
/// 配置错误
|
||||
#[error("Configuration error: {0}")]
|
||||
Config(String),
|
||||
|
||||
/// 通用错误
|
||||
#[error("{0}")]
|
||||
General(String),
|
||||
}
|
||||
|
||||
/// Tauri 命令返回的错误包装
|
||||
/// 实现 Serialize 以便在前端使用
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct CommandError {
|
||||
/// 错误代码
|
||||
pub code: String,
|
||||
/// 错误消息
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl From<AppError> for CommandError {
|
||||
fn from(err: AppError) -> Self {
|
||||
let (code, message) = match &err {
|
||||
AppError::Io(_) => ("IO_ERROR", err.to_string()),
|
||||
AppError::Database(_) => ("DB_ERROR", err.to_string()),
|
||||
AppError::Serialization(_) => ("SERIALIZATION_ERROR", err.to_string()),
|
||||
AppError::Tls(_) => ("TLS_ERROR", err.to_string()),
|
||||
AppError::WebSocket(_) => ("WS_ERROR", err.to_string()),
|
||||
AppError::Network(_) => ("NETWORK_ERROR", err.to_string()),
|
||||
AppError::FileTransfer(_) => ("FILE_TRANSFER_ERROR", err.to_string()),
|
||||
AppError::DeviceNotFound(_) => ("DEVICE_NOT_FOUND", err.to_string()),
|
||||
AppError::Connection(_) => ("CONNECTION_ERROR", err.to_string()),
|
||||
AppError::Config(_) => ("CONFIG_ERROR", err.to_string()),
|
||||
AppError::General(_) => ("GENERAL_ERROR", err.to_string()),
|
||||
};
|
||||
Self {
|
||||
code: code.to_string(),
|
||||
message,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for CommandError {
|
||||
fn from(err: std::io::Error) -> Self {
|
||||
AppError::Io(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<rusqlite::Error> for CommandError {
|
||||
fn from(err: rusqlite::Error) -> Self {
|
||||
AppError::Database(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<serde_json::Error> for CommandError {
|
||||
fn from(err: serde_json::Error) -> Self {
|
||||
AppError::Serialization(err).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Tauri 命令返回类型
|
||||
pub type CommandResult<T> = Result<T, CommandError>;
|
||||
@ -0,0 +1,78 @@
|
||||
//! Windows 防火墙自动配置
|
||||
//!
|
||||
//! 在应用启动时自动添加防火墙规则,避免用户手动配置
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use std::os::windows::process::CommandExt;
|
||||
use std::process::Command;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
|
||||
/// 检查并配置 Windows 防火墙规则
|
||||
/// 如果规则不存在,则静默添加(需要管理员权限时会自动请求)
|
||||
#[cfg(target_os = "windows")]
|
||||
pub fn configure_firewall(app_name: &str, udp_port: u16, ws_port: u16, http_port: u16) {
|
||||
// 检查规则是否已存在
|
||||
let check_result = Command::new("netsh")
|
||||
.args(["advfirewall", "firewall", "show", "rule", &format!("name={}", app_name)])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
|
||||
if let Ok(output) = check_result {
|
||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
||||
if output_str.contains(app_name) {
|
||||
log::info!("Firewall rules already configured");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Configuring firewall rules...");
|
||||
|
||||
// 添加 UDP 规则(设备发现)
|
||||
let _ = Command::new("netsh")
|
||||
.args([
|
||||
"advfirewall", "firewall", "add", "rule",
|
||||
&format!("name={} UDP", app_name),
|
||||
"dir=in",
|
||||
"action=allow",
|
||||
"protocol=UDP",
|
||||
&format!("localport={}", udp_port),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
|
||||
// 添加 WebSocket TCP 规则
|
||||
let _ = Command::new("netsh")
|
||||
.args([
|
||||
"advfirewall", "firewall", "add", "rule",
|
||||
&format!("name={} WebSocket", app_name),
|
||||
"dir=in",
|
||||
"action=allow",
|
||||
"protocol=TCP",
|
||||
&format!("localport={}", ws_port),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
|
||||
// 添加 HTTP TCP 规则
|
||||
let _ = Command::new("netsh")
|
||||
.args([
|
||||
"advfirewall", "firewall", "add", "rule",
|
||||
&format!("name={} HTTP", app_name),
|
||||
"dir=in",
|
||||
"action=allow",
|
||||
"protocol=TCP",
|
||||
&format!("localport={}", http_port),
|
||||
])
|
||||
.creation_flags(CREATE_NO_WINDOW)
|
||||
.output();
|
||||
|
||||
log::info!("Firewall rules configuration attempted");
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
pub fn configure_firewall(_app_name: &str, _udp_port: u16, _ws_port: u16, _http_port: u16) {
|
||||
// macOS 和 Linux 通常不需要手动配置防火墙
|
||||
log::info!("Firewall configuration not needed on this platform");
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
//! 工具模块
|
||||
|
||||
mod error;
|
||||
mod config;
|
||||
mod firewall;
|
||||
|
||||
pub use error::*;
|
||||
pub use config::*;
|
||||
pub use firewall::*;
|
||||
@ -0,0 +1,171 @@
|
||||
//! WebSocket 客户端
|
||||
//! 用于主动连接其他设备
|
||||
//!
|
||||
//! 注意: 客户端使用独立的连接管理,因为 TLS 流类型与服务端不同
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures_util::stream::SplitSink;
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio_rustls::client::TlsStream;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::{client_async, WebSocketStream};
|
||||
|
||||
use crate::models::{ChatMessage, DeviceInfo};
|
||||
use crate::tls::client as tls_client;
|
||||
use crate::utils::AppError;
|
||||
|
||||
use super::{ConnectionManager, MessageHandler};
|
||||
|
||||
/// 客户端 WebSocket 写入端类型
|
||||
type ClientWsWriter = SplitSink<WebSocketStream<TlsStream<TcpStream>>, Message>;
|
||||
type ClientWsWriterArc = Arc<tokio::sync::Mutex<ClientWsWriter>>;
|
||||
|
||||
/// WebSocket 客户端
|
||||
#[derive(Clone)]
|
||||
pub struct WsClient {
|
||||
/// 本机设备 ID
|
||||
local_device_id: String,
|
||||
/// 连接管理器 (用于事件通知)
|
||||
connection_manager: ConnectionManager,
|
||||
/// 客户端连接存储
|
||||
client_connections: Arc<DashMap<String, ClientWsWriterArc>>,
|
||||
}
|
||||
|
||||
impl WsClient {
|
||||
/// 创建新的客户端
|
||||
pub fn new(local_device_id: String, connection_manager: ConnectionManager) -> Self {
|
||||
Self {
|
||||
local_device_id,
|
||||
connection_manager,
|
||||
client_connections: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
/// 连接到指定设备
|
||||
pub async fn connect(&self, device: &DeviceInfo) -> Result<(), AppError> {
|
||||
let addr = format!("{}:{}", device.ip, device.ws_port);
|
||||
let ws_url = format!("wss://{}", addr);
|
||||
|
||||
log::info!("Connecting to {} at {}", device.device_name, ws_url);
|
||||
|
||||
// 建立 TCP 连接
|
||||
let tcp_stream = TcpStream::connect(&addr)
|
||||
.await
|
||||
.map_err(|e| AppError::Connection(format!("TCP connection failed: {}", e)))?;
|
||||
|
||||
// TLS 握手
|
||||
let tls_connector = tls_client::build_connector()?;
|
||||
let server_name = rustls::ServerName::try_from(device.ip.as_str())
|
||||
.map_err(|_| AppError::Tls("Invalid server name".to_string()))?;
|
||||
|
||||
let tls_stream = tls_connector
|
||||
.connect(server_name, tcp_stream)
|
||||
.await
|
||||
.map_err(|e| AppError::Tls(format!("TLS handshake failed: {}", e)))?;
|
||||
|
||||
// WebSocket 握手
|
||||
let (ws_stream, _) = client_async(&ws_url, tls_stream)
|
||||
.await
|
||||
.map_err(|e| AppError::WebSocket(format!("WebSocket handshake failed: {}", e)))?;
|
||||
|
||||
let (writer, mut reader) = ws_stream.split();
|
||||
let writer_arc = Arc::new(tokio::sync::Mutex::new(writer));
|
||||
|
||||
// 存储连接
|
||||
let device_id = device.device_id.clone();
|
||||
self.client_connections.insert(device_id.clone(), writer_arc.clone());
|
||||
|
||||
// 发送初始消息以标识自己
|
||||
let hello = ChatMessage::new_event(
|
||||
self.local_device_id.clone(),
|
||||
device.device_id.clone(),
|
||||
"online",
|
||||
);
|
||||
|
||||
{
|
||||
let data = serde_json::to_string(&hello)
|
||||
.map_err(|e| AppError::Serialization(e))?;
|
||||
let mut w = writer_arc.lock().await;
|
||||
w.send(Message::Text(data)).await
|
||||
.map_err(|e| AppError::WebSocket(format!("Send failed: {}", e)))?;
|
||||
}
|
||||
|
||||
log::info!("Connected to device {}", device.device_name);
|
||||
|
||||
// 启动读取任务
|
||||
let connection_manager = self.connection_manager.clone();
|
||||
let local_device_id = self.local_device_id.clone();
|
||||
let peer_device_id = device.device_id.clone();
|
||||
let client_connections = self.client_connections.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Some(msg_result) = reader.next().await {
|
||||
match msg_result {
|
||||
Ok(msg) => {
|
||||
if msg.is_text() {
|
||||
let text = msg.to_text().unwrap_or("");
|
||||
if let Ok(message) = serde_json::from_str::<ChatMessage>(text) {
|
||||
MessageHandler::handle_message(
|
||||
message,
|
||||
&connection_manager,
|
||||
&local_device_id,
|
||||
).await;
|
||||
}
|
||||
} else if msg.is_close() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("WebSocket read error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
client_connections.remove(&peer_device_id);
|
||||
log::info!("Disconnected from device {}", peer_device_id);
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 向设备发送消息
|
||||
pub async fn send_to(&self, device_id: &str, message: &ChatMessage) -> Result<(), String> {
|
||||
if let Some(writer) = self.client_connections.get(device_id) {
|
||||
let data = serde_json::to_string(message)
|
||||
.map_err(|e| format!("Serialization error: {}", e))?;
|
||||
|
||||
let mut w = writer.lock().await;
|
||||
w.send(Message::Text(data)).await
|
||||
.map_err(|e| format!("Send error: {}", e))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Device {} not connected", device_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查是否已连接
|
||||
pub fn is_connected(&self, device_id: &str) -> bool {
|
||||
self.client_connections.contains_key(device_id)
|
||||
}
|
||||
|
||||
/// 断开与指定设备的连接
|
||||
pub async fn disconnect(&self, device_id: &str) {
|
||||
// 发送离线通知
|
||||
let goodbye = ChatMessage::new_event(
|
||||
self.local_device_id.clone(),
|
||||
device_id.to_string(),
|
||||
"offline",
|
||||
);
|
||||
|
||||
let _ = self.send_to(device_id, &goodbye).await;
|
||||
self.client_connections.remove(device_id);
|
||||
}
|
||||
|
||||
/// 获取连接管理器引用
|
||||
pub fn connection_manager(&self) -> &ConnectionManager {
|
||||
&self.connection_manager
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,156 @@
|
||||
//! WebSocket 连接管理
|
||||
|
||||
use dashmap::DashMap;
|
||||
use futures_util::stream::SplitSink;
|
||||
use futures_util::SinkExt;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpStream;
|
||||
use tokio::sync::broadcast;
|
||||
use tokio_rustls::server::TlsStream;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
|
||||
use crate::models::ChatMessage;
|
||||
|
||||
/// WebSocket 连接事件
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum WsConnectionEvent {
|
||||
/// 新连接建立
|
||||
Connected { device_id: String },
|
||||
/// 连接断开
|
||||
Disconnected { device_id: String, reason: Option<String> },
|
||||
/// 收到消息
|
||||
MessageReceived { message: ChatMessage },
|
||||
}
|
||||
|
||||
/// WebSocket 写入端类型别名 (服务端TLS连接)
|
||||
pub type WsWriter = SplitSink<WebSocketStream<TlsStream<TcpStream>>, Message>;
|
||||
|
||||
/// 包装后的写入端类型
|
||||
pub type WsWriterArc = Arc<tokio::sync::Mutex<WsWriter>>;
|
||||
|
||||
/// 连接管理器
|
||||
pub struct ConnectionManager {
|
||||
/// 活跃连接 (device_id -> writer)
|
||||
connections: Arc<DashMap<String, WsWriterArc>>,
|
||||
/// 事件发送器
|
||||
event_tx: broadcast::Sender<WsConnectionEvent>,
|
||||
}
|
||||
|
||||
impl ConnectionManager {
|
||||
/// 创建新的连接管理器
|
||||
pub fn new() -> Self {
|
||||
let (event_tx, _) = broadcast::channel(100);
|
||||
Self {
|
||||
connections: Arc::new(DashMap::new()),
|
||||
event_tx,
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取事件订阅
|
||||
pub fn subscribe(&self) -> broadcast::Receiver<WsConnectionEvent> {
|
||||
self.event_tx.subscribe()
|
||||
}
|
||||
|
||||
/// 注册新连接
|
||||
pub fn register(&self, device_id: String, writer: WsWriter) {
|
||||
let writer = Arc::new(tokio::sync::Mutex::new(writer));
|
||||
self.connections.insert(device_id.clone(), writer);
|
||||
let _ = self.event_tx.send(WsConnectionEvent::Connected { device_id });
|
||||
}
|
||||
|
||||
/// 注册新连接 (使用已包装的 Arc)
|
||||
pub fn register_arc(&self, device_id: String, writer: WsWriterArc) {
|
||||
self.connections.insert(device_id.clone(), writer);
|
||||
let _ = self.event_tx.send(WsConnectionEvent::Connected { device_id });
|
||||
}
|
||||
|
||||
/// 移除连接
|
||||
pub fn unregister(&self, device_id: &str, reason: Option<String>) {
|
||||
if self.connections.remove(device_id).is_some() {
|
||||
let _ = self.event_tx.send(WsConnectionEvent::Disconnected {
|
||||
device_id: device_id.to_string(),
|
||||
reason,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 检查设备是否已连接
|
||||
pub fn is_connected(&self, device_id: &str) -> bool {
|
||||
self.connections.contains_key(device_id)
|
||||
}
|
||||
|
||||
/// 获取所有已连接的设备 ID
|
||||
pub fn connected_devices(&self) -> Vec<String> {
|
||||
self.connections.iter().map(|e| e.key().clone()).collect()
|
||||
}
|
||||
|
||||
/// 向指定设备发送消息
|
||||
pub async fn send_to(&self, device_id: &str, message: &ChatMessage) -> Result<(), String> {
|
||||
if let Some(writer) = self.connections.get(device_id) {
|
||||
let data = serde_json::to_string(message)
|
||||
.map_err(|e| format!("Serialization error: {}", e))?;
|
||||
|
||||
let mut writer = writer.lock().await;
|
||||
writer
|
||||
.send(Message::Text(data))
|
||||
.await
|
||||
.map_err(|e| format!("Send error: {}", e))?;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("Device {} not connected", device_id))
|
||||
}
|
||||
}
|
||||
|
||||
/// 广播消息到所有连接
|
||||
pub async fn broadcast(&self, message: &ChatMessage) {
|
||||
let data = match serde_json::to_string(message) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
log::error!("Failed to serialize message: {}", e);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in self.connections.iter() {
|
||||
let mut writer = entry.value().lock().await;
|
||||
if let Err(e) = writer.send(Message::Text(data.clone())).await {
|
||||
log::error!("Failed to send to {}: {}", entry.key(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 发送消息接收事件
|
||||
pub fn emit_message_received(&self, message: ChatMessage) {
|
||||
let _ = self.event_tx.send(WsConnectionEvent::MessageReceived { message });
|
||||
}
|
||||
|
||||
/// 获取连接数
|
||||
pub fn connection_count(&self) -> usize {
|
||||
self.connections.len()
|
||||
}
|
||||
|
||||
/// 关闭所有连接
|
||||
pub async fn close_all(&self) {
|
||||
for entry in self.connections.iter() {
|
||||
let mut writer = entry.value().lock().await;
|
||||
let _ = writer.close().await;
|
||||
}
|
||||
self.connections.clear();
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConnectionManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for ConnectionManager {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
connections: self.connections.clone(),
|
||||
event_tx: self.event_tx.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
//! WebSocket 聊天服务模块
|
||||
//! 实现实时文本消息、心跳、在线状态管理
|
||||
|
||||
mod server;
|
||||
mod client;
|
||||
mod connection;
|
||||
mod handler;
|
||||
|
||||
pub use server::*;
|
||||
pub use client::*;
|
||||
pub use connection::*;
|
||||
pub use handler::*;
|
||||
@ -0,0 +1,178 @@
|
||||
//! WebSocket 服务端
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::Arc;
|
||||
use tokio::net::TcpListener;
|
||||
use tokio_rustls::TlsAcceptor;
|
||||
use tokio_tungstenite::accept_async;
|
||||
|
||||
use crate::models::ChatMessage;
|
||||
use crate::tls::{CertificateManager, TlsConfigBuilder};
|
||||
use crate::utils::AppError;
|
||||
|
||||
use super::{ConnectionManager, MessageHandler};
|
||||
|
||||
/// WebSocket 服务端
|
||||
pub struct WsServer {
|
||||
/// 监听端口
|
||||
port: u16,
|
||||
/// 本机设备 ID
|
||||
local_device_id: String,
|
||||
/// 连接管理器
|
||||
connection_manager: ConnectionManager,
|
||||
/// 停止信号
|
||||
stop_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
||||
}
|
||||
|
||||
impl WsServer {
|
||||
/// 创建新的 WebSocket 服务端
|
||||
pub fn new(port: u16, local_device_id: String, connection_manager: ConnectionManager) -> Self {
|
||||
Self {
|
||||
port,
|
||||
local_device_id,
|
||||
connection_manager,
|
||||
stop_tx: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 启动服务
|
||||
pub async fn start(&mut self) -> Result<(), AppError> {
|
||||
// 获取 TLS 配置
|
||||
let cert_bundle = CertificateManager::get()
|
||||
.ok_or_else(|| AppError::Tls("Certificate not initialized".to_string()))?;
|
||||
|
||||
let tls_acceptor = TlsConfigBuilder::build_acceptor(&cert_bundle)?;
|
||||
|
||||
// 绑定端口
|
||||
let addr = SocketAddr::from(([0, 0, 0, 0], self.port));
|
||||
let listener = TcpListener::bind(addr).await?;
|
||||
|
||||
log::info!("WebSocket server started on port {}", self.port);
|
||||
|
||||
// 创建停止信号
|
||||
let (stop_tx, mut stop_rx) = tokio::sync::oneshot::channel();
|
||||
self.stop_tx = Some(stop_tx);
|
||||
|
||||
let connection_manager = self.connection_manager.clone();
|
||||
let local_device_id = self.local_device_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
loop {
|
||||
tokio::select! {
|
||||
result = listener.accept() => {
|
||||
match result {
|
||||
Ok((stream, peer_addr)) => {
|
||||
let tls_acceptor = tls_acceptor.clone();
|
||||
let conn_mgr = connection_manager.clone();
|
||||
let device_id = local_device_id.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = Self::handle_connection(
|
||||
stream,
|
||||
peer_addr,
|
||||
tls_acceptor,
|
||||
conn_mgr,
|
||||
device_id,
|
||||
).await {
|
||||
log::error!("Connection error from {}: {}", peer_addr, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Accept error: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ = &mut stop_rx => {
|
||||
log::info!("WebSocket server stopping");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 处理单个连接
|
||||
async fn handle_connection(
|
||||
stream: tokio::net::TcpStream,
|
||||
peer_addr: SocketAddr,
|
||||
tls_acceptor: TlsAcceptor,
|
||||
connection_manager: ConnectionManager,
|
||||
local_device_id: String,
|
||||
) -> Result<(), AppError> {
|
||||
log::info!("New connection from {}", peer_addr);
|
||||
|
||||
// TLS 握手
|
||||
let tls_stream = tls_acceptor
|
||||
.accept(stream)
|
||||
.await
|
||||
.map_err(|e| AppError::Tls(format!("TLS handshake failed: {}", e)))?;
|
||||
|
||||
// WebSocket 握手
|
||||
let ws_stream = accept_async(tls_stream)
|
||||
.await
|
||||
.map_err(|e| AppError::WebSocket(format!("WebSocket handshake failed: {}", e)))?;
|
||||
|
||||
let (write_half, mut read_half) = ws_stream.split();
|
||||
|
||||
// 使用 Arc<Mutex> 包装 writer 以便在读取第一条消息后注册
|
||||
let writer = Arc::new(tokio::sync::Mutex::new(write_half));
|
||||
let mut peer_device_id: Option<String> = None;
|
||||
let mut registered = false;
|
||||
|
||||
while let Some(msg_result) = read_half.next().await {
|
||||
match msg_result {
|
||||
Ok(msg) => {
|
||||
if msg.is_text() {
|
||||
let text = msg.to_text().unwrap_or("");
|
||||
if let Ok(message) = serde_json::from_str::<ChatMessage>(text) {
|
||||
if !registered {
|
||||
// 第一条消息,注册连接
|
||||
peer_device_id = Some(message.from.clone());
|
||||
connection_manager.register_arc(message.from.clone(), writer.clone());
|
||||
registered = true;
|
||||
log::info!("Device {} connected", message.from);
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
MessageHandler::handle_message(
|
||||
message,
|
||||
&connection_manager,
|
||||
&local_device_id,
|
||||
).await;
|
||||
}
|
||||
} else if msg.is_close() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("WebSocket error: {}", e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 连接关闭,移除注册
|
||||
if let Some(device_id) = peer_device_id {
|
||||
connection_manager.unregister(&device_id, Some("Connection closed".to_string()));
|
||||
log::info!("Device {} disconnected", device_id);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 停止服务
|
||||
pub fn stop(&mut self) {
|
||||
if let Some(stop_tx) = self.stop_tx.take() {
|
||||
let _ = stop_tx.send(());
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取连接管理器
|
||||
pub fn connection_manager(&self) -> &ConnectionManager {
|
||||
&self.connection_manager
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Flash Send",
|
||||
"version": "1.0.0",
|
||||
"identifier": "com.flashsend.app",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "Flash Send",
|
||||
"width": 1200,
|
||||
"height": 800,
|
||||
"resizable": true,
|
||||
"fullscreen": false,
|
||||
"minWidth": 800,
|
||||
"minHeight": 600
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
]
|
||||
},
|
||||
"plugins": {}
|
||||
}
|
||||
@ -0,0 +1,97 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted } from 'vue'
|
||||
import { listen, UnlistenFn } from '@tauri-apps/api/event'
|
||||
import { useDeviceStore } from '@/stores/deviceStore'
|
||||
import { useChatStore } from '@/stores/chatStore'
|
||||
import { EventNames } from '@/types'
|
||||
import type { DeviceInfo, ChatMessage, TransferProgressEvent } from '@/types'
|
||||
import Sidebar from '@/components/Sidebar.vue'
|
||||
|
||||
const deviceStore = useDeviceStore()
|
||||
const chatStore = useChatStore()
|
||||
|
||||
let unlistenFns: UnlistenFn[] = []
|
||||
|
||||
// 初始化主题
|
||||
function initTheme() {
|
||||
const theme = localStorage.getItem('theme') || 'system'
|
||||
const root = document.documentElement
|
||||
if (theme === 'system') {
|
||||
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
root.classList.toggle('dark', isDark)
|
||||
} else {
|
||||
root.classList.toggle('dark', theme === 'dark')
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化主题
|
||||
initTheme()
|
||||
|
||||
// 初始化应用
|
||||
await deviceStore.initialize()
|
||||
|
||||
// 监听设备发现事件
|
||||
unlistenFns.push(
|
||||
await listen<DeviceInfo>(EventNames.DEVICE_FOUND, (event) => {
|
||||
deviceStore.addDevice(event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 监听设备离线事件
|
||||
unlistenFns.push(
|
||||
await listen<{ deviceId: string }>(EventNames.DEVICE_LOST, (event) => {
|
||||
deviceStore.removeDevice(event.payload.deviceId)
|
||||
})
|
||||
)
|
||||
|
||||
// 监听消息接收事件
|
||||
unlistenFns.push(
|
||||
await listen<ChatMessage>(EventNames.MESSAGE_RECEIVED, (event) => {
|
||||
chatStore.receiveMessage(event.payload)
|
||||
})
|
||||
)
|
||||
|
||||
// 监听 WebSocket 连接事件
|
||||
unlistenFns.push(
|
||||
await listen<{ deviceId: string }>(EventNames.WEBSOCKET_CONNECTED, (event) => {
|
||||
deviceStore.setDeviceConnected(event.payload.deviceId, true)
|
||||
})
|
||||
)
|
||||
|
||||
// 监听 WebSocket 断开事件
|
||||
unlistenFns.push(
|
||||
await listen<{ deviceId: string }>(EventNames.WEBSOCKET_DISCONNECTED, (event) => {
|
||||
deviceStore.setDeviceConnected(event.payload.deviceId, false)
|
||||
})
|
||||
)
|
||||
|
||||
// 监听文件传输进度
|
||||
unlistenFns.push(
|
||||
await listen<TransferProgressEvent>(EventNames.TRANSFER_PROGRESS, (event) => {
|
||||
chatStore.updateTransferProgress(event.payload)
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理事件监听
|
||||
unlistenFns.forEach(fn => fn())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-screen bg-surface-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
<!-- 侧边栏 -->
|
||||
<Sidebar />
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="flex-1 overflow-hidden">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition name="fade" mode="out-in">
|
||||
<component :is="Component" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
@ -0,0 +1,96 @@
|
||||
/**
|
||||
* API 模块
|
||||
* 封装所有 Tauri 命令调用
|
||||
*/
|
||||
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import type {
|
||||
DeviceInfo,
|
||||
ChatMessage,
|
||||
FileTransfer,
|
||||
FileMetadata,
|
||||
AppConfig,
|
||||
} from '@/types'
|
||||
|
||||
/** 发现服务 API */
|
||||
export const discoveryApi = {
|
||||
/** 启动设备发现服务 */
|
||||
start: () => invoke('start_discovery_service'),
|
||||
|
||||
/** 停止设备发现服务 */
|
||||
stop: () => invoke('stop_discovery_service'),
|
||||
|
||||
/** 获取已发现的设备列表 */
|
||||
getDevices: () => invoke<DeviceInfo[]>('get_discovered_devices'),
|
||||
}
|
||||
|
||||
/** WebSocket API */
|
||||
export const websocketApi = {
|
||||
/** 启动 WebSocket 服务端 */
|
||||
startServer: () => invoke('start_websocket_server'),
|
||||
|
||||
/** 停止 WebSocket 服务端 */
|
||||
stopServer: () => invoke('stop_websocket_server'),
|
||||
|
||||
/** 连接到指定设备 */
|
||||
connect: (deviceId: string) => invoke('connect_to_peer', { deviceId }),
|
||||
|
||||
/** 断开与指定设备的连接 */
|
||||
disconnect: (deviceId: string) => invoke('disconnect_from_peer', { deviceId }),
|
||||
}
|
||||
|
||||
/** 聊天 API */
|
||||
export const chatApi = {
|
||||
/** 发送聊天消息 */
|
||||
sendMessage: (deviceId: string, content: string, messageType?: string) =>
|
||||
invoke<ChatMessage>('send_chat_message', { deviceId, content, messageType }),
|
||||
|
||||
/** 获取聊天历史 */
|
||||
getHistory: (deviceId: string, limit?: number, offset?: number) =>
|
||||
invoke<ChatMessage[]>('get_chat_history', { deviceId, limit, offset }),
|
||||
|
||||
/** 删除聊天历史 */
|
||||
deleteHistory: (deviceId: string) =>
|
||||
invoke<number>('delete_chat_history', { deviceId }),
|
||||
}
|
||||
|
||||
/** 文件传输 API */
|
||||
export const fileApi = {
|
||||
/** 启动 HTTP 服务端 */
|
||||
startServer: () => invoke('start_http_server'),
|
||||
|
||||
/** 停止 HTTP 服务端 */
|
||||
stopServer: () => invoke('stop_http_server'),
|
||||
|
||||
/** 选择要发送的文件 */
|
||||
selectFile: () => invoke<FileMetadata | null>('select_file_to_send'),
|
||||
|
||||
/** 发送文件 */
|
||||
sendFile: (deviceId: string, fileId: string, filePath: string) =>
|
||||
invoke<FileTransfer>('send_file', { deviceId, fileId, filePath }),
|
||||
|
||||
/** 获取传输历史 */
|
||||
getHistory: (deviceId: string, limit?: number) =>
|
||||
invoke<FileTransfer[]>('get_transfer_history', { deviceId, limit }),
|
||||
|
||||
/** 打开文件位置 */
|
||||
openLocation: (path: string) => invoke('open_file_location', { path }),
|
||||
|
||||
/** 取消传输 */
|
||||
cancelTransfer: (fileId: string) => invoke('cancel_transfer', { fileId }),
|
||||
}
|
||||
|
||||
/** 配置 API */
|
||||
export const configApi = {
|
||||
/** 获取应用配置 */
|
||||
get: () => invoke<AppConfig>('get_app_config'),
|
||||
|
||||
/** 更新设备名称 */
|
||||
updateDeviceName: (name: string) => invoke('update_device_name', { name }),
|
||||
|
||||
/** 更新下载目录 */
|
||||
updateDownloadDir: (dir: string) => invoke('update_download_dir', { dir }),
|
||||
|
||||
/** 获取本机设备信息 */
|
||||
getLocalDevice: () => invoke<DeviceInfo>('get_local_device_info'),
|
||||
}
|
||||