update
This commit is contained in:
parent
8761b7cc23
commit
bfb1d31cb0
99
README.md
99
README.md
@ -3,10 +3,10 @@
|
|||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
**现代化多数据库管理工具**
|
**现代化多数据库管理工具**
|
||||||
|
|
||||||
@ -18,36 +18,35 @@
|
|||||||
|
|
||||||
## ✨ 特性
|
## ✨ 特性
|
||||||
|
|
||||||
- 🚀 **超轻量** - 基于 Tauri 2.0 + Rust,安装包仅 ~10MB
|
- 🚀 **跨平台** - 基于 Electron,支持 Windows、macOS、Linux
|
||||||
- ⚡ **高性能** - Rust 原生数据库驱动,毫秒级响应
|
- ⚡ **高性能** - 原生数据库驱动,毫秒级响应
|
||||||
- 🎨 **精美 UI** - Windows Metro 风格,深色主题
|
- 🎨 **精美 UI** - Windows Metro 风格,深色主题,无边框窗口
|
||||||
- 🔌 **多数据库** - 支持 MySQL、PostgreSQL、SQLite、SQL Server 等
|
- 🔌 **多数据库** - 支持 MySQL、PostgreSQL、SQLite、SQL Server、MongoDB、Redis、MariaDB
|
||||||
- 🔐 **SSH 隧道** - 安全连接远程数据库
|
- 📝 **智能编辑器** - Monaco Editor,SQL 语法高亮、智能补全
|
||||||
- 📝 **智能编辑器** - SQL 语法高亮、智能补全、代码片段
|
- 📊 **数据编辑** - 支持直接编辑表格数据,虚拟滚动大数据量
|
||||||
- 📊 **数据编辑** - 支持直接编辑表格数据
|
- 🛠️ **表设计器** - Navicat 风格,可视化编辑字段、索引、外键、表选项
|
||||||
- 📤 **导入导出** - 支持 JSON、Navicat NCX 格式
|
- 🗃️ **完整管理** - 创建/删除/重命名/复制数据库和表
|
||||||
|
- 📤 **导入导出** - 支持 JSON、Navicat NCX 格式连接配置导入导出
|
||||||
|
- 🔄 **批量操作** - 支持多选连接批量删除管理
|
||||||
|
|
||||||
## 🗃️ 支持的数据库
|
## 🗃️ 支持的数据库
|
||||||
|
|
||||||
| 数据库 | 状态 | 说明 |
|
| 数据库 | 状态 | 驱动 |
|
||||||
|--------|------|------|
|
|--------|------|------|
|
||||||
| 🐬 MySQL | ✅ | 完全支持 |
|
| 🐬 MySQL | ✅ | mysql2 |
|
||||||
| 🐘 PostgreSQL | ✅ | 完全支持 |
|
| 🐘 PostgreSQL | ✅ | pg |
|
||||||
| 💾 SQLite | ✅ | 完全支持 |
|
| 💾 SQLite | ✅ | sql.js |
|
||||||
| 📊 SQL Server | ✅ | 完全支持 |
|
| 📊 SQL Server | ✅ | mssql |
|
||||||
| 🦭 MariaDB | ✅ | 完全支持 |
|
| 🦭 MariaDB | ✅ | mysql2 |
|
||||||
| 🍃 MongoDB | 🔜 | 开发中 |
|
| 🍃 MongoDB | ✅ | mongodb |
|
||||||
| ⚡ Redis | 🔜 | 开发中 |
|
| ⚡ Redis | ✅ | ioredis |
|
||||||
| 🔶 Oracle | 🔜 | 计划中 |
|
|
||||||
| ❄️ Snowflake | 🔜 | 计划中 |
|
|
||||||
|
|
||||||
## 🚀 快速开始
|
## 🚀 快速开始
|
||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
- Node.js 18+
|
- Node.js 18+
|
||||||
- Rust (rustup)
|
- npm 或 yarn
|
||||||
- [Tauri 依赖](https://tauri.app/v1/guides/getting-started/prerequisites)
|
|
||||||
|
|
||||||
### 安装
|
### 安装
|
||||||
|
|
||||||
@ -60,10 +59,10 @@ cd easysql
|
|||||||
npm install
|
npm install
|
||||||
|
|
||||||
# 开发模式运行
|
# 开发模式运行
|
||||||
npm run tauri:dev
|
npm run electron:dev
|
||||||
|
|
||||||
# 构建应用
|
# 构建应用
|
||||||
npm run tauri:build
|
npm run electron:build
|
||||||
```
|
```
|
||||||
|
|
||||||
## 📸 界面预览
|
## 📸 界面预览
|
||||||
@ -92,36 +91,35 @@ npm run tauri:build
|
|||||||
|
|
||||||
## 🛠️ 技术栈
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
- **运行时**: Tauri 2.0 (Rust + WebView)
|
- **运行时**: Electron 33
|
||||||
- **后端**: Rust + SQLx + Tiberius
|
|
||||||
- **前端**: React 18 + TypeScript 5
|
- **前端**: React 18 + TypeScript 5
|
||||||
- **样式**: Tailwind CSS 3
|
- **样式**: Tailwind CSS 3
|
||||||
- **构建**: Vite 5
|
- **构建**: Vite 5
|
||||||
- **编辑器**: Monaco Editor
|
- **编辑器**: Monaco Editor
|
||||||
|
- **数据库驱动**: mysql2, pg, sql.js, mssql, mongodb, ioredis
|
||||||
|
|
||||||
## 📁 项目结构
|
## 📁 项目结构
|
||||||
|
|
||||||
```
|
```
|
||||||
easysql/
|
easysql/
|
||||||
├── src-tauri/ # Tauri/Rust 后端
|
├── electron/ # Electron 主进程
|
||||||
│ ├── src/
|
│ ├── main.js # 主程序入口
|
||||||
│ │ ├── main.rs # 主程序入口
|
│ └── preload.js # 预加载脚本
|
||||||
│ │ ├── commands.rs # Tauri 命令
|
├── src/ # React 前端
|
||||||
│ │ ├── database.rs # 数据库连接管理
|
│ ├── components/ # UI 组件
|
||||||
│ │ ├── config.rs # 配置管理
|
│ │ ├── TitleBar.tsx # 标题栏
|
||||||
│ │ └── ssh.rs # SSH 隧道
|
│ │ ├── Sidebar.tsx # 侧边栏(连接/数据库/表树)
|
||||||
│ ├── Cargo.toml
|
│ │ ├── MainContent.tsx # 主内容区
|
||||||
│ └── tauri.conf.json
|
│ │ ├── SqlEditor.tsx # SQL 编辑器(Monaco)
|
||||||
├── src/ # React 前端
|
│ │ ├── VirtualDataTable.tsx # 虚拟滚动数据表格
|
||||||
│ ├── components/ # UI 组件
|
│ │ ├── TableDesigner.tsx # 表设计器(Navicat 风格)
|
||||||
│ │ ├── TitleBar.tsx
|
│ │ ├── ConnectionModal.tsx # 连接配置弹窗
|
||||||
│ │ ├── Sidebar.tsx
|
│ │ ├── CreateDatabaseModal.tsx # 新建数据库弹窗
|
||||||
│ │ ├── MainContent.tsx
|
│ │ ├── CreateTableModal.tsx # 快速新建表弹窗
|
||||||
│ │ ├── SqlEditor.tsx
|
│ │ └── InputDialog.tsx # 通用输入对话框
|
||||||
│ │ └── ConnectionModal.tsx
|
|
||||||
│ ├── lib/
|
│ ├── lib/
|
||||||
│ │ ├── tauri-api.ts # Tauri API 封装
|
│ │ ├── electron-api.ts # Electron API 封装
|
||||||
│ │ └── hooks.ts # 自定义 Hooks
|
│ │ └── hooks.ts # 自定义 Hooks
|
||||||
│ ├── App.tsx
|
│ ├── App.tsx
|
||||||
│ ├── types.ts
|
│ ├── types.ts
|
||||||
│ └── index.css
|
│ └── index.css
|
||||||
@ -143,6 +141,8 @@ easysql/
|
|||||||
| `Ctrl+Q` | 新建查询 |
|
| `Ctrl+Q` | 新建查询 |
|
||||||
| `Ctrl+W` | 关闭当前标签 |
|
| `Ctrl+W` | 关闭当前标签 |
|
||||||
| `Ctrl+F` | 搜索(侧边栏/表格) |
|
| `Ctrl+F` | 搜索(侧边栏/表格) |
|
||||||
|
| `双击连接` | 快速连接数据库 |
|
||||||
|
| `右键菜单` | 连接/数据库/表操作 |
|
||||||
|
|
||||||
## 🔧 配置说明
|
## 🔧 配置说明
|
||||||
|
|
||||||
@ -151,6 +151,15 @@ easysql/
|
|||||||
- macOS: `~/Library/Application Support/easysql/connections.json`
|
- macOS: `~/Library/Application Support/easysql/connections.json`
|
||||||
- Linux: `~/.config/easysql/connections.json`
|
- Linux: `~/.config/easysql/connections.json`
|
||||||
|
|
||||||
|
## 📦 npm 脚本
|
||||||
|
|
||||||
|
| 命令 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `npm run dev` | 启动 Vite 开发服务器 |
|
||||||
|
| `npm run build` | 构建前端资源 |
|
||||||
|
| `npm run electron:dev` | 开发模式运行 Electron |
|
||||||
|
| `npm run electron:build` | 打包 Electron 应用 |
|
||||||
|
|
||||||
## 🤝 贡献
|
## 🤝 贡献
|
||||||
|
|
||||||
欢迎提交 Issue 和 Pull Request!
|
欢迎提交 Issue 和 Pull Request!
|
||||||
@ -162,5 +171,5 @@ MIT
|
|||||||
---
|
---
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
Made with ❤️ using Tauri + React + Rust
|
Made with ❤️ using Electron + React + Node.js
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
35
app-icon.svg
Normal file
35
app-icon.svg
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#6366f1"/>
|
||||||
|
<stop offset="100%" style="stop-color:#4f46e5"/>
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient id="db" x1="0%" y1="0%" x2="0%" y2="100%">
|
||||||
|
<stop offset="0%" style="stop-color:#ffffff"/>
|
||||||
|
<stop offset="100%" style="stop-color:#e0e7ff"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<!-- Background -->
|
||||||
|
<rect width="1024" height="1024" rx="180" fill="url(#bg)"/>
|
||||||
|
|
||||||
|
<!-- Database Icon -->
|
||||||
|
<g transform="translate(512, 512)">
|
||||||
|
<!-- Database cylinder -->
|
||||||
|
<ellipse cx="0" cy="-200" rx="220" ry="70" fill="url(#db)" stroke="#c7d2fe" stroke-width="8"/>
|
||||||
|
<rect x="-220" y="-200" width="440" height="400" fill="url(#db)"/>
|
||||||
|
<ellipse cx="0" cy="200" rx="220" ry="70" fill="url(#db)" stroke="#c7d2fe" stroke-width="8"/>
|
||||||
|
|
||||||
|
<!-- Middle lines -->
|
||||||
|
<ellipse cx="0" cy="-50" rx="220" ry="70" fill="none" stroke="#a5b4fc" stroke-width="6"/>
|
||||||
|
<ellipse cx="0" cy="100" rx="220" ry="70" fill="none" stroke="#a5b4fc" stroke-width="6"/>
|
||||||
|
|
||||||
|
<!-- Side borders -->
|
||||||
|
<line x1="-220" y1="-200" x2="-220" y2="200" stroke="#c7d2fe" stroke-width="8"/>
|
||||||
|
<line x1="220" y1="-200" x2="220" y2="200" stroke="#c7d2fe" stroke-width="8"/>
|
||||||
|
|
||||||
|
<!-- SQL text -->
|
||||||
|
<text x="0" y="350" text-anchor="middle" font-family="Arial, sans-serif" font-size="120" font-weight="bold" fill="#ffffff">SQL</text>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
1981
electron/main.js
Normal file
1981
electron/main.js
Normal file
File diff suppressed because it is too large
Load Diff
74
electron/preload.js
Normal file
74
electron/preload.js
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
const { contextBridge, ipcRenderer } = require('electron')
|
||||||
|
|
||||||
|
// 暴露安全的 API 给渲染进程
|
||||||
|
contextBridge.exposeInMainWorld('electronAPI', {
|
||||||
|
// 窗口控制
|
||||||
|
minimize: () => ipcRenderer.invoke('window:minimize'),
|
||||||
|
maximize: () => ipcRenderer.invoke('window:maximize'),
|
||||||
|
close: () => ipcRenderer.invoke('window:close'),
|
||||||
|
|
||||||
|
// 配置存储
|
||||||
|
loadConnections: () => ipcRenderer.invoke('config:load'),
|
||||||
|
saveConnections: (connections) => ipcRenderer.invoke('config:save', connections),
|
||||||
|
|
||||||
|
// 数据库操作
|
||||||
|
testConnection: (config) => ipcRenderer.invoke('db:test', config),
|
||||||
|
connect: (config) => ipcRenderer.invoke('db:connect', config),
|
||||||
|
disconnect: (id) => ipcRenderer.invoke('db:disconnect', id),
|
||||||
|
query: (id, sql) => ipcRenderer.invoke('db:query', id, sql),
|
||||||
|
getDatabases: (id) => ipcRenderer.invoke('db:getDatabases', id),
|
||||||
|
getTables: (id, database) => ipcRenderer.invoke('db:getTables', id, database),
|
||||||
|
getColumns: (id, database, table) => ipcRenderer.invoke('db:getColumns', id, database, table),
|
||||||
|
getTableData: (id, database, table, page, pageSize) =>
|
||||||
|
ipcRenderer.invoke('db:getTableData', id, database, table, page, pageSize),
|
||||||
|
updateRow: (id, database, table, primaryKey, updates) =>
|
||||||
|
ipcRenderer.invoke('db:updateRow', id, database, table, primaryKey, updates),
|
||||||
|
deleteRow: (id, database, table, primaryKey) =>
|
||||||
|
ipcRenderer.invoke('db:deleteRow', id, database, table, primaryKey),
|
||||||
|
|
||||||
|
// 数据库管理
|
||||||
|
createDatabase: (id, dbName, charset, collation) =>
|
||||||
|
ipcRenderer.invoke('db:createDatabase', id, dbName, charset, collation),
|
||||||
|
dropDatabase: (id, dbName) =>
|
||||||
|
ipcRenderer.invoke('db:dropDatabase', id, dbName),
|
||||||
|
|
||||||
|
// 表管理
|
||||||
|
createTable: (id, database, tableName, columns) =>
|
||||||
|
ipcRenderer.invoke('db:createTable', id, database, tableName, columns),
|
||||||
|
dropTable: (id, database, tableName) =>
|
||||||
|
ipcRenderer.invoke('db:dropTable', id, database, tableName),
|
||||||
|
truncateTable: (id, database, tableName) =>
|
||||||
|
ipcRenderer.invoke('db:truncateTable', id, database, tableName),
|
||||||
|
renameTable: (id, database, oldName, newName) =>
|
||||||
|
ipcRenderer.invoke('db:renameTable', id, database, oldName, newName),
|
||||||
|
duplicateTable: (id, database, sourceTable, newTable, withData) =>
|
||||||
|
ipcRenderer.invoke('db:duplicateTable', id, database, sourceTable, newTable, withData),
|
||||||
|
|
||||||
|
// 列管理
|
||||||
|
addColumn: (id, database, tableName, column) =>
|
||||||
|
ipcRenderer.invoke('db:addColumn', id, database, tableName, column),
|
||||||
|
modifyColumn: (id, database, tableName, oldName, column) =>
|
||||||
|
ipcRenderer.invoke('db:modifyColumn', id, database, tableName, oldName, column),
|
||||||
|
dropColumn: (id, database, tableName, columnName) =>
|
||||||
|
ipcRenderer.invoke('db:dropColumn', id, database, tableName, columnName),
|
||||||
|
|
||||||
|
// 表设计器相关
|
||||||
|
getTableInfo: (id, database, tableName) =>
|
||||||
|
ipcRenderer.invoke('db:getTableInfo', id, database, tableName),
|
||||||
|
getIndexes: (id, database, tableName) =>
|
||||||
|
ipcRenderer.invoke('db:getIndexes', id, database, tableName),
|
||||||
|
getForeignKeys: (id, database, tableName) =>
|
||||||
|
ipcRenderer.invoke('db:getForeignKeys', id, database, tableName),
|
||||||
|
getColumnNames: (id, database, tableName) =>
|
||||||
|
ipcRenderer.invoke('db:getColumnNames', id, database, tableName),
|
||||||
|
executeMultiSQL: (id, sqls) =>
|
||||||
|
ipcRenderer.invoke('db:executeMultiSQL', id, sqls),
|
||||||
|
|
||||||
|
// 文件操作
|
||||||
|
openFile: () => ipcRenderer.invoke('file:open'),
|
||||||
|
saveFile: (filePath, content) => ipcRenderer.invoke('file:save', filePath, content),
|
||||||
|
selectFile: (extensions) => ipcRenderer.invoke('file:select', extensions),
|
||||||
|
saveDialog: (options) => ipcRenderer.invoke('file:saveDialog', options),
|
||||||
|
writeFile: (filePath, content) => ipcRenderer.invoke('file:write', filePath, content),
|
||||||
|
readFile: (filePath) => ipcRenderer.invoke('file:read', filePath)
|
||||||
|
})
|
||||||
5573
package-lock.json
generated
5573
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@ -2,33 +2,65 @@
|
|||||||
"name": "easysql",
|
"name": "easysql",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Modern Database Management Tool",
|
"description": "Modern Database Management Tool",
|
||||||
|
"main": "electron/main.js",
|
||||||
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri": "tauri",
|
"electron:dev": "concurrently \"vite\" \"wait-on http://localhost:5173 && electron .\"",
|
||||||
"tauri:dev": "tauri dev",
|
"electron:build": "vite build && electron-builder"
|
||||||
"tauri:build": "tauri build"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@monaco-editor/react": "^4.7.0",
|
"@monaco-editor/react": "^4.7.0",
|
||||||
"@tauri-apps/api": "^2.9.1",
|
"ioredis": "^5.8.2",
|
||||||
"lucide-react": "^0.294.0",
|
"lucide-react": "^0.294.0",
|
||||||
"monaco-editor": "^0.55.1",
|
"monaco-editor": "^0.55.1",
|
||||||
"sql-formatter": "^15.6.12"
|
"mongodb": "^6.21.0",
|
||||||
|
"mssql": "^11.0.1",
|
||||||
|
"mysql2": "^3.11.0",
|
||||||
|
"pg": "^8.13.0",
|
||||||
|
"sql-formatter": "^15.6.12",
|
||||||
|
"sql.js": "^1.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tauri-apps/cli": "^2.9.6",
|
|
||||||
"@types/node": "^20.10.0",
|
"@types/node": "^20.10.0",
|
||||||
"@types/react": "^18.2.0",
|
"@types/react": "^18.2.0",
|
||||||
"@types/react-dom": "^18.2.0",
|
"@types/react-dom": "^18.2.0",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.16",
|
||||||
|
"concurrently": "^9.1.0",
|
||||||
|
"electron": "^33.2.1",
|
||||||
|
"electron-builder": "^25.1.8",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.32",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"tailwindcss": "^3.3.6",
|
"tailwindcss": "^3.3.6",
|
||||||
"typescript": "^5.3.0",
|
"typescript": "^5.3.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0",
|
||||||
|
"wait-on": "^8.0.1"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "com.easysql.app",
|
||||||
|
"productName": "EasySQL",
|
||||||
|
"directories": {
|
||||||
|
"output": "release"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"dist/**/*",
|
||||||
|
"electron/**/*"
|
||||||
|
],
|
||||||
|
"win": {
|
||||||
|
"target": "nsis",
|
||||||
|
"icon": "public/icon.ico"
|
||||||
|
},
|
||||||
|
"mac": {
|
||||||
|
"target": "dmg",
|
||||||
|
"icon": "public/icon.icns"
|
||||||
|
},
|
||||||
|
"linux": {
|
||||||
|
"target": "AppImage",
|
||||||
|
"icon": "public/icon.png"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6847
src-tauri/Cargo.lock
generated
6847
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,45 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "easysql"
|
|
||||||
version = "1.0.0"
|
|
||||||
description = "A modern database management tool"
|
|
||||||
authors = ["EasySQL"]
|
|
||||||
edition = "2021"
|
|
||||||
|
|
||||||
[build-dependencies]
|
|
||||||
tauri-build = { version = "2", features = [] }
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
tauri = { version = "2", features = ["tray-icon"] }
|
|
||||||
tauri-plugin-shell = "2"
|
|
||||||
tauri-plugin-dialog = "2"
|
|
||||||
tauri-plugin-fs = "2"
|
|
||||||
tauri-plugin-single-instance = "2"
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
|
||||||
serde_json = "1"
|
|
||||||
tokio = { version = "1", features = ["full"] }
|
|
||||||
sqlx = { version = "0.8", features = ["runtime-tokio", "mysql", "postgres", "sqlite"] }
|
|
||||||
tiberius = { version = "0.12", default-features = false, features = ["tds73", "rustls", "chrono"] }
|
|
||||||
tokio-util = { version = "0.7", features = ["compat"] }
|
|
||||||
async-std = { version = "1", features = ["attributes"] }
|
|
||||||
ssh2 = "0.9"
|
|
||||||
uuid = { version = "1", features = ["v4"] }
|
|
||||||
dirs = "5"
|
|
||||||
chrono = { version = "0.4", features = ["serde"] }
|
|
||||||
thiserror = "1"
|
|
||||||
parking_lot = "0.12"
|
|
||||||
once_cell = "1"
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-subscriber = "0.3"
|
|
||||||
rfd = "0.14"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
default = ["custom-protocol"]
|
|
||||||
custom-protocol = ["tauri/custom-protocol"]
|
|
||||||
|
|
||||||
[profile.release]
|
|
||||||
panic = "abort"
|
|
||||||
codegen-units = 1
|
|
||||||
lto = true
|
|
||||||
opt-level = "s"
|
|
||||||
strip = true
|
|
||||||
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
tauri_build::build()
|
|
||||||
}
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,31 +0,0 @@
|
|||||||
use crate::database::ConnectionConfig;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
fn get_config_path() -> PathBuf {
|
|
||||||
let config_dir = dirs::config_dir()
|
|
||||||
.unwrap_or_else(|| PathBuf::from("."))
|
|
||||||
.join("easysql");
|
|
||||||
|
|
||||||
fs::create_dir_all(&config_dir).ok();
|
|
||||||
config_dir.join("connections.json")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_connections(connections: &[ConnectionConfig]) -> Result<(), std::io::Error> {
|
|
||||||
let path = get_config_path();
|
|
||||||
let json = serde_json::to_string_pretty(connections)
|
|
||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
|
||||||
fs::write(path, json)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load_connections() -> Result<Vec<ConnectionConfig>, std::io::Error> {
|
|
||||||
let path = get_config_path();
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(vec![]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let content = fs::read_to_string(path)?;
|
|
||||||
serde_json::from_str(&content)
|
|
||||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,262 +0,0 @@
|
|||||||
use once_cell::sync::Lazy;
|
|
||||||
use parking_lot::RwLock;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum DbError {
|
|
||||||
#[error("连接失败: {0}")]
|
|
||||||
ConnectionError(String),
|
|
||||||
#[error("查询失败: {0}")]
|
|
||||||
QueryError(String),
|
|
||||||
#[error("未连接")]
|
|
||||||
NotConnected,
|
|
||||||
#[error("不支持的数据库类型: {0}")]
|
|
||||||
UnsupportedType(String),
|
|
||||||
#[error("SSH 隧道错误: {0}")]
|
|
||||||
SshError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct ConnectionConfig {
|
|
||||||
pub id: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub db_type: String,
|
|
||||||
pub name: String,
|
|
||||||
pub host: String,
|
|
||||||
pub port: u16,
|
|
||||||
pub username: String,
|
|
||||||
pub password: String,
|
|
||||||
pub database: Option<String>,
|
|
||||||
pub ssh_enabled: Option<bool>,
|
|
||||||
pub ssh_host: Option<String>,
|
|
||||||
pub ssh_port: Option<u16>,
|
|
||||||
pub ssh_user: Option<String>,
|
|
||||||
pub ssh_password: Option<String>,
|
|
||||||
pub ssh_key: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct TableInfo {
|
|
||||||
pub name: String,
|
|
||||||
pub rows: i64,
|
|
||||||
#[serde(rename = "isView")]
|
|
||||||
pub is_view: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct ColumnInfo {
|
|
||||||
pub name: String,
|
|
||||||
#[serde(rename = "type")]
|
|
||||||
pub data_type: String,
|
|
||||||
pub nullable: bool,
|
|
||||||
pub key: Option<String>,
|
|
||||||
pub comment: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct QueryResult {
|
|
||||||
pub columns: Vec<String>,
|
|
||||||
pub rows: Vec<Vec<serde_json::Value>>,
|
|
||||||
pub error: Option<String>,
|
|
||||||
#[serde(rename = "affectedRows")]
|
|
||||||
pub affected_rows: Option<i64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct TableDataResult {
|
|
||||||
pub columns: Vec<ColumnInfo>,
|
|
||||||
pub rows: Vec<Vec<serde_json::Value>>,
|
|
||||||
pub total: i64,
|
|
||||||
pub page: i32,
|
|
||||||
#[serde(rename = "pageSize")]
|
|
||||||
pub page_size: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 数据库连接枚举
|
|
||||||
pub enum DbConnection {
|
|
||||||
MySql(sqlx::MySqlPool),
|
|
||||||
Postgres(sqlx::PgPool),
|
|
||||||
Sqlite(sqlx::SqlitePool),
|
|
||||||
SqlServer(SqlServerConnection),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SqlServerConnection {
|
|
||||||
pub config: tiberius::Config,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 连接信息存储
|
|
||||||
pub struct ConnectionInfo {
|
|
||||||
pub connection: DbConnection,
|
|
||||||
pub config: ConnectionConfig,
|
|
||||||
pub ssh_tunnel: Option<crate::ssh::SshTunnel>,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局连接管理器
|
|
||||||
pub static CONNECTIONS: Lazy<RwLock<HashMap<String, Arc<ConnectionInfo>>>> =
|
|
||||||
Lazy::new(|| RwLock::new(HashMap::new()));
|
|
||||||
|
|
||||||
pub fn init() {
|
|
||||||
tracing::info!("数据库管理器初始化完成");
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DbConnection {
|
|
||||||
pub async fn test_mysql(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<(), DbError> {
|
|
||||||
let db = database.unwrap_or("mysql");
|
|
||||||
let url = format!("mysql://{}:{}@{}:{}/{}", user, password, host, port, db);
|
|
||||||
|
|
||||||
let pool = sqlx::mysql::MySqlPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.acquire_timeout(std::time::Duration::from_secs(10))
|
|
||||||
.connect(&url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
|
|
||||||
|
|
||||||
sqlx::query("SELECT 1")
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::QueryError(e.to_string()))?;
|
|
||||||
|
|
||||||
pool.close().await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn test_postgres(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<(), DbError> {
|
|
||||||
let db = database.unwrap_or("postgres");
|
|
||||||
let url = format!("postgres://{}:{}@{}:{}/{}", user, password, host, port, db);
|
|
||||||
|
|
||||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.acquire_timeout(std::time::Duration::from_secs(10))
|
|
||||||
.connect(&url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
|
|
||||||
|
|
||||||
sqlx::query("SELECT 1")
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::QueryError(e.to_string()))?;
|
|
||||||
|
|
||||||
pool.close().await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn test_sqlite(path: &str) -> Result<(), DbError> {
|
|
||||||
let url = format!("sqlite:{}?mode=rwc", path);
|
|
||||||
|
|
||||||
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
||||||
.max_connections(1)
|
|
||||||
.connect(&url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
|
|
||||||
|
|
||||||
sqlx::query("SELECT 1")
|
|
||||||
.execute(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::QueryError(e.to_string()))?;
|
|
||||||
|
|
||||||
pool.close().await;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn test_sqlserver(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<(), DbError> {
|
|
||||||
use tiberius::{Client, Config, AuthMethod};
|
|
||||||
use tokio::net::TcpStream;
|
|
||||||
use tokio_util::compat::TokioAsyncWriteCompatExt;
|
|
||||||
|
|
||||||
let mut config = Config::new();
|
|
||||||
config.host(host);
|
|
||||||
config.port(port);
|
|
||||||
config.authentication(AuthMethod::sql_server(user, password));
|
|
||||||
if let Some(db) = database {
|
|
||||||
config.database(db);
|
|
||||||
}
|
|
||||||
config.trust_cert();
|
|
||||||
|
|
||||||
let tcp = TcpStream::connect(config.get_addr())
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
|
|
||||||
|
|
||||||
tcp.set_nodelay(true).ok();
|
|
||||||
|
|
||||||
let mut client = Client::connect(config, tcp.compat_write())
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
|
|
||||||
|
|
||||||
client.simple_query("SELECT 1")
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::QueryError(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_mysql(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<Self, DbError> {
|
|
||||||
let db = database.unwrap_or("mysql");
|
|
||||||
let url = format!("mysql://{}:{}@{}:{}/{}", user, password, host, port, db);
|
|
||||||
|
|
||||||
let pool = sqlx::mysql::MySqlPoolOptions::new()
|
|
||||||
.max_connections(10)
|
|
||||||
.min_connections(1)
|
|
||||||
.acquire_timeout(std::time::Duration::from_secs(30))
|
|
||||||
.idle_timeout(std::time::Duration::from_secs(600))
|
|
||||||
.connect(&url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(DbConnection::MySql(pool))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_postgres(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<Self, DbError> {
|
|
||||||
let db = database.unwrap_or("postgres");
|
|
||||||
let url = format!("postgres://{}:{}@{}:{}/{}", user, password, host, port, db);
|
|
||||||
|
|
||||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
|
||||||
.max_connections(10)
|
|
||||||
.min_connections(1)
|
|
||||||
.acquire_timeout(std::time::Duration::from_secs(30))
|
|
||||||
.idle_timeout(std::time::Duration::from_secs(600))
|
|
||||||
.connect(&url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(DbConnection::Postgres(pool))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_sqlite(path: &str) -> Result<Self, DbError> {
|
|
||||||
let url = format!("sqlite:{}?mode=rwc", path);
|
|
||||||
|
|
||||||
let pool = sqlx::sqlite::SqlitePoolOptions::new()
|
|
||||||
.max_connections(5)
|
|
||||||
.connect(&url)
|
|
||||||
.await
|
|
||||||
.map_err(|e| DbError::ConnectionError(e.to_string()))?;
|
|
||||||
|
|
||||||
Ok(DbConnection::Sqlite(pool))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn connect_sqlserver(host: &str, port: u16, user: &str, password: &str, database: Option<&str>) -> Result<Self, DbError> {
|
|
||||||
let mut config = tiberius::Config::new();
|
|
||||||
config.host(host);
|
|
||||||
config.port(port);
|
|
||||||
config.authentication(tiberius::AuthMethod::sql_server(user, password));
|
|
||||||
if let Some(db) = database {
|
|
||||||
config.database(db);
|
|
||||||
}
|
|
||||||
config.trust_cert();
|
|
||||||
|
|
||||||
Ok(DbConnection::SqlServer(SqlServerConnection { config }))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析 localhost 为 127.0.0.1
|
|
||||||
pub fn resolve_host(host: &str) -> String {
|
|
||||||
if host == "localhost" {
|
|
||||||
"127.0.0.1".to_string()
|
|
||||||
} else {
|
|
||||||
host.to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,74 +0,0 @@
|
|||||||
// Prevents additional console window on Windows in release
|
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
|
||||||
|
|
||||||
mod database;
|
|
||||||
mod commands;
|
|
||||||
mod config;
|
|
||||||
mod ssh;
|
|
||||||
|
|
||||||
use tauri::Manager;
|
|
||||||
use tracing_subscriber;
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
// 初始化日志
|
|
||||||
tracing_subscriber::fmt::init();
|
|
||||||
|
|
||||||
tauri::Builder::default()
|
|
||||||
.plugin(tauri_plugin_shell::init())
|
|
||||||
.plugin(tauri_plugin_dialog::init())
|
|
||||||
.plugin(tauri_plugin_fs::init())
|
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
|
||||||
// 当尝试打开第二个实例时,聚焦现有窗口
|
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
|
||||||
let _ = window.set_focus();
|
|
||||||
let _ = window.unminimize();
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.setup(|app| {
|
|
||||||
// 初始化数据库连接管理器
|
|
||||||
database::init();
|
|
||||||
|
|
||||||
// 获取主窗口并设置
|
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
|
||||||
// Windows 上启用窗口阴影效果
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
{
|
|
||||||
use tauri::WebviewWindow;
|
|
||||||
let _ = window.set_decorations(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.invoke_handler(tauri::generate_handler![
|
|
||||||
// 窗口控制
|
|
||||||
commands::window_minimize,
|
|
||||||
commands::window_maximize,
|
|
||||||
commands::window_close,
|
|
||||||
// 数据库操作
|
|
||||||
commands::db_test,
|
|
||||||
commands::db_connect,
|
|
||||||
commands::db_disconnect,
|
|
||||||
commands::db_query,
|
|
||||||
commands::db_get_databases,
|
|
||||||
commands::db_get_tables,
|
|
||||||
commands::db_get_columns,
|
|
||||||
commands::db_get_table_data,
|
|
||||||
commands::db_update_row,
|
|
||||||
commands::db_delete_row,
|
|
||||||
commands::db_backup,
|
|
||||||
commands::db_export_table,
|
|
||||||
// 配置操作
|
|
||||||
commands::config_save,
|
|
||||||
commands::config_load,
|
|
||||||
commands::config_export,
|
|
||||||
commands::config_import,
|
|
||||||
// 文件操作
|
|
||||||
commands::file_open,
|
|
||||||
commands::file_save,
|
|
||||||
commands::file_select,
|
|
||||||
])
|
|
||||||
.run(tauri::generate_context!())
|
|
||||||
.expect("error while running tauri application");
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
use std::net::{TcpListener, TcpStream};
|
|
||||||
use std::io::{Read, Write};
|
|
||||||
use std::sync::Arc;
|
|
||||||
use std::thread;
|
|
||||||
use ssh2::Session;
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
|
||||||
pub enum SshError {
|
|
||||||
#[error("连接失败: {0}")]
|
|
||||||
ConnectionError(String),
|
|
||||||
#[error("认证失败: {0}")]
|
|
||||||
AuthError(String),
|
|
||||||
#[error("隧道创建失败: {0}")]
|
|
||||||
TunnelError(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct SshTunnel {
|
|
||||||
pub local_port: u16,
|
|
||||||
_handle: Option<thread::JoinHandle<()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SshTunnel {
|
|
||||||
pub async fn create(
|
|
||||||
ssh_host: &str,
|
|
||||||
ssh_port: u16,
|
|
||||||
ssh_user: &str,
|
|
||||||
ssh_password: Option<&str>,
|
|
||||||
ssh_key: Option<&str>,
|
|
||||||
remote_host: &str,
|
|
||||||
remote_port: u16,
|
|
||||||
) -> Result<Self, SshError> {
|
|
||||||
// 找一个可用的本地端口
|
|
||||||
let listener = TcpListener::bind("127.0.0.1:0")
|
|
||||||
.map_err(|e| SshError::TunnelError(e.to_string()))?;
|
|
||||||
let local_port = listener.local_addr()
|
|
||||||
.map_err(|e| SshError::TunnelError(e.to_string()))?
|
|
||||||
.port();
|
|
||||||
|
|
||||||
let ssh_host = ssh_host.to_string();
|
|
||||||
let ssh_user = ssh_user.to_string();
|
|
||||||
let ssh_password = ssh_password.map(|s| s.to_string());
|
|
||||||
let ssh_key = ssh_key.map(|s| s.to_string());
|
|
||||||
let remote_host = remote_host.to_string();
|
|
||||||
|
|
||||||
// 在后台线程中运行隧道
|
|
||||||
let handle = thread::spawn(move || {
|
|
||||||
run_tunnel(
|
|
||||||
listener,
|
|
||||||
&ssh_host,
|
|
||||||
ssh_port,
|
|
||||||
&ssh_user,
|
|
||||||
ssh_password.as_deref(),
|
|
||||||
ssh_key.as_deref(),
|
|
||||||
&remote_host,
|
|
||||||
remote_port,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 等待一小段时间确保隧道建立
|
|
||||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
|
||||||
|
|
||||||
Ok(SshTunnel {
|
|
||||||
local_port,
|
|
||||||
_handle: Some(handle),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_tunnel(
|
|
||||||
listener: TcpListener,
|
|
||||||
ssh_host: &str,
|
|
||||||
ssh_port: u16,
|
|
||||||
ssh_user: &str,
|
|
||||||
ssh_password: Option<&str>,
|
|
||||||
ssh_key: Option<&str>,
|
|
||||||
remote_host: &str,
|
|
||||||
remote_port: u16,
|
|
||||||
) {
|
|
||||||
// 连接 SSH 服务器
|
|
||||||
let tcp = match TcpStream::connect(format!("{}:{}", ssh_host, ssh_port)) {
|
|
||||||
Ok(t) => t,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("SSH 连接失败: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut sess = match Session::new() {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("创建 SSH 会话失败: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
sess.set_tcp_stream(tcp);
|
|
||||||
if let Err(e) = sess.handshake() {
|
|
||||||
tracing::error!("SSH 握手失败: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 认证
|
|
||||||
let auth_result = if let Some(key_path) = ssh_key {
|
|
||||||
sess.userauth_pubkey_file(ssh_user, None, std::path::Path::new(key_path), None)
|
|
||||||
} else if let Some(password) = ssh_password {
|
|
||||||
sess.userauth_password(ssh_user, password)
|
|
||||||
} else {
|
|
||||||
tracing::error!("SSH 需要密码或密钥");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = auth_result {
|
|
||||||
tracing::error!("SSH 认证失败: {}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let sess = Arc::new(sess);
|
|
||||||
|
|
||||||
// 监听本地连接并转发
|
|
||||||
for stream in listener.incoming() {
|
|
||||||
match stream {
|
|
||||||
Ok(mut local_stream) => {
|
|
||||||
let sess = sess.clone();
|
|
||||||
let remote_host = remote_host.to_string();
|
|
||||||
|
|
||||||
thread::spawn(move || {
|
|
||||||
match sess.channel_direct_tcpip(&remote_host, remote_port, None) {
|
|
||||||
Ok(mut channel) => {
|
|
||||||
let mut buf = [0u8; 8192];
|
|
||||||
loop {
|
|
||||||
// 从本地读取
|
|
||||||
match local_stream.read(&mut buf) {
|
|
||||||
Ok(0) => break,
|
|
||||||
Ok(n) => {
|
|
||||||
if channel.write_all(&buf[..n]).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从远程读取
|
|
||||||
match channel.read(&mut buf) {
|
|
||||||
Ok(0) => break,
|
|
||||||
Ok(n) => {
|
|
||||||
if local_stream.write_all(&buf[..n]).is_err() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(_) => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("创建 SSH 通道失败: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
tracing::error!("接受本地连接失败: {}", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc_fingerprint":2442455887007604170,"outputs":{"7971740275564407648":{"success":true,"status":"","code":0,"stdout":"___.exe\nlib___.rlib\n___.dll\n___.dll\n___.lib\n___.dll\nC:\\Users\\Ethan\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc\npacked\n___\ndebug_assertions\npanic=\"unwind\"\nproc_macro\ntarget_abi=\"\"\ntarget_arch=\"x86_64\"\ntarget_endian=\"little\"\ntarget_env=\"msvc\"\ntarget_family=\"windows\"\ntarget_feature=\"cmpxchg16b\"\ntarget_feature=\"fxsr\"\ntarget_feature=\"sse\"\ntarget_feature=\"sse2\"\ntarget_feature=\"sse3\"\ntarget_has_atomic=\"128\"\ntarget_has_atomic=\"16\"\ntarget_has_atomic=\"32\"\ntarget_has_atomic=\"64\"\ntarget_has_atomic=\"8\"\ntarget_has_atomic=\"ptr\"\ntarget_os=\"windows\"\ntarget_pointer_width=\"64\"\ntarget_vendor=\"pc\"\nwindows\n","stderr":""},"17747080675513052775":{"success":true,"status":"","code":0,"stdout":"rustc 1.92.0 (ded5c06cf 2025-12-08)\nbinary: rustc\ncommit-hash: ded5c06cf21d2b93bffd5d884aa6e96934ee4234\ncommit-date: 2025-12-08\nhost: x86_64-pc-windows-msvc\nrelease: 1.92.0\nLLVM version: 21.1.3\n","stderr":""}},"successes":{}}
|
|
||||||
@ -1,3 +0,0 @@
|
|||||||
Signature: 8a477f597d28d172789f06886806bc55
|
|
||||||
# This file is a cache directory tag created by cargo.
|
|
||||||
# For information about cache directory tags see https://bford.info/cachedir/
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
81513b4bc4935786
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"core\", \"default\", \"rustc-dep-of-std\", \"std\"]","target":6569825234462323107,"profile":2225463790103693989,"path":14185826767970760088,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\adler2-963931fa5ded092c\\dep-lib-adler2","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
74bf58d300cfc3fe
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"perf-literal\", \"std\"]","declared_features":"[\"default\", \"logging\", \"perf-literal\", \"std\"]","target":7534583537114156500,"profile":15657897354478470176,"path":5842747710153497419,"deps":[[198136567835728122,"memchr",false,14663827455635897954]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\aho-corasick-753d9ff25945fcf1\\dep-lib-aho_corasick","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
df86ee9da5a896b5
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"unsafe\"]","target":1942380541186272485,"profile":15657897354478470176,"path":15208557227947978230,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\alloc-no-stdlib-df988be783143443\\dep-lib-alloc_no_stdlib","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
c83304d10457a342
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"unsafe\"]","target":8756844401079878655,"profile":15657897354478470176,"path":13188036246537209495,"deps":[[9611597350722197978,"alloc_no_stdlib",false,13084831196644280031]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\alloc-stdlib-0b919bce3bf82dab\\dep-lib-alloc_stdlib","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
5a764d98e5da6686
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"alloc\"]","declared_features":"[\"alloc\", \"default\", \"fresh-rust\", \"nightly\", \"serde\", \"std\"]","target":5388200169723499962,"profile":12994027242049262075,"path":9537980989915801285,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\allocator-api2-8865e6a00b4e72ee\\dep-lib-allocator_api2","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
7dea2967765352fa
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[1852463361802237065,"build_script_build",false,11562533667458260168]],"local":[{"RerunIfChanged":{"output":"debug\\build\\anyhow-5f742c575df35aa8\\output","paths":["src/nightly.rs"]}},{"RerunIfEnvChanged":{"var":"RUSTC_BOOTSTRAP","val":null}}],"rustflags":[],"config":0,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
25b95a78b4580001
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":16100955855663461252,"profile":15657897354478470176,"path":4793198230926724775,"deps":[[1852463361802237065,"build_script_build",false,18037571225574304381]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\anyhow-aa601dd51ab6285d\\dep-lib-anyhow","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
c81c58101c5f76a0
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"backtrace\", \"default\", \"std\"]","target":17883862002600103897,"profile":2225463790103693989,"path":16467683319812530953,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\anyhow-d034abc77daf7f05\\dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
4383ca541ec2a2c1
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"portable-atomic\", \"std\"]","target":2348331682808714104,"profile":15657897354478470176,"path":7386643265001597429,"deps":[[1906322745568073236,"pin_project_lite",false,2763931684887686990],[7620660491849607393,"futures_core",false,13482104094475111900],[12100481297174703255,"concurrent_queue",false,9710515496302887928],[17148897597675491682,"event_listener_strategy",false,17468647974699608726]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\async-channel-012b9538802153a7\\dep-lib-async_channel","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
a9b21d6e4790dac2
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"","declared_features":"","target":0,"profile":0,"path":0,"deps":[[15550619062825872913,"build_script_build",false,11785065948707700023]],"local":[{"Precalculated":"2.6.0"}],"rustflags":[],"config":0,"compile_kind":0}
|
|
||||||
@ -1 +0,0 @@
|
|||||||
373d46570af78ca3
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"tracing\"]","target":5408242616063297496,"profile":4831801323318853768,"path":7761328004364916373,"deps":[[13927012481677012980,"autocfg",false,10807305518230055627]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\async-io-6ca54b3d18dcec1d\\dep-build-script-build-script-build","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
d2c49e4be3350915
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"tracing\"]","target":10084595033463382892,"profile":17582455124764123298,"path":14659531420356540962,"deps":[[5103565458935487,"futures_io",false,952584329958915671],[189982446159473706,"parking",false,15973587823657450042],[6568467691589961976,"windows_sys",false,7246976454518788371],[7667230146095136825,"cfg_if",false,2895588346767177823],[9090520973410485560,"futures_lite",false,2780583266334981273],[12100481297174703255,"concurrent_queue",false,9710515496302887928],[14271827750077741315,"polling",false,3541362140770724921],[14767213526276824509,"slab",false,2368147902018235069],[15550619062825872913,"build_script_build",false,14040693424745460393],[18377328279789821306,"rustix",false,14707779014435926099]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\async-io-ff38d4d37548e5d4\\dep-lib-async_io","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
24de5f045ab4a5e6
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"portable-atomic\", \"std\"]","target":9397226730057430065,"profile":15657897354478470176,"path":10776882996198157094,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\async-task-83aaeca8303bab8f\\dep-lib-async_task","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
91f45a8f710d237a
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":2515742790907851906,"profile":15657897354478470176,"path":14408917710461266522,"deps":[[5157631553186200874,"num_traits",false,2487166575225505192]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\atoi-3b1ff09aa596eaea\\dep-lib-atoi","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
ae00a7479dbd0d3e
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"default\", \"std\"]","declared_features":"[\"default\", \"std\"]","target":2515742790907851906,"profile":15657897354478470176,"path":14408917710461266522,"deps":[[5157631553186200874,"num_traits",false,6589197341473135743]],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\atoi-88fbb6b4610daefc\\dep-lib-atoi","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
599aaabdd810da02
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[]","declared_features":"[\"portable-atomic\"]","target":14411119108718288063,"profile":15657897354478470176,"path":9403445180054510354,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\atomic-waker-0cc2f644b7474aaa\\dep-lib-atomic_waker","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
cbaa38f91b43fb95
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[]","declared_features":"[]","target":6962977057026645649,"profile":2225463790103693989,"path":11879731547235993323,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\autocfg-0837c6aef9c33bfd\\dep-lib-autocfg","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
2f6bf29a2ce2f192
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"alloc\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":15657897354478470176,"path":13544571482266270501,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\base64-15167b4203d0c3e5\\dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
9aeab062cce15bc3
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":15657897354478470176,"path":17663575163679045343,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\base64-859384f4ec5c3793\\dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
1276579c8125b5ad
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"alloc\", \"default\", \"std\"]","declared_features":"[\"alloc\", \"default\", \"std\"]","target":13060062996227388079,"profile":2225463790103693989,"path":13544571482266270501,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\base64-fbf8e75ae7768384\\dep-lib-base64","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
4998c2777483af84
|
|
||||||
@ -1 +0,0 @@
|
|||||||
{"rustc":7895727629726570510,"features":"[\"alloc\"]","declared_features":"[\"alloc\", \"std\"]","target":15548948006327107948,"profile":15657897354478470176,"path":16698247819254552203,"deps":[],"local":[{"CheckDepInfo":{"dep_info":"debug\\.fingerprint\\base64ct-b7895d159a254796\\dep-lib-base64ct","checksum":false}}],"rustflags":[],"config":2069994364910194474,"compile_kind":0}
|
|
||||||
Binary file not shown.
@ -1 +0,0 @@
|
|||||||
This file has an mtime of when this was started.
|
|
||||||
@ -1 +0,0 @@
|
|||||||
65a4f550205e0708
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user