Add local client compilation from source, build logs viewer, and submodules support
This commit is contained in:
@@ -44,9 +44,16 @@ RUN if [ "$COUNTRY" = "CN" ] ; then \
|
||||
|
||||
ARG FRONTEND_GIT_REPO=https://github.com/lejianwen/rustdesk-api-web.git
|
||||
ARG FRONTEND_GIT_BRANCH=master
|
||||
# Clone the frontend repository
|
||||
|
||||
RUN git clone -b $FRONTEND_GIT_BRANCH $FRONTEND_GIT_REPO .
|
||||
# Copy local frontend if exists, otherwise clone from git
|
||||
COPY frontend-temp /tmp/frontend-temp
|
||||
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
|
||||
RUN if [ "$COUNTRY" = "CN" ] ; then \
|
||||
@@ -69,7 +76,7 @@ RUN if [ "$COUNTRY" = "CN" ] ; then \
|
||||
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; \
|
||||
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 --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 --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
|
||||
RUN file /app/apimain && \
|
||||
mkdir -p /app/data && \
|
||||
mkdir -p /app/runtime
|
||||
mkdir -p /app/runtime && \
|
||||
mkdir -p /app/resources/builds
|
||||
|
||||
# Set up a volume for persistent data
|
||||
VOLUME /app/data
|
||||
@@ -90,5 +103,10 @@ VOLUME /app/data
|
||||
# Expose the necessary port
|
||||
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
|
||||
CMD ["./apimain"]
|
||||
|
||||
@@ -283,6 +283,22 @@ func DatabaseAutoUpdate() {
|
||||
if v.Version < 246 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -306,6 +322,8 @@ func Migrate(version uint) {
|
||||
&model.AddressBookCollectionRule{},
|
||||
&model.ServerCmd{},
|
||||
&model.DeviceGroup{},
|
||||
&model.ClientConfig{},
|
||||
&model.ClientBuild{},
|
||||
)
|
||||
if err != nil {
|
||||
global.Logger.Error("migrate err :=>", err)
|
||||
|
||||
@@ -20,5 +20,8 @@ services:
|
||||
volumes:
|
||||
- ./data/rustdesk/api:/app/data #将数据库挂载出来方便备份
|
||||
- ./conf:/app/conf # config
|
||||
- ./resources/clients:/app/resources/clients # 生成的 клиенты
|
||||
- ./resources/builds:/app/resources/builds # 编译任务
|
||||
- ./resources/uploads:/app/resources/uploads # 上传的文件
|
||||
# - ./resources:/app/resources # 静态资源
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -12,6 +12,9 @@ services:
|
||||
- 21114:21114
|
||||
volumes:
|
||||
- ./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
|
||||
# - ./resources:/app/resources # 静态资源
|
||||
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
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)
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -50,6 +50,8 @@ func Init(g *gin.Engine) {
|
||||
|
||||
RustdeskCmdBind(adg)
|
||||
DeviceGroupBind(adg)
|
||||
ClientConfigBind(adg)
|
||||
ClientBuildBind(adg)
|
||||
//访问静态文件
|
||||
//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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ type Service struct {
|
||||
*ServerCmdService
|
||||
*LdapService
|
||||
*AppService
|
||||
*ClientConfigService
|
||||
*ClientBuildService
|
||||
}
|
||||
|
||||
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
|
||||
Jwt = j
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user