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) }