Browse Source

change store

master
godo 10 months ago
parent
commit
6398e6d68f
  1. 37
      README.en.md
  2. 35
      README.md
  3. 236
      docs/Store.en.md
  4. 48
      docs/Store.md
  5. 7
      docs/demo/mysql5.7/install.json
  6. 1
      docs/demo/mysql5.7/store.json
  7. 7
      docs/demo/mysql8.0/install.json
  8. 1
      docs/demo/nginx/store.json
  9. 12
      docs/demo/redis5.0/install.json
  10. 1
      docs/demo/redis5.0/store.json
  11. 25
      frontend/src/assets/store.scss
  12. 37
      frontend/src/components/store/AddApp.vue
  13. 95
      frontend/src/components/store/AppItem.vue
  14. 116
      frontend/src/components/store/Store.vue
  15. 53
      frontend/src/i18n/lang/en.json
  16. 88
      frontend/src/i18n/lang/zh.json
  17. 62
      frontend/src/stores/store.ts
  18. 5
      frontend/src/system/config.ts
  19. 1
      go.mod
  20. 2
      go.sum
  21. 2
      godo/cmd/main.go
  22. 4
      godo/files/fs.go
  23. 47
      godo/files/os.go
  24. 134
      godo/files/unzip.go
  25. 1
      godo/go.mod
  26. 2
      godo/go.sum
  27. 3
      godo/libs/msg.go
  28. 3
      godo/store/cmds.go
  29. 281
      godo/store/download.go
  30. 207
      godo/store/install.go
  31. 85
      godo/store/store.go
  32. 32
      godo/store/types.go
  33. 36
      godo/store/upload.go

37
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) | [AppStore](./docs/Store.md)
[FAQ](./docs/Faq.md) | [API](./docs/Api.md) | [AppStore](./docs/Store.en.md)
### Install
@ -46,6 +46,8 @@ English | [简体中文](README.md)
- ***Small in size, packaged for only 61M, it does include all the office suites***
- ***Unlimited scalability, supports custom applications***
- ***Golang develops backend with low resource consumption and high performance***
- ***Supports multiple platforms, Windows、Linux、MacOS***
- ***A complete app store system, simply learn [App Store Configuration](./docs/Store.en.md) to develop complex applications***
## Function Description
@ -53,65 +55,66 @@ English | [简体中文](README.md)
- Exquisite imitation of Windows style
- Desktop file management
- Support native file drag and drop upload
<img src="./docs/img/home.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/home.png" width="600" />
### 2、 File management
- Drag and drop file upload
- File Search
- Native file storage
<img src="./docs/img/file.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/file.png" width="600" />
### 3、 Internal chat
- No need for complicated registration process, simply discover and list all available chat partners within the same local area network, and start instant messaging immediately. Support LAN based instant messaging, file transfer, and other functions to facilitate seamless communication and collaboration within the team.
<img src="./docs/img/localchat.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/localchat.png" width="600" />
### 4、 Documents
- Simple Word editor, native storage, supports QR codes, handwritten signatures, import and export
<img src="./docs/img/doc.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/doc.png" width="600" />
### 5、 Table
- Native storage, Excel editor, supports import and export, supports images and formulas
<img src="./docs/img/excel.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/excel.png" width="600" />
### 6、 Markdown
- Native storage, using VDitors, supporting import and export, outline and real-time preview
<img src="./docs/img/markdown.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/markdown.png" width="600" />
### 7、 Mind map
- Built in multiple themes; Support shortcut keys; Node content supports images, icons, hyperlinks, notes, and tags.
<img src="./docs/img/mind.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/mind.png" width="600" />
### 8、 Presentation Presentation
- Native storage, using pptist, supporting text, images, shapes, lines, charts, tables, videos, formulas, etc.
<img src="./docs/img/ppt.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/ppt.png" width="600" />
### 9、 File Editor
- Native storage, supports opening text/html/css/js/svg/xml/md, etc., can be used as a simple online editplus.
<img src="./docs/img/fileeditor.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/fileeditor.png" width="600" />
### 10、 Whiteboard
- Integrating various creative expression abilities such as free layout, paintbrushes, and notes, it inspires team creativity and enables communication around a whiteboard anytime, anywhere.
<img src="./docs/img/baiban.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/baiban.png" width="600" />
### 11、 Image editing
- A small Photoshop with native storage, supporting image search, cropping, rotation, scaling, filtering, and other functions
<img src="./docs/img/pic.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/pic.png" width="600" />
### 12、 Gantt Chart
- A must-have tool for project management, supporting custom project personnel and roles, and supporting drag and drop/management allocation (resources, roles, work), etc.
<img src="./docs/img/gant.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/gant.png" width="600" />
### 13、 Browser
- A simple built-in browser
<img src="./docs/img/ie.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/ie.png" width="600" />
### 14、 System settings
- You can switch storage methods and system backgrounds here.
<img src="./docs/img/setting-store.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/setting-store.png" width="600" />
### 15、 App Store
- App store management allows for the addition of external applications.
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/store.png" width="600" />
### 16、 Screenshot
- A simple screenshot tool. Save the screenshot file locally.
@ -121,7 +124,7 @@ English | [简体中文](README.md)
### 18、 Calculator
- A calculator that mimics Windows 10 and supports historical records.
<img src="./docs/img/cal.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/cal.png" width="600" />
### 19、 Music Library
- A simple sound storage library that supports playing music.
@ -131,7 +134,7 @@ English | [简体中文](README.md)
### 21、 Kanban board
- Support standard kanban, a must-have tool for project management, which can quickly create kanban and place it in different folders
<img src="./docs/img/kanban.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/kanban.png" width="600" />
### 22、 Process management
- Support process management, can view process list, kill processes

35
README.md

@ -67,66 +67,67 @@
- 精仿windows风格
- 桌面文件管理
- 支持原生文件拖拽上传
<img src="./docs/img/home.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/home.png" width="600" />
### 二、文件管理
- 文件拖拽上传
- 文件搜索
- 原生文件存储
- 直接压缩/解压文件夹(本地存储支持zip/tar/gz/bz2)
<img src="./docs/img/file.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/file.png" width="600" />
### 三、内网聊天
- 无需注册流程,只需在同一内网,即可自动发现并列出所有可用的聊天对象,支持基于局域网的即时消息传输、文件传输等功能。
<img src="./docs/img/localchat.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/localchat.png" width="600" />
### 四、文档
- 简便的word编辑器,原生存储,支持二维码、手写签名,导入导出
<img src="./docs/img/doc.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/doc.png" width="600" />
### 五、表格
- 原生存储,Excel编辑器,支持导入、导出,支持图片、公式
<img src="./docs/img/excel.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/excel.png" width="600" />
### 六、markdown
- 原生存储,采用vditor,支持导入、导出,支持大纲、实时预览
<img src="./docs/img/markdown.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/markdown.png" width="600" />
### 七、思维导图
- 内置多种主题;支持快捷键;节点内容支持图片、图标、超链接、备注、标签。
<img src="./docs/img/mind.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/mind.png" width="600" />
### 八、演示文稿
- 原生存储,采用pptist,支持文字、图片、形状、线条、图表、表格、视频、公式等。
<img src="./docs/img/ppt.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/ppt.png" width="600" />
### 九、文件编辑器
- 原生存储,支持打开text/html/css/js/svg/xml/md等,可以当作一个简单的在线editplus。
<img src="./docs/img/fileeditor.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/fileeditor.png" width="600" />
### 十、白板
- 集自由布局、画笔、便签多种创意表达能力于一体,激发团队创造力,随时随地,围绕一块白板沟通。
<img src="./docs/img/baiban.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/baiban.png" width="600" />
### 十一、图片编辑
- 一个小型的photoshop,原生存储,支持搜索图片,支持图片裁剪、旋转、缩放、滤镜等功能
<img src="./docs/img/pic.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/pic.png" width="600" />
### 十二、甘特图
- 项目管理必备工具,支持自定义项目人员和角色,支持拖拽/管理分配(资源、角色、工作)等。
<img src="./docs/img/gant.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/gant.png" width="600" />
### 十三、浏览器
- 一款简单的内置浏览器
<img src="./docs/img/ie.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/ie.png" width="600" />
### 十四、系统设置
- 可在这里切换存储方式,可切换系统背景。
<img src="./docs/img/setting-store.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/setting-store.png" width="600" />
### 十五、应用商店
- 应用商店管理,丰富的外部接口,可导入/添加/下载外部应用。
- 应用商店管理,丰富的外部接口,可导入/添加/下载外部应用。支持依赖库安装/卸载。
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/store.png" width="600" />
### 十六、截图
- 一个简单的截图工具。截图后文件存到本地。
@ -136,7 +137,7 @@
### 十八、计算器
- 一个仿windows10的计算器,支持历史记录。
<img src="./docs/img/cal.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/cal.png" width="600" />
### 十九、音乐库
- 一个简单的声音存储库,支持播放音乐。
@ -146,7 +147,7 @@
### 二十一、看板
- 支持标准看板,项目管理必备工具,可快速创建看板并放置到不同的文件夹
<img src="./docs/img/kanban.png" width="600" />
<img src="https://gitee.com/ruitao_admin/godoos-image/raw/master/img/kanban.png" width="600" />
### 二十二、进程管理
- 支持进程管理,可以查看进程列表,杀死进程

236
docs/Store.en.md

@ -0,0 +1,236 @@
## GodoOS App Store Development Tutorial
### Quick Start
1. Download the [mysql5.7 zip package](https://downloads.mysql.com/archives/get/p/23/file/mysql-5.7.44-winx64.zip), and extract it to the `.godoos/run/windows/` directory under the user directory, naming the folder `mysql5.7`.
2. Copy the `mysql5.7` from this program's `docs/demo/mysql5.7` to the `.godoos/run/windows/mysql5.7` directory under the user directory.
3. Open the app store, add an app, select development mode, input `mysql5.7` as the local path, and click OK.
### Development Requirements
1. Basic HTML development skills.
2. Familiarity with the executable file startup process, then configure the JSON file according to the following flow.
### How to Add an Application
1. Create an application folder under the `.godoos/run/windows/` directory in the user directory.
2. An application requires two configuration files; create the `install.json` and `store.json` files at the root of the application directory. The configuration file formats are as follows:
- `install.json` [Sample](./demo/mysql5.7/install.json)
```json
{
"name": "", // string, Name of the application.
"url": "", // string, Download URL of the application or adapter package.
"pkg": "", // string, Official download URL of the application. Can be empty.
"webUrl":"", // string, If set, the application will be displayed on the desktop.
"isDev": true, // boolean, Whether it is a development environment. If set to true, data will not be downloaded.
"version": "1.0.0", // string, Version of the application.
"icon": "", // string, Icon of the application, accessible network address.
"hasStart": true, // boolean, Indicates whether startup and shutdown are shown.
"hasRestart": true, // boolean, Whether a restart is needed.
"setting": true // boolean, Whether settings are needed. Only shown when the application is stopped.
}
```
- Note: If a web application does not require a backend process, store.json does not need to be configured.
The structure of install.json is:
```json
type InstallInfo struct {
Name string `json:"name"` // Name of the application. Important, must match the directory name of the application.
URL string `json:"url"` // Download URL of the application or adapter package.
Pkg string `json:"pkg"` // Official download URL of the application.
WebUrl string `json:"webUrl"` // Web address of the application.
IsDev bool `json:"isDev"` // Flag indicating whether it is a developer version.
Version string `json:"version"` // Version number of the application.
Desc string `json:"desc"` // Description information of the application.
Icon string `json:"icon"` // Path to the application icon.
HasStart bool `json:"hasStart"` // Flag indicating whether startup and shutdown are shown.
HasRestart bool `json:"hasRestart"` // Flag indicating whether a restart is needed after installation.
Setting bool `json:"setting"` // Flag indicating whether configuration is needed.
Dependencies []Item `json:"dependencies"` // Dependencies.
History []InstallHastory `json:"history"`// History
}
type InstallHastory struct {
Version string `json:"version"`
URL string `json:"url"`
Pkg string `json:"pkg"` // Official download URL of the application.
}
```
- store.json [Sample](./demo/mysql5.7/store.json)
```json
{
"setting": {
"binPath": "{exePath}/bin/mysqld.exe", // string, Important, must be set. Path to the startup program.
"confPath": "{exePath}/my.ini", // string, Can be empty. Path to the configuration file.
"progressName": "mysqld.exe", // string, Process name. Not required if single-threaded.
"isOn": true // boolean, Whether to start the daemon process.
},
"config": { // object, Configuration file. Any configuration inside can be filled out, used in conjunction with commands. Can be set via HTTP.
},
"commands": {}, // object, List of commands. Available for invocation by `installCmds` inside `install`, also callable through external HTTP requests.
"install": { // object, Installation configuration.
"installEnvs": [], // object[], Environment variables.
"installCmds": [] // object[], Startup commands. Can invoke commands from the command list set `commands`.
},
"start": {
"startEnvs": [],
"beforeCmds": [], // List of commands to execute before startup. Can invoke commands from the command list set `commands`.
"startCmds": [ // object[], Pure parameter command set. Will start `setting.binPath`, cannot invoke commands from the `commands` list.
"--defaults-file={exePath}/my.ini"
],
"AfterCmds": [] // List of commands to execute after startup. Can invoke commands from the command list set `commands`.
}
}
```
- Note: The core replacement parameter is `{exePath}`, which is the execution directory of the program. Other `{parameters}` correspond to the config in `store.json`.
The structure for `store.json` is as follows:
```json
type StoreInfo struct {
Setting Setting `json:"setting"` // Configuration information for the application store.
Config map[string]any `json:"config"` // Mapping of application configuration information.
Commands map[string][]Cmd `json:"commands"` // Collection of application commands.
Install InstallStruct `json:"install"` // Information for installing the application.
Start StartStruct `json:"start"` // Information for starting the application.
}
```
The structure for Setting is:
```json
// Contains critical setting information such as the binary file path and configuration file path of the application.
type Setting struct {
BinPath string `json:"binPath"` // Path to the application's binary file.
ConfPath string `json:"confPath"` // Path to the application's configuration file.
ProgressName string `json:"progressName"` // Name of the process.
IsOn bool `json:"isOn"` // Indicates if the daemon process is running.
}
```
The structure for Cmd is:
```json
type Cmd struct {
Name string `json:"name"` // Name of the command.
FilePath string `json:"filePath,omitempty"` // Path to the command file.
Content string `json:"content,omitempty"` // Content of the command.
BinPath string `json:"binPath,omitempty"` // Path to the binary file for executing the command.
TplPath string `json:"tplPath,omitempty"` // Template path for the command.
Cmds []string `json:"cmds,omitempty"` // List of subcommands to be executed.
Waiting int `json:"waiting"` // Waiting time.
Kill bool `json:"kill"` // Flag indicating whether to terminate previous commands. If the process name is set in `content`, priority is given to killing the process by name.
Envs []Item `json:"envs"` // Environment variables during command execution.
}
```
The structure for Install is:
```json
// `InstallStruct` describes environment variables and command lists during the installation process.
type InstallStruct struct {
InstallEnvs []Item `json:"installEnvs"` // Environment variables required during installation.
InstallCmds []string `json:"installCmds"` // List of commands to execute during installation.
}
type StartStruct struct {
StartEnvs []Item `json:"startEnvs"` // Environment variables required during startup.
BeforeCmds []string `json:"beforeCmds"` // List of commands to execute before startup. Commands can be invoked from the `commands` list.
StartCmds []string `json:"startCmds"` // List of commands to execute during startup.
AfterCmds []string `json:"afterCmds"` // List of commands to execute after startup. Commands can be invoked from the `commands` list.
}
// `Item` is a generic key-value pair structure used to represent configuration items or environment variables, etc.
type Item struct {
Name string `json:"name"` // Name of the configuration item.
Value any `json:"value"` // Value of the configuration item.
}
```
3. To add an application in the app store, select local addition and input the name of the application directory (no need to fill in the entire directory).
### Explanation of the Configuration File `store.json`
1. install can invoke commands from the commands list.
2. Commands in the commands list can also invoke their own commands.
3. All commands can be chained together.
### How to Set Configuration
1. Create a static directory under the application directory and create an index.html file. Set setting to true in install.json. Frontend configuration example [Sample](./demo/mysql5.7/static/index.html)
```js
const postData = {
dataDir: dataDir, // Corresponds to the config configuration item in store.json
logDir: logDir, // Corresponds to the config configuration item in store.json
port: port, // Corresponds to the config configuration item in store.json
name: "mysql5.7", // Application name
cmdKey: "setting" // Command key, name of cmds
};
const comp = await fetch('http://localhost:56780/store/setting', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(postData)
});
```
- It is crucial to set name and cmdKey correctly; name is the application name, and cmdKey corresponds to the key in the commands object in store.json. An object can configure a series of commands, which can refer to the structure of Cmd, sample:
```json
"commands": {
"initData": [
{
"name": "exec",
"binPath": "{exePath}/bin/mysqld.exe",
"cmds": [
"--defaults-file={exePath}/my.ini",
"--initialize"
],
"waiting": 1 //Waiting seconds
},
{
"name": "exec",
"binPath": "{exePath}/bin/mysqld.exe",
"cmds": [
"--defaults-file={exePath}/my.ini",
"--init-file={exePath}/password.txt"
],
"waiting": 3,
"content": "mysqld.exe",
"kill": true
},
{
"name": "start"
}
],
"setting": [
{
"name": "changeFile",
"tplPath": "{exePath}/my.ini.tpl",
"filePath": "{exePath}/my.ini"
},
{
"name": "initData"
}
],
}
```
- In the above sample, setting invokes the initData command again.
- The fixed address for POST is http://localhost:56780/store/setting
- The principle works by sending configuration information to the store service through an http request to the /store/setting interface. Then, the store service will automatically update the configuration information and start the application based on the configuration information.
### Explanation of the static Directory
1. `index.html` is the homepage of the application.
2. When install is executed, files under the static directory are automatically copied to the .godoos/static/ application directory.
3. If store.json sets icon and the file exists in the static directory, the application icon is that file. Otherwise, it is the icon in install.json.
### Description of Built-in Applications
- The system encapsulates some functions for handling process control and file operations. Below are detailed descriptions of each function:
1. `start` Start the application.
2. `stop` Stop the application.
3. `restart` Restart the application.
4. `exec` Execute a command. Must set binPath and cmds.
5. `writeFile` Write to a file. Must set filePath and content. Based on config, replaces {parameters} in content.
6. `changeFile` Modify a file. Must set filePath and tplPath. Based on config, performs replacements in the template file.
7. `deleteFile` Delete a file.
8. `unzip` Unzip a file. Must set filePath and content. content is the extraction directory.
9. `zip` Compress a file. Must set filePath and content. filePath is the folder to be compressed, and content is the name of the compressed file.
10. `mkdir` Create a directory. Must set FilePath, which is the path of the directory to be created.
11. `startApp` Start another application. content is the name of the application.
12. `stopApp` Stop another application. content is the name of the application.
### Advanced Operations
1. Download [mysql8.0](https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.39-winx64.zip)
2. Refer to the mysql8.0 directory under demo and attempt to create your own installation package.

48
docs/Store.md

@ -18,16 +18,15 @@
```json
{
"name": "", // string, 应用程序名称。
"url": "", // string, 应用程序下载地址。
"url": "", // string, 应用程序下载地址或适配包的下载地址。
"pkg": "", // string, 应用程序的官方下载地址。可为空。
"webUrl":"", // string, 如何设置,应用程序将显示到桌面。
"isDev": true, // boolean, 是否为开发环境。如果设置为true将不会下载数据。
"needDownload": true, // boolean, 是否需要下载,本地开发设置为false。
"needInstall": true, // boolean, 是否需要安装。
"version": "1.0.0", // string, 应用程序版本。
"version": "1.0.0", // string, 应用程序版本。
"icon": "", // string, 应用程序图标,为可访问的网络地址。
"checkProgress": true, // boolean, 表示是否显示启动和停止。
"hasStart": true, // boolean, 表示是否显示启动和停止。
"hasRestart": true, // boolean, 是否需要重启。
"setting": true // boolean, 是否需要设置。
"setting": true // boolean, 是否需要设置。只有应用停止才会显示。
}
```
@ -36,19 +35,25 @@
install.json的结构体为:
```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"` // 标志位,表示是否需要配置。
Dependencies []Item `json:"dependencies"` // 依赖项。
Name string `json:"name"` // 应用程序名称。
URL string `json:"url"` // 应用程序下载地址或适配包的下载地址。
Pkg string `json:"pkg"` // 应用程序的官方下载地址。
WebUrl string `json:"webUrl"` // 应用程序的网页地址。
IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
Version string `json:"version"` // 应用程序的版本号。
Desc string `json:"desc"` // 应用程序的描述信息。
Icon string `json:"icon"` // 应用程序的图标路径。
HasStart bool `json:"hasStart"` // 标志位,表示是否显示启动和停止。
HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
Setting bool `json:"setting"` // 标志位,表示是否需要配置。
Dependencies []Item `json:"dependencies"` // 依赖项。
Categrory string `json:"category"` // 应用程序的分类。
History []InstallHastory `json:"history"` // 应用程序的历史版本。
}
type InstallHastory struct {
Version string `json:"version"`
URL string `json:"url"`
Pkg string `json:"pkg"` // 应用程序的官方下载地址。
}
```
@ -57,7 +62,6 @@ type InstallInfo struct {
```json
{
"name": "mysql5.7", // string, 应用程序名称。可不设置,会继承自install.json
"icon": "mysql.png", // string, 应用程序图标,一般放在应用程序static目录下。
"setting": {
"binPath": "{exePath}/bin/mysqld.exe", // string, 重要,必须设置。为启动程序路径。
"confPath": "{exePath}/my.ini",// string, 可为空。为配置文件路径。
@ -84,11 +88,11 @@ type InstallInfo struct {
- 备注:核心的替换参数为`{exePath}`即程序的执行目录。其他`{参数}`对应`store.json`里的`config`。
store.json的struct为
store.json的结构体为:
```json
type StoreInfo struct {
Name string `json:"name"` // 应用程序商店的名称。
Icon string `json:"icon"` // 应用程序商店的图标路径。
Setting Setting `json:"setting"` // 应用程序商店的配置信息。
Config map[string]any `json:"config"` // 应用程序的配置信息映射。
Commands map[string][]Cmd `json:"commands"` // 应用程序的命令集合。

7
docs/demo/mysql5.7/install.json

@ -1,12 +1,11 @@
{
"name": "mysql5.7",
"url": "https://downloads.mysql.com/archives/get/p/23/file/mysql-5.7.44-winx64.zip",
"pkg": "https://downloads.mysql.com/archives/get/p/23/file/mysql-5.7.44-winx64.zip",
"url": "https://gitee.com/ruitao_admin/godoos-image/releases/download/godoos-store/mysql5.7.zip",
"isDev": true,
"needDownload": true,
"needInstall": true,
"version": "5.7.44",
"icon": "https://img0.baidu.com/it/u=1087008535,3349487719&fm=253&fmt=auto&app=138&f=PNG?w=969&h=500",
"checkProgress": true,
"hasStart": true,
"hasRestart": true,
"setting": true,
"isOn": true

1
docs/demo/mysql5.7/store.json

@ -1,5 +1,4 @@
{
"icon": "mysql.png",
"setting": {
"binPath": "{exePath}/bin/mysqld.exe",
"confPath": "{exePath}/my.ini",

7
docs/demo/mysql8.0/install.json

@ -1,12 +1,11 @@
{
"name": "mysql8.0",
"url": "https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.39-winx64.zip",
"pkg": "https://dev.mysql.com/get/Downloads/MySQL-8.0/mysql-8.0.39-winx64.zip",
"url": "",
"isDev": true,
"needDownload": true,
"needInstall": true,
"version": "8.0.39",
"icon": "https://img0.baidu.com/it/u=1087008535,3349487719&fm=253&fmt=auto&app=138&f=PNG?w=969&h=500",
"checkProgress": true,
"hasStart": true,
"hasRestart": true,
"setting": true,
"isOn": true

1
docs/demo/nginx/store.json

@ -1,6 +1,5 @@
{
"name": "nginx",
"icon": "nginx.png",
"setting": {
"binPath": "{exePath}/nginx.exe",
"progressName":"nginx.exe"

12
docs/demo/redis5.0/install.json

@ -1,12 +0,0 @@
{
"name": "redis5.0",
"icon": "redis.png",
"setting": {
"binPath": "{exePath}/redis-server.exe"
},
"start": {
"startCmds": [
"{exePath}/redis.windows.conf"
]
}
}

1
docs/demo/redis5.0/store.json

@ -1,6 +1,5 @@
{
"name": "redis5.0",
"icon": "redis.png",
"setting": {
"binPath": "{exePath}/redis-server.exe"
},

25
frontend/src/assets/store.scss

@ -216,17 +216,34 @@
}
.main-app {
width: 100%;
width: 96%;
/* height: 100%; */
display: flex;
padding:2%;
flex-direction: row;
flex-wrap: wrap;
gap: 10px;
justify-content: flex-start;
align-items: flex-start;
padding: 10px;
overflow: auto;
align-content: flex-start;
}
.store-item {
width: calc(33.333% - 10px); /* Subtract the gap size */
box-sizing: border-box; /* Include padding and border in element's total width */
}
/* For screens smaller than 768px, make it one column */
@media (max-width: 768px) {
.store-item {
width: 100%;
}
}
/* For screens between 768px and 1024px, make it two columns */
@media (min-width: 768px) and (max-width: 1024px) {
.store-item {
width: calc(50% - 10px);
}
}
.store-noready {

37
frontend/src/components/store/AddApp.vue

@ -22,16 +22,16 @@ const formData = ref({
const progress = ref(0)
const addType = [
{
'name': '远程下载',
'type': 'download'
name: t('store.remoteDownload'),
type: 'download'
},
{
'name': '本地导入',
'type': 'local'
name: t('store.localImport'),
type: 'local'
},
{
'name': '开发模式',
'type': 'dev'
name: t('store.devMode'),
type: 'dev'
}
]
function selectFile() {
@ -96,15 +96,15 @@ async function upload(e: any) {
}
const item = await completion.json()
//console.log(item)
if(item.data){
if (item.data) {
await installPlugin(item.data)
}
}
}
async function addAppByDev() {
if(formData.value.devPath && formData.value.devPath != ""){
if (formData.value.devPath && formData.value.devPath != "") {
await installPlugin(formData.value.devPath)
}
}
}
async function installPlugin(pluginName: string) {
const completion = await fetch(apiUrl + '/store/installInfo?name=' + pluginName)
@ -121,30 +121,31 @@ async function installPlugin(pluginName: string) {
</script>
<template>
<el-form label-width="auto" style="max-width: 600px;padding-left: 30px;">
<el-form-item label="添加方式">
<el-form-item :label="t('store.addMethod')">
<el-select v-model="formData.importType" style="width: 280px;">
<el-option v-for="item in addType" :key="item.type" :label="item.name" :value="item.type" />
</el-select>
</el-form-item>
<template v-if="formData.importType == 'download'">
<el-form-item label="下载地址">
<el-form-item :label="t('store.downloadUrl')">
<el-input v-model="formData.url" style="width: 280px;" />
</el-form-item>
<el-form-item label="下载进度" v-if="progress > 0">
<el-form-item :label="t('store.downloadProgress')" v-if="progress > 0">
<el-progress :text-inside="true" :stroke-width="20" :percentage="progress" />
</el-form-item>
<el-button type="primary" @click="addAppByDownload()">添加</el-button>
<el-button type="primary" @click="addAppByDownload()">{{ t('store.add') }}</el-button>
</template>
<template v-if="formData.importType == 'local'">
<input type="file" style="display: none;" ref="fileInput" accept=".zip,.tar,.tar.gz,.tar.bz2" @change="upload">
<el-button type="primary" @click="addAppByImport()">导入压缩包</el-button>
<input type="file" style="display: none;" ref="fileInput" accept=".zip,.tar,.tar.gz,.tar.bz2"
@change="upload">
<el-button type="primary" @click="addAppByImport()">{{ t('store.importZip') }}</el-button>
</template>
<template v-if="formData.importType == 'dev'">
<el-form-item label="本地路径">
<el-input v-model="formData.devPath" style="width: 280px;" placeholder="用户目录下.godoos/run,只填目录名称"
<el-form-item :label="t('store.localPath')">
<el-input v-model="formData.devPath" style="width: 280px;" :placeholder="t('store.userDirectory')"
@click="selectFile()" />
</el-form-item>
<el-button type="primary" @click="addAppByDev()">添加</el-button>
<el-button type="primary" @click="addAppByDev()">{{ t('store.add') }}</el-button>
</template>
</el-form>
</template>

95
frontend/src/components/store/AppItem.vue

@ -1,41 +1,14 @@
<template>
<div class="app-item">
<img v-if="item?.icon" draggable="false" class="app-img" :src="item?.icon" alt="" />
<div class="app-content">
<div class="app-title">{{ item?.name }}</div>
<div class="app-desc">
<span>{{ item?.desc ?? item?.name }}</span>
<el-progress :text-inside="true" style="margin-top: 3px;" v-if="item?.progress" :stroke-width="20"
:percentage="item?.progress" />
</div>
<div class="app-button">
<template v-if="installedList!.includes(item?.name)">
<div v-if="item!.checkProgress">
<button @click="pause?.(item)" v-if="item?.isRuning">暂停</button>
<button @click="start?.(item)" v-else>启动</button>
</div>
<div v-if="item!.hasRestart && item?.isRuning">
<button @click="restart?.(item)">重启</button>
</div>
<button @click="setting?.(item)" v-if="!item?.isRuning && item?.setting">配置</button>
<button @click="uninstall?.(item)" v-if="!item?.isRuning">卸载</button>
</template>
<template v-else>
<button @click="install?.(item)" :disabled="item?.progress">安装</button>
</template>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { BrowserWindow } from "@/system";
defineProps({
import { t } from "@/i18n";
const downloading = ref(false)
const props = defineProps({
item: Object,
installedList: Array,
install: Function,
install: {
type: Function,
required: true,
},
uninstall: Function,
pause: Function,
start: Function,
@ -44,7 +17,7 @@ defineProps({
function setting(item: any) {
const win = new BrowserWindow({
title: "配置",
title: t('store.setting'),
url: `http://localhost:56780/static/${item.name}/index.html`,
icon: "gallery",
width: 500,
@ -55,13 +28,45 @@ function setting(item: any) {
});
win.show()
}
async function installApp(item: any) {
downloading.value = true;
await props.install(item);
}
</script>
<template>
<div class="app-item">
<img v-if="item?.icon" draggable="false" class="app-img" :src="item?.icon" alt="" />
<div class="app-content">
<div class="app-title">{{ item?.name }}</div>
<div class="app-desc">
<span>{{ item?.desc ?? item?.name }}</span>
<el-progress :text-inside="true" style="margin-top: 3px;" v-if="item?.progress > 0" :stroke-width="20"
:percentage="item?.progress" />
</div>
<div class="app-button">
<template v-if="installedList!.includes(item?.name)">
<template v-if="item!.hasStart">
<el-button @click="pause?.(item)" v-if="item?.isRuning">{{ t('store.stop') }}</el-button>
<el-button @click="start?.(item)" v-else>{{ t('store.start') }}</el-button>
</template>
<el-button @click="restart?.(item)" v-if="item!.hasRestart && item?.isRuning">{{ t('store.restart') }}</el-button>
<el-button @click="setting?.(item)" v-if="!item?.isRuning && item?.setting">{{ t('store.setting') }}</el-button>
<el-button @click="uninstall?.(item)" v-if="!item?.isRuning">{{ t('store.uninstall') }}</el-button>
</template>
<template v-else>
<el-button type="primary" @click="installApp(item)" :disabled="item?.progress" :loading="downloading">{{ t('store.install') }}</el-button>
</template>
</div>
</div>
</div>
</template>
<style scoped>
.app-item {
width: calc(100% - 20px);
min-height: 100px;
min-width:200px;
margin: 10px;
height: 120px;
display: flex;
flex-direction: row;
justify-content: flex-start;
@ -113,7 +118,7 @@ function setting(item: any) {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
width: 100%;
-webkit-box-orient: vertical;
padding: 5px; /* 添加内边距 */
/* 可选:添加边框 */
@ -121,6 +126,9 @@ function setting(item: any) {
/* 可选:添加背景色 */
/* background-color: #f9f9f9; */
}
.app-desc .el-progress {
width: 90% !important; /* 或者任何你想要的具体宽度 */
}
@media (min-width: 769px) {
.app-desc {
font-size: 14px; /* 在大屏幕上使用更大的字体大小 */
@ -141,18 +149,17 @@ function setting(item: any) {
flex-direction: row;
justify-content: flex-end;
align-items: center;
gap: 10px;
margin-top: 20px;
gap: 5px;
margin-top: 10px;
}
.app-button button {
width: 60px;
width: 50px;
height: 30px;
border-radius: 4px;
border: 1px solid #ccc;
background-color: #fff;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

116
frontend/src/components/store/Store.vue

@ -1,75 +1,79 @@
<script setup lang="ts">
import { inject, onMounted } from "vue";
import { System } from "@/system";
import { onMounted } from "vue";
import { notifySuccess, notifyError } from "@/util/msg";
import { useStoreStore } from "@/stores/store";
import { setSystemKey,parseJson } from "@/system/config";
import { parseJson } from "@/system/config";
import { t } from "@/i18n";
const sys: any = inject<System>("system");
const store = useStoreStore()
onMounted(async () => {
await store.getList();
});
function setCache() {
setSystemKey("intstalledPlugins", store.installedList);
setTimeout(() => {
sys.refershAppList();
}, 1000);
}
async function install(item: any) {
//console.log(item)
if (item.isOut) {
item.progress = 0
const flag = await store.addOutList(item)
if(!flag){
if (!flag) {
notifyError(t("store.installError"))
}
}
if (item.needDownload && !item.isDev) {
await download(item)
}
if (item.needInstall) {
item.progress = 0
const completion = await fetch(store.apiUrl + '/store/install?name=' + item.name)
if (!completion.ok) {
notifyError(t("store.installError"))
return
if ((item.pkg != "" || item.url != "") && !item.isDev) {
const postData = {
name: item.name,
pkg: item.pkg,
url: item.url,
needInstall: item.needInstall,
}
const res = await completion.json()
if (res.code && res.code < 0) {
notifyError(res.message)
const url = store.apiUrl + '/store/download'
const res = await download(item, url, postData)
console.log(res)
if(res.code < 0){
if(res.data && res.data.dependencies && res.data.dependencies.length > 0){
const names:any = []
res.data.dependencies.forEach((element:any) => {
names.push(element.name)
});
notifyError("you need install these plugins first" + names.join(","))
}else{
notifyError(t("store.installError"))
}
return
}
if (res.data) {
item.icon = res.data.icon
}
}
if (item.webUrl) {
await sys.fs.writeFile(
`${sys._options.userLocation}Desktop/${item.name}.url`,
`link::url::${item.webUrl}::${item.icon}`
);
item = res.data
item.progress = 0
await store.addDesktop(item);
await store.checkProgress()
notifySuccess(t("store.installSuccess"))
}
notifySuccess(t("store.installSuccess"))
store.installedList.push(item.name);
await store.checkProgress()
setCache();
// if (item.needInstall) {
// item.progress = 0
// const completion = await fetch(store.apiUrl + '/store/install?name=' + item.name)
// if (!completion.ok) {
// notifyError(t("store.installError"))
// return
// }
// const res = await completion.json()
// if (res.code && res.code < 0) {
// notifyError(res.message)
// return
// }
// if (res.data) {
// item.icon = res.data.icon
// }
// }
// await store.addDesktop(item);
// await store.checkProgress()
// notifySuccess(t("store.installSuccess"))
}
async function uninstall(item: any) {
if (item.webUrl) {
await sys.fs.unlink(`${sys._options.userLocation}Desktop/${item.name}.url`);
}
delete store.installedList[store.installedList.indexOf(item.name)];
setCache();
await store.removeDesktop(item);
if (item.needInstall) {
item.progress = 0
const completion = await fetch(store.apiUrl + '/store/uninstall?name=' + item.name)
if (!completion.ok) {
notifyError(t("store.hasSameName"))
@ -83,10 +87,13 @@ async function uninstall(item: any) {
}
notifySuccess(t("store.uninstallSuccess"))
}
async function download(item: any) {
async function download(item: any, url: string, postData: any) {
//console.log(item)
if (item.progress) return;
const completion = await fetch(store.apiUrl + '/store/download?url=' + item.url)
if (item.progress && item.progress > 0) return;
const completion = await fetch(url, {
method: 'POST',
body: JSON.stringify(postData),
})
if (!completion.ok) {
notifyError(t("store.downloadError"))
}
@ -95,6 +102,7 @@ async function download(item: any) {
if (!reader) {
notifyError(t("store.cantStream"));
}
let res:any
while (true) {
const { done, value } = await reader?.read();
if (done) {
@ -102,20 +110,15 @@ async function download(item: any) {
}
// console.log(value)
const json = await new TextDecoder().decode(value);
//console.log(json)
const res = parseJson(json)
res = parseJson(json)
//console.log(res)
if (res) {
if (res.progress) {
item.progress = res.progress
}
if (res.done) {
notifySuccess(t("store.downloadSuccess"))
item.progress = 0
break;
}
}
}
return res
}
async function pauseApp(item: any) {
const res: any = await fetch(store.apiUrl + '/store/stop/' + item.name)
@ -162,7 +165,8 @@ async function startApp(item: any) {
<span class="sub-title">{{ store.currentTitle }} </span>
</div>
<div class="main-app">
<div v-for="item in store.storeList" v-if="store.currentCate != 'add'" class="store-item" :key="item.name">
<div v-for="item in store.storeList" v-if="store.currentCate != 'add'" class="store-item"
:key="item.name">
<AppItem :item="item" :installed-list="store.installedList" :install="install" :uninstall="uninstall"
:pause="pauseApp" :start="startApp" :restart="restartApp" />
</div>

53
frontend/src/i18n/lang/en.json

@ -146,22 +146,45 @@
"utilities": "Utilities",
"others": "Others",
"add": "Add",
"errorList": "Cannot get the list of applications"
"errorList": "Cannot get the list of applications",
"urlEmpty": "The Url is empty",
"hasSameName": "There are applications with the same name!",
"installError": "Installation failed!",
"installSuccess": "Installation successful!",
"uninstallSuccess": "Uninstalling successful",
"downloadError": "Download failed!",
"downloadSuccess":"Download successful!",
"cantStream": "Streaming download is currently not supported!",
"setting": "Setting",
"stop": "Stop",
"start": "Start",
"restart": "Restart",
"uninstall": "Uninstall",
"install": "Install",
"remoteDownload": "Remote Download",
"localImport": "Local Import",
"devMode": "Development Mode",
"importZip": "Import Zip",
"localPath": "Local Path",
"userDirectory": "Under the user directory .godoos/run, only fill in the directory name",
"addMethod": "Add Method",
"downloadUrl": "Download URL",
"downloadProgress": "Download Progress"
},
"process":{
"title":"Process Manager",
"port":"Port",
"pid":"PID",
"name":"Name",
"proto":"Protocol",
"action":"Actions"
"process": {
"title": "Process Manager",
"port": "Port",
"pid": "PID",
"name": "Name",
"proto": "Protocol",
"action": "Actions"
},
"upgrade": {
"title": "New version",
"msg": "The new version is available, please update it now! Dont worry, the update is fast!",
"desc": "Prompt: Update will restore the default configuration",
"btnOne": "Cruel refusal",
"btnTwo": "Update now",
"btnTwoLoading": "Updating"
}
"title": "New version",
"msg": "The new version is available, please update it now! Dont worry, the update is fast!",
"desc": "Prompt: Update will restore the default configuration",
"btnOne": "Cruel refusal",
"btnTwo": "Update now",
"btnTwoLoading": "Updating"
}
}

88
frontend/src/i18n/lang/zh.json

@ -37,8 +37,8 @@
"backup": "备份",
"import.success.reboot.soon": "导入成功,即将重启",
"import.failed": "导入失败",
"zip":"压缩",
"unzip":"解压",
"zip": "压缩",
"unzip": "解压",
"import": "导入",
"export": "导出",
"refresh": "刷新",
@ -135,41 +135,57 @@
"cannot.create.shortcut": "无法创建快捷方式",
"shortcut.has.been.created": "快捷方式已创建",
"system.message": "系统消息",
"store" : {
"hots":"热门",
"work": "办公",
"development": "开发工具",
"games": "游戏",
"education": "教育",
"news": "新闻",
"shopping":"购物",
"social": "社交",
"utilities": "实用工具",
"others": "其他",
"add":"添加应用",
"errorList":"获取列表失败!",
"urlEmpty":"网址为空",
"hasSameName":"存在相同名称的应用!",
"installError":"安装失败!",
"installSuccess":"安装成功!",
"uninstallSuccess":"卸载成功",
"downloadError":"下载失败!",
"cantStream":"暂不支持流下载!"
"store": {
"hots": "热门",
"work": "办公",
"development": "开发工具",
"games": "游戏",
"education": "教育",
"news": "新闻",
"shopping": "购物",
"social": "社交",
"utilities": "实用工具",
"others": "其他",
"add": "添加应用",
"errorList": "获取列表失败!",
"urlEmpty": "网址为空",
"hasSameName": "存在相同名称的应用!",
"installError": "安装失败!",
"installSuccess": "安装成功!",
"uninstallSuccess": "卸载成功",
"downloadError": "下载失败!",
"downloadSuccess":"下载成功!",
"cantStream": "暂不支持流下载!",
"setting": "设置",
"stop": "停止",
"start": "启动",
"restart": "重启",
"uninstall": "卸载",
"install": "安装",
"remoteDownload": "远程下载",
"localImport": "本地导入",
"devMode": "开发模式",
"importZip": "导入压缩包",
"localPath": "本地路径",
"userDirectory": "用户目录下.godoos/run/(windows or darwin or linux),只填目录名称",
"addMethod": "添加方式",
"downloadUrl": "下载地址",
"downloadProgress": "下载进度"
},
"process":{
"title":"进程管理",
"port":"端口",
"pid":"PID",
"name":"名称",
"proto":"类型",
"action":"操作"
"process": {
"title": "进程管理",
"port": "端口",
"pid": "PID",
"name": "名称",
"proto": "类型",
"action": "操作"
},
"upgrade": {
"title": "新版本升级",
"msg": "新版本来啦,马上更新尝鲜吧!不用担心,更新很快的哦!",
"desc": "提示:更新会还原默认配置",
"btnOne": "残忍拒绝",
"btnTwo": "马上更新",
"btnTwoLoading": "更新中"
}
"title": "新版本升级",
"msg": "新版本来啦,马上更新尝鲜吧!不用担心,更新很快的哦!",
"desc": "提示:更新会还原默认配置",
"btnOne": "残忍拒绝",
"btnTwo": "马上更新",
"btnTwoLoading": "更新中"
}
}

62
frontend/src/stores/store.ts

@ -1,18 +1,19 @@
import { defineStore } from 'pinia'
import { ref } from "vue";
import { ref, inject } from "vue";
import { t } from "@/i18n";
// import storeInitList from "@/assets/store.json";
import { System } from "@/system";
import { getSystemKey } from "@/system/config";
export const useStoreStore = defineStore('storeStore', () => {
const sys: any = inject<System>("system");
const currentCateId = ref(0)
const currentTitle = ref(t("store.hots"))
const currentCate = ref('hots')
const categoryList = ['hots', 'work', 'development', 'games', 'education', 'news', 'shopping', 'social', 'utilities', 'others', 'add']
const categoryIcon = ['HomeFilled', 'Odometer', 'Postcard', 'TrendCharts', 'School', 'HelpFilled', 'ShoppingCart', 'ChatLineRound', 'MessageBox', 'Ticket', 'CirclePlusFilled']
const isready = ref(false);
const installed = getSystemKey("intstalledPlugins");
const apiUrl = getSystemKey("apiUrl");
const installedList: any = ref(installed);
const installedList: any = ref([]);
const storeList: any = ref([])
const outList: any = ref([])
async function getList() {
@ -21,14 +22,15 @@ export const useStoreStore = defineStore('storeStore', () => {
const storeUrl = apiUrl + '/store/storelist?cate=' + currentCate.value
const res = await fetch(storeUrl)
if (!res.ok) {
return []
return []
}
let list:any = await res.json()
const json: any = await res.json()
let list = json.data || []
//console.log(data)
if (outList.value.length > 0 && currentCate.value == 'hots' && list && list.length > 0) {
const names = list.map((item : any) => item.name)
const adds:any = []
outList.value.forEach((item : any) => {
const names = list.map((item: any) => item.name)
const adds: any = []
outList.value.forEach((item: any) => {
if (!names.includes(item.name)) {
adds.push(item)
}
@ -39,16 +41,14 @@ export const useStoreStore = defineStore('storeStore', () => {
await checkProgress()
isready.value = true;
}
async function addOutList(item:any) {
console.log(item)
const has = outList.value.find((i:any) => i.name === item.name)
console.log(has)
if(!has) {
async function addOutList(item: any) {
const has = outList.value.find((i: any) => i.name === item.name)
if (!has) {
item.isOut = true
outList.value.push(item)
await getList()
await getList()
return true
}else{
} else {
return false
}
}
@ -57,7 +57,7 @@ export const useStoreStore = defineStore('storeStore', () => {
currentCate.value = item
currentTitle.value = t("store." + item)
await getList()
}
}
async function checkProgress() {
const completion: any = await fetch(apiUrl + '/store/listporgress')
if (!completion.ok) {
@ -77,8 +77,27 @@ export const useStoreStore = defineStore('storeStore', () => {
}
})
}
async function addDesktop(item: any) {
if (item.webUrl) {
await sys.fs.writeFile(
`${sys._options.userLocation}Desktop/${item.name}.url`,
`link::url::${item.webUrl}::${item.icon}`
);
setTimeout(() => {
sys.refershAppList();
}, 1000);
}
installedList.value.push(item.name);
}
async function removeDesktop(item: any) {
if (item.webUrl) {
await sys.fs.unlink(`${sys._options.userLocation}Desktop/${item.name}.url`);
setTimeout(() => {
sys.refershAppList();
}, 1000);
}
delete installedList.value[installedList.value.indexOf(item.name)];
}
return {
currentCateId,
categoryIcon,
@ -93,7 +112,9 @@ export const useStoreStore = defineStore('storeStore', () => {
changeCate,
getList,
addOutList,
checkProgress
checkProgress,
addDesktop,
removeDesktop
}
}, {
persist: {
@ -102,7 +123,8 @@ export const useStoreStore = defineStore('storeStore', () => {
{
storage: localStorage,
paths: [
"outList"
"outList",
"installedList"
]
}, // name 字段用localstorage存储
],

5
frontend/src/system/config.ts

@ -102,10 +102,7 @@ export const getSystemConfig = (ifset = false) => {
dbname: ''
};
}
// 初始化已安装插件列表,若本地存储中已存在则不进行覆盖
if (!config.intstalledPlugins) {
config.intstalledPlugins = [];
}
// 初始化桌面快捷方式列表,若本地存储中已存在则不进行覆盖
if (!config.desktopList) {
config.desktopList = [];

1
go.mod

@ -6,6 +6,7 @@ require github.com/wailsapp/wails/v2 v2.9.1
require (
aead.dev/minisign v0.2.0 // indirect
github.com/cavaliergopher/grab/v3 v3.0.1 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/gorilla/mux v1.8.1 // indirect
github.com/minio/selfupdate v0.6.0 // indirect

2
go.sum

@ -2,6 +2,8 @@ aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=

2
godo/cmd/main.go

@ -42,7 +42,7 @@ func OsStart() {
progressRouter.HandleFunc("/listport", store.ListAllProcessesHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/killport", store.KillProcessByNameHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/storelist", store.GetStoreListHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/download", store.DownloadHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/download", store.DownloadHandler).Methods(http.MethodPost)
progressRouter.HandleFunc("/install", store.InstallHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/installInfo", store.GetInstallInfoHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/installOut", store.RunOutHandler).Methods(http.MethodGet)

4
godo/files/fs.go

@ -442,12 +442,12 @@ func HandleUnZip(w http.ResponseWriter, r *http.Request) {
zipFilePath := filepath.Join(basePath, path)
destPath := filepath.Dir(zipFilePath)
err = Decompress(zipFilePath, destPath)
zipPath, err := Decompress(zipFilePath, destPath)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
res := libs.APIResponse{Message: fmt.Sprintf("success unzip %s.", path)}
res := libs.APIResponse{Message: fmt.Sprintf("success unzip %s.", path), Data: zipPath}
json.NewEncoder(w).Encode(res)
}

47
godo/files/os.go

@ -104,6 +104,53 @@ func Rmdir(basePath, dirPath string) error {
fullPath := filepath.Join(basePath, dirPath)
return os.RemoveAll(fullPath)
}
func CopyResource(src, dst string) error {
// 检查源路径是否为目录或文件
info, err := os.Stat(src)
if err != nil {
return err
}
// 如果源路径是一个目录,则递归复制整个目录
if info.IsDir() {
return CopyDirectory(src, dst)
}
// 如果源路径是一个文件,则直接复制文件
return CopyFile(src, dst)
}
func CopyDirectory(src, dst string) error {
// 创建目标目录
if err := os.MkdirAll(dst, os.ModePerm); err != nil {
return err
}
// 遍历源目录中的所有文件和子目录
files, err := os.ReadDir(src)
if err != nil {
return err
}
for _, file := range files {
// 构建源文件和目标文件的完整路径
srcPath := filepath.Join(src, file.Name())
dstPath := filepath.Join(dst, file.Name())
// 递归复制子目录或复制文件
if file.IsDir() {
if err := CopyDirectory(srcPath, dstPath); err != nil {
return err
}
} else {
if err := CopyFile(srcPath, dstPath); err != nil {
return err
}
}
}
return nil
}
// CopyFile copies a file
func CopyFile(src, dst string) error {

134
godo/files/unzip.go

@ -15,7 +15,7 @@ import (
)
// Decompress 解压文件到指定目录,支持多种压缩格式
func Decompress(compressedFilePath, destPath string) error {
func Decompress(compressedFilePath, destPath string) (string, error) {
ext := filepath.Ext(compressedFilePath)
switch ext {
case ".zip":
@ -23,7 +23,7 @@ func Decompress(compressedFilePath, destPath string) error {
case ".tar":
tarFile, err := os.Open(compressedFilePath)
if err != nil {
return err
return "", err
}
defer tarFile.Close()
return Untar(tarFile, destPath)
@ -32,12 +32,13 @@ func Decompress(compressedFilePath, destPath string) error {
case ".bz2":
return Untarbz2(compressedFilePath, destPath)
default:
return fmt.Errorf("unsupported compression format: %s", ext)
return "", fmt.Errorf("unsupported compression format: %s", ext)
}
}
// Untar 解压.tar文件到指定目录
func Untar(reader io.Reader, destPath string) error {
func Untar(reader io.Reader, destPath string) (string, error) {
var topLevelDirName string
tarReader := tar.NewReader(reader)
for {
header, err := tarReader.Next()
@ -45,121 +46,147 @@ func Untar(reader io.Reader, destPath string) error {
break
}
if err != nil {
return err
return "", err
}
// Determine the top-level directory name
if header.Typeflag == tar.TypeDir && topLevelDirName == "" {
topLevelDirName = header.Name
} else {
topLevelDirName = destPath
}
target := filepath.Join(destPath, header.Name)
// Ensure that all directories in the path are created.
if header.Typeflag == tar.TypeDir {
if !libs.PathExists(target) {
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
return err
}
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
return "", err
}
continue
}
// If it's a regular file, ensure its parent directories exist.
if !libs.PathExists(target) {
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
return "", err
}
// Create the file and write the contents from the tar archive.
outFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
return "", err
}
defer outFile.Close()
if _, err := io.Copy(outFile, tarReader); err != nil {
return err
return "", err
}
}
return nil
// Return the full path of the top-level directory
topLevelDestPath := filepath.Join(destPath, topLevelDirName)
return topLevelDestPath, nil
}
// Untargz 解压.tar.gz文件到指定目录
func Untargz(targzFilePath, destPath string) error {
func Untargz(targzFilePath, destPath string) (string, error) {
gzFile, err := os.Open(targzFilePath)
if err != nil {
return err
return "", err
}
defer gzFile.Close()
gzReader, err := gzip.NewReader(gzFile)
if err != nil {
return err
return "", err
}
defer gzReader.Close()
return Untar(gzReader, destPath) // 传递gzReader给Untar
return Untar(gzReader, destPath)
}
// Untarbz2 解压.tar.bz2文件到指定目录
func Untarbz2(tarbz2FilePath, destPath string) error {
func Untarbz2(tarbz2FilePath, destPath string) (string, error) {
bz2File, err := os.Open(tarbz2FilePath)
if err != nil {
return err
return "", err
}
defer bz2File.Close()
bz2Reader := bzip2.NewReader(bz2File)
return Untar(bz2Reader, destPath) // 传递bz2Reader给Untar
return Untar(bz2Reader, destPath)
}
// DecompressZip 解压zip文件到指定目录
func Unzip(zipFilePath, destPath string) error {
func Unzip(zipFilePath, destPath string) (string, error) {
r, err := zip.OpenReader(zipFilePath)
if err != nil {
return err
return "", err
}
defer r.Close()
// 初始化顶层目录名称为空
var topLevelDirName string
var topLevelDestPath string
// 遍历ZIP文件中的每个条目
for _, f := range r.File {
// 如果是目录且不是根目录,则更新顶层目录名称
if f.FileInfo().IsDir() && f.Name != "" {
// 去掉末尾的斜杠
cleanedName := strings.TrimSuffix(f.Name, string(os.PathSeparator))
if topLevelDirName == "" || cleanedName < topLevelDirName {
topLevelDirName = cleanedName
}
}
}
// 检查是否找到了顶层目录
if topLevelDirName == "" {
topLevelDestPath = destPath
} else {
// 完整的顶层目录路径
topLevelDestPath = filepath.Join(destPath, topLevelDirName)
}
// 再次遍历ZIP文件中的每个条目,这次进行解压操作
for _, f := range r.File {
rc, err := f.Open()
if err != nil {
return err
return "", err
}
defer rc.Close()
path := filepath.Join(destPath, f.Name)
// 调整路径以确保所有文件都在顶层目录内
//path := filepath.Join(topLevelDestPath, strings.TrimPrefix(f.Name, topLevelDirName+string(os.PathSeparator)))
path := filepath.Join(topLevelDestPath, strings.TrimPrefix(f.Name, topLevelDirName))
// Ensure parent directory exists before creating the file or directory.
// 确保父目录存在
if !libs.PathExists(filepath.Dir(path)) {
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
return "", err
}
}
if f.FileInfo().IsDir() {
// Create the directory itself if it doesn't exist yet.
// 创建目录
if !libs.PathExists(path) {
if err := os.Mkdir(path, f.Mode()); err != nil {
return err
return "", err
}
}
} else {
// 创建并写入文件
outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return "", err
}
defer outFile.Close()
continue
}
// Open the file for writing, truncating if it already exists.
outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer outFile.Close()
// Write the contents of the zip file entry to the output file.
if _, err := io.Copy(outFile, rc); err != nil {
return err
if _, err := io.Copy(outFile, rc); err != nil {
return "", err
}
}
}
return nil
// 返回完整的顶层目录路径
return topLevelDestPath, nil
}
// IsSupportedCompressionFormat 判断文件后缀是否为支持的压缩格式
@ -180,16 +207,13 @@ func IsSupportedCompressionFormat(filename string) bool {
}
// HandlerFile 根据文件后缀判断是否支持解压,支持则解压,否则移动文件
func HandlerFile(filePath string, destDir string) error {
func HandlerFile(filePath string, destDir string) (string, error) {
// log.Printf("HandlerFile: %s", filePath)
// log.Printf("destDir: %s", destDir)
if IsSupportedCompressionFormat(filePath) {
// 移动文件
// newPath := filepath.Join(destDir, filepath.Base(filePath))
// return os.Rename(filePath, newPath)
// 解压文件
return Decompress(filePath, destDir)
} else {
return "", fmt.Errorf("unsupported compression format: %s", filePath)
}
return nil
}

1
godo/go.mod

@ -11,6 +11,7 @@ require (
require (
aead.dev/minisign v0.2.0 // indirect
github.com/cavaliergopher/grab/v3 v3.0.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/stretchr/testify v1.9.0 // indirect
github.com/tklauser/go-sysconf v0.3.14 // indirect

2
godo/go.sum

@ -1,5 +1,7 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=

3
godo/libs/msg.go

@ -25,6 +25,9 @@ func HTTPError(w http.ResponseWriter, status int, message string) {
func ErrorMsg(w http.ResponseWriter, message string) {
WriteJSONResponse(w, APIResponse{Message: message, Code: -1}, 200)
}
func ErrorData(w http.ResponseWriter, data any, message string) {
WriteJSONResponse(w, APIResponse{Message: message, Data: data, Code: -1}, 200)
}
func SuccessMsg(w http.ResponseWriter, data any, message string) {
WriteJSONResponse(w, APIResponse{Message: message, Data: data, Code: 0}, 200)
}

3
godo/store/cmds.go

@ -167,7 +167,8 @@ func MkDir(cmd Cmd) error {
return nil
}
func Unzip(cmd Cmd) error {
return files.Decompress(cmd.FilePath, cmd.Content)
_, err := files.Decompress(cmd.FilePath, cmd.Content)
return err
}
func Zip(cmd Cmd) error {
return files.Encompress(cmd.FilePath, cmd.Content)

281
godo/store/download.go

@ -2,155 +2,236 @@ package store
import (
"encoding/json"
"fmt"
"godo/files"
"godo/libs"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"sync"
"time"
"github.com/cavaliergopher/grab/v3"
)
type ProgressReader struct {
reader io.Reader
total int64
err error
}
const (
concurrency = 6 // 并发下载数
)
type DownloadStatus struct {
Name string `json:"name"`
Path string `json:"path"`
Url string `json:"url"`
Current int64 `json:"current"`
Size int64 `json:"size"`
Speed float64 `json:"speed"`
Progress int `json:"progress"`
Downloading bool `json:"downloading"`
Done bool `json:"done"`
var downloads = make(map[string]*grab.Response)
var downloadsMutex sync.Mutex
type FileProgress struct {
Progress int `json:"progress"` // 将进度改为浮点数,以百分比表示
IsFinished bool `json:"is_finished"`
Total int64 `json:"total"`
Current int64 `json:"completed"`
Status string `json:"status"`
}
func (pr *ProgressReader) Read(p []byte) (n int, err error) {
n, err = pr.reader.Read(p)
pr.err = err
pr.total += int64(n)
return
type ReqBody struct {
DownloadUrl string `json:"url"`
PkgUrl string `json:"pkg"`
Name string `json:"name"`
}
func DownloadHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
log.Printf("Download url: %s", url)
// 获取下载目录,这里假设从请求参数中获取,如果没有则使用默认值
var reqBody ReqBody
err := json.NewDecoder(r.Body).Decode(&reqBody)
if err != nil {
libs.ErrorMsg(w, "first Decode request body error")
return
}
downloadDir := libs.GetCacheDir()
if downloadDir == "" {
downloadDir = "./downloads"
}
// 拼接完整的文件路径
fileName := filepath.Base(url)
filePath := filepath.Join(downloadDir, fileName)
// 开始下载
resp, err := http.Get(url)
if err != nil {
log.Printf("Failed to get file: %v", err)
http.Error(w, "Failed to get file", http.StatusInternalServerError)
if reqBody.Name == "" {
libs.ErrorMsg(w, "the name is empty")
return
}
defer resp.Body.Close()
if reqBody.DownloadUrl == "" && reqBody.PkgUrl == "" {
libs.ErrorMsg(w, "the url is empty")
return
}
paths := []string{}
for _, url := range []string{reqBody.DownloadUrl, reqBody.PkgUrl} {
if url == "" {
continue
}
if rsp, ok := downloads[url]; ok {
// 如果URL正在下载,跳过创建新的下载器实例
go trackProgress(w, rsp, url)
continue
}
// 创建新的下载器实例
client := grab.NewClient()
client.HTTPClient = &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: concurrency, // 可选,设置并发连接数
},
}
filePath := filepath.Join(downloadDir, filepath.Base(url))
paths = append(paths, filePath)
log.Printf("filePath is %s", filePath)
// 创建下载请求
req, err := grab.NewRequest(filePath, url)
if err != nil {
libs.ErrorMsg(w, "Invalid download URL")
return
}
// 检查文件是否已存在且大小一致
if fileInfo, err := os.Stat(filePath); err == nil {
if fileInfo.Size() == resp.ContentLength {
// 文件已存在且大小一致,无需下载
runDir := libs.GetRunDir()
err := files.HandlerFile(filePath, runDir)
if err != nil {
log.Printf("Error moving file: %v", err)
resp := client.Do(req)
if fileInfo, err := os.Stat(filePath); err == nil {
// 文件存在,检查文件大小是否与远程资源匹配
if fileInfo.Size() == resp.Size() { // 这里的req.Size需要从下载请求中获取,或通过其他方式预知
log.Printf("File %s already exists and is up-to-date.", filePath)
continue
}
libs.SuccessMsg(w, "success", "File already exists and is of correct size")
}
downloads[url] = resp
//log.Printf("Download urls: %v\n", reqBody.DownloadUrl)
// // 跟踪进度
go trackProgress(w, resp, url)
// 等待下载完成并检查错误
if err := resp.Err(); err != nil {
libs.ErrorMsg(w, "Download failed")
return
} else {
// 重新打开响应体以便后续读取
resp.Body = http.NoBody
resp, err = http.Get(url)
if err != nil {
log.Printf("Failed to get file: %v", err)
http.Error(w, "Failed to get file", http.StatusInternalServerError)
return
}
}
}
// 创建文件
file, err := os.Create(filePath)
if len(paths) > 0 {
// 解压文件
err := HandlerZipFiles(paths, reqBody.Name)
if err != nil {
libs.ErrorMsg(w, "Decompress failed:"+err.Error())
return
}
}
installInfo, err := Installation(reqBody.Name)
if err != nil {
log.Printf("Failed to create file: %v", err)
http.Error(w, "Failed to create file", http.StatusInternalServerError)
libs.ErrorData(w, installInfo, "the install.json is error:"+err.Error())
return
}
defer file.Close()
libs.SuccessMsg(w, installInfo, "install the app success!")
}
func HandlerZipFiles(paths []string, name string) error {
runDir := libs.GetRunDir()
targetDir := filepath.Join(runDir, name)
downloadDir := libs.GetCacheDir()
for _, filePath := range paths {
zipPath, err := files.Decompress(filePath, downloadDir)
if err != nil {
return fmt.Errorf("decompress failed: %v", err)
}
err = files.CopyResource(zipPath, targetDir)
if err != nil {
return fmt.Errorf("copyResource failed")
}
err = os.RemoveAll(zipPath)
if err != nil {
return fmt.Errorf("removeAll failed")
}
// 使用ProgressReader来跟踪进度
pr := &ProgressReader{reader: resp.Body}
}
// 启动定时器来报告进度
ticker := time.NewTicker(200 * time.Millisecond)
return nil
}
func trackProgress(w http.ResponseWriter, resp *grab.Response, md5url string) {
defer func() {
if r := recover(); r != nil {
log.Printf("Recovered panic in trackProgress: %v", r)
}
downloadsMutex.Lock()
defer downloadsMutex.Unlock()
delete(downloads, md5url)
}()
ticker := time.NewTicker(100 * time.Millisecond)
defer ticker.Stop()
flusher, ok := w.(http.Flusher)
if !ok {
log.Printf("Streaming unsupported")
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
go func() {
for {
<-ticker.C
rp := &DownloadStatus{
Name: fileName,
Path: filePath,
Url: url,
Current: pr.total,
Size: resp.ContentLength,
Speed: 0, // 这里可以计算速度,但为了简化示例,我们暂时设为0
Progress: int(100 * (float64(pr.total) / float64(resp.ContentLength))),
Downloading: pr.err == nil && pr.total < resp.ContentLength,
Done: pr.total == resp.ContentLength,
for {
select {
case <-ticker.C:
fp := FileProgress{
Progress: int(resp.Progress() * 100),
IsFinished: resp.IsComplete(),
Total: resp.Size(),
Current: resp.BytesComplete(),
Status: "loading",
}
if pr.err != nil || rp.Done {
rp.Downloading = false
//log.Printf("Download complete: %s", filePath)
runDir := libs.GetRunDir()
err := files.HandlerFile(filePath, runDir)
if err != nil {
log.Printf("Error moving file: %v", err)
}
break
//log.Printf("Progress: %v", fp)
if resp.IsComplete() && fp.Current == fp.Total {
fp.Status = "loaded"
}
jsonBytes, err := json.Marshal(fp)
if err != nil {
log.Printf("Error marshaling FileProgress to JSON: %v", err)
continue
}
if w != nil {
jsonBytes, err := json.Marshal(rp)
if err != nil {
log.Printf("Error marshaling DownloadStatus to JSON: %v", err)
continue
}
w.Write(jsonBytes)
io.WriteString(w, string(jsonBytes))
w.Write([]byte("\n"))
flusher.Flush()
} else {
log.Println("ResponseWriter is nil, cannot send progress")
}
if fp.Status == "loaded" {
return
}
}
}()
}
}
// Predefined filename for the icon
const iconFilename = "icon.png"
// 将响应体的内容写入文件
_, err = io.Copy(file, pr)
// DownloadIcon downloads the icon from the given URL to the target path with a predefined filename.
func DownloadIcon(url, iconTargetPath string) error {
resp, err := http.Get(url)
if err != nil {
log.Printf("Failed to write file: %v", err)
http.Error(w, "Failed to write file", http.StatusInternalServerError)
return
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
out, err := os.Create(iconTargetPath)
if err != nil {
return err
}
libs.SuccessMsg(w, "success", "Download complete")
defer out.Close()
if _, err := io.Copy(out, resp.Body); err != nil {
return err
}
return nil
}
// HandlerIcon handles the icon by downloading it and updating the installInfo.
func HandlerIcon(installInfo InstallInfo, targetPath string) (string, error) {
var iconUrl string
if url, err := url.Parse(installInfo.Icon); err == nil && url.Scheme != "" {
// Download the icon using the predefined filename
iconTargetPath := filepath.Join(targetPath, installInfo.Name, "_icon.png")
if err := DownloadIcon(installInfo.Icon, iconTargetPath); err != nil {
return "", fmt.Errorf("error downloading icon: %v", err)
}
iconUrl = "http://localhost:56780/static/" + installInfo.Name + "/" + installInfo.Name + "_icon.png"
} else {
iconUrl = "http://localhost:56780/static/" + installInfo.Name + "/" + installInfo.Icon
}
return iconUrl, nil
}

207
godo/store/install.go

@ -18,151 +18,193 @@ func InstallHandler(w http.ResponseWriter, r *http.Request) {
libs.ErrorMsg(w, "the app name is empty!")
return
}
installInfo, err := Installation(pluginName)
if err != nil {
libs.ErrorData(w, installInfo, "the install.json is error:"+err.Error())
return
}
libs.SuccessMsg(w, installInfo, "install the app success!")
}
// Installation 处理安装逻辑
func Installation(pluginName string) (InstallInfo, error) {
var installInfo InstallInfo
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
return installInfo, fmt.Errorf("the app path is not exists")
}
installInfo, err := GetInstallInfo(pluginName)
if err != nil {
libs.ErrorMsg(w, "the install.json is error:"+err.Error())
return
return installInfo, fmt.Errorf("the install.json is error: %v", err)
}
if len(installInfo.Dependencies) > 0 {
var needInstalls []Item
for _, item := range installInfo.Dependencies {
info, err := GetInstallInfo(item.Value.(string))
depInfo, err := GetInstallInfo(item.Value.(string))
if err != nil {
needInstalls = append(needInstalls, item)
continue
}
if info.Version != installInfo.Version {
if depInfo.Version != installInfo.Version {
needInstalls = append(needInstalls, item)
}
}
libs.WriteJSONResponse(w, libs.APIResponse{Message: "you need install apps!", Data: needInstalls, Code: -1}, 200)
return
installInfo.Dependencies = needInstalls
return installInfo, fmt.Errorf("dependencies require installation")
}
// Check if the plugin name matches the install.json name
if pluginName != installInfo.Name {
libs.ErrorMsg(w, "the app name must equal the install.json!")
return
return installInfo, fmt.Errorf("plugin name does not match install.json")
}
if !installInfo.NeedInstall {
libs.SuccessMsg(w, "success", "the app is installed!")
return
// Copy static directory
staticPath := filepath.Join(exePath, "static")
if libs.PathExists(staticPath) {
staticDir := libs.GetStaticDir()
targetPath := filepath.Join(staticDir, pluginName)
if !libs.PathExists(targetPath) {
if err := os.Rename(staticPath, targetPath); err != nil {
return installInfo, fmt.Errorf("error copying static directory: %w", err)
}
iconUrl, err := HandlerIcon(installInfo, targetPath)
if err == nil {
installInfo.Icon = iconUrl
err = SaveInstallInfo(installInfo)
if err != nil {
return installInfo, fmt.Errorf("error saving install.json: %w", err)
}
}
}
}
// Process store.json
storeFile := filepath.Join(exePath, "store.json")
if !libs.PathExists(storeFile) {
libs.ErrorMsg(w, "the store.json is not exists!")
return
return installInfo, nil
}
var storeInfo StoreInfo
content, err := os.ReadFile(storeFile)
if err != nil {
libs.ErrorMsg(w, "cant read the store.json!")
return
return installInfo, fmt.Errorf("cannot read store.json: %w", err)
}
//设置 info.json
//设置 store.json
exePath = strings.ReplaceAll(exePath, "\\", "/")
contentBytes := []byte(strings.ReplaceAll(string(content), "{exePath}", exePath))
//log.Printf("====content: %s", string(contentBytes))
err = json.Unmarshal(contentBytes, &storeInfo)
if err != nil {
log.Printf("Error during unmarshal: %v", err)
libs.ErrorMsg(w, "the store.json is error: "+err.Error())
return
return installInfo, fmt.Errorf("error unmarshalling store.json: %w", err)
}
replacePlaceholdersInCmds(&storeInfo)
storeInfo.Name = installInfo.Name
err = SaveInfoFile(storeInfo)
if err != nil {
libs.ErrorMsg(w, "the store info.json is error: "+err.Error())
return
// Save store info
if err := SaveInfoFile(storeInfo); err != nil {
return installInfo, fmt.Errorf("error saving store info.json: %w", err)
}
//设置config
// Set up configuration
if libs.PathExists(storeInfo.Setting.ConfPath + ".tpl") {
err = SaveStoreConfig(storeInfo, exePath)
if err != nil {
libs.ErrorMsg(w, "save the config is error!")
return
if err := SaveStoreConfig(storeInfo, exePath); err != nil {
return installInfo, fmt.Errorf("error saving config: %w", err)
}
}
err = InstallStore(storeInfo)
if err != nil {
libs.ErrorMsg(w, "install the app is error!")
return
}
//复制static目录
staticPath := filepath.Join(exePath, "static")
if libs.PathExists(staticPath) {
staticDir := libs.GetStaticDir()
targetPath := filepath.Join(staticDir, pluginName)
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) {
installInfo.Icon = "http://localhost:56780/static/" + pluginName + "/" + storeInfo.Icon
}
}
// Install the store
if err := InstallStore(storeInfo); err != nil {
return installInfo, fmt.Errorf("error installing app: %w", err)
}
libs.SuccessMsg(w, installInfo, "install the app success!")
return installInfo, nil
}
// UnInstallHandler 处理卸载请求
func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Query().Get("name")
if pluginName == "" {
libs.ErrorMsg(w, "the app name is empty!")
return
}
log.Printf("uninstall the app %s", pluginName)
err := StopCmd(pluginName)
if err != nil {
// libs.ErrorMsg(w, "stop the app is error!")
// return
log.Printf("stop the app is error! %s", err)
// Execute the uninstallation process
if err := Uninstallation(pluginName); err != nil {
libs.ErrorMsg(w, err.Error())
return
}
// Success response
libs.SuccessMsg(w, "success", "uninstall the app success!")
}
// Uninstallation 执行插件卸载逻辑
func Uninstallation(pluginName string) error {
if err := StopCmd(pluginName); err != nil {
log.Printf("stopping the app encountered an error: %s", err)
}
// Retrieve install information
installInfo, err := GetInstallInfo(pluginName)
if err != nil {
libs.ErrorMsg(w, "the install.json is error:"+err.Error())
return
return fmt.Errorf("error retrieving install.json: %w", err)
}
// Check if the app is in development mode
if installInfo.IsDev {
libs.SuccessMsg(w, "success", "uninstall the app success!")
return
return nil // No further action required for dev mode apps
}
//检查是否有其他应用依赖于它
installedList := GetInstalled()
if len(installedList) > 1 {
hasDeps := []Item{}
for _, item := range installedList {
for _, dep := range item.Dependencies {
if dep.Value == pluginName {
hasDeps = append(hasDeps, dep)
}
}
}
if len(hasDeps) > 0 {
return fmt.Errorf("the app is being used by other applications: %v", hasDeps)
}
}
// Remove the application directory
exePath := GetExePath(pluginName)
//log.Printf("the app path is %s", exePath)
if libs.PathExists(exePath) {
err := os.RemoveAll(exePath)
if err != nil {
libs.ErrorMsg(w, "delete the app is error!")
return
if err := os.RemoveAll(exePath); err != nil {
return fmt.Errorf("error deleting the app: %w", err)
}
}
// Remove the static directory
staticDir := libs.GetStaticDir()
staticPath := filepath.Join(staticDir, pluginName)
if libs.PathExists(staticPath) {
err := os.RemoveAll(staticPath)
if err != nil {
libs.ErrorMsg(w, "delete the static is error!")
return
if err := os.RemoveAll(staticPath); err != nil {
return fmt.Errorf("error deleting the static files: %w", err)
}
}
libs.SuccessMsg(w, "success", "uninstall the app success!")
return nil
}
func GetInstallInfo(pluginName string) (InstallInfo, error) {
var installInfo InstallInfo
func GetInstallPath(pluginName string) (string, error) {
exePath := GetExePath(pluginName)
installFile := filepath.Join(exePath, "install.json")
if !libs.PathExists(installFile) {
return installInfo, fmt.Errorf("install.json is not exist:%s", installFile)
return "", fmt.Errorf("install.json is not exist:%s", installFile)
}
return installFile, nil
}
func GetInstallInfo(pluginName string) (InstallInfo, error) {
var installInfo InstallInfo
installFile, err := GetInstallPath(pluginName)
if err != nil {
return installInfo, err
}
return GetInstallInfoByPath(installFile)
}
func GetInstallInfoByPath(installFile string) (InstallInfo, error) {
var installInfo InstallInfo
content, err := os.ReadFile(installFile)
if err != nil {
return installInfo, err
@ -306,6 +348,21 @@ func SaveStoreInfo(storeInfo StoreInfo, infoFile string) error {
}
return nil
}
func SaveInstallInfo(installIonfo InstallInfo) error {
infoFile, err := GetInstallPath(installIonfo.Name)
if err != nil {
return err
}
// 使用 json.MarshalIndent 直接获取内容的字节切片
content, err := json.MarshalIndent(installIonfo, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal reqBodies to JSON: %w", err)
}
if err := os.WriteFile(infoFile, content, 0644); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
return nil
}
func SaveStoreConfig(storeInfo StoreInfo, exePath string) error {
content, err := os.ReadFile(storeInfo.Setting.ConfPath + ".tpl")
if err != nil {

85
godo/store/store.go

@ -14,33 +14,57 @@ import (
func GetStoreListHandler(w http.ResponseWriter, r *http.Request) {
cate := r.URL.Query().Get("cate")
os := runtime.GOOS
arch := runtime.GOARCH
if cate == "" {
libs.ErrorMsg(w, "cate is required")
list, err := GetInstallList(cate)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
libs.SuccessMsg(w, list, "")
}
func InstallByName(name string, cate string) (InstallInfo, error) {
var installInfo InstallInfo
list, err := GetInstallList(cate)
if err != nil {
return installInfo, fmt.Errorf("failed to get plugin list: %v", err)
}
for _, item := range list {
if item.Name == name {
installInfo = item
break
}
}
if installInfo.Name == "" {
return installInfo, fmt.Errorf("plugin not found")
}
return Installation(installInfo.Name)
}
func GetInstallList(cate string) ([]InstallInfo, error) {
if cate == "" {
cate = "hots"
}
os := runtime.GOOS
arch := runtime.GOARCH
var list []InstallInfo
pluginUrl := "https://gitee.com/ruitao_admin/godoos-image/raw/master/store/" + os + "/" + arch + "/" + cate + ".json"
res, err := http.Get(pluginUrl)
if err != nil {
libs.ErrorMsg(w, err.Error())
return list, fmt.Errorf("failed to get plugin list: %v", err)
}
defer res.Body.Close()
if res.StatusCode == 200 {
body, err := io.ReadAll(res.Body)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
var info interface{}
err = json.Unmarshal(body, &info)
if err != nil {
fmt.Println("Error unmarshalling JSON:", err)
return
}
json.NewEncoder(w).Encode(info)
if res.StatusCode != 200 {
return list, fmt.Errorf("failed to get plugin list")
}
body, err := io.ReadAll(res.Body)
if err != nil {
return list, fmt.Errorf("failed to get plugin list: %v", err)
}
err = json.Unmarshal(body, &list)
if err != nil {
return list, fmt.Errorf("failed to get plugin list: %v", err)
}
return list, nil
}
func GetInstallInfoHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
@ -115,7 +139,10 @@ func GetStoreInfo(name string) (StoreInfo, error) {
if !libs.PathExists(infoPath) {
return storeInfo, fmt.Errorf("process information for '%s' not found", name)
}
return GetStoreInfoByPath(infoPath)
}
func GetStoreInfoByPath(infoPath string) (StoreInfo, error) {
var storeInfo StoreInfo
content, err := os.ReadFile(infoPath)
if err != nil {
return storeInfo, fmt.Errorf("failed to read info.json: %v", err)
@ -129,3 +156,21 @@ func GetStoreInfo(name string) (StoreInfo, error) {
}
return storeInfo, nil
}
func GetInstalled() []InstallInfo {
runDir := libs.GetRunDir()
var list []InstallInfo
filepath.Walk(runDir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
infoPath := filepath.Join(path, "install.json")
if libs.PathExists(infoPath) {
installInfo, err := GetInstallInfoByPath(infoPath)
if err == nil {
list = append(list, installInfo)
}
}
}
return nil // 返回nil表示继续遍历
})
return list
}

32
godo/store/types.go

@ -3,19 +3,25 @@ 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"` // 标志位,表示是否需要配置。
Dependencies []Item `json:"dependencies"` // 依赖项。
Name string `json:"name"` // 应用程序名称。
URL string `json:"url"` // 应用程序下载地址或适配包的下载地址。
Pkg string `json:"pkg"` // 应用程序的官方下载地址。
WebUrl string `json:"webUrl"` // 应用程序的网页地址。
IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
Version string `json:"version"` // 应用程序的版本号。
Desc string `json:"desc"` // 应用程序的描述信息。
Icon string `json:"icon"` // 应用程序的图标路径。
HasStart bool `json:"hasStart"` // 标志位,表示是否显示启动和停止。
HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
Setting bool `json:"setting"` // 标志位,表示是否需要配置。
Dependencies []Item `json:"dependencies"` // 依赖项。
Categrory string `json:"category"` // 应用程序的分类。
History []InstallHastory `json:"history"` // 应用程序的历史版本。
}
type InstallHastory struct {
Version string `json:"version"`
URL string `json:"url"`
Pkg string `json:"pkg"` // 应用程序的官方下载地址。
}
// StoreInfo 维护了应用程序商店的信息。

36
godo/store/upload.go

@ -8,7 +8,6 @@ import (
"net/http"
"os"
"path/filepath"
"strings"
)
// UploadHandler 处理上传的HTTP请求
@ -37,7 +36,6 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
}
cachePath := libs.GetCacheDir()
baseName := filepath.Base(header.Filename)
filenameNoExt := strings.TrimSuffix(baseName, filepath.Ext(baseName))
savePath := filepath.Join(cachePath, baseName)
@ -56,11 +54,39 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
http.Error(w, "写入文件出错", http.StatusInternalServerError)
return
}
zipPath, err := files.Decompress(savePath, cachePath)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
storeFile := filepath.Join(zipPath, "install.json")
if !libs.PathExists(storeFile) {
libs.ErrorMsg(w, "store.json not found")
return
}
installCacheInfo, err := GetInstallInfoByPath(storeFile)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
runDir := libs.GetRunDir()
err = files.HandlerFile(savePath, runDir)
targetDir := filepath.Join(runDir, installCacheInfo.Name)
err = files.CopyResource(zipPath, targetDir)
if err != nil {
libs.ErrorMsg(w, err.Error())
return
}
err = os.RemoveAll(zipPath)
if err != nil {
log.Printf("Error moving file: %v", err)
libs.ErrorMsg(w, err.Error())
return
}
libs.SuccessMsg(w, filenameNoExt, "File already exists and is of correct size")
installInfo, err := Installation(installCacheInfo.Name)
if err != nil {
libs.ErrorData(w, installInfo, "the install.json is error:"+err.Error())
return
}
libs.SuccessMsg(w, installInfo, "install the app success!")
}

Loading…
Cancel
Save