diff --git a/README.en.md b/README.en.md index 45b8ff4..cbfad4a 100644 --- a/README.en.md +++ b/README.en.md @@ -11,7 +11,7 @@ An efficient intranet office platform that includes various tools such as Word, English | [简体中文](README.md) -[FAQ](./docs/Faq.md) | [API](./docs/Api.md) +[FAQ](./docs/Faq.md) | [API](./docs/Api.md) | [AppStore](./docs/Store.md) ### Install diff --git a/README.md b/README.md index 3aa4902..af7400f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@

GodoOS

-一款高效的内网办公平台,内含word/excel/ppt/pdf/内网聊天/白板/思维导图等多个办公系统工具,支持原生文件存储。平台界面精仿windows风格,操作简便,同时保持低资源消耗和高性能运行。无需注册即可自动连接内网用户,实现即时通讯和文件共享。 +一款高效的内网办公平台,内含word/excel/ppt/pdf/内网聊天/白板/思维导图等多个办公系统工具,支持原生文件存储。平台界面精仿windows风格,操作简便,同时保持低资源消耗和高性能运行。无需注册即可自动连接内网用户,实现即时通讯和文件共享。灵活高配置的应用商店,可无限扩展。
@@ -11,7 +11,7 @@ [English](README.en.md) | 简体中文 -[FAQ](./docs/Faq.md) | [API](./docs/Api.md) +[FAQ](./docs/Faq.md) | [API](./docs/Api.md) | [应用开发](./docs/Store.md) @@ -48,6 +48,8 @@ - ***精小,打包后仅65M,确包含了所有的办公套件*** - ***可无限扩展,支持自定义应用*** - ***golang开发后端,低资源消耗和高性能*** +- ***支持多平台,Windows、Linux、MacOS*** +- ***只会html和拥有可执行的应用,简单学习一下应用商店配置即可开发出复杂的应用*** ## 🚧 开发进程 - 2024年7月,发布v1.0.0版本,发布后,项目进入第二阶段。 diff --git a/docs/Api.md b/docs/Api.md index a5087c8..8c0e5a0 100644 --- a/docs/Api.md +++ b/docs/Api.md @@ -16,6 +16,11 @@ - **Content-Type**: `application/json` - **响应体**: 目录内容列表 +#### 示例 +```bash +PATH_PARAM="/path/to/directory" +curl -X GET "http://localhost:56780/files/read?path=$PATH_PARAM" +``` --- ### 获取文件或目录状态 @@ -33,6 +38,12 @@ - **Content-Type**: `application/json` - **响应体**: 文件或目录的状态信息 +#### 示例 +```bash +# 获取文件或目录状态 +PATH_PARAM="/path/to/file_or_directory" +curl -X GET "http://localhost:56780/files/stat?path=$PATH_PARAM" +``` --- ### 更改文件权限 @@ -51,6 +62,12 @@ - **Content-Type**: `application/json` - **响应体**: 操作结果 +#### 示例 +```bash +# 更改文件权限 +JSON='{"path":"/path/to/file","mode":"0644"}' +curl -X POST -H "Content-Type: application/json" -d "$JSON" "http://localhost:56780/files/chmod" +``` --- ### 检查文件或目录是否存在 @@ -67,7 +84,12 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 存在性检查结果 - +#### 示例 +```bash +# 检查文件或目录是否存在 +PATH_PARAM="/path/to/file_or_directory" +curl -X GET "http://localhost:56780/files/exists?path=$PATH_PARAM" +``` --- ### 读取文件内容 @@ -84,7 +106,12 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 文件内容 - +#### 示例 +```bash +# 读取文件内容 +PATH_PARAM="/path/to/file" +curl -X GET "http://localhost:56780/files/readfile?path=$PATH_PARAM" +``` --- ### 删除文件 @@ -101,7 +128,12 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 操作结果 - +#### 示例 +```bash +# 删除文件 +PATH_PARAM="/path/to/file" +curl -X GET "http://localhost:56780/files/unlink?path=$PATH_PARAM" +``` --- ### 清空文件系统 @@ -115,7 +147,11 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 操作结果 - +#### 示例 +```bash +# 清空文件系统 +curl -X GET "http://localhost:56780/files/clear" +``` --- ### 重命名文件或目录 @@ -132,7 +168,13 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 操作结果 - +#### 示例 +```bash +# 重命名文件或目录 +OLD_PATH_PARAM="/path/to/old_file_or_directory" +NEW_PATH_PARAM="/path/to/new_file_or_directory" +curl -X GET "http://localhost:56780/files/rename?oldPath=$OLD_PATH_PARAM&newPath=$NEW_PATH_PARAM" +``` --- ### 创建目录 @@ -149,7 +191,12 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 操作结果 - +#### 示例 +```bash +# 创建目录 +DIR_PATH_PARAM="/path/to/new_directory" +curl -X POST "http://localhost:56780/files/mkdir?dirPath=$DIR_PATH_PARAM" +``` --- ### 删除目录 @@ -166,7 +213,12 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 操作结果 - +#### 示例 +```bash +# 删除目录 +DIR_PATH_PARAM="/path/to/directory" +curl -X GET "http://localhost:56780/files/rmdir?dirPath=$DIR_PATH_PARAM" +``` --- ### 复制文件 @@ -183,7 +235,13 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 操作结果 - +#### 示例 +```bash +# 复制文件 +SRC_PATH_PARAM="/path/to/source_file" +DST_PATH_PARAM="/path/to/destination_file" +curl -X GET "http://localhost:56780/files/copyfile?srcPath=$SRC_PATH_PARAM&dstPath=$DST_PATH_PARAM" +``` --- ### 写入文件 @@ -204,7 +262,13 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 操作结果 - +#### 示例 +```bash +# 写入文件 +FILE_PATH_PARAM="/path/to/file" +CONTENT="This is the content to be written." +curl -X POST -F "content=@-$CONTENT" "http://localhost:56780/files/writefile?filePath=$FILE_PATH_PARAM" +``` --- ### 追加文件内容 @@ -225,7 +289,13 @@ #### 响应 - **Content-Type**: `application/json` - **响应体**: 操作结果 - +#### 示例 +```bash +# 追加文件内容 +FILE_PATH_PARAM="/path/to/file" +CONTENT="This is the content to be appended." +curl -X POST -F "content=@-$CONTENT" "http://localhost:56780/files/appendfile?filePath=$FILE_PATH_PARAM" +``` --- ### 文件系统事件监听 @@ -237,3 +307,4 @@ - **path** (监听的文件或目录路径) - **callback** (事件回调函数) - **errback** (错误回调函数) + diff --git a/docs/Markdown.md b/docs/Markdown.md index 18e2cca..8f88b5e 100644 --- a/docs/Markdown.md +++ b/docs/Markdown.md @@ -1,7 +1,3 @@ ---- -title: Markdown编辑器 -icon: circle-info ---- ## 概览 Markdown 编辑器允许您使用丰富的文本编辑体验创建和编辑 Markdown 文件。它包括实时保存、文件导入/导出以及访问最近的 Markdown 文件等功能。 diff --git a/docs/Store.md b/docs/Store.md new file mode 100644 index 0000000..c8d0257 --- /dev/null +++ b/docs/Store.md @@ -0,0 +1,159 @@ +## 应用管理 + +### 如何添加一个应用 + +1. 在用户目录.godoos/run下建立一个应用文件夹。 +2. 一个应用需要包含两个配置文件,在应用根目录下创建`install.json`和`store.json`两个文件。 配置文件格式如下: +- install.json +```json +{ + "name": "", // string, 应用程序名称。 + "url": "", // string, 应用程序下载地址。 + "webUrl":"", // string, 如何设置,应用程序将显示到桌面。 + "isDev": true, // boolean, 是否为开发环境。 + "needDownload": true, // boolean, 是否需要下载。 + "needInstall": true, // boolean, 是否需要安装。 + "version": "1.0.0", // string, 应用程序版本。 + "icon": "", // string, 应用程序图标,为可访问的网络地址。 + "checkProgress": true, // boolean, 表示是否显示启动和停止。 + "hasRestart": true, // boolean, 是否需要重启。 + "setting": true // boolean, 是否需要设置。 +} + +``` +如果不需要后端进程的web应用,store.json可以不用配置。 + +install.json的struct为: +```json +type InstallInfo struct { + Name string `json:"name"` // 应用程序名称。重要,必须和应用的目录名称一致。 + URL string `json:"url"` // 应用程序下载地址。 + WebUrl string `json:"webUrl"` // 应用程序的网页地址。 + IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。 + NeedDownload bool `json:"needDownload"` // 标志位,表示是否需要下载。 + NeedInstall bool `json:"needInstall"` // 标志位,表示是否需要安装。 + Version string `json:"version"` // 应用程序的版本号。 + Desc string `json:"desc"` // 应用程序的描述信息。 + Icon string `json:"icon"` // 应用程序的图标路径。 + CheckProgress bool `json:"checkProgress"` // 标志位,表示是否显示启动和停止。 + HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。 + Setting bool `json:"setting"` // 标志位,表示是否需要配置。 +} +``` + +- store.json + +```json +{ + "name": "mysql5.7", // string, 应用程序名称。可不设置,会继承自install.json + "icon": "mysql.png", // string, 应用程序图标,一般放在应用程序static目录下。 + "setting": { + "binPath": "{exePath}/bin/mysqld.exe", // string, 重要,必须设置。为启动程序路径。 + "confPath": "{exePath}/my.ini",// string, 重要,必须设置。为配置文件路径。 + "progressName": "mysqld.exe",// string, 进程名称。如果为单线程可不设置。 + "isOn": true // boolean, 是否启动守护进程 + }, + "config": { // object, 配置文件。里面的配置可以任意填写,和cmds配合使用。 + }, + "cmds": {},// object, 命令集。 + "install": { // object, 安装配置。 + "envs": [],// object[], 环境变量。 + "cmds": []// object[], 启动命令。 + }, + "start": {// object, 启动配置。 + "envs": [],// object[], 环境变量。 + "cmds": []// object[], 启动命令。 + } +} +``` +store.json的struct为 +```json +type StoreInfo struct { + Name string `json:"name"` // 应用程序的名称。 + Icon string `json:"icon"` // 应用程序的图标路径。 + Setting Setting `json:"setting"` // 应用程序的配置信息。 + Config map[string]any `json:"config"` // 应用程序的配置信息映射。 + Cmds map[string][]Cmd `json:"cmds"` // 应用程序的命令集合。 + Install Install `json:"install"` // 安装应用程序的信息。 + Start Install `json:"start"` // 启动应用程序的信息。 +} +``` +Setting的结构体为: +```json +// 包含应用程序的二进制文件路径、配置文件路径等关键设置信息。 +type Setting struct { + BinPath string `json:"binPath"` // 应用程序二进制文件的路径。 + ConfPath string `json:"confPath"` // 应用程序配置文件的路径。 + ProgressName string `json:"progressName"` // 进程的名称。 + IsOn bool `json:"isOn"` //是否守护进程运行。 +} +``` +Cmd的结构体为: +```json +type Cmd struct { + Name string `json:"name"` // 命令的名称。 + FilePath string `json:"filePath,omitempty"` // 命令文件的路径。 + Content string `json:"content,omitempty"` // 命令的内容。 + BinPath string `json:"binPath,omitempty"` // 执行命令的二进制文件路径。 + TplPath string `json:"tplPath,omitempty"` // 命令的模板路径。 + Cmds []string `json:"cmds,omitempty"` // 要执行的子命令列表。 + Waiting int `json:"waiting"` // 等待的时间。 + Kill bool `json:"kill"` // 标志位,表示是否需要终止之前的命令。如果setting中设置了progressName会优先杀死整个进程 + Envs []Item `json:"envs"` // 命令执行时的环境变量。 +} +``` +Install的struct为: +```json +// Install 描述了安装过程中的环境变量和命令列表。 +type Install struct { + Envs []Item `json:"envs"` // 安装过程中需要的环境变量。 + Cmds []string `json:"cmds"` // 安装过程中需要执行的命令列表。 +} +// Item 是一个通用的键值对结构体,用于表示配置项或环境变量等。 +type Item struct { + Name string `json:"name"` // 配置项的名称。 + Value any `json:"value"` // 配置项的值。 +} +``` + +3. 在应用商店添加应用,选择本地添加,输入应用的目录名字(不需要填写整个目录)。 + +### 配置文件`store.json`说明 + +1. `install`和`start`配置可以调用`cmds`里的命令。 +2. `cmds`里的命令也可以调用自身的命令。 +3. 所有的命令都可以串联使用。 + +### 如何设置配置 +1. 在应用目录下建立`static`目录,并创建`index.html`文件。`install.json`中`setting`设置为`true`。 +前端配置样列 +```js +const postData = { +dataDir: dataDir,// 此处对应store.json中的config配置项 +logDir: logDir,// 此处对应store.json中的config配置项 +port: port, // 此处对应store.json中的config配置项 +name: "mysql5.7",// 应用名称 +cmdKey: "setting"// 命令键,cmds的name + } +const comp = await fetch('http://localhost:56780/store/setting', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(postData) +}); +``` +关键要设置好`name`和`cmdKey`,`cmdKey`对应`store.json`中的`cmds`数组配置项中的`name`,一个数组中又可以配置一系列的命令,可以参考Cmd的结构体 + +### 内置应用说明 +- 系列封装了一些用于处理进程控制和文件操作的功能函数,以下是各函数的详细描述: +1. `start` 启动应用 +2. `stop` 停止应用 +3. `restart` 重启应用 +4. `exec` 执行命令 必须设置binPath和cmds +5. `writeFile` 写入文件 必须设置filePath和content,以config为依据,对content进行替换 +6. `changeFile` 修改文件 必须设置filePath和tplPath,以config为依据,在模板文件中进行替换 +7. `deleteFile` 删除文件 +8. `unzip` 解压文件 必须设置filePath和content,content为解压目录 +9. `zip` 压缩文件 必须设置filePath和content,filePath为将要压缩的文件夹,content为压缩后的文件名 +10. `mkdir` 创建文件夹 必须设置FilePath,FilePath为创建的文件夹路径 diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 3eb1fa7..baeb150 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -44,6 +44,7 @@ declare module 'vue' { ElCard: typeof import('element-plus/es')['ElCard'] ElCol: typeof import('element-plus/es')['ElCol'] ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] + ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElForm: typeof import('element-plus/es')['ElForm'] diff --git a/frontend/src/assets/store.json b/frontend/src/assets/store.json index 325920d..f4be2b9 100644 --- a/frontend/src/assets/store.json +++ b/frontend/src/assets/store.json @@ -4,19 +4,12 @@ "url": "https://gitee.com/ruitao_admin/godoos-mysql5.7/releases/download/mysql5.7/mysql5.7.zip", "needDownload": true, "needInstall": true, + "isDev":true, "version": "5.7.37", "icon": "https://img0.baidu.com/it/u=1087008535,3349487719&fm=253&fmt=auto&app=138&f=PNG?w=969&h=500", "checkProgress": true, "hasRestart": true, - "setting": [ - { - "path": "setting.html", - "lang":{ - "cn-zh": "设置", - "en": "Setting" - } - } - ] + "setting": true }, { "name": "nginx", diff --git a/frontend/src/assets/store.scss b/frontend/src/assets/store.scss index e819416..e0de62b 100644 --- a/frontend/src/assets/store.scss +++ b/frontend/src/assets/store.scss @@ -221,6 +221,7 @@ display: flex; flex-direction: row; flex-wrap: wrap; + gap: 10px; justify-content: flex-start; align-items: flex-start; padding: 10px; diff --git a/frontend/src/components/store/AddApp.vue b/frontend/src/components/store/AddApp.vue index 72279b6..e643520 100644 --- a/frontend/src/components/store/AddApp.vue +++ b/frontend/src/components/store/AddApp.vue @@ -1,11 +1,17 @@ diff --git a/frontend/src/components/store/Store.vue b/frontend/src/components/store/Store.vue index 59870b0..56641cc 100644 --- a/frontend/src/components/store/Store.vue +++ b/frontend/src/components/store/Store.vue @@ -24,15 +24,17 @@ onMounted(async () => { async function getList() { if (currentCate.value == 'add') return; storeList.value = storeInitList + // const apiUrl = getSystemKey("apiUrl"); // const storeUrl = apiUrl + '/store/storelist?cate=' + currentCate.value - // fetch(storeUrl).then(res => { - // res.json().then(data => { - // storeList.value = data - // }) - // }).catch(() => { - // notifyError(t("store.errorList")) - // }) + // const res = await fetch(storeUrl) + // if (!res.ok) { + // notifyError(t('store.getStoreListError')) + // return + // } + // const data = await res.json() + // //console.log(data) + // storeList.value = data await checkProgress() isready.value = true; } @@ -41,8 +43,8 @@ async function checkProgress() { if (!completion.ok) { return } - let res:any = await completion.json() - if(!res || res.length < 1){ + let res: any = await completion.json() + if (!res || res.length < 1) { res = [] } storeList.value.forEach((item: any, index: number) => { @@ -50,7 +52,7 @@ async function checkProgress() { console.log(pitem) if (pitem) { storeList.value[index].isRuning = pitem.running - }else{ + } else { storeList.value[index].isRuning = false } }) @@ -69,7 +71,7 @@ function setCache() { } async function install(item: any) { - if (item.needDownload) { + if (item.needDownload && !item.isDev) { await download(item) } if (item.needInstall) { @@ -84,8 +86,11 @@ async function install(item: any) { notifyError(res.message) return } + if(res.data){ + item.icon = res.data + } } - if(item.isOut){ + if (item.isOut) { item.progress = 0 const completion = await fetch(apiUrl + '/store/installOut?url=' + item.url) if (!completion.ok) { @@ -98,10 +103,10 @@ async function install(item: any) { return } } - if (item.isWeb) { + if (item.webUrl) { sys.fs.writeFile( `${sys._options.userLocation}Desktop/${item.name}.url`, - `link::url::${item.url}::${item.icon}` + `link::url::${item.webUrl}::${item.icon}` ); } @@ -165,26 +170,26 @@ async function download(item: any) { } } async function pauseApp(item: any) { - const res:any = await fetch(apiUrl + '/store/stop/' + item.name) - if(!res.ok){ + const res: any = await fetch(apiUrl + '/store/stop/' + item.name) + if (!res.ok) { const msg = await res.text() notifyError(msg) return } - setTimeout(async() => { + setTimeout(async () => { await checkProgress() }, 1000) - + } async function restartApp(item: any) { await fetch(apiUrl + '/store/restart/' + item.name) - setTimeout(async() => { + setTimeout(async () => { await checkProgress() }, 1000) } async function startApp(item: any) { await fetch(apiUrl + '/store/start/' + item.name) - setTimeout(async() => { + setTimeout(async () => { await checkProgress() }, 1000) } @@ -204,25 +209,10 @@ async function startApp(item: any) {
-
{{ currentTitle }}
-
0 { + cmd = exec.Command(storeInfo.Setting.BinPath, storeInfo.Start.Cmds...) + } else { + cmd = exec.Command(storeInfo.Setting.BinPath) + } + if runtime.GOOS == "windows" { + // 在Windows上,通过设置CreationFlags来隐藏窗口 + cmd = SetHideConsoleCursor(cmd) + } + if err := cmd.Start(); err != nil { + //log.Println("Error starting script:", err) + return fmt.Errorf("error starting script: %v", err) + } + + // 启动脚本命令并返回可能的错误 + go func(cmd *exec.Cmd) { + RegisterProcess(storeInfo.Name, storeInfo.Setting.ProgressName, storeInfo.Setting.IsOn, cmd) + if err := cmd.Wait(); err != nil { + log.Println("Error starting script:", err) + return + } + + }(cmd) + + return nil +} +func runRestart(storeInfo StoreInfo) error { + err := runStop(storeInfo) + if err != nil { + return fmt.Errorf("failed to stop process %s: %w", storeInfo.Name, err) + } + return runStart(storeInfo) +} +func runExec(storeInfo StoreInfo, cmdParam Cmd) error { + err := SetEnvs(cmdParam.Envs) + if err != nil { + return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err) + } + log.Printf("bin path:%v", cmdParam.BinPath) + log.Printf("cmds:%v", cmdParam.Cmds) + cmd := exec.Command(cmdParam.BinPath, cmdParam.Cmds...) + if runtime.GOOS == "windows" { + // 在Windows上,通过设置CreationFlags来隐藏窗口 + cmd = SetHideConsoleCursor(cmd) + } + if err := cmd.Start(); err != nil { + return fmt.Errorf("failed to run exec process %s: %w", storeInfo.Name, err) + } + go func() { + if cmdParam.Waiting > 0 { + if err = cmd.Wait(); err != nil { + log.Printf("failed to wait exec process %s: %s", storeInfo.Name, err.Error()) + return + } + time.Sleep(time.Second * time.Duration(cmdParam.Waiting)) + } + if cmdParam.Kill { + if storeInfo.Setting.ProgressName != "" { + if err := KillProcessByName(storeInfo.Setting.ProgressName); err != nil { + log.Printf("failed to kill process %s: %s", storeInfo.Setting.ProgressName, err.Error()) + } + } else { + if err := cmd.Process.Kill(); err != nil { + log.Printf("failed to kill process %s: %s", storeInfo.Name, err.Error()) + return + } + } + + } + //log.Printf("run exec process %s, name is %s", storeInfo.Name, cmdParam.Name) + }() + + return nil +} +func WriteFile(cmd Cmd) error { + if cmd.FilePath != "" { + content := cmd.Content + if content != "" { + err := os.WriteFile(cmd.FilePath, []byte(content), 0644) + if err != nil { + return fmt.Errorf("failed to write to file: %w", err) + } + } + } + return nil +} +func ChangeFile(storeInfo StoreInfo, cmd Cmd) error { + if cmd.FilePath != "" && cmd.TplPath != "" && libs.PathExists(cmd.TplPath) && libs.PathExists(cmd.FilePath) { + content, err := os.ReadFile(cmd.TplPath) + if err != nil { + return fmt.Errorf("failed to read tpl file: %w", err) + } + exePath := GetExePath(storeInfo.Name) + contentstr := strings.ReplaceAll(string(content), "{exePath}", exePath) + contentstr = ChangeConfig(storeInfo, contentstr) + if err := os.WriteFile(cmd.FilePath, []byte(contentstr), 0644); err != nil { + return fmt.Errorf("failed to write to file: %w", err) + } + } + return nil +} +func DeleteFile(cmd Cmd) error { + if cmd.FilePath != "" { + err := os.Remove(cmd.FilePath) + if err != nil { + return fmt.Errorf("failed to delete file: %w", err) + } + } + return nil +} +func MkDir(cmd Cmd) error { + err := os.MkdirAll(cmd.FilePath, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to make dir: %w", err) + } + return nil +} +func Unzip(cmd Cmd) error { + return files.Decompress(cmd.FilePath, cmd.Content) +} +func Zip(cmd Cmd) error { + return files.Encompress(cmd.FilePath, cmd.Content) +} diff --git a/godo/store/install.go b/godo/store/install.go index 13dcb06..758af35 100644 --- a/godo/store/install.go +++ b/godo/store/install.go @@ -3,79 +3,49 @@ package store import ( "encoding/json" "fmt" - "godo/files" "godo/libs" "log" "net/http" "os" - "os/exec" "path/filepath" "regexp" - "runtime" "strings" - "time" ) -type StoreInfo struct { - Name string `json:"name"` - URL string `json:"url"` - NeedDownload bool `json:"needDownload"` - Icon string `json:"icon"` - Setting Setting `json:"setting"` - Config map[string]any `json:"config"` - Cmds map[string][]Cmd `json:"cmds"` - Install Install `json:"install"` - Start Install `json:"start"` -} -type Setting struct { - BinPath string `json:"binPath"` - ConfPath string `json:"confPath"` - DataDir string `json:"dataDir"` - LogDir string `json:"logDir"` -} - -type Item struct { - Name string `json:"name"` - Value any `json:"value"` -} -type Cmd struct { - Name string `json:"name"` - FilePath string `json:"filePath,omitempty"` - Content string `json:"content,omitempty"` - BinPath string `json:"binPath,omitempty"` - Cmds []string `json:"cmds,omitempty"` - Waiting int `json:"waiting"` - Kill bool `json:"kill"` - Envs []Item `json:"envs"` -} - -type Install struct { - Envs []Item `json:"envs"` - Cmds []string `json:"cmds"` -} - func InstallHandler(w http.ResponseWriter, r *http.Request) { pluginName := r.URL.Query().Get("name") if pluginName == "" { libs.ErrorMsg(w, "the app name is empty!") return } - exeDir := libs.GetRunDir() - exePath := filepath.Join(exeDir, pluginName) - log.Printf("the app path is %s", exePath) + exePath := GetExePath(pluginName) + //log.Printf("the app path is %s", exePath) if !libs.PathExists(exePath) { libs.ErrorMsg(w, "the app path is not exists!") return } - installFile := filepath.Join(exePath, "install.json") - if !libs.PathExists(installFile) { - libs.ErrorMsg(w, "the install.json is not exists!") + installInfo, err := GetInstallInfo(pluginName) + if err != nil { + libs.ErrorMsg(w, "the install.json is error:"+err.Error()) + return + } + if pluginName != installInfo.Name { + libs.ErrorMsg(w, "the app name must equal the install.json!") + return + } + if !installInfo.NeedInstall { + libs.SuccessMsg(w, "success", "the app is installed!") + return + } + storeFile := filepath.Join(exePath, "store.json") + if !libs.PathExists(storeFile) { + libs.ErrorMsg(w, "the store.json is not exists!") return } var storeInfo StoreInfo - content, err := os.ReadFile(installFile) + content, err := os.ReadFile(storeFile) if err != nil { - libs.ErrorMsg(w, "cant read the install.json!") + libs.ErrorMsg(w, "cant read the store.json!") return } //设置 info.json @@ -85,17 +55,16 @@ func InstallHandler(w http.ResponseWriter, r *http.Request) { err = json.Unmarshal(contentBytes, &storeInfo) if err != nil { log.Printf("Error during unmarshal: %v", err) - libs.ErrorMsg(w, "the install.json is error: "+err.Error()) + libs.ErrorMsg(w, "the store.json is error: "+err.Error()) return } replacePlaceholdersInCmds(&storeInfo) - infoFile := filepath.Join(exePath, "info.json") - err = SaveStoreInfo(storeInfo, infoFile) + storeInfo.Name = installInfo.Name + err = SaveInfoFile(storeInfo) if err != nil { - libs.ErrorMsg(w, "save the info.json is error!") + libs.ErrorMsg(w, "the store info.json is error: "+err.Error()) return } - //设置config if libs.PathExists(storeInfo.Setting.ConfPath + ".tpl") { err = SaveStoreConfig(storeInfo, exePath) @@ -109,20 +78,27 @@ func InstallHandler(w http.ResponseWriter, r *http.Request) { libs.ErrorMsg(w, "install the app is error!") return } + var res string //复制static目录 staticPath := filepath.Join(exePath, "static") if libs.PathExists(staticPath) { staticDir := libs.GetStaticDir() targetPath := filepath.Join(staticDir, pluginName) - err = os.Rename(staticPath, targetPath) - if err != nil { - // libs.ErrorMsg(w, "copy the static is error!") - // return - log.Printf("copy the static is error! %v", err) + if !libs.PathExists(targetPath) { + err = os.Rename(staticPath, targetPath) + if err != nil { + log.Printf("copy the static is error! %v", err) + } + iconPath := filepath.Join(targetPath, storeInfo.Icon) + if libs.PathExists(iconPath) { + res = "http://localhost:56780/static/" + pluginName + "/" + storeInfo.Icon + } } + } - libs.SuccessMsg(w, "success", "install the app success!") + libs.SuccessMsg(w, res, "install the app success!") } + func UnInstallHandler(w http.ResponseWriter, r *http.Request) { pluginName := r.URL.Query().Get("name") if pluginName == "" { @@ -136,8 +112,16 @@ func UnInstallHandler(w http.ResponseWriter, r *http.Request) { // return log.Printf("stop the app is error! %s", err) } - exeDir := libs.GetRunDir() - exePath := filepath.Join(exeDir, pluginName) + installInfo, err := GetInstallInfo(pluginName) + if err != nil { + libs.ErrorMsg(w, "the install.json is error:"+err.Error()) + return + } + if installInfo.IsDev { + libs.SuccessMsg(w, "success", "uninstall the app success!") + return + } + exePath := GetExePath(pluginName) //log.Printf("the app path is %s", exePath) if libs.PathExists(exePath) { err := os.RemoveAll(exePath) @@ -158,6 +142,28 @@ func UnInstallHandler(w http.ResponseWriter, r *http.Request) { libs.SuccessMsg(w, "success", "uninstall the app success!") } +func GetInstallInfo(pluginName string) (InstallInfo, error) { + var installInfo InstallInfo + exePath := GetExePath(pluginName) + installFile := filepath.Join(exePath, "install.json") + if !libs.PathExists(installFile) { + return installInfo, fmt.Errorf("install.json is not exist:%s", installFile) + } + content, err := os.ReadFile(installFile) + if err != nil { + return installInfo, err + } + err = json.Unmarshal(content, &installInfo) + if err != nil { + return installInfo, err + } + return installInfo, nil +} +func GetExePath(pluginName string) string { + exeDir := libs.GetRunDir() + return filepath.Join(exeDir, pluginName) +} + func convertValueToString(value any) string { var strValue string switch v := value.(type) { @@ -187,157 +193,69 @@ func InstallStore(storeInfo StoreInfo) error { } return nil } -func RunCmds(storeInfo StoreInfo, cmdKey string) error { - cmds := storeInfo.Cmds[cmdKey] - for _, cmd := range cmds { - - if cmd.Name == "stop" { - runStop(storeInfo) - } - if cmd.Name == "start" { - runStart(storeInfo) - } - if cmd.Name == "restart" { - runRestart(storeInfo) - } - if cmd.Name == "exec" { - runExec(storeInfo, cmd) - } - if cmd.Name == "writeFile" { - err := WriteFile(cmd) - if err != nil { - return fmt.Errorf("failed to write to file: %w", err) - } - } - if cmd.Name == "deleteFile" { - err := DeleteFile(cmd) - if err != nil { - return fmt.Errorf("failed to delete file: %w", err) - } - } - if cmd.Name == "unzip" { - err := Unzip(cmd) - if err != nil { - return fmt.Errorf("failed to unzip file: %w", err) - } - } - if cmd.Name == "zip" { - err := Zip(cmd) - if err != nil { - return fmt.Errorf("failed to unzip file: %w", err) - } - } - if cmd.Name == "mkdir" { - err := MkDir(cmd) - if err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } +func RunCmd(storeInfo StoreInfo, cmd Cmd) error { + if cmd.Name == "stop" { + runStop(storeInfo) + } + if cmd.Name == "start" { + runStart(storeInfo) + } + if cmd.Name == "restart" { + runRestart(storeInfo) + } + if cmd.Name == "exec" { + runExec(storeInfo, cmd) + } + if cmd.Name == "writeFile" { + err := WriteFile(cmd) + if err != nil { + return fmt.Errorf("failed to write to file: %w", err) } } - return nil -} -func runStop(storeInfo StoreInfo) error { - return StopCmd(storeInfo.Name) -} -func runStart(storeInfo StoreInfo) error { - err := SetEnvs(storeInfo.Start.Envs) - if err != nil { - return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err) - } - if !libs.PathExists(storeInfo.Setting.BinPath) { - return fmt.Errorf("script file %s does not exist", storeInfo.Setting.BinPath) - } - var cmd *exec.Cmd - if len(storeInfo.Start.Cmds) > 0 { - cmd = exec.Command(storeInfo.Setting.BinPath, storeInfo.Start.Cmds...) - } else { - cmd = exec.Command(storeInfo.Setting.BinPath) - } - if runtime.GOOS == "windows" { - // 在Windows上,通过设置CreationFlags来隐藏窗口 - cmd = SetHideConsoleCursor(cmd) - } - // 启动脚本命令并返回可能的错误 - go func(cmd *exec.Cmd) { - if err := cmd.Start(); err != nil { - log.Println("Error starting script:", err) - return - } - RegisterProcess(storeInfo.Name, cmd) - }(cmd) - - return nil -} -func runRestart(storeInfo StoreInfo) error { - err := runStop(storeInfo) - if err != nil { - return fmt.Errorf("failed to stop process %s: %w", storeInfo.Name, err) - } - return runStart(storeInfo) -} -func runExec(storeInfo StoreInfo, cmdParam Cmd) error { - err := SetEnvs(cmdParam.Envs) - if err != nil { - return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err) - } - log.Printf("bin path:%v", cmdParam.BinPath) - log.Printf("cmds:%v", cmdParam.Cmds) - cmd := exec.Command(cmdParam.BinPath, cmdParam.Cmds...) - if runtime.GOOS == "windows" { - // 在Windows上,通过设置CreationFlags来隐藏窗口 - cmd = SetHideConsoleCursor(cmd) - } - if err := cmd.Start(); err != nil { - return fmt.Errorf("failed to run exec process %s: %w", storeInfo.Name, err) - } - if cmdParam.Waiting > 0 { - if err = cmd.Wait(); err != nil { - return fmt.Errorf("failed to wait for exec process %s: %w", storeInfo.Name, err) - } - time.Sleep(time.Second * time.Duration(cmdParam.Waiting)) - } - if cmdParam.Kill { - if err := cmd.Process.Kill(); err != nil { - return fmt.Errorf("failed to kill process: %v", err) + if cmd.Name == "changeFile" { + err := ChangeFile(storeInfo, cmd) + if err != nil { + return fmt.Errorf("failed to change to file: %w", err) } } - log.Printf("run exec process %s, name is %s", storeInfo.Name, cmdParam.Name) - return nil -} -func WriteFile(cmd Cmd) error { - if cmd.FilePath != "" { - content := cmd.Content - if content != "" { - err := os.WriteFile(cmd.FilePath, []byte(content), 0644) - if err != nil { - return fmt.Errorf("failed to write to file: %w", err) - } - } - } - return nil -} -func DeleteFile(cmd Cmd) error { - if cmd.FilePath != "" { - err := os.Remove(cmd.FilePath) + if cmd.Name == "deleteFile" { + err := DeleteFile(cmd) if err != nil { return fmt.Errorf("failed to delete file: %w", err) } } - return nil -} -func MkDir(cmd Cmd) error { - err := os.MkdirAll(cmd.FilePath, os.ModePerm) - if err != nil { - return fmt.Errorf("failed to make dir: %w", err) + if cmd.Name == "unzip" { + err := Unzip(cmd) + if err != nil { + return fmt.Errorf("failed to unzip file: %w", err) + } + } + if cmd.Name == "zip" { + err := Zip(cmd) + if err != nil { + return fmt.Errorf("failed to unzip file: %w", err) + } + } + if cmd.Name == "mkdir" { + err := MkDir(cmd) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } } return nil } -func Unzip(cmd Cmd) error { - return files.Decompress(cmd.FilePath, cmd.Content) -} -func Zip(cmd Cmd) error { - return files.Encompress(cmd.FilePath, cmd.Content) +func RunCmds(storeInfo StoreInfo, cmdKey string) error { + cmds := storeInfo.Cmds[cmdKey] + for _, cmd := range cmds { + if _, ok := storeInfo.Cmds[cmd.Name]; ok { + RunCmds(storeInfo, cmd.Name) + } else { + RunCmd(storeInfo, cmd) + } + } + return nil } + func SetEnvs(envs []Item) error { if len(envs) > 0 { for _, item := range envs { @@ -352,6 +270,11 @@ func SetEnvs(envs []Item) error { } return nil } +func SaveInfoFile(storeInfo StoreInfo) error { + exePath := GetExePath(storeInfo.Name) + infoFile := filepath.Join(exePath, "info.json") + return SaveStoreInfo(storeInfo, infoFile) +} func SaveStoreInfo(storeInfo StoreInfo, infoFile string) error { // 使用 json.MarshalIndent 直接获取内容的字节切片 content, err := json.MarshalIndent(storeInfo, "", " ") diff --git a/godo/store/progress.go b/godo/store/progress.go index 3c672ae..0391aa6 100644 --- a/godo/store/progress.go +++ b/godo/store/progress.go @@ -10,12 +10,15 @@ import ( ) type Process struct { - Name string `json:"name"` - Running bool `json:"running"` - ExitCode int `json:"exitCode"` - Cmd *exec.Cmd `json:"cmd"` - Waiting bool `json:"waiting"` - LastPing time.Time `json:"lastPing"` + Name string `json:"name"` + Running bool `json:"running"` + ExitCode int `json:"exitCode"` + Cmd *exec.Cmd `json:"cmd"` + Pid int `json:"pid"` + ProgressName string `json:"progressName"` + Waiting bool `json:"waiting"` + IsOn bool `json:"isOn"` + LastPing time.Time `json:"lastPing"` } var ( @@ -26,13 +29,16 @@ var ( func respondWithError(w http.ResponseWriter, code int, message string) { http.Error(w, message, code) } -func RegisterProcess(name string, cmdstr *exec.Cmd) { +func RegisterProcess(name string, progressName string, isOn bool, cmdstr *exec.Cmd) { processesMu.Lock() defer processesMu.Unlock() processes[name] = &Process{ - Name: name, - Running: true, - Cmd: cmdstr, + Name: name, + Running: true, + Cmd: cmdstr, + Pid: cmdstr.Process.Pid, + IsOn: isOn, + ProgressName: progressName, } } func GetCmd(name string) *Process { @@ -42,20 +48,9 @@ func GetCmd(name string) *Process { return processes[name] } func Status(w http.ResponseWriter, r *http.Request) { - // processesMu.RLock() - // defer processesMu.RUnlock() - var ps []Process for name, cmd := range processes { - // if cmd.Cmd.ProcessState != nil && cmd.Cmd.ProcessState.Exited() { - // cmd.Running = false - // // 进程已经退出 - // ps = append(ps, Process{Name: name, Running: false, Waiting: cmd.Waiting, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()}) - // } else { - // // 进程仍在运行 - // ps = append(ps, Process{Name: name, Running: true, Waiting: cmd.Waiting, LastPing: cmd.LastPing}) - // } - ps = append(ps, Process{Name: name, Running: cmd.Running, Waiting: cmd.Waiting, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()}) + ps = append(ps, Process{Name: name, Running: cmd.Running, Waiting: cmd.Waiting, Pid: cmd.Pid, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()}) } jsonBytes, err := json.MarshalIndent(ps, "", " ") diff --git a/godo/store/start.go b/godo/store/start.go index ef4b80a..0f9609c 100644 --- a/godo/store/start.go +++ b/godo/store/start.go @@ -1,12 +1,9 @@ package store import ( - "encoding/json" "fmt" "godo/libs" - "log" "net/http" - "os" "os/exec" "path/filepath" @@ -49,27 +46,6 @@ func StartAll(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "All processes started.") } -func GetStoreInfo(name string) (StoreInfo, error) { - var storeInfo StoreInfo - exeDir := libs.GetRunDir() - infoPath := filepath.Join(exeDir, name, "info.json") - if !libs.PathExists(infoPath) { - return storeInfo, fmt.Errorf("process information for '%s' not found", name) - } - - content, err := os.ReadFile(infoPath) - if err != nil { - return storeInfo, fmt.Errorf("failed to read info.json: %v", err) - } - if err := json.Unmarshal(content, &storeInfo); err != nil { - return storeInfo, fmt.Errorf("failed to unmarshal info.json: %v", err) - } - scriptPath := storeInfo.Setting.BinPath - if !libs.PathExists(scriptPath) { - return storeInfo, fmt.Errorf("script file '%s' not found", scriptPath) - } - return storeInfo, nil -} // ExecuteScript 执行指定名称的脚本。 // 参数: @@ -77,42 +53,14 @@ func GetStoreInfo(name string) (StoreInfo, error) { // 返回值: // 返回可能遇到的错误,如果执行成功,则返回nil。 func ExecuteScript(name string) error { - cmd, exists := processes[name] - if !exists { - log.Printf("process information for '%s' not found", name) - //return fmt.Errorf("process information for '%s' not found", name) - storeInfo, err := GetStoreInfo(name) - if err != nil { - return err - } - err = runStart(storeInfo) - if err != nil { - return fmt.Errorf("failed to run script: %v", err) - } - return nil - + storeInfo, err := GetStoreInfo(name) + if err != nil { + return err } - if cmd.Running { - return nil + err = runStart(storeInfo) + if err != nil { + return fmt.Errorf("failed to run script: %v", err) } - - // if err := cmd.Cmd.Start(); err != nil { - // log.Printf("failed to start process %s: %v", name, err) - // return nil - // } - processes[name].Running = true - go func() { - - if err := cmd.Cmd.Start(); err != nil { - log.Printf("failed to start process %s: %v", name, err) - return - } - // processesMu.Lock() - // defer processesMu.Unlock() - - processes[name].Cmd = cmd.Cmd - }() - return nil } func RunOutHandler(w http.ResponseWriter, r *http.Request) { diff --git a/godo/store/stop.go b/godo/store/stop.go index 4ddc2c7..2986233 100644 --- a/godo/store/stop.go +++ b/godo/store/stop.go @@ -4,6 +4,10 @@ import ( "fmt" "log" "net/http" + "os/exec" + "runtime" + "strconv" + "strings" "time" "github.com/gorilla/mux" @@ -11,6 +15,7 @@ import ( ) func KillByPid(pid int) error { + log.Println("Killing process with PID:", pid) p, err := process.NewProcess(int32(pid)) if err != nil { return fmt.Errorf("failed to create process object: %w", err) @@ -23,15 +28,25 @@ func KillByPid(pid int) error { return nil } func StopCmd(name string) error { + processesMu.Lock() + defer processesMu.Unlock() cmd, ok := processes[name] if !ok { return fmt.Errorf("Process not found") } - processes[name].Running = false - err := KillByPid(cmd.Cmd.Process.Pid) - if err != nil { - return fmt.Errorf("failed to kill process: %w", err) + //processes[name].Running = false + if cmd.ProgressName != "" { + err := KillProcessByName(cmd.ProgressName) + if err != nil { + return fmt.Errorf("failed to kill process: %w", err) + } + } else { + err := KillByPid(cmd.Pid) + if err != nil { + return fmt.Errorf("failed to kill process: %w", err) + } } + delete(processes, name) return nil } func StopProcess(w http.ResponseWriter, r *http.Request) { @@ -54,7 +69,8 @@ func StopAllHandler() error { if err := KillByPid(cmd.Cmd.Process.Pid); err != nil { return fmt.Errorf("failed to stop process %s: %v", name, err) } - processes[name].Running = false + //processes[name].Running = false + delete(processes, name) } return nil } @@ -75,15 +91,15 @@ func ReStartProcess(w http.ResponseWriter, r *http.Request) { name := vars["name"] // Stop the process first - if cmd, ok := processes[name]; ok { - if err := KillByPid(cmd.Cmd.Process.Pid); err != nil { + if _, ok := processes[name]; ok { + if err := StopCmd(name); err != nil { // respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to stop process %s before restart: %v", name, err)) // return log.Printf("Failed to stop process %s before restart: %v", name, err) } // processesMu.Lock() // defer processesMu.Unlock() - processes[name].Running = false + //processes[name].Running = false } else { respondWithError(w, http.StatusNotFound, fmt.Sprintf("Process %s not found to restart", name)) return @@ -98,3 +114,82 @@ func ReStartProcess(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) fmt.Fprintf(w, "Process %s restarted.", name) } +func killByPids(pids []int) error { + for _, pid := range pids { + if err := KillByPid(pid); err != nil { + return fmt.Errorf("failed to kill process with PID %d: %w", pid, err) + } + fmt.Printf("Killed process with PID %d\n", pid) + } + return nil +} + +// KillProcessByName 终止所有与给定名称匹配的进程 +func KillProcessByName(processName string) error { + switch runtime.GOOS { + case "windows": + // Windows 系统下的实现 + pids, err := findPidsWindows(processName) + if err != nil { + return err + } + err = killByPids(pids) + if err != nil { + return err + } + default: + // Unix/Linux 系统下的实现 + pids, err := findPidsUnix(processName) + if err != nil { + return err + } + err = killByPids(pids) + if err != nil { + return err + } + } + return nil +} + +// findPidsUnix 在 Unix/Linux 系统下查找具有指定名称的进程的 PID +func findPidsUnix(name string) ([]int, error) { + cmd := exec.Command("pgrep", "-f", name) + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to execute pgrep: %w", err) + } + pids := strings.Fields(string(output)) + result := make([]int, len(pids)) + for i, pidStr := range pids { + pid, err := strconv.Atoi(pidStr) + if err != nil { + return nil, fmt.Errorf("failed to convert PID to integer: %w", err) + } + result[i] = pid + } + return result, nil +} + +// findPidsWindows 在 Windows 系统下查找具有指定名称的进程的 PID +func findPidsWindows(name string) ([]int, error) { + cmd := exec.Command("tasklist") + output, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("failed to execute tasklist: %w", err) + } + lines := strings.Split(string(output), "\r\n") + var pids []int + for _, line := range lines { + fields := strings.Fields(line) + if len(fields) >= 3 { + if strings.Contains(strings.ToLower(fields[0]), strings.ToLower(name)) { + pid, err := strconv.Atoi(fields[1]) + if err != nil { + return nil, fmt.Errorf("failed to convert PID to integer: %w", err) + } + pids = append(pids, pid) + } + } + } + return pids, nil +} diff --git a/godo/store/store.go b/godo/store/store.go index d0ae65a..d5188c4 100644 --- a/godo/store/store.go +++ b/godo/store/store.go @@ -6,10 +6,13 @@ import ( "godo/libs" "io" "net/http" + "os" + "path/filepath" "runtime" + "strings" ) -func GetStoreInfoHandler(w http.ResponseWriter, r *http.Request) { +func GetStoreListHandler(w http.ResponseWriter, r *http.Request) { cate := r.URL.Query().Get("cate") os := runtime.GOOS arch := runtime.GOARCH @@ -39,3 +42,76 @@ func GetStoreInfoHandler(w http.ResponseWriter, r *http.Request) { } } +func StoreSettingHandler(w http.ResponseWriter, r *http.Request) { + var req map[string]any + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + libs.ErrorMsg(w, err.Error()) + return + } + if req["name"] == nil { + libs.ErrorMsg(w, "name is required") + return + } + if req["cmdKey"] == nil { + libs.ErrorMsg(w, "cmdKey is required") + return + } + name := req["name"].(string) + cmdKey := req["cmdKey"].(string) + // 替换字符串类型的值中的反斜杠 + for key, value := range req { + if strValue, ok := value.(string); ok { + req[key] = strings.ReplaceAll(strValue, "\\", "/") + } + } + storeInfo, err := GetStoreInfo(name) + if err != nil { + libs.ErrorMsg(w, err.Error()) + return + } + // 更新storeInfo.Config + for k, v := range req { + if k != "name" && k != "cmdKey" { + storeInfo.Config[k] = v // 如果k存在,则更新;如果不存在,则新增 + } + } + err = SaveInfoFile(storeInfo) + if err != nil { + libs.ErrorMsg(w, "the store info.json is error: "+err.Error()) + return + } + _, ok := storeInfo.Cmds[cmdKey] + if !ok { + libs.ErrorMsg(w, "cmdKey is not found") + return + } + err = RunCmds(storeInfo, cmdKey) + if err != nil { + libs.ErrorMsg(w, err.Error()) + return + } + libs.SuccessMsg(w, "success", "success") + +} +func GetStoreInfo(name string) (StoreInfo, error) { + var storeInfo StoreInfo + exeDir := libs.GetRunDir() + infoPath := filepath.Join(exeDir, name, "info.json") + if !libs.PathExists(infoPath) { + return storeInfo, fmt.Errorf("process information for '%s' not found", name) + } + + content, err := os.ReadFile(infoPath) + if err != nil { + return storeInfo, fmt.Errorf("failed to read info.json: %v", err) + } + if err := json.Unmarshal(content, &storeInfo); err != nil { + return storeInfo, fmt.Errorf("failed to unmarshal info.json: %v", err) + } + scriptPath := storeInfo.Setting.BinPath + if !libs.PathExists(scriptPath) { + return storeInfo, fmt.Errorf("script file '%s' not found", scriptPath) + } + return storeInfo, nil +} diff --git a/godo/store/types.go b/godo/store/types.go new file mode 100644 index 0000000..bb05c31 --- /dev/null +++ b/godo/store/types.go @@ -0,0 +1,65 @@ +package store + +// InstallInfo 描述了应用程序的安装信息。 +// 包含应用程序的名称、下载地址、版本号等关键安装信息。 +type InstallInfo struct { + Name string `json:"name"` // 应用程序名称。 + URL string `json:"url"` // 应用程序下载地址。 + WebUrl string `json:"webUrl"` // 应用程序的网页地址。 + IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。 + NeedDownload bool `json:"needDownload"` // 标志位,表示是否需要下载。 + NeedInstall bool `json:"needInstall"` // 标志位,表示是否需要安装。 + Version string `json:"version"` // 应用程序的版本号。 + Desc string `json:"desc"` // 应用程序的描述信息。 + Icon string `json:"icon"` // 应用程序的图标路径。 + CheckProgress bool `json:"checkProgress"` // 标志位,表示是否显示启动和停止。 + HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。 + Setting bool `json:"setting"` // 标志位,表示是否需要配置。 +} + +// StoreInfo 维护了应用程序商店的信息。 +// 包含应用程序的名称、图标、配置信息等。 +type StoreInfo struct { + Name string `json:"name"` // 应用程序商店的名称。 + Icon string `json:"icon"` // 应用程序商店的图标路径。 + Setting Setting `json:"setting"` // 应用程序商店的配置信息。 + Config map[string]any `json:"config"` // 应用程序的配置信息映射。 + Cmds map[string][]Cmd `json:"cmds"` // 应用程序的命令集合。 + Install Install `json:"install"` // 安装应用程序的信息。 + Start Install `json:"start"` // 启动应用程序的信息。 +} + +// Setting 描述了应用程序的设置信息。 +// 包含应用程序的二进制文件路径、配置文件路径等关键设置信息。 +type Setting struct { + BinPath string `json:"binPath"` // 应用程序二进制文件的路径。 + ConfPath string `json:"confPath"` // 应用程序配置文件的路径。 + ProgressName string `json:"progressName"` // 进程的名称。 + IsOn bool `json:"isOn"` //是否守护进程运行。 +} + +// Item 是一个通用的键值对结构体,用于表示配置项或环境变量等。 +type Item struct { + Name string `json:"name"` // 配置项的名称。 + Value any `json:"value"` // 配置项的值。 +} + +// Cmd 描述了一个命令的详细信息。 +// 包含命令的名称、执行路径、环境变量等。 +type Cmd struct { + Name string `json:"name"` // 命令的名称。 + FilePath string `json:"filePath,omitempty"` // 命令文件的路径。 + Content string `json:"content,omitempty"` // 命令的内容。 + BinPath string `json:"binPath,omitempty"` // 执行命令的二进制文件路径。 + TplPath string `json:"tplPath,omitempty"` // 命令的模板路径。 + Cmds []string `json:"cmds,omitempty"` // 要执行的子命令列表。 + Waiting int `json:"waiting"` // 等待的时间。 + Kill bool `json:"kill"` // 标志位,表示是否需要终止之前的命令。 + Envs []Item `json:"envs"` // 命令执行时的环境变量。 +} + +// Install 描述了安装过程中的环境变量和命令列表。 +type Install struct { + Envs []Item `json:"envs"` // 安装过程中需要的环境变量。 + Cmds []string `json:"cmds"` // 安装过程中需要执行的命令列表。 +}