Compare commits
10 Commits
07dfba9d59
...
d158e85d53
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d158e85d53 | ||
|
|
c5687e1506 | ||
|
|
222792419f | ||
|
|
04789e08d5 | ||
|
|
929f6ddf20 | ||
|
|
6e3b16d425 | ||
|
|
ce0291a8f9 | ||
|
|
faee954f6d | ||
|
|
f09c898f16 | ||
|
|
a7c087afbb |
@@ -44,9 +44,16 @@ RUN if [ "$COUNTRY" = "CN" ] ; then \
|
|||||||
|
|
||||||
ARG FRONTEND_GIT_REPO=https://github.com/lejianwen/rustdesk-api-web.git
|
ARG FRONTEND_GIT_REPO=https://github.com/lejianwen/rustdesk-api-web.git
|
||||||
ARG FRONTEND_GIT_BRANCH=master
|
ARG FRONTEND_GIT_BRANCH=master
|
||||||
# Clone the frontend repository
|
# Copy local frontend if exists, otherwise clone from git
|
||||||
|
COPY frontend-temp /tmp/frontend-temp
|
||||||
RUN git clone -b $FRONTEND_GIT_BRANCH $FRONTEND_GIT_REPO .
|
RUN if [ -d "/tmp/frontend-temp" ] && [ -n "$(ls -A /tmp/frontend-temp 2>/dev/null)" ]; then \
|
||||||
|
echo "Using local frontend code"; \
|
||||||
|
cp -r /tmp/frontend-temp/* . 2>/dev/null || true; \
|
||||||
|
cp -r /tmp/frontend-temp/.[!.]* . 2>/dev/null || true; \
|
||||||
|
else \
|
||||||
|
echo "Cloning frontend from git"; \
|
||||||
|
git clone -b $FRONTEND_GIT_BRANCH $FRONTEND_GIT_REPO .; \
|
||||||
|
fi
|
||||||
|
|
||||||
# Install required tools without caching index to minimize image size
|
# Install required tools without caching index to minimize image size
|
||||||
RUN if [ "$COUNTRY" = "CN" ] ; then \
|
RUN if [ "$COUNTRY" = "CN" ] ; then \
|
||||||
@@ -69,7 +76,7 @@ RUN if [ "$COUNTRY" = "CN" ] ; then \
|
|||||||
echo "It is in China, updating the repositories"; \
|
echo "It is in China, updating the repositories"; \
|
||||||
sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories; \
|
sed -i 's#https\?://dl-cdn.alpinelinux.org/alpine#https://mirrors.tuna.tsinghua.edu.cn/alpine#g' /etc/apk/repositories; \
|
||||||
fi && \
|
fi && \
|
||||||
apk update && apk add --no-cache tzdata file
|
apk update && apk add --no-cache tzdata file git curl build-base openssl-dev
|
||||||
|
|
||||||
# Copy the built application and resources from the builder stage
|
# Copy the built application and resources from the builder stage
|
||||||
COPY --from=builder-backend /app/release /app/
|
COPY --from=builder-backend /app/release /app/
|
||||||
@@ -79,10 +86,16 @@ COPY --from=builder-backend /app/docs /app/docs/
|
|||||||
# Copy frontend build from builder2 stage
|
# Copy frontend build from builder2 stage
|
||||||
COPY --from=builder-admin-frontend /frontend/dist/ /app/resources/admin/
|
COPY --from=builder-admin-frontend /frontend/dist/ /app/resources/admin/
|
||||||
|
|
||||||
|
# Install Rust toolchain for client compilation
|
||||||
|
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable && \
|
||||||
|
. "$HOME/.cargo/env" && \
|
||||||
|
rustup target add x86_64-pc-windows-msvc x86_64-unknown-linux-gnu aarch64-apple-darwin x86_64-apple-darwin || true
|
||||||
|
|
||||||
# Ensure the binary is correctly built and linked
|
# Ensure the binary is correctly built and linked
|
||||||
RUN file /app/apimain && \
|
RUN file /app/apimain && \
|
||||||
mkdir -p /app/data && \
|
mkdir -p /app/data && \
|
||||||
mkdir -p /app/runtime
|
mkdir -p /app/runtime && \
|
||||||
|
mkdir -p /app/resources/builds
|
||||||
|
|
||||||
# Set up a volume for persistent data
|
# Set up a volume for persistent data
|
||||||
VOLUME /app/data
|
VOLUME /app/data
|
||||||
@@ -90,5 +103,10 @@ VOLUME /app/data
|
|||||||
# Expose the necessary port
|
# Expose the necessary port
|
||||||
EXPOSE 21114
|
EXPOSE 21114
|
||||||
|
|
||||||
|
# Add Rust to PATH and source cargo env
|
||||||
|
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||||
|
ENV CARGO_HOME="/root/.cargo"
|
||||||
|
ENV RUSTUP_HOME="/root/.rustup"
|
||||||
|
|
||||||
# Define the command to run the application
|
# Define the command to run the application
|
||||||
CMD ["./apimain"]
|
CMD ["./apimain"]
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -2,7 +2,8 @@
|
|||||||
|
|
||||||
[English Doc](README_EN.md)
|
[English Doc](README_EN.md)
|
||||||
|
|
||||||
本项目使用 Go 实现了 RustDesk 的 API,并包含了 Web Admin 和 Web 客户端。RustDesk 是一个远程桌面软件,提供了自托管的解决方案。
|
本项目使用 Go 实现了 RustDesk 的 API,并包含了 Web Admin 和 Web 客户端。
|
||||||
|
|
||||||
|
|
||||||
<div align=center>
|
<div align=center>
|
||||||
<img src="https://img.shields.io/badge/golang-1.22-blue"/>
|
<img src="https://img.shields.io/badge/golang-1.22-blue"/>
|
||||||
@@ -13,6 +14,14 @@
|
|||||||
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
|
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## 搭配[lejianwen/rustdesk-server]使用更佳。
|
||||||
|
> [lejianwen/rustdesk-server]fork自RustDesk Server官方仓库
|
||||||
|
> 1. 解决了使用API链接超时问题
|
||||||
|
> 2. 可以强制登录后才能发起链接
|
||||||
|
> 3. 支持客户端websocket
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 特性
|
# 特性
|
||||||
|
|
||||||
- PC端API
|
- PC端API
|
||||||
@@ -45,7 +54,6 @@
|
|||||||
- 自动获取ID服务器和KEY
|
- 自动获取ID服务器和KEY
|
||||||
- 自动获取地址簿
|
- 自动获取地址簿
|
||||||
- 游客通过临时分享链接直接远程到设备
|
- 游客通过临时分享链接直接远程到设备
|
||||||
- v2 Preview
|
|
||||||
- CLI
|
- CLI
|
||||||
- 重置管理员密码
|
- 重置管理员密码
|
||||||
|
|
||||||
@@ -118,9 +126,6 @@
|
|||||||
2. 如果没登录后台,点击右上角登录即可,api server已经自动配置好了
|
2. 如果没登录后台,点击右上角登录即可,api server已经自动配置好了
|
||||||
3. 登录后,会自动同步ID服务器和KEY
|
3. 登录后,会自动同步ID服务器和KEY
|
||||||
4. 登录后,会将地址簿自动保存到web client中,方便使用
|
4. 登录后,会将地址簿自动保存到web client中,方便使用
|
||||||
5. 现已支持`v2 Preview`,访问路径是`/webclient2`
|
|
||||||

|
|
||||||
6. `v2 preview` 部署,参考[WIKI](https://github.com/lejianwen/rustdesk-api/wiki)
|
|
||||||
|
|
||||||
|
|
||||||
### 自动化文档: 使用 Swag 生成 API 文档,方便开发者理解和使用 API。
|
### 自动化文档: 使用 Swag 生成 API 文档,方便开发者理解和使用 API。
|
||||||
@@ -182,6 +187,7 @@
|
|||||||
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
|
| RUSTDESK_API_MYSQL_PASSWORD | mysql密码 | 111111 |
|
||||||
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
|
| RUSTDESK_API_MYSQL_ADDR | mysql地址 | 192.168.1.66:3306 |
|
||||||
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
|
| RUSTDESK_API_MYSQL_DBNAME | mysql数据库名 | rustdesk |
|
||||||
|
| RUSTDESK_API_MYSQL_TLS | 是否启用TLS, 可选值: `true`, `false`, `skip-verify`, `custom` | `false` |
|
||||||
| -----RUSTDESK配置----- | ---------- | ---------- |
|
| -----RUSTDESK配置----- | ---------- | ---------- |
|
||||||
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 |
|
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk的id服务器地址 | 192.168.1.66:21116 |
|
||||||
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 |
|
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk的relay服务器地址 | 192.168.1.66:21117 |
|
||||||
@@ -325,3 +331,5 @@
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## 感谢你的支持!如果这个项目对你有帮助,请点个⭐️鼓励一下,谢谢!
|
## 感谢你的支持!如果这个项目对你有帮助,请点个⭐️鼓励一下,谢谢!
|
||||||
|
|
||||||
|
[lejianwen/rustdesk-server]: https://github.com/lejianwen/rustdesk-server
|
||||||
14
README_EN.md
14
README_EN.md
@@ -12,6 +12,13 @@ desktop software that provides self-hosted solutions.
|
|||||||
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
|
<img src="https://github.com/lejianwen/rustdesk-api/actions/workflows/build.yml/badge.svg"/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
## Better used with [lejianwen/rustdesk-server].
|
||||||
|
> [lejianwen/rustdesk-server] is a fork of the official RustDesk Server repository.
|
||||||
|
> 1. Solves the API connection timeout issue.
|
||||||
|
> 2. Can enforce login before initiating a connection.
|
||||||
|
> 3. Supports client websocket.
|
||||||
|
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
|
|
||||||
- PC API
|
- PC API
|
||||||
@@ -118,9 +125,6 @@ displaying data.Frontend code is available at [rustdesk-api-web](https://github.
|
|||||||
pre-configured.
|
pre-configured.
|
||||||
3. After logging in, the ID server and key will be automatically synced.
|
3. After logging in, the ID server and key will be automatically synced.
|
||||||
4. The address book will also be automatically saved to the web client for convenient use.
|
4. The address book will also be automatically saved to the web client for convenient use.
|
||||||
5. Now supports `v2 Preview`, accessible at `/webclient2`
|
|
||||||

|
|
||||||
6. `v2 preview` deployment, [WIKI](https://github.com/lejianwen/rustdesk-api/wiki)
|
|
||||||
|
|
||||||
### Automated Documentation : API documentation is generated using Swag, making it easier for developers to understand and use the API.
|
### Automated Documentation : API documentation is generated using Swag, making it easier for developers to understand and use the API.
|
||||||
|
|
||||||
@@ -181,6 +185,7 @@ The table below does not list all configurations. Please refer to the configurat
|
|||||||
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
|
| RUSTDESK_API_MYSQL_PASSWORD | MySQL password | 111111 |
|
||||||
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
|
| RUSTDESK_API_MYSQL_ADDR | MySQL address | 192.168.1.66:3306 |
|
||||||
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
|
| RUSTDESK_API_MYSQL_DBNAME | MySQL database name | rustdesk |
|
||||||
|
| RUSTDESK_API_MYSQL_TLS | Whether to enable TLS, optional values: `true`, `false`, `skip-verify`, `custom` | `false` |
|
||||||
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
|
| ----- RUSTDESK Configuration ----- | --------------------------------------- | ----------------------------- |
|
||||||
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 |
|
| RUSTDESK_API_RUSTDESK_ID_SERVER | Rustdesk ID server address | 192.168.1.66:21116 |
|
||||||
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 |
|
| RUSTDESK_API_RUSTDESK_RELAY_SERVER | Rustdesk relay server address | 192.168.1.66:21117 |
|
||||||
@@ -325,3 +330,6 @@ Thanks to everyone who contributed!
|
|||||||
</a>
|
</a>
|
||||||
|
|
||||||
## Thanks for your support! If you find this project useful, please give it a ⭐️. Thank you!
|
## Thanks for your support! If you find this project useful, please give it a ⭐️. Thank you!
|
||||||
|
|
||||||
|
|
||||||
|
[lejianwen/rustdesk-server]: https://github.com/lejianwen/rustdesk-server
|
||||||
@@ -23,7 +23,7 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
const DatabaseVersion = 264
|
const DatabaseVersion = 265
|
||||||
|
|
||||||
// @title 管理系统API
|
// @title 管理系统API
|
||||||
// @version 1.0
|
// @version 1.0
|
||||||
@@ -145,11 +145,12 @@ func InitGlobal() {
|
|||||||
//gorm
|
//gorm
|
||||||
if global.Config.Gorm.Type == config.TypeMysql {
|
if global.Config.Gorm.Type == config.TypeMysql {
|
||||||
|
|
||||||
dsn := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
|
dsn := fmt.Sprintf("%s:%s@(%s)/%s?charset=utf8mb4&parseTime=True&loc=Local&tls=%s",
|
||||||
global.Config.Mysql.Username,
|
global.Config.Mysql.Username,
|
||||||
global.Config.Mysql.Password,
|
global.Config.Mysql.Password,
|
||||||
global.Config.Mysql.Addr,
|
global.Config.Mysql.Addr,
|
||||||
global.Config.Mysql.Dbname,
|
global.Config.Mysql.Dbname,
|
||||||
|
global.Config.Mysql.Tls,
|
||||||
)
|
)
|
||||||
|
|
||||||
global.DB = orm.NewMysql(&orm.MysqlConfig{
|
global.DB = orm.NewMysql(&orm.MysqlConfig{
|
||||||
@@ -282,6 +283,22 @@ func DatabaseAutoUpdate() {
|
|||||||
if v.Version < 246 {
|
if v.Version < 246 {
|
||||||
db.Exec("update oauths set issuer = 'https://accounts.google.com' where op = 'google' and issuer is null")
|
db.Exec("update oauths set issuer = 'https://accounts.google.com' where op = 'google' and issuer is null")
|
||||||
}
|
}
|
||||||
|
// 265迁移 - добавление полей для кастомных конфигов
|
||||||
|
// Проверяем наличие колонок независимо от версии, на случай если AutoMigrate не добавил их
|
||||||
|
if db.Migrator().HasTable(&model.ClientConfig{}) {
|
||||||
|
if !db.Migrator().HasColumn(&model.ClientConfig{}, "custom_config_file") {
|
||||||
|
global.Logger.Info("Adding column custom_config_file to client_configs table")
|
||||||
|
if err := db.Exec("ALTER TABLE client_configs ADD COLUMN custom_config_file TEXT").Error; err != nil {
|
||||||
|
global.Logger.Errorf("Failed to add custom_config_file column: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !db.Migrator().HasColumn(&model.ClientConfig{}, "use_custom_config") {
|
||||||
|
global.Logger.Info("Adding column use_custom_config to client_configs table")
|
||||||
|
if err := db.Exec("ALTER TABLE client_configs ADD COLUMN use_custom_config INTEGER DEFAULT 0 NOT NULL").Error; err != nil {
|
||||||
|
global.Logger.Errorf("Failed to add use_custom_config column: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -305,6 +322,8 @@ func Migrate(version uint) {
|
|||||||
&model.AddressBookCollectionRule{},
|
&model.AddressBookCollectionRule{},
|
||||||
&model.ServerCmd{},
|
&model.ServerCmd{},
|
||||||
&model.DeviceGroup{},
|
&model.DeviceGroup{},
|
||||||
|
&model.ClientConfig{},
|
||||||
|
&model.ClientBuild{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.Logger.Error("migrate err :=>", err)
|
global.Logger.Error("migrate err :=>", err)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ mysql:
|
|||||||
password: ""
|
password: ""
|
||||||
addr: ""
|
addr: ""
|
||||||
dbname: ""
|
dbname: ""
|
||||||
|
tls: "false" # true / false / skip-verify / custom
|
||||||
|
|
||||||
postgresql:
|
postgresql:
|
||||||
host: "127.0.0.1"
|
host: "127.0.0.1"
|
||||||
@@ -80,4 +81,4 @@ ldap:
|
|||||||
last-name: "sn"
|
last-name: "sn"
|
||||||
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
|
sync: false # If true, the user will be synchronized to the database when the user logs in. If false, the user will be synchronized to the database when the user be created.
|
||||||
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.
|
admin-group: "cn=admin,dc=example,dc=com" # The group name of the admin group, if the user is in this group, the user will be an admin.
|
||||||
|
allow-group: "cn=users,dc=example,dc=com" # The group name of the users group, if the user is in this group, the user will be an login.
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type Mysql struct {
|
|||||||
Username string `mapstructure:"username"`
|
Username string `mapstructure:"username"`
|
||||||
Password string `mapstructure:"password"`
|
Password string `mapstructure:"password"`
|
||||||
Dbname string `mapstructure:"dbname"`
|
Dbname string `mapstructure:"dbname"`
|
||||||
|
Tls string `mapstructure:"tls"` // true / false / skip-verify / custom
|
||||||
}
|
}
|
||||||
|
|
||||||
type Postgresql struct {
|
type Postgresql struct {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type LdapUser struct {
|
|||||||
LastName string `mapstructure:"last-name"`
|
LastName string `mapstructure:"last-name"`
|
||||||
Sync bool `mapstructure:"sync"` // Will sync the user's information to the internal database
|
Sync bool `mapstructure:"sync"` // Will sync the user's information to the internal database
|
||||||
AdminGroup string `mapstructure:"admin-group"` // Which group is the admin group
|
AdminGroup string `mapstructure:"admin-group"` // Which group is the admin group
|
||||||
|
AllowGroup string `mapstructure:"allow-group"` // Which group is allowed to login
|
||||||
}
|
}
|
||||||
|
|
||||||
// type LdapGroup struct {
|
// type LdapGroup struct {
|
||||||
|
|||||||
@@ -20,5 +20,8 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ./data/rustdesk/api:/app/data #将数据库挂载出来方便备份
|
- ./data/rustdesk/api:/app/data #将数据库挂载出来方便备份
|
||||||
- ./conf:/app/conf # config
|
- ./conf:/app/conf # config
|
||||||
|
- ./resources/clients:/app/resources/clients # 生成的 клиенты
|
||||||
|
- ./resources/builds:/app/resources/builds # 编译任务
|
||||||
|
- ./resources/uploads:/app/resources/uploads # 上传的文件
|
||||||
# - ./resources:/app/resources # 静态资源
|
# - ./resources:/app/resources # 静态资源
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ services:
|
|||||||
- 21114:21114
|
- 21114:21114
|
||||||
volumes:
|
volumes:
|
||||||
- ./data/rustdesk/api:/app/data # database
|
- ./data/rustdesk/api:/app/data # database
|
||||||
|
- ./resources/clients:/app/resources/clients # 生成的客户端
|
||||||
|
- ./resources/builds:/app/resources/builds # 编译任务
|
||||||
|
- ./resources/uploads:/app/resources/uploads # 上传的文件
|
||||||
# - ./conf:/app/conf # config
|
# - ./conf:/app/conf # config
|
||||||
# - ./resources:/app/resources # 静态资源
|
# - ./resources:/app/resources # 静态资源
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
231
docs/CLIENT_BUILD_INTEGRATION.md
Normal file
231
docs/CLIENT_BUILD_INTEGRATION.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# Интеграция генератора клиентов RustDesk
|
||||||
|
|
||||||
|
## Обзор
|
||||||
|
|
||||||
|
Интегрирован функционал генерации и компиляции кастомных клиентов RustDesk на основе проекта [VenimK/creator](https://github.com/VenimK/creator.git).
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
### Генерация клиента
|
||||||
|
```
|
||||||
|
POST /api/admin/client_build/generate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Тело запроса:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"platform": "windows",
|
||||||
|
"version": "1.4.3",
|
||||||
|
"app_name": "MyRemoteDesktop",
|
||||||
|
"file_name": "myclient",
|
||||||
|
"server_ip": "192.168.1.66:21116",
|
||||||
|
"api_server": "http://192.168.1.66:21114",
|
||||||
|
"key": "your-key-here",
|
||||||
|
"description": "Описание клиента",
|
||||||
|
"config": "{}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Платформы:**
|
||||||
|
- `windows` - Windows 64-bit
|
||||||
|
- `linux` - Linux
|
||||||
|
- `macos` - macOS (Apple Silicon)
|
||||||
|
- `macos-x86` - macOS (Intel)
|
||||||
|
- `android` - Android
|
||||||
|
|
||||||
|
**Версии:**
|
||||||
|
- `master` - последняя версия из master ветки
|
||||||
|
- `1.4.3`, `1.4.2`, `1.4.1`, `1.4.0` - стабильные версии
|
||||||
|
- `1.3.9`, `1.3.8`, `1.3.7`, `1.3.6`, `1.3.5`, `1.3.4`, `1.3.3` - старые версии
|
||||||
|
|
||||||
|
### Список задач компиляции
|
||||||
|
```
|
||||||
|
GET /api/admin/client_build/list?page=1&page_size=10&platform=windows&status=success
|
||||||
|
```
|
||||||
|
|
||||||
|
### Статус компиляции
|
||||||
|
```
|
||||||
|
GET /api/admin/client_build/status/{uuid}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ответ:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"build_uuid": "uuid-here",
|
||||||
|
"status": "building",
|
||||||
|
"progress": 50,
|
||||||
|
"log": "Build log...",
|
||||||
|
"error_msg": "",
|
||||||
|
"file_url": "/api/admin/client_build/download/{uuid}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Статусы:**
|
||||||
|
- `pending` - ожидает начала компиляции
|
||||||
|
- `building` - компилируется
|
||||||
|
- `success` - успешно скомпилирован
|
||||||
|
- `failed` - ошибка компиляции
|
||||||
|
|
||||||
|
### Скачивание клиента
|
||||||
|
```
|
||||||
|
GET /api/admin/client_build/download/{uuid}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Удаление задачи
|
||||||
|
```
|
||||||
|
POST /api/admin/client_build/delete/{id}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Настройка локальной компиляции
|
||||||
|
|
||||||
|
### Требования
|
||||||
|
|
||||||
|
Для локальной компиляции RustDesk клиентов необходимо:
|
||||||
|
|
||||||
|
1. **Rust toolchain** (для Windows, Linux, macOS):
|
||||||
|
```bash
|
||||||
|
# Установка Rust
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Исходный код RustDesk**:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/rustdesk/rustdesk.git
|
||||||
|
cd rustdesk
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Дополнительные инструменты**:
|
||||||
|
- **Windows**: Visual Studio Build Tools, WiX Toolset (для установщика)
|
||||||
|
- **Linux**: build-essential, libssl-dev, libgtk-3-dev
|
||||||
|
- **macOS**: Xcode Command Line Tools
|
||||||
|
- **Android**: Android SDK, NDK
|
||||||
|
|
||||||
|
### Интеграция реальной компиляции
|
||||||
|
|
||||||
|
Для реализации реальной компиляции необходимо модифицировать методы в `service/clientBuild.go`:
|
||||||
|
|
||||||
|
#### Пример для Windows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func (cbs *ClientBuildService) buildWindows(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
|
||||||
|
cbs.appendLog(build, "Building Windows client...\n")
|
||||||
|
|
||||||
|
// Путь к исходникам RustDesk
|
||||||
|
rustdeskSourceDir := "/path/to/rustdesk"
|
||||||
|
|
||||||
|
// Создаем конфигурационный файл
|
||||||
|
customConfig := cbs.generateCustomConfig(config)
|
||||||
|
customPath := filepath.Join(buildDir, "custom.txt")
|
||||||
|
if err := os.WriteFile(customPath, []byte(customConfig), 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write custom.txt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем custom.txt в исходники
|
||||||
|
targetCustomPath := filepath.Join(rustdeskSourceDir, "custom.txt")
|
||||||
|
if err := os.WriteFile(targetCustomPath, []byte(customConfig), 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Компилируем
|
||||||
|
cmd := exec.Command("cargo", "build", "--release", "--target", "x86_64-pc-windows-msvc")
|
||||||
|
cmd.Dir = rustdeskSourceDir
|
||||||
|
cmd.Env = append(os.Environ(), "RUSTFLAGS=-C target-feature=+crt-static")
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
cbs.appendLog(build, string(output))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("build failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем скомпилированный файл
|
||||||
|
exePath := filepath.Join(rustdeskSourceDir, "target", "x86_64-pc-windows-msvc", "release", "rustdesk.exe")
|
||||||
|
outputPath := filepath.Join(buildDir, config.FileName+".exe")
|
||||||
|
|
||||||
|
if err := copyFile(exePath, outputPath); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputPath, nil
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Конфигурационный файл custom.txt
|
||||||
|
|
||||||
|
Формат файла `custom.txt` для RustDesk:
|
||||||
|
|
||||||
|
```
|
||||||
|
rendezvous-server = 192.168.1.66:21116
|
||||||
|
api-server = http://192.168.1.66:21114
|
||||||
|
key = your-key-here
|
||||||
|
app-name = MyRemoteDesktop
|
||||||
|
password = default-password
|
||||||
|
approve-mode = password-click
|
||||||
|
enable-lan-discovery = Y
|
||||||
|
```
|
||||||
|
|
||||||
|
### Дополнительные настройки
|
||||||
|
|
||||||
|
В поле `config` можно передать JSON с дополнительными настройками:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"config": "{\"password\":\"mypass\",\"approve-mode\":\"password\",\"enable-keyboard\":\"Y\",\"enable-clipboard\":\"Y\"}"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Структура файлов
|
||||||
|
|
||||||
|
```
|
||||||
|
resources/
|
||||||
|
builds/
|
||||||
|
{build_uuid}/
|
||||||
|
custom.txt # Конфигурация
|
||||||
|
{filename}.exe # Скомпилированный клиент (Windows)
|
||||||
|
{filename}.dmg # Скомпилированный клиент (macOS)
|
||||||
|
{filename}.deb # Скомпилированный клиент (Linux)
|
||||||
|
{filename}.apk # Скомпилированный клиент (Android)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Безопасность
|
||||||
|
|
||||||
|
- Все задачи компиляции привязаны к пользователю
|
||||||
|
- Пользователь может видеть и скачивать только свои клиенты
|
||||||
|
- Файлы хранятся в защищенной директории
|
||||||
|
- Логи компиляции доступны только владельцу задачи
|
||||||
|
|
||||||
|
## Примечания
|
||||||
|
|
||||||
|
1. **Время компиляции**: В зависимости от платформы компиляция может занимать 15-30 минут
|
||||||
|
2. **Ресурсы**: Компиляция требует значительных ресурсов (CPU, RAM, диск)
|
||||||
|
3. **Параллельные сборки**: Рекомендуется ограничить количество одновременных сборок
|
||||||
|
4. **Очистка**: Старые файлы сборок можно удалять через API или вручную
|
||||||
|
|
||||||
|
## Расширение функционала
|
||||||
|
|
||||||
|
Для добавления поддержки кастомных иконок и логотипов:
|
||||||
|
|
||||||
|
1. Добавьте поля `icon_path` и `logo_path` в `BuildConfig`
|
||||||
|
2. Реализуйте обработку изображений в методе `buildClient`
|
||||||
|
3. Интегрируйте с инструментами замены ресурсов RustDesk
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Ошибка "Rust toolchain not found"**:
|
||||||
|
- Установите Rust: https://rustup.rs/
|
||||||
|
- Убедитесь, что `cargo` доступен в PATH
|
||||||
|
|
||||||
|
**Ошибка компиляции**:
|
||||||
|
- Проверьте логи в поле `build_log`
|
||||||
|
- Убедитесь, что исходники RustDesk актуальны
|
||||||
|
- Проверьте наличие всех зависимостей
|
||||||
|
|
||||||
|
**Файл не найден после компиляции**:
|
||||||
|
- Проверьте путь к исходникам RustDesk
|
||||||
|
- Убедитесь, что компиляция завершилась успешно
|
||||||
|
- Проверьте права доступа к директориям
|
||||||
|
|
||||||
1
frontend-temp
Submodule
1
frontend-temp
Submodule
Submodule frontend-temp added at 3998c2a921
2
go.mod
2
go.mod
@@ -25,6 +25,7 @@ require (
|
|||||||
github.com/swaggo/files v1.0.1
|
github.com/swaggo/files v1.0.1
|
||||||
github.com/swaggo/gin-swagger v1.6.0
|
github.com/swaggo/gin-swagger v1.6.0
|
||||||
github.com/swaggo/swag v1.16.3
|
github.com/swaggo/swag v1.16.3
|
||||||
|
golang.org/x/crypto v0.33.0
|
||||||
golang.org/x/oauth2 v0.23.0
|
golang.org/x/oauth2 v0.23.0
|
||||||
golang.org/x/text v0.22.0
|
golang.org/x/text v0.22.0
|
||||||
gorm.io/driver/mysql v1.5.7
|
gorm.io/driver/mysql v1.5.7
|
||||||
@@ -84,7 +85,6 @@ require (
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.2.9 // indirect
|
github.com/ugorji/go/codec v1.2.9 // indirect
|
||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
|
||||||
golang.org/x/crypto v0.33.0 // indirect
|
|
||||||
golang.org/x/image v0.13.0 // indirect
|
golang.org/x/image v0.13.0 // indirect
|
||||||
golang.org/x/net v0.34.0 // indirect
|
golang.org/x/net v0.34.0 // indirect
|
||||||
golang.org/x/sync v0.11.0 // indirect
|
golang.org/x/sync v0.11.0 // indirect
|
||||||
|
|||||||
316
http/controller/admin/clientBuild.go
Normal file
316
http/controller/admin/clientBuild.go
Normal file
@@ -0,0 +1,316 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/http/response"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/service"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientBuild struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate запускает генерацию/компиляцию клиента
|
||||||
|
// @Tags 客户端编译
|
||||||
|
// @Summary 生成客户端
|
||||||
|
// @Description 启动客户端编译任务
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body admin.ClientBuildGenerateForm true "客户端配置"
|
||||||
|
// @Success 200 {object} response.Response{data=model.ClientBuild}
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_build/generate [post]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientBuild) Generate(c *gin.Context) {
|
||||||
|
f := &admin.ClientBuildGenerateForm{}
|
||||||
|
if err := c.ShouldBindJSON(f); err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errList := global.Validator.ValidStruct(c, f)
|
||||||
|
if len(errList) > 0 {
|
||||||
|
response.Fail(c, 101, errList[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Парсим дополнительные настройки из config
|
||||||
|
var customConfig map[string]string
|
||||||
|
if f.Config != "" {
|
||||||
|
if err := json.Unmarshal([]byte(f.Config), &customConfig); err != nil {
|
||||||
|
customConfig = make(map[string]string)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
customConfig = make(map[string]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Используем настройки сервера из конфигурации, если не указаны (только если не используется кастомный конфиг)
|
||||||
|
serverIP := f.ServerIP
|
||||||
|
if serverIP == "" && !f.UseCustomConfig {
|
||||||
|
serverIP = global.Config.Rustdesk.IdServer
|
||||||
|
}
|
||||||
|
apiServer := f.ApiServer
|
||||||
|
if apiServer == "" && !f.UseCustomConfig {
|
||||||
|
apiServer = global.Config.Rustdesk.ApiServer
|
||||||
|
}
|
||||||
|
key := f.Key
|
||||||
|
if key == "" && !f.UseCustomConfig {
|
||||||
|
key = global.Config.Rustdesk.Key
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем кастомный конфигурационный файл
|
||||||
|
// Принимаем любой конфигурационный файл без проверки сертификатов/подписей
|
||||||
|
customConfigFile := f.CustomConfigFile
|
||||||
|
if f.UseCustomConfig && customConfigFile != "" {
|
||||||
|
// Пытаемся декодировать base64, если не получается - используем как есть
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(customConfigFile)
|
||||||
|
if err == nil {
|
||||||
|
customConfigFile = string(decoded)
|
||||||
|
}
|
||||||
|
// Если декодирование не удалось, используем исходную строку как текст конфига
|
||||||
|
}
|
||||||
|
|
||||||
|
buildConfig := &service.BuildConfig{
|
||||||
|
Platform: f.Platform,
|
||||||
|
Version: f.Version,
|
||||||
|
AppName: f.AppName,
|
||||||
|
FileName: f.FileName,
|
||||||
|
ServerIP: serverIP,
|
||||||
|
ApiServer: apiServer,
|
||||||
|
Key: key,
|
||||||
|
Custom: customConfig,
|
||||||
|
CustomConfigFile: customConfigFile,
|
||||||
|
UseCustomConfig: f.UseCustomConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
build, err := service.AllService.ClientBuildService.CreateBuild(u.Id, buildConfig)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, build)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List получает список задач компиляции
|
||||||
|
// @Tags 客户端编译
|
||||||
|
// @Summary 编译任务列表
|
||||||
|
// @Description 获取当前用户的编译任务列表
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param page_size query int false "页大小"
|
||||||
|
// @Param platform query string false "平台"
|
||||||
|
// @Param status query string false "状态"
|
||||||
|
// @Success 200 {object} response.Response{data=model.ClientBuildList}
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_build/list [get]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientBuild) List(c *gin.Context) {
|
||||||
|
query := &admin.ClientBuildQuery{}
|
||||||
|
if err := c.ShouldBindQuery(query); err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := service.AllService.ClientBuildService.List(uint(query.Page), uint(query.PageSize), func(tx *gorm.DB) *gorm.DB {
|
||||||
|
tx = tx.Where("user_id = ?", u.Id)
|
||||||
|
if query.Platform != "" {
|
||||||
|
tx = tx.Where("platform = ?", query.Platform)
|
||||||
|
}
|
||||||
|
if query.Status != "" {
|
||||||
|
tx = tx.Where("status = ?", query.Status)
|
||||||
|
}
|
||||||
|
return tx
|
||||||
|
})
|
||||||
|
|
||||||
|
response.Success(c, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status получает статус задачи компиляции
|
||||||
|
// @Tags 客户端编译
|
||||||
|
// @Summary 编译状态
|
||||||
|
// @Description 获取编译任务的状态
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param uuid path string true "Build UUID"
|
||||||
|
// @Success 200 {object} response.Response{data=admin.ClientBuildStatusResponse}
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_build/status/{uuid} [get]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientBuild) Status(c *gin.Context) {
|
||||||
|
uuid := c.Param("uuid")
|
||||||
|
if uuid == "" {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
build := service.AllService.ClientBuildService.GetByUuid(uuid)
|
||||||
|
if build.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что задача принадлежит пользователю
|
||||||
|
if build.UserId != u.Id {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "AccessDenied"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем прогресс
|
||||||
|
progress := 0
|
||||||
|
if build.Status == "pending" {
|
||||||
|
progress = 10
|
||||||
|
} else if build.Status == "building" {
|
||||||
|
progress = 50
|
||||||
|
} else if build.Status == "success" {
|
||||||
|
progress = 100
|
||||||
|
} else if build.Status == "failed" {
|
||||||
|
progress = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
statusResp := &admin.ClientBuildStatusResponse{
|
||||||
|
BuildUuid: build.BuildUuid,
|
||||||
|
Status: build.Status,
|
||||||
|
Progress: progress,
|
||||||
|
Log: build.BuildLog,
|
||||||
|
ErrorMsg: build.ErrorMsg,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если сборка успешна, добавляем URL для скачивания
|
||||||
|
if build.Status == "success" && build.FilePath != "" {
|
||||||
|
statusResp.FileUrl = "/api/admin/client_build/download/" + build.BuildUuid
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, statusResp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download скачивает скомпилированный клиент
|
||||||
|
// @Tags 客户端编译
|
||||||
|
// @Summary 下载客户端
|
||||||
|
// @Description 下载编译完成的客户端文件
|
||||||
|
// @Accept json
|
||||||
|
// @Produce application/octet-stream
|
||||||
|
// @Param uuid path string true "Build UUID"
|
||||||
|
// @Success 200 {file} file
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_build/download/{uuid} [get]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientBuild) Download(c *gin.Context) {
|
||||||
|
uuid := c.Param("uuid")
|
||||||
|
if uuid == "" {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
build := service.AllService.ClientBuildService.GetByUuid(uuid)
|
||||||
|
if build.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что задача принадлежит пользователю
|
||||||
|
if build.UserId != u.Id {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "AccessDenied"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что сборка успешна
|
||||||
|
if build.Status != "success" {
|
||||||
|
response.Fail(c, 101, "Build is not completed yet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем существование файла
|
||||||
|
if _, err := os.Stat(build.FilePath); os.IsNotExist(err) {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "FileNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем файл
|
||||||
|
// Определяем Content-Type в зависимости от расширения файла
|
||||||
|
contentType := "application/octet-stream"
|
||||||
|
fileExt := strings.ToLower(filepath.Ext(build.FilePath))
|
||||||
|
switch fileExt {
|
||||||
|
case ".exe":
|
||||||
|
contentType = "application/x-msdownload"
|
||||||
|
case ".deb":
|
||||||
|
contentType = "application/vnd.debian.binary-package"
|
||||||
|
case ".dmg":
|
||||||
|
contentType = "application/x-apple-diskimage"
|
||||||
|
case ".apk":
|
||||||
|
contentType = "application/vnd.android.package-archive"
|
||||||
|
case ".zip":
|
||||||
|
contentType = "application/zip"
|
||||||
|
}
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+build.FileName)
|
||||||
|
c.Header("Content-Type", contentType)
|
||||||
|
c.File(build.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete удаляет задачу компиляции
|
||||||
|
// @Tags 客户端编译
|
||||||
|
// @Summary 删除编译任务
|
||||||
|
// @Description 删除编译任务及其文件
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param id path int true "Build ID"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_build/delete/{id} [post]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientBuild) Delete(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
idUint, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service.AllService.ClientBuildService.Delete(uint(idUint), u.Id)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
74
http/controller/admin/clientBuildUpload.go
Normal file
74
http/controller/admin/clientBuildUpload.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/http/response"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UploadConfigFile загружает конфигурационный файл для использования в генерации клиента
|
||||||
|
// @Tags 客户端编译
|
||||||
|
// @Summary 上传配置文件
|
||||||
|
// @Description 上传自定义配置文件(无限制,任何文件都可以上传)
|
||||||
|
// @Accept multipart/form-data
|
||||||
|
// @Produce json
|
||||||
|
// @Param file formData file true "配置文件"
|
||||||
|
// @Success 200 {object} response.Response{data=object{file_content=string}}
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_build/upload_config [post]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientBuild) UploadConfigFile(c *gin.Context) {
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем загруженный файл
|
||||||
|
file, err := c.FormFile("file")
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+": file is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Открываем файл
|
||||||
|
src, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+": "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer src.Close()
|
||||||
|
|
||||||
|
// Читаем содержимое файла
|
||||||
|
fileContent, err := io.ReadAll(src)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+": "+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Кодируем в base64 для передачи в JSON
|
||||||
|
fileContentBase64 := base64.StdEncoding.EncodeToString(fileContent)
|
||||||
|
|
||||||
|
// Опционально: сохраняем файл для справки
|
||||||
|
uploadDir := filepath.Join(global.Config.Gin.ResourcesPath, "uploads", "configs")
|
||||||
|
if err := os.MkdirAll(uploadDir, 0755); err == nil {
|
||||||
|
dst := filepath.Join(uploadDir, file.Filename)
|
||||||
|
if err := c.SaveUploadedFile(file, dst); err == nil {
|
||||||
|
// Файл сохранен
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, gin.H{
|
||||||
|
"file_name": file.Filename,
|
||||||
|
"file_size": file.Size,
|
||||||
|
"file_content": fileContentBase64,
|
||||||
|
"message": "File uploaded successfully. Use file_content in custom_config_file field.",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
188
http/controller/admin/clientConfig.go
Normal file
188
http/controller/admin/clientConfig.go
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/http/request/admin"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/http/response"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/service"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientConfig struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate генерирует клиент с настройками
|
||||||
|
// @Tags 客户端生成
|
||||||
|
// @Summary 生成客户端
|
||||||
|
// @Description 生成带有服务器配置的客户端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body admin.ClientConfigGenerateForm true "客户端信息"
|
||||||
|
// @Success 200 {object} response.Response{data=model.ClientConfig}
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_config/generate [post]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientConfig) Generate(c *gin.Context) {
|
||||||
|
f := &admin.ClientConfigGenerateForm{}
|
||||||
|
if err := c.ShouldBindJSON(f); err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errList := global.Validator.ValidStruct(c, f)
|
||||||
|
if len(errList) > 0 {
|
||||||
|
response.Fail(c, 101, errList[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обрабатываем кастомный конфигурационный файл
|
||||||
|
// Принимаем любой конфигурационный файл без проверки сертификатов/подписей
|
||||||
|
customConfigFile := ""
|
||||||
|
if f.UseCustomConfig && f.CustomConfigFile != "" {
|
||||||
|
customConfigFile = f.CustomConfigFile
|
||||||
|
// Пытаемся декодировать base64, если не получается - используем как есть
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(customConfigFile)
|
||||||
|
if err == nil {
|
||||||
|
customConfigFile = string(decoded)
|
||||||
|
}
|
||||||
|
// Если декодирование не удалось, используем исходную строку как текст конфига
|
||||||
|
}
|
||||||
|
|
||||||
|
clientConfig, err := service.AllService.ClientConfigService.GenerateClientConfig(u.Id, f.Password, f.Description, customConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, clientConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List получает список сгенерированных клиентов
|
||||||
|
// @Tags 客户端生成
|
||||||
|
// @Summary 客户端列表
|
||||||
|
// @Description 获取当前用户的客户端列表
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param page query int false "页码"
|
||||||
|
// @Param page_size query int false "页大小"
|
||||||
|
// @Success 200 {object} response.Response{data=model.ClientConfigList}
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_config/list [get]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientConfig) List(c *gin.Context) {
|
||||||
|
query := &admin.ClientConfigQuery{}
|
||||||
|
if err := c.ShouldBindQuery(query); err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res := service.AllService.ClientConfigService.List(uint(query.Page), uint(query.PageSize), func(tx *gorm.DB) *gorm.DB {
|
||||||
|
return tx.Where("user_id = ?", u.Id)
|
||||||
|
})
|
||||||
|
|
||||||
|
response.Success(c, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download скачивает сгенерированный клиент
|
||||||
|
// @Tags 客户端生成
|
||||||
|
// @Summary 下载客户端
|
||||||
|
// @Description 下载生成的客户端文件
|
||||||
|
// @Accept json
|
||||||
|
// @Produce application/zip
|
||||||
|
// @Param id path int true "客户端ID"
|
||||||
|
// @Success 200 {file} file
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_config/download/{id} [get]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientConfig) Download(c *gin.Context) {
|
||||||
|
id := c.Param("id")
|
||||||
|
idUint, err := strconv.ParseUint(id, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
clientConfig := service.AllService.ClientConfigService.GetById(uint(idUint))
|
||||||
|
if clientConfig.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ItemNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что клиент принадлежит пользователю
|
||||||
|
if clientConfig.UserId != u.Id {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "AccessDenied"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем существование файла
|
||||||
|
if _, err := os.Stat(clientConfig.FilePath); os.IsNotExist(err) {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "FileNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Отправляем файл
|
||||||
|
c.Header("Content-Disposition", "attachment; filename="+clientConfig.FileName)
|
||||||
|
c.Header("Content-Type", "application/zip")
|
||||||
|
c.File(clientConfig.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete удаляет сгенерированный клиент
|
||||||
|
// @Tags 客户端生成
|
||||||
|
// @Summary 删除客户端
|
||||||
|
// @Description 删除生成的客户端
|
||||||
|
// @Accept json
|
||||||
|
// @Produce json
|
||||||
|
// @Param body body admin.ClientConfigDeleteForm true "客户端信息"
|
||||||
|
// @Success 200 {object} response.Response
|
||||||
|
// @Failure 500 {object} response.Response
|
||||||
|
// @Router /admin/client_config/delete [post]
|
||||||
|
// @Security token
|
||||||
|
func (ct *ClientConfig) Delete(c *gin.Context) {
|
||||||
|
f := &admin.ClientConfigDeleteForm{}
|
||||||
|
if err := c.ShouldBindJSON(f); err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "ParamsError")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
errList := global.Validator.ValidStruct(c, f)
|
||||||
|
if len(errList) > 0 {
|
||||||
|
response.Fail(c, 101, errList[0])
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
u := service.AllService.UserService.CurUser(c)
|
||||||
|
if u.Id == 0 {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "UserNotFound"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := service.AllService.ClientConfigService.Delete(f.Id, u.Id)
|
||||||
|
if err != nil {
|
||||||
|
response.Fail(c, 101, response.TranslateMsg(c, "OperationFailed")+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response.Success(c, nil)
|
||||||
|
}
|
||||||
|
|
||||||
@@ -114,6 +114,9 @@ func (ct *Peer) List(c *gin.Context) {
|
|||||||
if query.Ip != "" {
|
if query.Ip != "" {
|
||||||
tx.Where("last_online_ip like ?", "%"+query.Ip+"%")
|
tx.Where("last_online_ip like ?", "%"+query.Ip+"%")
|
||||||
}
|
}
|
||||||
|
if query.Alias != "" {
|
||||||
|
tx.Where("alias like ?", "%"+query.Alias+"%")
|
||||||
|
}
|
||||||
})
|
})
|
||||||
response.Success(c, res)
|
response.Success(c, res)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func (i *Index) Heartbeat(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, gin.H{})
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
peer := service.AllService.PeerService.FindByUuid(info.Uuid)
|
peer := service.AllService.PeerService.FindById(info.Id)
|
||||||
if peer == nil || peer.RowId == 0 {
|
if peer == nil || peer.RowId == 0 {
|
||||||
c.JSON(http.StatusOK, gin.H{})
|
c.JSON(http.StatusOK, gin.H{})
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -80,7 +80,8 @@ func (o *Oauth) OidcAuthQueryPre(c *gin.Context) (*model.User, *model.UserToken)
|
|||||||
|
|
||||||
// 如果 UserId 为 0,说明还在授权中
|
// 如果 UserId 为 0,说明还在授权中
|
||||||
if v.UserId == 0 {
|
if v.UserId == 0 {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "Authorization in progress, please login and bind"})
|
//fix: 1.4.2 webclient oidc
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "Authorization in progress, please login and bind", "error": "No authed oidc is found"})
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ func (p *Peer) SysInfo(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
fpe := f.ToPeer()
|
fpe := f.ToPeer()
|
||||||
pe := service.AllService.PeerService.FindByUuid(f.Uuid)
|
pe := service.AllService.PeerService.FindById(f.Id)
|
||||||
if pe.RowId == 0 {
|
if pe.RowId == 0 {
|
||||||
pe = f.ToPeer()
|
pe = f.ToPeer()
|
||||||
pe.UserId = service.AllService.UserService.FindLatestUserIdFromLoginLogByUuid(pe.Uuid)
|
pe.UserId = service.AllService.UserService.FindLatestUserIdFromLoginLogByUuid(pe.Uuid, pe.Id)
|
||||||
err = service.AllService.PeerService.Create(pe)
|
err = service.AllService.PeerService.Create(pe)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.Error(c, response.TranslateMsg(c, "OperationFailed")+err.Error())
|
response.Error(c, response.TranslateMsg(c, "OperationFailed")+err.Error())
|
||||||
@@ -42,7 +42,7 @@ func (p *Peer) SysInfo(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if pe.UserId == 0 {
|
if pe.UserId == 0 {
|
||||||
pe.UserId = service.AllService.UserService.FindLatestUserIdFromLoginLogByUuid(pe.Uuid)
|
pe.UserId = service.AllService.UserService.FindLatestUserIdFromLoginLogByUuid(pe.Uuid, pe.Id)
|
||||||
}
|
}
|
||||||
fpe.RowId = pe.RowId
|
fpe.RowId = pe.RowId
|
||||||
fpe.UserId = pe.UserId
|
fpe.UserId = pe.UserId
|
||||||
|
|||||||
35
http/request/admin/clientBuild.go
Normal file
35
http/request/admin/clientBuild.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import "github.com/lejianwen/rustdesk-api/v2/model"
|
||||||
|
|
||||||
|
type ClientBuildGenerateForm struct {
|
||||||
|
Platform string `json:"platform" validate:"required,oneof=windows linux macos android macos-x86"` // платформа
|
||||||
|
Version string `json:"version" validate:"required"` // версия RustDesk
|
||||||
|
AppName string `json:"app_name" validate:"required,gte=1,lte=100"` // имя приложения
|
||||||
|
FileName string `json:"file_name" validate:"required,gte=1,lte=100"` // имя файла
|
||||||
|
ServerIP string `json:"server_ip"` // IP сервера
|
||||||
|
ApiServer string `json:"api_server"` // API сервер
|
||||||
|
Key string `json:"key"` // ключ
|
||||||
|
Description string `json:"description"` // описание
|
||||||
|
// Дополнительные настройки
|
||||||
|
Config string `json:"config"` // JSON с дополнительными настройками
|
||||||
|
// Кастомный конфигурационный файл (base64 или текст)
|
||||||
|
CustomConfigFile string `json:"custom_config_file"` // содержимое конфигурационного файла (base64 или текст)
|
||||||
|
UseCustomConfig bool `json:"use_custom_config"` // использовать кастомный конфиг вместо автогенерации
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientBuildQuery struct {
|
||||||
|
model.Pagination
|
||||||
|
Platform string `form:"platform"`
|
||||||
|
Status string `form:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientBuildStatusResponse struct {
|
||||||
|
BuildUuid string `json:"build_uuid"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Progress int `json:"progress"` // 0-100
|
||||||
|
Log string `json:"log"`
|
||||||
|
ErrorMsg string `json:"error_msg"`
|
||||||
|
FileUrl string `json:"file_url,omitempty"` // URL для скачивания, если готово
|
||||||
|
}
|
||||||
|
|
||||||
20
http/request/admin/clientConfig.go
Normal file
20
http/request/admin/clientConfig.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package admin
|
||||||
|
|
||||||
|
import "github.com/lejianwen/rustdesk-api/v2/model"
|
||||||
|
|
||||||
|
type ClientConfigGenerateForm struct {
|
||||||
|
Password string `json:"password" validate:"required,gte=4,lte=32"` // пароль для клиента
|
||||||
|
Description string `json:"description"` // описание/комментарий
|
||||||
|
CustomConfigFile string `json:"custom_config_file"` // кастомный конфигурационный файл (опционально)
|
||||||
|
UseCustomConfig bool `json:"use_custom_config"` // использовать кастомный конфиг
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientConfigQuery struct {
|
||||||
|
model.Pagination
|
||||||
|
UserId uint `form:"user_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientConfigDeleteForm struct {
|
||||||
|
Id uint `json:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
@@ -13,6 +13,7 @@ type PeerForm struct {
|
|||||||
Uuid string `json:"uuid"`
|
Uuid string `json:"uuid"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
GroupId uint `json:"group_id"`
|
GroupId uint `json:"group_id"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PeerBatchDeleteForm struct {
|
type PeerBatchDeleteForm struct {
|
||||||
@@ -32,6 +33,7 @@ func (f *PeerForm) ToPeer() *model.Peer {
|
|||||||
Uuid: f.Uuid,
|
Uuid: f.Uuid,
|
||||||
Version: f.Version,
|
Version: f.Version,
|
||||||
GroupId: f.GroupId,
|
GroupId: f.GroupId,
|
||||||
|
Alias: f.Alias,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +45,7 @@ type PeerQuery struct {
|
|||||||
Uuids string `json:"uuids" form:"uuids"`
|
Uuids string `json:"uuids" form:"uuids"`
|
||||||
Ip string `json:"ip" form:"ip"`
|
Ip string `json:"ip" form:"ip"`
|
||||||
Username string `json:"username" form:"username"`
|
Username string `json:"username" form:"username"`
|
||||||
|
Alias string `json:"alias" form:"alias"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SimpleDataQuery struct {
|
type SimpleDataQuery struct {
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ func Init(g *gin.Engine) {
|
|||||||
|
|
||||||
RustdeskCmdBind(adg)
|
RustdeskCmdBind(adg)
|
||||||
DeviceGroupBind(adg)
|
DeviceGroupBind(adg)
|
||||||
|
ClientConfigBind(adg)
|
||||||
|
ClientBuildBind(adg)
|
||||||
//访问静态文件
|
//访问静态文件
|
||||||
//g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload"))
|
//g.StaticFS("/upload", http.Dir(global.Config.Gin.ResourcesPath+"/upload"))
|
||||||
}
|
}
|
||||||
@@ -322,3 +324,27 @@ func ShareRecordBind(rg *gin.RouterGroup) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ClientConfigBind(rg *gin.RouterGroup) {
|
||||||
|
aR := rg.Group("/client_config")
|
||||||
|
{
|
||||||
|
cont := &admin.ClientConfig{}
|
||||||
|
aR.POST("/generate", cont.Generate)
|
||||||
|
aR.GET("/list", cont.List)
|
||||||
|
aR.GET("/download/:id", cont.Download)
|
||||||
|
aR.POST("/delete", cont.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientBuildBind(rg *gin.RouterGroup) {
|
||||||
|
aR := rg.Group("/client_build")
|
||||||
|
{
|
||||||
|
cont := &admin.ClientBuild{}
|
||||||
|
aR.POST("/generate", cont.Generate)
|
||||||
|
aR.POST("/upload_config", cont.UploadConfigFile)
|
||||||
|
aR.GET("/list", cont.List)
|
||||||
|
aR.GET("/status/:uuid", cont.Status)
|
||||||
|
aR.GET("/download/:uuid", cont.Download)
|
||||||
|
aR.POST("/delete/:id", cont.Delete)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
26
model/clientBuild.go
Normal file
26
model/clientBuild.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// ClientBuild модель для хранения задач компиляции клиентов
|
||||||
|
type ClientBuild struct {
|
||||||
|
IdModel
|
||||||
|
UserId uint `json:"user_id" gorm:"default:0;not null;index"`
|
||||||
|
BuildUuid string `json:"uuid" gorm:"default:'';not null;uniqueIndex"` // уникальный ID сборки
|
||||||
|
Platform string `json:"platform" gorm:"default:'';not null;"` // windows, linux, macos, android
|
||||||
|
Version string `json:"version" gorm:"default:'';not null;"` // версия RustDesk
|
||||||
|
AppName string `json:"app_name" gorm:"default:'';not null;"` // имя приложения
|
||||||
|
FileName string `json:"file_name" gorm:"default:'';not null;"` // имя файла клиента
|
||||||
|
Status string `json:"status" gorm:"default:'pending';not null;index"` // pending, building, success, failed
|
||||||
|
BuildConfig string `json:"-" gorm:"type:text;"` // JSON конфигурация сборки
|
||||||
|
BuildLog string `json:"build_log" gorm:"type:text;"` // лог сборки
|
||||||
|
FilePath string `json:"-" gorm:"default:'';not null;"` // путь к скомпилированному файлу
|
||||||
|
FileSize int64 `json:"file_size" gorm:"default:0;not null;"` // размер файла
|
||||||
|
ErrorMsg string `json:"error_msg" gorm:"type:text;"` // сообщение об ошибке
|
||||||
|
TimeModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientBuildList список задач компиляции
|
||||||
|
type ClientBuildList struct {
|
||||||
|
ClientBuilds []*ClientBuild `json:"list,omitempty"`
|
||||||
|
Pagination
|
||||||
|
}
|
||||||
|
|
||||||
24
model/clientConfig.go
Normal file
24
model/clientConfig.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// ClientConfig модель для хранения сгенерированных клиентов
|
||||||
|
type ClientConfig struct {
|
||||||
|
IdModel
|
||||||
|
UserId uint `json:"user_id" gorm:"default:0;not null;index"`
|
||||||
|
Password string `json:"-" gorm:"default:'';not null;"` // пароль в зашифрованном виде
|
||||||
|
FileName string `json:"file_name" gorm:"default:'';not null;"` // имя файла клиента
|
||||||
|
FileHash string `json:"file_hash" gorm:"default:'';not null;index"` // хеш файла для проверки
|
||||||
|
FileSize int64 `json:"file_size" gorm:"default:0;not null;"` // размер файла
|
||||||
|
FilePath string `json:"-" gorm:"default:'';not null;"` // путь к файлу на сервере
|
||||||
|
Description string `json:"description" gorm:"default:'';not null;"` // описание/комментарий
|
||||||
|
Status StatusCode `json:"status" gorm:"default:1;not null;"` // статус (1: активен, 2: удален)
|
||||||
|
CustomConfigFile string `json:"-" gorm:"type:text;"` // Store custom config content
|
||||||
|
UseCustomConfig bool `json:"-" gorm:"default:0;not null;"` // использовать кастомный конфиг
|
||||||
|
TimeModel
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClientConfigList список сгенерированных клиентов
|
||||||
|
type ClientConfigList struct {
|
||||||
|
ClientConfigs []*ClientConfig `json:"list,omitempty"`
|
||||||
|
Pagination
|
||||||
|
}
|
||||||
|
|
||||||
@@ -15,6 +15,7 @@ type Peer struct {
|
|||||||
LastOnlineTime int64 `json:"last_online_time" gorm:"default:0;not null;"`
|
LastOnlineTime int64 `json:"last_online_time" gorm:"default:0;not null;"`
|
||||||
LastOnlineIp string `json:"last_online_ip" gorm:"default:'';not null;"`
|
LastOnlineIp string `json:"last_online_ip" gorm:"default:'';not null;"`
|
||||||
GroupId uint `json:"group_id" gorm:"default:0;not null;index"`
|
GroupId uint `json:"group_id" gorm:"default:0;not null;index"`
|
||||||
|
Alias string `json:"alias" gorm:"default:'';not null;index"`
|
||||||
TimeModel
|
TimeModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
627
service/clientBuild.go
Normal file
627
service/clientBuild.go
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/model"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientBuildService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildConfig конфигурация для сборки клиента
|
||||||
|
type BuildConfig struct {
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
AppName string `json:"app_name"`
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
ServerIP string `json:"server_ip"`
|
||||||
|
ApiServer string `json:"api_server"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Custom map[string]string `json:"custom"` // дополнительные настройки
|
||||||
|
IconPath string `json:"icon_path,omitempty"`
|
||||||
|
LogoPath string `json:"logo_path,omitempty"`
|
||||||
|
CustomConfigFile string `json:"custom_config_file,omitempty"` // кастомный конфигурационный файл
|
||||||
|
UseCustomConfig bool `json:"use_custom_config"` // использовать кастомный конфиг
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateBuild создает новую задачу компиляции
|
||||||
|
func (cbs *ClientBuildService) CreateBuild(userId uint, config *BuildConfig) (*model.ClientBuild, error) {
|
||||||
|
buildUuid := uuid.New().String()
|
||||||
|
|
||||||
|
// Сохраняем конфигурацию в JSON
|
||||||
|
configJson, err := json.Marshal(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to marshal config: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
build := &model.ClientBuild{
|
||||||
|
UserId: userId,
|
||||||
|
BuildUuid: buildUuid,
|
||||||
|
Platform: config.Platform,
|
||||||
|
Version: config.Version,
|
||||||
|
AppName: config.AppName,
|
||||||
|
FileName: config.FileName,
|
||||||
|
Status: "pending",
|
||||||
|
BuildConfig: string(configJson),
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := DB.Create(build).Error; err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create build: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Запускаем компиляцию в фоне
|
||||||
|
go cbs.buildClient(build)
|
||||||
|
|
||||||
|
return build, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildClient выполняет компиляцию клиента
|
||||||
|
func (cbs *ClientBuildService) buildClient(build *model.ClientBuild) {
|
||||||
|
// Обновляем статус на "building"
|
||||||
|
DB.Model(build).Update("status", "building")
|
||||||
|
DB.Model(build).Update("build_log", "Starting build process...\n")
|
||||||
|
|
||||||
|
// Парсим конфигурацию
|
||||||
|
var config BuildConfig
|
||||||
|
if err := json.Unmarshal([]byte(build.BuildConfig), &config); err != nil {
|
||||||
|
cbs.updateBuildStatus(build, "failed", fmt.Sprintf("Failed to parse config: %v", err), "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию для сборки
|
||||||
|
buildDir := filepath.Join(global.Config.Gin.ResourcesPath, "builds", build.BuildUuid)
|
||||||
|
if err := os.MkdirAll(buildDir, 0755); err != nil {
|
||||||
|
cbs.updateBuildStatus(build, "failed", fmt.Sprintf("Failed to create build directory: %v", err), "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Выполняем компиляцию в зависимости от платформы
|
||||||
|
var err error
|
||||||
|
var outputPath string
|
||||||
|
|
||||||
|
switch config.Platform {
|
||||||
|
case "windows":
|
||||||
|
outputPath, err = cbs.buildWindows(build, config, buildDir)
|
||||||
|
case "linux":
|
||||||
|
outputPath, err = cbs.buildLinux(build, config, buildDir)
|
||||||
|
case "macos", "macos-x86":
|
||||||
|
outputPath, err = cbs.buildMacOS(build, config, buildDir)
|
||||||
|
case "android":
|
||||||
|
outputPath, err = cbs.buildAndroid(build, config, buildDir)
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("unsupported platform: %s", config.Platform)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
cbs.updateBuildStatus(build, "failed", err.Error(), "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем размер файла
|
||||||
|
fileInfo, err := os.Stat(outputPath)
|
||||||
|
if err != nil {
|
||||||
|
cbs.updateBuildStatus(build, "failed", fmt.Sprintf("Failed to get file info: %v", err), "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем имя файла, если оно не соответствует расширению
|
||||||
|
fileName := build.FileName
|
||||||
|
// Получаем расширение из пути
|
||||||
|
if len(outputPath) > 4 {
|
||||||
|
ext := filepath.Ext(outputPath)
|
||||||
|
if ext != "" && !strings.HasSuffix(strings.ToLower(fileName), strings.ToLower(ext)) {
|
||||||
|
// Обновляем расширение
|
||||||
|
if strings.Contains(fileName, ".") {
|
||||||
|
fileName = fileName[:strings.LastIndex(fileName, ".")] + ext
|
||||||
|
} else {
|
||||||
|
fileName = fileName + ext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем статус на успех
|
||||||
|
DB.Model(build).Updates(map[string]interface{}{
|
||||||
|
"status": "success",
|
||||||
|
"file_path": outputPath,
|
||||||
|
"file_name": fileName,
|
||||||
|
"file_size": fileInfo.Size(),
|
||||||
|
"build_log": build.BuildLog + "\nBuild completed successfully!",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// cloneRustDeskSource клонирует исходный код RustDesk
|
||||||
|
func (cbs *ClientBuildService) cloneRustDeskSource(build *model.ClientBuild, version string, sourceDir string) error {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Cloning RustDesk source code (version: %s)...\n", version))
|
||||||
|
|
||||||
|
// Проверяем, существует ли уже клонированный репозиторий
|
||||||
|
if _, err := os.Stat(filepath.Join(sourceDir, ".git")); err == nil {
|
||||||
|
cbs.appendLog(build, "Repository already exists, updating...\n")
|
||||||
|
// Обновляем существующий репозиторий
|
||||||
|
cmd := exec.Command("git", "fetch", "origin")
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Warning: failed to fetch updates: %s\n", string(output)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем submodules
|
||||||
|
cbs.appendLog(build, "Updating submodules...\n")
|
||||||
|
cmd = exec.Command("git", "submodule", "update", "--init", "--recursive")
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Warning: failed to update submodules: %s\n", string(output)))
|
||||||
|
} else {
|
||||||
|
cbs.appendLog(build, "Submodules updated successfully\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Клонируем репозиторий
|
||||||
|
cbs.appendLog(build, "Cloning repository...\n")
|
||||||
|
normalizedVersion := strings.TrimPrefix(version, "v")
|
||||||
|
|
||||||
|
// Клонируем репозиторий (без --depth 1, чтобы получить все файлы и submodules)
|
||||||
|
cmd := exec.Command("git", "clone", "--recursive", "https://github.com/rustdesk/rustdesk.git", sourceDir)
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to clone repository: %v\nOutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
cbs.appendLog(build, "Repository cloned successfully\n")
|
||||||
|
|
||||||
|
// Переключаемся на нужную версию, если указана
|
||||||
|
if normalizedVersion != "master" && normalizedVersion != "" {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Checking out version: %s\n", normalizedVersion))
|
||||||
|
cmd := exec.Command("git", "checkout", normalizedVersion)
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
// Пробуем с префиксом v
|
||||||
|
cmd = exec.Command("git", "checkout", "v"+normalizedVersion)
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
if output2, err2 := cmd.CombinedOutput(); err2 != nil {
|
||||||
|
return fmt.Errorf("failed to checkout version %s: %v\nOutput: %s\nTried v%s: %s", normalizedVersion, err, string(output), normalizedVersion, string(output2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Checked out version: %s\n", normalizedVersion))
|
||||||
|
|
||||||
|
// Обновляем submodules для конкретной версии
|
||||||
|
cbs.appendLog(build, "Updating submodules...\n")
|
||||||
|
cmd = exec.Command("git", "submodule", "update", "--init", "--recursive")
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update submodules: %v\nOutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
cbs.appendLog(build, "Submodules updated successfully\n")
|
||||||
|
} else {
|
||||||
|
// Для master обновляем submodules
|
||||||
|
cbs.appendLog(build, "Updating submodules...\n")
|
||||||
|
cmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
if output, err := cmd.CombinedOutput(); err != nil {
|
||||||
|
return fmt.Errorf("failed to update submodules: %v\nOutput: %s", err, string(output))
|
||||||
|
}
|
||||||
|
cbs.appendLog(build, "Submodules updated successfully\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// copyFile копирует файл из источника в назначение
|
||||||
|
func (cbs *ClientBuildService) copyFile(src, dst string) error {
|
||||||
|
sourceFile, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to open source file: %v", err)
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
destFile, err := os.Create(dst)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create destination file: %v", err)
|
||||||
|
}
|
||||||
|
defer destFile.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(destFile, sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to copy file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildWindows компилирует клиент для Windows
|
||||||
|
func (cbs *ClientBuildService) buildWindows(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
|
||||||
|
cbs.appendLog(build, "Building Windows client from source...\n")
|
||||||
|
|
||||||
|
// Проверяем наличие Rust toolchain
|
||||||
|
if _, err := exec.LookPath("cargo"); err != nil {
|
||||||
|
return "", fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию для исходников
|
||||||
|
sourceDir := filepath.Join(buildDir, "rustdesk-source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Клонируем исходный код
|
||||||
|
if err := cbs.cloneRustDeskSource(build, config.Version, sourceDir); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to clone source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем конфигурационный файл custom.txt
|
||||||
|
var customConfig string
|
||||||
|
if config.UseCustomConfig && config.CustomConfigFile != "" {
|
||||||
|
customConfig = config.CustomConfigFile
|
||||||
|
cbs.appendLog(build, "Using custom configuration file\n")
|
||||||
|
} else {
|
||||||
|
customConfig = cbs.generateCustomConfig(config)
|
||||||
|
cbs.appendLog(build, "Using auto-generated configuration\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем custom.txt в исходники
|
||||||
|
customPath := filepath.Join(sourceDir, "custom.txt")
|
||||||
|
if err := os.WriteFile(customPath, []byte(customConfig), 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write custom.txt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Компилируем
|
||||||
|
cbs.appendLog(build, "Compiling Windows client with cargo...\n")
|
||||||
|
cmd := exec.Command("cargo", "build", "--release", "--target", "x86_64-pc-windows-msvc")
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
cmd.Env = append(os.Environ(), "RUSTFLAGS=-C target-feature=+crt-static")
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
cbs.appendLog(build, string(output))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("build failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем скомпилированный файл
|
||||||
|
exePath := filepath.Join(sourceDir, "target", "x86_64-pc-windows-msvc", "release", "rustdesk.exe")
|
||||||
|
if _, err := os.Stat(exePath); os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("compiled file not found: %s", exePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем имя выходного файла
|
||||||
|
outputFileName := config.FileName
|
||||||
|
if !strings.HasSuffix(strings.ToLower(outputFileName), ".exe") {
|
||||||
|
outputFileName = outputFileName + ".exe"
|
||||||
|
}
|
||||||
|
outputPath := filepath.Join(buildDir, outputFileName)
|
||||||
|
|
||||||
|
if err := cbs.copyFile(exePath, outputPath); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to copy compiled file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем custom.txt рядом с исполняемым файлом
|
||||||
|
customOutputPath := filepath.Join(buildDir, "custom.txt")
|
||||||
|
if err := os.WriteFile(customOutputPath, []byte(customConfig), 0644); err != nil {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Warning: failed to copy custom.txt: %v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Windows client compiled successfully: %s\n", outputFileName))
|
||||||
|
return outputPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildLinux компилирует клиент для Linux
|
||||||
|
func (cbs *ClientBuildService) buildLinux(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
|
||||||
|
cbs.appendLog(build, "Building Linux client from source...\n")
|
||||||
|
|
||||||
|
// Проверяем наличие Rust toolchain
|
||||||
|
if _, err := exec.LookPath("cargo"); err != nil {
|
||||||
|
return "", fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию для исходников
|
||||||
|
sourceDir := filepath.Join(buildDir, "rustdesk-source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Клонируем исходный код
|
||||||
|
if err := cbs.cloneRustDeskSource(build, config.Version, sourceDir); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to clone source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем конфигурационный файл custom.txt
|
||||||
|
var customConfig string
|
||||||
|
if config.UseCustomConfig && config.CustomConfigFile != "" {
|
||||||
|
customConfig = config.CustomConfigFile
|
||||||
|
cbs.appendLog(build, "Using custom configuration file\n")
|
||||||
|
} else {
|
||||||
|
customConfig = cbs.generateCustomConfig(config)
|
||||||
|
cbs.appendLog(build, "Using auto-generated configuration\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем custom.txt в исходники
|
||||||
|
customPath := filepath.Join(sourceDir, "custom.txt")
|
||||||
|
if err := os.WriteFile(customPath, []byte(customConfig), 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write custom.txt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Компилируем
|
||||||
|
cbs.appendLog(build, "Compiling Linux client with cargo...\n")
|
||||||
|
cmd := exec.Command("cargo", "build", "--release", "--target", "x86_64-unknown-linux-gnu")
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
cbs.appendLog(build, string(output))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("build failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем скомпилированный файл
|
||||||
|
binPath := filepath.Join(sourceDir, "target", "x86_64-unknown-linux-gnu", "release", "rustdesk")
|
||||||
|
if _, err := os.Stat(binPath); os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("compiled file not found: %s", binPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем имя выходного файла (без расширения для Linux бинарника)
|
||||||
|
outputFileName := config.FileName
|
||||||
|
if strings.HasSuffix(strings.ToLower(outputFileName), ".deb") || strings.HasSuffix(strings.ToLower(outputFileName), ".rpm") {
|
||||||
|
// Если указано расширение пакета, оставляем как есть, но создаем бинарник
|
||||||
|
outputFileName = strings.TrimSuffix(outputFileName, filepath.Ext(outputFileName))
|
||||||
|
}
|
||||||
|
outputPath := filepath.Join(buildDir, outputFileName)
|
||||||
|
|
||||||
|
if err := cbs.copyFile(binPath, outputPath); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to copy compiled file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Делаем файл исполняемым
|
||||||
|
if err := os.Chmod(outputPath, 0755); err != nil {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Warning: failed to set executable permissions: %v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем custom.txt рядом с исполняемым файлом
|
||||||
|
customOutputPath := filepath.Join(buildDir, "custom.txt")
|
||||||
|
if err := os.WriteFile(customOutputPath, []byte(customConfig), 0644); err != nil {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Warning: failed to copy custom.txt: %v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Linux client compiled successfully: %s\n", outputFileName))
|
||||||
|
return outputPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildMacOS компилирует клиент для macOS
|
||||||
|
func (cbs *ClientBuildService) buildMacOS(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
|
||||||
|
cbs.appendLog(build, "Building macOS client from source...\n")
|
||||||
|
|
||||||
|
// Проверяем наличие Rust toolchain
|
||||||
|
if _, err := exec.LookPath("cargo"); err != nil {
|
||||||
|
return "", fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем целевую архитектуру
|
||||||
|
target := "x86_64-apple-darwin"
|
||||||
|
if config.Platform == "macos" {
|
||||||
|
// Для Apple Silicon
|
||||||
|
target = "aarch64-apple-darwin"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем директорию для исходников
|
||||||
|
sourceDir := filepath.Join(buildDir, "rustdesk-source")
|
||||||
|
if err := os.MkdirAll(sourceDir, 0755); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to create source directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Клонируем исходный код
|
||||||
|
if err := cbs.cloneRustDeskSource(build, config.Version, sourceDir); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to clone source: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем конфигурационный файл custom.txt
|
||||||
|
var customConfig string
|
||||||
|
if config.UseCustomConfig && config.CustomConfigFile != "" {
|
||||||
|
customConfig = config.CustomConfigFile
|
||||||
|
cbs.appendLog(build, "Using custom configuration file\n")
|
||||||
|
} else {
|
||||||
|
customConfig = cbs.generateCustomConfig(config)
|
||||||
|
cbs.appendLog(build, "Using auto-generated configuration\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Записываем custom.txt в исходники
|
||||||
|
customPath := filepath.Join(sourceDir, "custom.txt")
|
||||||
|
if err := os.WriteFile(customPath, []byte(customConfig), 0644); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to write custom.txt: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Компилируем
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Compiling macOS client with cargo (target: %s)...\n", target))
|
||||||
|
cmd := exec.Command("cargo", "build", "--release", "--target", target)
|
||||||
|
cmd.Dir = sourceDir
|
||||||
|
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
cbs.appendLog(build, string(output))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("build failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем скомпилированный файл
|
||||||
|
binPath := filepath.Join(sourceDir, "target", target, "release", "rustdesk")
|
||||||
|
if _, err := os.Stat(binPath); os.IsNotExist(err) {
|
||||||
|
return "", fmt.Errorf("compiled file not found: %s", binPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Определяем имя выходного файла
|
||||||
|
outputFileName := config.FileName
|
||||||
|
if !strings.HasSuffix(strings.ToLower(outputFileName), ".app") && !strings.HasSuffix(strings.ToLower(outputFileName), ".dmg") {
|
||||||
|
outputFileName = outputFileName + ".app"
|
||||||
|
}
|
||||||
|
outputPath := filepath.Join(buildDir, outputFileName)
|
||||||
|
|
||||||
|
// Для macOS создаем .app bundle (упрощенная версия)
|
||||||
|
// В реальности нужно создать полноценный .app bundle, но для начала просто копируем бинарник
|
||||||
|
if err := cbs.copyFile(binPath, outputPath); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to copy compiled file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Делаем файл исполняемым
|
||||||
|
if err := os.Chmod(outputPath, 0755); err != nil {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Warning: failed to set executable permissions: %v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Копируем custom.txt рядом с исполняемым файлом
|
||||||
|
customOutputPath := filepath.Join(buildDir, "custom.txt")
|
||||||
|
if err := os.WriteFile(customOutputPath, []byte(customConfig), 0644); err != nil {
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("Warning: failed to copy custom.txt: %v\n", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
cbs.appendLog(build, fmt.Sprintf("macOS client compiled successfully: %s\n", outputFileName))
|
||||||
|
return outputPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildAndroid компилирует клиент для Android
|
||||||
|
func (cbs *ClientBuildService) buildAndroid(build *model.ClientBuild, config BuildConfig, buildDir string) (string, error) {
|
||||||
|
cbs.appendLog(build, "Building Android client from source...\n")
|
||||||
|
|
||||||
|
// Проверяем наличие Rust toolchain
|
||||||
|
if _, err := exec.LookPath("cargo"); err != nil {
|
||||||
|
return "", fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем наличие Android SDK
|
||||||
|
if os.Getenv("ANDROID_HOME") == "" && os.Getenv("ANDROID_SDK_ROOT") == "" {
|
||||||
|
return "", fmt.Errorf("Android SDK not found. Please set ANDROID_HOME or ANDROID_SDK_ROOT environment variable")
|
||||||
|
}
|
||||||
|
|
||||||
|
cbs.appendLog(build, "Android compilation requires additional setup (NDK, etc.)\n")
|
||||||
|
cbs.appendLog(build, "This is a placeholder - full Android build requires Android NDK and Gradle setup\n")
|
||||||
|
|
||||||
|
// Для Android компиляция более сложная и требует настройки NDK
|
||||||
|
// Пока возвращаем ошибку с инструкциями
|
||||||
|
return "", fmt.Errorf("Android compilation from source requires Android NDK setup. Please refer to RustDesk documentation for Android build instructions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateCustomConfig генерирует конфигурационный файл custom.txt
|
||||||
|
func (cbs *ClientBuildService) generateCustomConfig(config BuildConfig) string {
|
||||||
|
var lines []string
|
||||||
|
|
||||||
|
// Базовые настройки сервера
|
||||||
|
if config.ServerIP != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("rendezvous-server = %s", config.ServerIP))
|
||||||
|
}
|
||||||
|
if config.ApiServer != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("api-server = %s", config.ApiServer))
|
||||||
|
}
|
||||||
|
if config.Key != "" {
|
||||||
|
lines = append(lines, fmt.Sprintf("key = %s", config.Key))
|
||||||
|
}
|
||||||
|
if config.AppName != "" && strings.ToLower(config.AppName) != "rustdesk" {
|
||||||
|
lines = append(lines, fmt.Sprintf("app-name = %s", config.AppName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Добавляем дополнительные настройки из custom
|
||||||
|
for key, value := range config.Custom {
|
||||||
|
lines = append(lines, fmt.Sprintf("%s = %s", key, value))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(lines, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateBuildStatus обновляет статус сборки
|
||||||
|
func (cbs *ClientBuildService) updateBuildStatus(build *model.ClientBuild, status, errorMsg, log string) {
|
||||||
|
updates := map[string]interface{}{
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
if errorMsg != "" {
|
||||||
|
updates["error_msg"] = errorMsg
|
||||||
|
}
|
||||||
|
if log != "" {
|
||||||
|
updates["build_log"] = build.BuildLog + "\n" + log
|
||||||
|
}
|
||||||
|
DB.Model(build).Updates(updates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// appendLog добавляет строку в лог сборки
|
||||||
|
func (cbs *ClientBuildService) appendLog(build *model.ClientBuild, logLine string) {
|
||||||
|
var currentBuild model.ClientBuild
|
||||||
|
DB.First(¤tBuild, build.Id)
|
||||||
|
newLog := currentBuild.BuildLog + logLine
|
||||||
|
DB.Model(build).Update("build_log", newLog)
|
||||||
|
build.BuildLog = newLog
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUuid получает задачу компиляции по UUID
|
||||||
|
func (cbs *ClientBuildService) GetByUuid(uuid string) *model.ClientBuild {
|
||||||
|
build := &model.ClientBuild{}
|
||||||
|
DB.Where("build_uuid = ?", uuid).First(build)
|
||||||
|
return build
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUserId получает список задач компиляции пользователя
|
||||||
|
func (cbs *ClientBuildService) GetByUserId(userId uint) []*model.ClientBuild {
|
||||||
|
var builds []*model.ClientBuild
|
||||||
|
DB.Where("user_id = ?", userId).Order("created_at DESC").Find(&builds)
|
||||||
|
return builds
|
||||||
|
}
|
||||||
|
|
||||||
|
// List получает список задач компиляции с пагинацией
|
||||||
|
func (cbs *ClientBuildService) List(page, pageSize uint, scopes ...func(*gorm.DB) *gorm.DB) *model.ClientBuildList {
|
||||||
|
var builds []*model.ClientBuild
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := DB.Model(&model.ClientBuild{})
|
||||||
|
for _, scope := range scopes {
|
||||||
|
query = scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Count(&total)
|
||||||
|
query = Paginate(page, pageSize)(query)
|
||||||
|
query.Order("created_at DESC").Find(&builds)
|
||||||
|
|
||||||
|
return &model.ClientBuildList{
|
||||||
|
ClientBuilds: builds,
|
||||||
|
Pagination: model.Pagination{
|
||||||
|
Page: int64(page),
|
||||||
|
PageSize: int64(pageSize),
|
||||||
|
Total: total,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete удаляет задачу компиляции
|
||||||
|
func (cbs *ClientBuildService) Delete(id, userId uint) error {
|
||||||
|
build := &model.ClientBuild{}
|
||||||
|
DB.Where("id = ? AND user_id = ?", id, userId).First(build)
|
||||||
|
if build.Id == 0 {
|
||||||
|
return fmt.Errorf("build not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем файлы сборки
|
||||||
|
if build.FilePath != "" {
|
||||||
|
os.Remove(build.FilePath)
|
||||||
|
// Удаляем директорию сборки
|
||||||
|
buildDir := filepath.Dir(build.FilePath)
|
||||||
|
os.RemoveAll(buildDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB.Delete(build).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkBuildTools проверяет наличие инструментов для сборки
|
||||||
|
func (cbs *ClientBuildService) checkBuildTools(platform string) error {
|
||||||
|
switch platform {
|
||||||
|
case "windows", "linux", "macos":
|
||||||
|
// Проверяем наличие Rust
|
||||||
|
if _, err := exec.LookPath("cargo"); err != nil {
|
||||||
|
return fmt.Errorf("Rust toolchain not found. Please install Rust: https://rustup.rs/")
|
||||||
|
}
|
||||||
|
case "android":
|
||||||
|
// Проверяем наличие Android SDK
|
||||||
|
if os.Getenv("ANDROID_HOME") == "" {
|
||||||
|
return fmt.Errorf("Android SDK not found. Please set ANDROID_HOME environment variable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
232
service/clientConfig.go
Normal file
232
service/clientConfig.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/global"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/model"
|
||||||
|
"github.com/lejianwen/rustdesk-api/v2/utils"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientConfigService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateClientConfig генерирует конфигурационный файл клиента
|
||||||
|
// customConfigFile - опциональный кастомный конфигурационный файл (если пустой, генерируется автоматически)
|
||||||
|
func (ccs *ClientConfigService) GenerateClientConfig(userId uint, password, description, customConfigFile string) (*model.ClientConfig, error) {
|
||||||
|
// Создаем директорию для хранения клиентов
|
||||||
|
clientsDir := filepath.Join(global.Config.Gin.ResourcesPath, "clients")
|
||||||
|
if err := os.MkdirAll(clientsDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create clients directory: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Генерируем уникальное имя файла
|
||||||
|
timestamp := time.Now().Format("20060102150405")
|
||||||
|
fileName := fmt.Sprintf("rustdesk-client-%d-%s.zip", userId, timestamp)
|
||||||
|
filePath := filepath.Join(clientsDir, fileName)
|
||||||
|
|
||||||
|
// Создаем ZIP архив
|
||||||
|
zipFile, err := os.Create(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create zip file: %v", err)
|
||||||
|
}
|
||||||
|
defer zipFile.Close()
|
||||||
|
|
||||||
|
zipWriter := zip.NewWriter(zipFile)
|
||||||
|
defer zipWriter.Close()
|
||||||
|
|
||||||
|
// Создаем конфигурационный файл TOML
|
||||||
|
var configContent string
|
||||||
|
if customConfigFile != "" {
|
||||||
|
// Используем кастомный конфигурационный файл
|
||||||
|
configContent = customConfigFile
|
||||||
|
} else {
|
||||||
|
// Генерируем автоматически
|
||||||
|
configContent = ccs.generateTomlConfig()
|
||||||
|
}
|
||||||
|
configFile, err := zipWriter.Create("RustDesk.toml")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create config file in zip: %v", err)
|
||||||
|
}
|
||||||
|
_, err = configFile.Write([]byte(configContent))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write config to zip: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем файл с паролем (опционально, для справки)
|
||||||
|
if password != "" {
|
||||||
|
passwordFile, err := zipWriter.Create("password.txt")
|
||||||
|
if err == nil {
|
||||||
|
passwordFile.Write([]byte(fmt.Sprintf("Password: %s\n", password)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем информацию о файле
|
||||||
|
fileInfo, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get file info: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Вычисляем хеш файла
|
||||||
|
fileHash, err := ccs.calculateFileHash(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to calculate file hash: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Хешируем пароль для хранения в БД
|
||||||
|
hashedPassword, err := utils.EncryptPassword(password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to hash password: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем запись в БД
|
||||||
|
clientConfig := &model.ClientConfig{
|
||||||
|
UserId: userId,
|
||||||
|
Password: hashedPassword,
|
||||||
|
FileName: fileName,
|
||||||
|
FileHash: fileHash,
|
||||||
|
FileSize: fileInfo.Size(),
|
||||||
|
FilePath: filePath,
|
||||||
|
Description: description,
|
||||||
|
Status: model.COMMON_STATUS_ENABLE,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создаем базовую запись
|
||||||
|
if err := DB.Create(clientConfig).Error; err != nil {
|
||||||
|
// Удаляем файл в случае ошибки
|
||||||
|
os.Remove(filePath)
|
||||||
|
return nil, fmt.Errorf("failed to save client config to database: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обновляем кастомные поля, если колонки существуют
|
||||||
|
if customConfigFile != "" {
|
||||||
|
// Проверяем наличие колонки перед обновлением
|
||||||
|
if DB.Migrator().HasColumn(&model.ClientConfig{}, "custom_config_file") {
|
||||||
|
DB.Model(clientConfig).Updates(map[string]interface{}{
|
||||||
|
"custom_config_file": customConfigFile,
|
||||||
|
"use_custom_config": true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clientConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateTomlConfig генерирует TOML конфигурацию для RustDesk клиента
|
||||||
|
func (ccs *ClientConfigService) generateTomlConfig() string {
|
||||||
|
// Формируем TOML строку с настройками сервера
|
||||||
|
tomlStr := fmt.Sprintf(`rendezvous_server = "%s"
|
||||||
|
relay_server = "%s"
|
||||||
|
api_server = "%s"
|
||||||
|
key = "%s"
|
||||||
|
`, global.Config.Rustdesk.IdServer, global.Config.Rustdesk.RelayServer, global.Config.Rustdesk.ApiServer, global.Config.Rustdesk.Key)
|
||||||
|
|
||||||
|
return tomlStr
|
||||||
|
}
|
||||||
|
|
||||||
|
// calculateFileHash вычисляет MD5 хеш файла
|
||||||
|
func (ccs *ClientConfigService) calculateFileHash(filePath string) (string, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hash := md5.New()
|
||||||
|
if _, err := io.Copy(hash, file); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByUserId получает список клиентов пользователя
|
||||||
|
func (ccs *ClientConfigService) GetByUserId(userId uint) []*model.ClientConfig {
|
||||||
|
var configs []*model.ClientConfig
|
||||||
|
DB.Where("user_id = ? AND status = ?", userId, model.COMMON_STATUS_ENABLE).
|
||||||
|
Order("created_at DESC").
|
||||||
|
Find(&configs)
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetById получает клиент по ID
|
||||||
|
func (ccs *ClientConfigService) GetById(id uint) *model.ClientConfig {
|
||||||
|
config := &model.ClientConfig{}
|
||||||
|
DB.Where("id = ? AND status = ?", id, model.COMMON_STATUS_ENABLE).First(config)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
// List получает список клиентов с пагинацией
|
||||||
|
func (ccs *ClientConfigService) List(page, pageSize uint, scopes ...func(*gorm.DB) *gorm.DB) *model.ClientConfigList {
|
||||||
|
var configs []*model.ClientConfig
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
query := DB.Model(&model.ClientConfig{}).Where("status = ?", model.COMMON_STATUS_ENABLE)
|
||||||
|
for _, scope := range scopes {
|
||||||
|
query = scope(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
query.Count(&total)
|
||||||
|
query = Paginate(page, pageSize)(query)
|
||||||
|
query.Order("created_at DESC").Find(&configs)
|
||||||
|
|
||||||
|
return &model.ClientConfigList{
|
||||||
|
ClientConfigs: configs,
|
||||||
|
Pagination: model.Pagination{
|
||||||
|
Page: int64(page),
|
||||||
|
PageSize: int64(pageSize),
|
||||||
|
Total: total,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete удаляет клиент (помечает как удаленный)
|
||||||
|
func (ccs *ClientConfigService) Delete(id, userId uint) error {
|
||||||
|
// Ищем запись без фильтра по status, чтобы можно было удалить даже если она уже помечена как удаленная
|
||||||
|
config := &model.ClientConfig{}
|
||||||
|
if err := DB.Where("id = ?", id).First(config).Error; err != nil {
|
||||||
|
return fmt.Errorf("client config not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Проверяем, что клиент принадлежит пользователю
|
||||||
|
if config.UserId != userId {
|
||||||
|
return fmt.Errorf("access denied")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Если уже удален, просто возвращаем успех
|
||||||
|
if config.Status == model.COMMON_STATUS_DISABLED {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Помечаем как удаленный - обновляем только поле status, чтобы избежать проблем с отсутствующими колонками
|
||||||
|
if err := DB.Model(&model.ClientConfig{}).Where("id = ?", id).Update("status", model.COMMON_STATUS_DISABLED).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удаляем файл
|
||||||
|
if config.FilePath != "" {
|
||||||
|
os.Remove(config.FilePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyPassword проверяет пароль клиента
|
||||||
|
func (ccs *ClientConfigService) VerifyPassword(configId uint, password string) bool {
|
||||||
|
config := ccs.GetById(configId)
|
||||||
|
if config.Id == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
ok, _, err := utils.VerifyPassword(config.Password, password)
|
||||||
|
return err == nil && ok
|
||||||
|
}
|
||||||
|
|
||||||
@@ -137,6 +137,17 @@ func (ls *LdapService) Authenticate(username, password string) (*model.User, err
|
|||||||
return nil, ErrLdapUserDisabled
|
return nil, ErrLdapUserDisabled
|
||||||
}
|
}
|
||||||
cfg := &Config.Ldap
|
cfg := &Config.Ldap
|
||||||
|
|
||||||
|
// Skip allow-group check for admins
|
||||||
|
isAdmin := ls.isUserAdmin(cfg, ldapUser)
|
||||||
|
|
||||||
|
// non-admins only check if allow-group is configured
|
||||||
|
if !isAdmin && cfg.User.AllowGroup != "" {
|
||||||
|
if !ls.isUserInGroup(cfg, ldapUser, cfg.User.AllowGroup) {
|
||||||
|
return nil, errors.New("user not in allowed group")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = ls.verifyCredentials(cfg, ldapUser.Dn, password)
|
err = ls.verifyCredentials(cfg, ldapUser.Dn, password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -148,6 +159,46 @@ func (ls *LdapService) Authenticate(username, password string) (*model.User, err
|
|||||||
return user, nil
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isUserInGroup checks if the user is a member of the specified group. by_sw
|
||||||
|
func (ls *LdapService) isUserInGroup(cfg *config.Ldap, ldapUser *LdapUser, groupDN string) bool {
|
||||||
|
// Check "memberOf" directly
|
||||||
|
if len(ldapUser.MemberOf) > 0 {
|
||||||
|
for _, group := range ldapUser.MemberOf {
|
||||||
|
if strings.EqualFold(group, groupDN) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For "member" attribute, perform a reverse search on the group
|
||||||
|
member := "member"
|
||||||
|
userDN := ldap.EscapeFilter(ldapUser.Dn)
|
||||||
|
groupDN = ldap.EscapeFilter(groupDN)
|
||||||
|
groupFilter := fmt.Sprintf("(%s=%s)", member, userDN)
|
||||||
|
|
||||||
|
// Create the LDAP search request
|
||||||
|
groupSearchRequest := ldap.NewSearchRequest(
|
||||||
|
groupDN,
|
||||||
|
ldap.ScopeWholeSubtree,
|
||||||
|
ldap.NeverDerefAliases,
|
||||||
|
0, // Unlimited search results
|
||||||
|
0, // No time limit
|
||||||
|
false, // Return both attributes and DN
|
||||||
|
groupFilter,
|
||||||
|
[]string{"dn"},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Perform the group search
|
||||||
|
groupResult, err := ls.searchResult(cfg, groupSearchRequest)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any results are returned, the user is part of the group
|
||||||
|
return len(groupResult.Entries) > 0
|
||||||
|
}
|
||||||
|
|
||||||
// mapToLocalUser checks whether the user exists locally; if not, creates one.
|
// mapToLocalUser checks whether the user exists locally; if not, creates one.
|
||||||
// If the user exists and Ldap.Sync is enabled, it updates local info.
|
// If the user exists and Ldap.Sync is enabled, it updates local info.
|
||||||
func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.User, error) {
|
func (ls *LdapService) mapToLocalUser(cfg *config.Ldap, lu *LdapUser) (*model.User, error) {
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ type Service struct {
|
|||||||
*ServerCmdService
|
*ServerCmdService
|
||||||
*LdapService
|
*LdapService
|
||||||
*AppService
|
*AppService
|
||||||
|
*ClientConfigService
|
||||||
|
*ClientBuildService
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dependencies struct {
|
type Dependencies struct {
|
||||||
@@ -48,7 +50,22 @@ func New(c *config.Config, g *gorm.DB, l *log.Logger, j *jwt.Jwt, lo lock.Locker
|
|||||||
Logger = l
|
Logger = l
|
||||||
Jwt = j
|
Jwt = j
|
||||||
Lock = lo
|
Lock = lo
|
||||||
AllService = new(Service)
|
AllService = &Service{
|
||||||
|
UserService: &UserService{},
|
||||||
|
AddressBookService: &AddressBookService{},
|
||||||
|
TagService: &TagService{},
|
||||||
|
PeerService: &PeerService{},
|
||||||
|
GroupService: &GroupService{},
|
||||||
|
OauthService: &OauthService{},
|
||||||
|
LoginLogService: &LoginLogService{},
|
||||||
|
AuditService: &AuditService{},
|
||||||
|
ShareRecordService: &ShareRecordService{},
|
||||||
|
ServerCmdService: &ServerCmdService{},
|
||||||
|
LdapService: &LdapService{},
|
||||||
|
AppService: &AppService{},
|
||||||
|
ClientConfigService: &ClientConfigService{},
|
||||||
|
ClientBuildService: &ClientBuildService{},
|
||||||
|
}
|
||||||
return AllService
|
return AllService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -395,10 +395,10 @@ func (us *UserService) UserThirdInfo(userId uint, op string) *model.UserThird {
|
|||||||
return ut
|
return ut
|
||||||
}
|
}
|
||||||
|
|
||||||
// FindLatestUserIdFromLoginLogByUuid 根据uuid查找最后登录的用户id
|
// FindLatestUserIdFromLoginLogByUuid 根据uuid和设备id查找最后登录的用户id
|
||||||
func (us *UserService) FindLatestUserIdFromLoginLogByUuid(uuid string) uint {
|
func (us *UserService) FindLatestUserIdFromLoginLogByUuid(uuid string, deviceId string) uint {
|
||||||
llog := &model.LoginLog{}
|
llog := &model.LoginLog{}
|
||||||
DB.Where("uuid = ?", uuid).Order("id desc").First(llog)
|
DB.Where("uuid = ? and device_id = ?", uuid, deviceId).Order("id desc").First(llog)
|
||||||
return llog.UserId
|
return llog.UserId
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user