change store
This commit is contained in:
parent
03c0bf0cea
commit
6398e6d68f
33 changed files with 1181 additions and 571 deletions
37
README.en.md
37
README.en.md
|
|
@ -11,7 +11,7 @@ An efficient intranet office platform that includes various tools such as Word,
|
||||||
|
|
||||||
English | [简体中文](README.md)
|
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
|
### Install
|
||||||
|
|
||||||
|
|
@ -46,6 +46,8 @@ English | [简体中文](README.md)
|
||||||
- ***Small in size, packaged for only 61M, it does include all the office suites***
|
- ***Small in size, packaged for only 61M, it does include all the office suites***
|
||||||
- ***Unlimited scalability, supports custom applications***
|
- ***Unlimited scalability, supports custom applications***
|
||||||
- ***Golang develops backend with low resource consumption and high performance***
|
- ***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
|
## Function Description
|
||||||
|
|
||||||
|
|
@ -53,65 +55,66 @@ English | [简体中文](README.md)
|
||||||
- Exquisite imitation of Windows style
|
- Exquisite imitation of Windows style
|
||||||
- Desktop file management
|
- Desktop file management
|
||||||
- Support native file drag and drop upload
|
- 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
|
### 2、 File management
|
||||||
- Drag and drop file upload
|
- Drag and drop file upload
|
||||||
- File Search
|
- File Search
|
||||||
- Native file storage
|
- 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
|
### 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.
|
- 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
|
### 4、 Documents
|
||||||
- Simple Word editor, native storage, supports QR codes, handwritten signatures, import and export
|
- 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
|
### 5、 Table
|
||||||
- Native storage, Excel editor, supports import and export, supports images and formulas
|
- 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
|
### 6、 Markdown
|
||||||
- Native storage, using VDitors, supporting import and export, outline and real-time preview
|
- 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
|
### 7、 Mind map
|
||||||
- Built in multiple themes; Support shortcut keys; Node content supports images, icons, hyperlinks, notes, and tags.
|
- 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
|
### 8、 Presentation Presentation
|
||||||
- Native storage, using pptist, supporting text, images, shapes, lines, charts, tables, videos, formulas, etc.
|
- 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
|
### 9、 File Editor
|
||||||
- Native storage, supports opening text/html/css/js/svg/xml/md, etc., can be used as a simple online editplus.
|
- 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
|
### 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.
|
- 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
|
### 11、 Image editing
|
||||||
- A small Photoshop with native storage, supporting image search, cropping, rotation, scaling, filtering, and other functions
|
- 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
|
### 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.
|
- 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
|
### 13、 Browser
|
||||||
- A simple built-in 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
|
### 14、 System settings
|
||||||
- You can switch storage methods and system backgrounds here.
|
- 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
|
### 15、 App Store
|
||||||
- App store management allows for the addition of external applications.
|
- 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
|
### 16、 Screenshot
|
||||||
- A simple screenshot tool. Save the screenshot file locally.
|
- A simple screenshot tool. Save the screenshot file locally.
|
||||||
|
|
@ -121,7 +124,7 @@ English | [简体中文](README.md)
|
||||||
|
|
||||||
### 18、 Calculator
|
### 18、 Calculator
|
||||||
- A calculator that mimics Windows 10 and supports historical records.
|
- 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
|
### 19、 Music Library
|
||||||
- A simple sound storage library that supports playing music.
|
- A simple sound storage library that supports playing music.
|
||||||
|
|
@ -131,7 +134,7 @@ English | [简体中文](README.md)
|
||||||
|
|
||||||
### 21、 Kanban board
|
### 21、 Kanban board
|
||||||
- Support standard kanban, a must-have tool for project management, which can quickly create kanban and place it in different folders
|
- 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
|
### 22、 Process management
|
||||||
- Support process management, can view process list, kill processes
|
- Support process management, can view process list, kill processes
|
||||||
|
|
|
||||||
35
README.md
35
README.md
|
|
@ -67,66 +67,67 @@
|
||||||
- 精仿windows风格
|
- 精仿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)
|
- 直接压缩/解压文件夹(本地存储支持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编辑器,原生存储,支持二维码、手写签名,导入导出
|
- 简便的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编辑器,支持导入、导出,支持图片、公式
|
- 原生存储,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
|
### 六、markdown
|
||||||
- 原生存储,采用vditor,支持导入、导出,支持大纲、实时预览
|
- 原生存储,采用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,支持文字、图片、形状、线条、图表、表格、视频、公式等。
|
- 原生存储,采用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。
|
- 原生存储,支持打开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,原生存储,支持搜索图片,支持图片裁剪、旋转、缩放、滤镜等功能
|
- 一个小型的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的计算器,支持历史记录。
|
- 一个仿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
Normal file
236
docs/Store.en.md
Normal file
|
|
@ -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.
|
||||||
|
|
||||||
|
|
@ -18,16 +18,15 @@
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"name": "", // string, 应用程序名称。
|
"name": "", // string, 应用程序名称。
|
||||||
"url": "", // string, 应用程序下载地址。
|
"url": "", // string, 应用程序下载地址或适配包的下载地址。
|
||||||
|
"pkg": "", // string, 应用程序的官方下载地址。可为空。
|
||||||
"webUrl":"", // string, 如何设置,应用程序将显示到桌面。
|
"webUrl":"", // string, 如何设置,应用程序将显示到桌面。
|
||||||
"isDev": true, // boolean, 是否为开发环境。如果设置为true将不会下载数据。
|
"isDev": true, // boolean, 是否为开发环境。如果设置为true将不会下载数据。
|
||||||
"needDownload": true, // boolean, 是否需要下载,本地开发设置为false。
|
|
||||||
"needInstall": true, // boolean, 是否需要安装。
|
|
||||||
"version": "1.0.0", // string, 应用程序版本。
|
"version": "1.0.0", // string, 应用程序版本。
|
||||||
"icon": "", // string, 应用程序图标,为可访问的网络地址。
|
"icon": "", // string, 应用程序图标,为可访问的网络地址。
|
||||||
"checkProgress": true, // boolean, 表示是否显示启动和停止。
|
"hasStart": true, // boolean, 表示是否显示启动和停止。
|
||||||
"hasRestart": true, // boolean, 是否需要重启。
|
"hasRestart": true, // boolean, 是否需要重启。
|
||||||
"setting": true // boolean, 是否需要设置。
|
"setting": true // boolean, 是否需要设置。只有应用停止才会显示。
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
@ -36,19 +35,25 @@
|
||||||
install.json的结构体为:
|
install.json的结构体为:
|
||||||
```json
|
```json
|
||||||
type InstallInfo struct {
|
type InstallInfo struct {
|
||||||
Name string `json:"name"` // 应用程序名称。重要,必须和应用的目录名称一致。
|
Name string `json:"name"` // 应用程序名称。
|
||||||
URL string `json:"url"` // 应用程序下载地址。
|
URL string `json:"url"` // 应用程序下载地址或适配包的下载地址。
|
||||||
|
Pkg string `json:"pkg"` // 应用程序的官方下载地址。
|
||||||
WebUrl string `json:"webUrl"` // 应用程序的网页地址。
|
WebUrl string `json:"webUrl"` // 应用程序的网页地址。
|
||||||
IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
|
IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
|
||||||
NeedDownload bool `json:"needDownload"` // 标志位,表示是否需要下载。
|
|
||||||
NeedInstall bool `json:"needInstall"` // 标志位,表示是否需要安装。
|
|
||||||
Version string `json:"version"` // 应用程序的版本号。
|
Version string `json:"version"` // 应用程序的版本号。
|
||||||
Desc string `json:"desc"` // 应用程序的描述信息。
|
Desc string `json:"desc"` // 应用程序的描述信息。
|
||||||
Icon string `json:"icon"` // 应用程序的图标路径。
|
Icon string `json:"icon"` // 应用程序的图标路径。
|
||||||
CheckProgress bool `json:"checkProgress"` // 标志位,表示是否显示启动和停止。
|
HasStart bool `json:"hasStart"` // 标志位,表示是否显示启动和停止。
|
||||||
HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
|
HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
|
||||||
Setting bool `json:"setting"` // 标志位,表示是否需要配置。
|
Setting bool `json:"setting"` // 标志位,表示是否需要配置。
|
||||||
Dependencies []Item `json:"dependencies"` // 依赖项。
|
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
|
```json
|
||||||
{
|
{
|
||||||
"name": "mysql5.7", // string, 应用程序名称。可不设置,会继承自install.json
|
"name": "mysql5.7", // string, 应用程序名称。可不设置,会继承自install.json
|
||||||
"icon": "mysql.png", // string, 应用程序图标,一般放在应用程序static目录下。
|
|
||||||
"setting": {
|
"setting": {
|
||||||
"binPath": "{exePath}/bin/mysqld.exe", // string, 重要,必须设置。为启动程序路径。
|
"binPath": "{exePath}/bin/mysqld.exe", // string, 重要,必须设置。为启动程序路径。
|
||||||
"confPath": "{exePath}/my.ini",// string, 可为空。为配置文件路径。
|
"confPath": "{exePath}/my.ini",// string, 可为空。为配置文件路径。
|
||||||
|
|
@ -84,11 +88,11 @@ type InstallInfo struct {
|
||||||
|
|
||||||
- 备注:核心的替换参数为`{exePath}`即程序的执行目录。其他`{参数}`对应`store.json`里的`config`。
|
- 备注:核心的替换参数为`{exePath}`即程序的执行目录。其他`{参数}`对应`store.json`里的`config`。
|
||||||
|
|
||||||
store.json的struct为
|
store.json的结构体为:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
type StoreInfo struct {
|
type StoreInfo struct {
|
||||||
Name string `json:"name"` // 应用程序商店的名称。
|
Name string `json:"name"` // 应用程序商店的名称。
|
||||||
Icon string `json:"icon"` // 应用程序商店的图标路径。
|
|
||||||
Setting Setting `json:"setting"` // 应用程序商店的配置信息。
|
Setting Setting `json:"setting"` // 应用程序商店的配置信息。
|
||||||
Config map[string]any `json:"config"` // 应用程序的配置信息映射。
|
Config map[string]any `json:"config"` // 应用程序的配置信息映射。
|
||||||
Commands map[string][]Cmd `json:"commands"` // 应用程序的命令集合。
|
Commands map[string][]Cmd `json:"commands"` // 应用程序的命令集合。
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "mysql5.7",
|
"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,
|
"isDev": true,
|
||||||
"needDownload": true,
|
|
||||||
"needInstall": true,
|
|
||||||
"version": "5.7.44",
|
"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",
|
"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,
|
"hasRestart": true,
|
||||||
"setting": true,
|
"setting": true,
|
||||||
"isOn": true
|
"isOn": true
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{
|
{
|
||||||
"icon": "mysql.png",
|
|
||||||
"setting": {
|
"setting": {
|
||||||
"binPath": "{exePath}/bin/mysqld.exe",
|
"binPath": "{exePath}/bin/mysqld.exe",
|
||||||
"confPath": "{exePath}/my.ini",
|
"confPath": "{exePath}/my.ini",
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,11 @@
|
||||||
{
|
{
|
||||||
"name": "mysql8.0",
|
"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,
|
"isDev": true,
|
||||||
"needDownload": true,
|
|
||||||
"needInstall": true,
|
|
||||||
"version": "8.0.39",
|
"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",
|
"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,
|
"hasRestart": true,
|
||||||
"setting": true,
|
"setting": true,
|
||||||
"isOn": true
|
"isOn": true
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "nginx",
|
"name": "nginx",
|
||||||
"icon": "nginx.png",
|
|
||||||
"setting": {
|
"setting": {
|
||||||
"binPath": "{exePath}/nginx.exe",
|
"binPath": "{exePath}/nginx.exe",
|
||||||
"progressName":"nginx.exe"
|
"progressName":"nginx.exe"
|
||||||
|
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"name": "redis5.0",
|
|
||||||
"icon": "redis.png",
|
|
||||||
"setting": {
|
|
||||||
"binPath": "{exePath}/redis-server.exe"
|
|
||||||
},
|
|
||||||
"start": {
|
|
||||||
"startCmds": [
|
|
||||||
"{exePath}/redis.windows.conf"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "redis5.0",
|
"name": "redis5.0",
|
||||||
"icon": "redis.png",
|
|
||||||
"setting": {
|
"setting": {
|
||||||
"binPath": "{exePath}/redis-server.exe"
|
"binPath": "{exePath}/redis-server.exe"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -216,17 +216,34 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-app {
|
.main-app {
|
||||||
width: 100%;
|
width: 96%;
|
||||||
/* height: 100%; */
|
/* height: 100%; */
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding:2%;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: flex-start;
|
|
||||||
padding: 10px;
|
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 {
|
.store-noready {
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,16 @@ const formData = ref({
|
||||||
const progress = ref(0)
|
const progress = ref(0)
|
||||||
const addType = [
|
const addType = [
|
||||||
{
|
{
|
||||||
'name': '远程下载',
|
name: t('store.remoteDownload'),
|
||||||
'type': 'download'
|
type: 'download'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '本地导入',
|
name: t('store.localImport'),
|
||||||
'type': 'local'
|
type: 'local'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'name': '开发模式',
|
name: t('store.devMode'),
|
||||||
'type': 'dev'
|
type: 'dev'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
function selectFile() {
|
function selectFile() {
|
||||||
|
|
@ -96,13 +96,13 @@ async function upload(e: any) {
|
||||||
}
|
}
|
||||||
const item = await completion.json()
|
const item = await completion.json()
|
||||||
//console.log(item)
|
//console.log(item)
|
||||||
if(item.data){
|
if (item.data) {
|
||||||
await installPlugin(item.data)
|
await installPlugin(item.data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
async function addAppByDev() {
|
async function addAppByDev() {
|
||||||
if(formData.value.devPath && formData.value.devPath != ""){
|
if (formData.value.devPath && formData.value.devPath != "") {
|
||||||
await installPlugin(formData.value.devPath)
|
await installPlugin(formData.value.devPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -121,30 +121,31 @@ async function installPlugin(pluginName: string) {
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<el-form label-width="auto" style="max-width: 600px;padding-left: 30px;">
|
<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-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-option v-for="item in addType" :key="item.type" :label="item.name" :value="item.type" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<template v-if="formData.importType == 'download'">
|
<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-input v-model="formData.url" style="width: 280px;" />
|
||||||
</el-form-item>
|
</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-progress :text-inside="true" :stroke-width="20" :percentage="progress" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="addAppByDownload()">添加</el-button>
|
<el-button type="primary" @click="addAppByDownload()">{{ t('store.add') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="formData.importType == 'local'">
|
<template v-if="formData.importType == 'local'">
|
||||||
<input type="file" style="display: none;" ref="fileInput" accept=".zip,.tar,.tar.gz,.tar.bz2" @change="upload">
|
<input type="file" style="display: none;" ref="fileInput" accept=".zip,.tar,.tar.gz,.tar.bz2"
|
||||||
<el-button type="primary" @click="addAppByImport()">导入压缩包</el-button>
|
@change="upload">
|
||||||
|
<el-button type="primary" @click="addAppByImport()">{{ t('store.importZip') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
<template v-if="formData.importType == 'dev'">
|
<template v-if="formData.importType == 'dev'">
|
||||||
<el-form-item label="本地路径">
|
<el-form-item :label="t('store.localPath')">
|
||||||
<el-input v-model="formData.devPath" style="width: 280px;" placeholder="用户目录下.godoos/run,只填目录名称"
|
<el-input v-model="formData.devPath" style="width: 280px;" :placeholder="t('store.userDirectory')"
|
||||||
@click="selectFile()" />
|
@click="selectFile()" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-button type="primary" @click="addAppByDev()">添加</el-button>
|
<el-button type="primary" @click="addAppByDev()">{{ t('store.add') }}</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-form>
|
</el-form>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
<script setup lang="ts">
|
||||||
import { BrowserWindow } from "@/system";
|
import { BrowserWindow } from "@/system";
|
||||||
defineProps({
|
import { t } from "@/i18n";
|
||||||
|
const downloading = ref(false)
|
||||||
|
const props = defineProps({
|
||||||
item: Object,
|
item: Object,
|
||||||
installedList: Array,
|
installedList: Array,
|
||||||
install: Function,
|
install: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
uninstall: Function,
|
uninstall: Function,
|
||||||
pause: Function,
|
pause: Function,
|
||||||
start: Function,
|
start: Function,
|
||||||
|
|
@ -44,7 +17,7 @@ defineProps({
|
||||||
|
|
||||||
function setting(item: any) {
|
function setting(item: any) {
|
||||||
const win = new BrowserWindow({
|
const win = new BrowserWindow({
|
||||||
title: "配置",
|
title: t('store.setting'),
|
||||||
url: `http://localhost:56780/static/${item.name}/index.html`,
|
url: `http://localhost:56780/static/${item.name}/index.html`,
|
||||||
icon: "gallery",
|
icon: "gallery",
|
||||||
width: 500,
|
width: 500,
|
||||||
|
|
@ -55,13 +28,45 @@ function setting(item: any) {
|
||||||
});
|
});
|
||||||
win.show()
|
win.show()
|
||||||
}
|
}
|
||||||
|
async function installApp(item: any) {
|
||||||
|
downloading.value = true;
|
||||||
|
await props.install(item);
|
||||||
|
}
|
||||||
</script>
|
</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>
|
<style scoped>
|
||||||
.app-item {
|
.app-item {
|
||||||
width: calc(100% - 20px);
|
width: calc(100% - 20px);
|
||||||
min-height: 100px;
|
height: 120px;
|
||||||
min-width:200px;
|
|
||||||
margin: 10px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
|
|
@ -113,7 +118,7 @@ function setting(item: any) {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
-webkit-line-clamp: 2;
|
width: 100%;
|
||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
padding: 5px; /* 添加内边距 */
|
padding: 5px; /* 添加内边距 */
|
||||||
/* 可选:添加边框 */
|
/* 可选:添加边框 */
|
||||||
|
|
@ -121,6 +126,9 @@ function setting(item: any) {
|
||||||
/* 可选:添加背景色 */
|
/* 可选:添加背景色 */
|
||||||
/* background-color: #f9f9f9; */
|
/* background-color: #f9f9f9; */
|
||||||
}
|
}
|
||||||
|
.app-desc .el-progress {
|
||||||
|
width: 90% !important; /* 或者任何你想要的具体宽度 */
|
||||||
|
}
|
||||||
@media (min-width: 769px) {
|
@media (min-width: 769px) {
|
||||||
.app-desc {
|
.app-desc {
|
||||||
font-size: 14px; /* 在大屏幕上使用更大的字体大小 */
|
font-size: 14px; /* 在大屏幕上使用更大的字体大小 */
|
||||||
|
|
@ -141,18 +149,17 @@ function setting(item: any) {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 5px;
|
||||||
margin-top: 20px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-button button {
|
.app-button button {
|
||||||
width: 60px;
|
width: 50px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #ccc;
|
|
||||||
background-color: #fff;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
font-size: 14px;
|
||||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,75 +1,79 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, onMounted } from "vue";
|
import { onMounted } from "vue";
|
||||||
import { System } from "@/system";
|
|
||||||
import { notifySuccess, notifyError } from "@/util/msg";
|
import { notifySuccess, notifyError } from "@/util/msg";
|
||||||
import { useStoreStore } from "@/stores/store";
|
import { useStoreStore } from "@/stores/store";
|
||||||
import { setSystemKey,parseJson } from "@/system/config";
|
import { parseJson } from "@/system/config";
|
||||||
import { t } from "@/i18n";
|
import { t } from "@/i18n";
|
||||||
const sys: any = inject<System>("system");
|
|
||||||
const store = useStoreStore()
|
const store = useStoreStore()
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await store.getList();
|
await store.getList();
|
||||||
});
|
});
|
||||||
|
|
||||||
function setCache() {
|
|
||||||
setSystemKey("intstalledPlugins", store.installedList);
|
|
||||||
setTimeout(() => {
|
|
||||||
sys.refershAppList();
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
async function install(item: any) {
|
async function install(item: any) {
|
||||||
//console.log(item)
|
//console.log(item)
|
||||||
if (item.isOut) {
|
if (item.isOut) {
|
||||||
item.progress = 0
|
item.progress = 0
|
||||||
const flag = await store.addOutList(item)
|
const flag = await store.addOutList(item)
|
||||||
if(!flag){
|
if (!flag) {
|
||||||
notifyError(t("store.installError"))
|
notifyError(t("store.installError"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (item.needDownload && !item.isDev) {
|
if ((item.pkg != "" || item.url != "") && !item.isDev) {
|
||||||
await download(item)
|
const postData = {
|
||||||
|
name: item.name,
|
||||||
|
pkg: item.pkg,
|
||||||
|
url: item.url,
|
||||||
|
needInstall: item.needInstall,
|
||||||
}
|
}
|
||||||
|
const url = store.apiUrl + '/store/download'
|
||||||
if (item.needInstall) {
|
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
|
||||||
|
}
|
||||||
|
item = res.data
|
||||||
item.progress = 0
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.webUrl) {
|
await store.addDesktop(item);
|
||||||
await sys.fs.writeFile(
|
|
||||||
`${sys._options.userLocation}Desktop/${item.name}.url`,
|
|
||||||
`link::url::${item.webUrl}::${item.icon}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
notifySuccess(t("store.installSuccess"))
|
|
||||||
store.installedList.push(item.name);
|
|
||||||
await store.checkProgress()
|
await store.checkProgress()
|
||||||
setCache();
|
notifySuccess(t("store.installSuccess"))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
async function uninstall(item: any) {
|
||||||
if (item.webUrl) {
|
await store.removeDesktop(item);
|
||||||
await sys.fs.unlink(`${sys._options.userLocation}Desktop/${item.name}.url`);
|
|
||||||
}
|
|
||||||
|
|
||||||
delete store.installedList[store.installedList.indexOf(item.name)];
|
|
||||||
setCache();
|
|
||||||
if (item.needInstall) {
|
if (item.needInstall) {
|
||||||
item.progress = 0
|
|
||||||
const completion = await fetch(store.apiUrl + '/store/uninstall?name=' + item.name)
|
const completion = await fetch(store.apiUrl + '/store/uninstall?name=' + item.name)
|
||||||
if (!completion.ok) {
|
if (!completion.ok) {
|
||||||
notifyError(t("store.hasSameName"))
|
notifyError(t("store.hasSameName"))
|
||||||
|
|
@ -83,10 +87,13 @@ async function uninstall(item: any) {
|
||||||
}
|
}
|
||||||
notifySuccess(t("store.uninstallSuccess"))
|
notifySuccess(t("store.uninstallSuccess"))
|
||||||
}
|
}
|
||||||
async function download(item: any) {
|
async function download(item: any, url: string, postData: any) {
|
||||||
//console.log(item)
|
//console.log(item)
|
||||||
if (item.progress) return;
|
if (item.progress && item.progress > 0) return;
|
||||||
const completion = await fetch(store.apiUrl + '/store/download?url=' + item.url)
|
const completion = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(postData),
|
||||||
|
})
|
||||||
if (!completion.ok) {
|
if (!completion.ok) {
|
||||||
notifyError(t("store.downloadError"))
|
notifyError(t("store.downloadError"))
|
||||||
}
|
}
|
||||||
|
|
@ -95,6 +102,7 @@ async function download(item: any) {
|
||||||
if (!reader) {
|
if (!reader) {
|
||||||
notifyError(t("store.cantStream"));
|
notifyError(t("store.cantStream"));
|
||||||
}
|
}
|
||||||
|
let res:any
|
||||||
while (true) {
|
while (true) {
|
||||||
const { done, value } = await reader?.read();
|
const { done, value } = await reader?.read();
|
||||||
if (done) {
|
if (done) {
|
||||||
|
|
@ -102,20 +110,15 @@ async function download(item: any) {
|
||||||
}
|
}
|
||||||
// console.log(value)
|
// console.log(value)
|
||||||
const json = await new TextDecoder().decode(value);
|
const json = await new TextDecoder().decode(value);
|
||||||
//console.log(json)
|
res = parseJson(json)
|
||||||
const res = parseJson(json)
|
|
||||||
//console.log(res)
|
//console.log(res)
|
||||||
if (res) {
|
if (res) {
|
||||||
if (res.progress) {
|
if (res.progress) {
|
||||||
item.progress = res.progress
|
item.progress = res.progress
|
||||||
}
|
}
|
||||||
if (res.done) {
|
|
||||||
notifySuccess(t("store.downloadSuccess"))
|
|
||||||
item.progress = 0
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return res
|
||||||
}
|
}
|
||||||
async function pauseApp(item: any) {
|
async function pauseApp(item: any) {
|
||||||
const res: any = await fetch(store.apiUrl + '/store/stop/' + item.name)
|
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>
|
<span class="sub-title">{{ store.currentTitle }} </span>
|
||||||
</div>
|
</div>
|
||||||
<div class="main-app">
|
<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"
|
<AppItem :item="item" :installed-list="store.installedList" :install="install" :uninstall="uninstall"
|
||||||
:pause="pauseApp" :start="startApp" :restart="restartApp" />
|
:pause="pauseApp" :start="startApp" :restart="restartApp" />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -146,15 +146,38 @@
|
||||||
"utilities": "Utilities",
|
"utilities": "Utilities",
|
||||||
"others": "Others",
|
"others": "Others",
|
||||||
"add": "Add",
|
"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":{
|
"process": {
|
||||||
"title":"Process Manager",
|
"title": "Process Manager",
|
||||||
"port":"Port",
|
"port": "Port",
|
||||||
"pid":"PID",
|
"pid": "PID",
|
||||||
"name":"Name",
|
"name": "Name",
|
||||||
"proto":"Protocol",
|
"proto": "Protocol",
|
||||||
"action":"Actions"
|
"action": "Actions"
|
||||||
},
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"title": "New version",
|
"title": "New version",
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@
|
||||||
"backup": "备份",
|
"backup": "备份",
|
||||||
"import.success.reboot.soon": "导入成功,即将重启",
|
"import.success.reboot.soon": "导入成功,即将重启",
|
||||||
"import.failed": "导入失败",
|
"import.failed": "导入失败",
|
||||||
"zip":"压缩",
|
"zip": "压缩",
|
||||||
"unzip":"解压",
|
"unzip": "解压",
|
||||||
"import": "导入",
|
"import": "导入",
|
||||||
"export": "导出",
|
"export": "导出",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
|
|
@ -135,34 +135,50 @@
|
||||||
"cannot.create.shortcut": "无法创建快捷方式",
|
"cannot.create.shortcut": "无法创建快捷方式",
|
||||||
"shortcut.has.been.created": "快捷方式已创建",
|
"shortcut.has.been.created": "快捷方式已创建",
|
||||||
"system.message": "系统消息",
|
"system.message": "系统消息",
|
||||||
"store" : {
|
"store": {
|
||||||
"hots":"热门",
|
"hots": "热门",
|
||||||
"work": "办公",
|
"work": "办公",
|
||||||
"development": "开发工具",
|
"development": "开发工具",
|
||||||
"games": "游戏",
|
"games": "游戏",
|
||||||
"education": "教育",
|
"education": "教育",
|
||||||
"news": "新闻",
|
"news": "新闻",
|
||||||
"shopping":"购物",
|
"shopping": "购物",
|
||||||
"social": "社交",
|
"social": "社交",
|
||||||
"utilities": "实用工具",
|
"utilities": "实用工具",
|
||||||
"others": "其他",
|
"others": "其他",
|
||||||
"add":"添加应用",
|
"add": "添加应用",
|
||||||
"errorList":"获取列表失败!",
|
"errorList": "获取列表失败!",
|
||||||
"urlEmpty":"网址为空",
|
"urlEmpty": "网址为空",
|
||||||
"hasSameName":"存在相同名称的应用!",
|
"hasSameName": "存在相同名称的应用!",
|
||||||
"installError":"安装失败!",
|
"installError": "安装失败!",
|
||||||
"installSuccess":"安装成功!",
|
"installSuccess": "安装成功!",
|
||||||
"uninstallSuccess":"卸载成功",
|
"uninstallSuccess": "卸载成功",
|
||||||
"downloadError":"下载失败!",
|
"downloadError": "下载失败!",
|
||||||
"cantStream":"暂不支持流下载!"
|
"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":{
|
"process": {
|
||||||
"title":"进程管理",
|
"title": "进程管理",
|
||||||
"port":"端口",
|
"port": "端口",
|
||||||
"pid":"PID",
|
"pid": "PID",
|
||||||
"name":"名称",
|
"name": "名称",
|
||||||
"proto":"类型",
|
"proto": "类型",
|
||||||
"action":"操作"
|
"action": "操作"
|
||||||
},
|
},
|
||||||
"upgrade": {
|
"upgrade": {
|
||||||
"title": "新版本升级",
|
"title": "新版本升级",
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,19 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from "vue";
|
import { ref, inject } from "vue";
|
||||||
import { t } from "@/i18n";
|
import { t } from "@/i18n";
|
||||||
// import storeInitList from "@/assets/store.json";
|
// import storeInitList from "@/assets/store.json";
|
||||||
|
import { System } from "@/system";
|
||||||
import { getSystemKey } from "@/system/config";
|
import { getSystemKey } from "@/system/config";
|
||||||
export const useStoreStore = defineStore('storeStore', () => {
|
export const useStoreStore = defineStore('storeStore', () => {
|
||||||
|
const sys: any = inject<System>("system");
|
||||||
const currentCateId = ref(0)
|
const currentCateId = ref(0)
|
||||||
const currentTitle = ref(t("store.hots"))
|
const currentTitle = ref(t("store.hots"))
|
||||||
const currentCate = ref('hots')
|
const currentCate = ref('hots')
|
||||||
const categoryList = ['hots', 'work', 'development', 'games', 'education', 'news', 'shopping', 'social', 'utilities', 'others', 'add']
|
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 categoryIcon = ['HomeFilled', 'Odometer', 'Postcard', 'TrendCharts', 'School', 'HelpFilled', 'ShoppingCart', 'ChatLineRound', 'MessageBox', 'Ticket', 'CirclePlusFilled']
|
||||||
const isready = ref(false);
|
const isready = ref(false);
|
||||||
const installed = getSystemKey("intstalledPlugins");
|
|
||||||
const apiUrl = getSystemKey("apiUrl");
|
const apiUrl = getSystemKey("apiUrl");
|
||||||
const installedList: any = ref(installed);
|
const installedList: any = ref([]);
|
||||||
const storeList: any = ref([])
|
const storeList: any = ref([])
|
||||||
const outList: any = ref([])
|
const outList: any = ref([])
|
||||||
async function getList() {
|
async function getList() {
|
||||||
|
|
@ -23,12 +24,13 @@ export const useStoreStore = defineStore('storeStore', () => {
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
let list:any = await res.json()
|
const json: any = await res.json()
|
||||||
|
let list = json.data || []
|
||||||
//console.log(data)
|
//console.log(data)
|
||||||
if (outList.value.length > 0 && currentCate.value == 'hots' && list && list.length > 0) {
|
if (outList.value.length > 0 && currentCate.value == 'hots' && list && list.length > 0) {
|
||||||
const names = list.map((item : any) => item.name)
|
const names = list.map((item: any) => item.name)
|
||||||
const adds:any = []
|
const adds: any = []
|
||||||
outList.value.forEach((item : any) => {
|
outList.value.forEach((item: any) => {
|
||||||
if (!names.includes(item.name)) {
|
if (!names.includes(item.name)) {
|
||||||
adds.push(item)
|
adds.push(item)
|
||||||
}
|
}
|
||||||
|
|
@ -39,16 +41,14 @@ export const useStoreStore = defineStore('storeStore', () => {
|
||||||
await checkProgress()
|
await checkProgress()
|
||||||
isready.value = true;
|
isready.value = true;
|
||||||
}
|
}
|
||||||
async function addOutList(item:any) {
|
async function addOutList(item: any) {
|
||||||
console.log(item)
|
const has = outList.value.find((i: any) => i.name === item.name)
|
||||||
const has = outList.value.find((i:any) => i.name === item.name)
|
if (!has) {
|
||||||
console.log(has)
|
|
||||||
if(!has) {
|
|
||||||
item.isOut = true
|
item.isOut = true
|
||||||
outList.value.push(item)
|
outList.value.push(item)
|
||||||
await getList()
|
await getList()
|
||||||
return true
|
return true
|
||||||
}else{
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 {
|
return {
|
||||||
currentCateId,
|
currentCateId,
|
||||||
categoryIcon,
|
categoryIcon,
|
||||||
|
|
@ -93,7 +112,9 @@ export const useStoreStore = defineStore('storeStore', () => {
|
||||||
changeCate,
|
changeCate,
|
||||||
getList,
|
getList,
|
||||||
addOutList,
|
addOutList,
|
||||||
checkProgress
|
checkProgress,
|
||||||
|
addDesktop,
|
||||||
|
removeDesktop
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
persist: {
|
persist: {
|
||||||
|
|
@ -102,7 +123,8 @@ export const useStoreStore = defineStore('storeStore', () => {
|
||||||
{
|
{
|
||||||
storage: localStorage,
|
storage: localStorage,
|
||||||
paths: [
|
paths: [
|
||||||
"outList"
|
"outList",
|
||||||
|
"installedList"
|
||||||
]
|
]
|
||||||
}, // name 字段用localstorage存储
|
}, // name 字段用localstorage存储
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -102,10 +102,7 @@ export const getSystemConfig = (ifset = false) => {
|
||||||
dbname: ''
|
dbname: ''
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// 初始化已安装插件列表,若本地存储中已存在则不进行覆盖
|
|
||||||
if (!config.intstalledPlugins) {
|
|
||||||
config.intstalledPlugins = [];
|
|
||||||
}
|
|
||||||
// 初始化桌面快捷方式列表,若本地存储中已存在则不进行覆盖
|
// 初始化桌面快捷方式列表,若本地存储中已存在则不进行覆盖
|
||||||
if (!config.desktopList) {
|
if (!config.desktopList) {
|
||||||
config.desktopList = [];
|
config.desktopList = [];
|
||||||
|
|
|
||||||
1
go.mod
1
go.mod
|
|
@ -6,6 +6,7 @@ require github.com/wailsapp/wails/v2 v2.9.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
aead.dev/minisign v0.2.0 // indirect
|
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/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
github.com/gorilla/mux v1.8.1 // indirect
|
github.com/gorilla/mux v1.8.1 // indirect
|
||||||
github.com/minio/selfupdate v0.6.0 // indirect
|
github.com/minio/selfupdate v0.6.0 // indirect
|
||||||
|
|
|
||||||
2
go.sum
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=
|
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 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ func OsStart() {
|
||||||
progressRouter.HandleFunc("/listport", store.ListAllProcessesHandler).Methods(http.MethodGet)
|
progressRouter.HandleFunc("/listport", store.ListAllProcessesHandler).Methods(http.MethodGet)
|
||||||
progressRouter.HandleFunc("/killport", store.KillProcessByNameHandler).Methods(http.MethodGet)
|
progressRouter.HandleFunc("/killport", store.KillProcessByNameHandler).Methods(http.MethodGet)
|
||||||
progressRouter.HandleFunc("/storelist", store.GetStoreListHandler).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("/install", store.InstallHandler).Methods(http.MethodGet)
|
||||||
progressRouter.HandleFunc("/installInfo", store.GetInstallInfoHandler).Methods(http.MethodGet)
|
progressRouter.HandleFunc("/installInfo", store.GetInstallInfoHandler).Methods(http.MethodGet)
|
||||||
progressRouter.HandleFunc("/installOut", store.RunOutHandler).Methods(http.MethodGet)
|
progressRouter.HandleFunc("/installOut", store.RunOutHandler).Methods(http.MethodGet)
|
||||||
|
|
|
||||||
|
|
@ -442,12 +442,12 @@ func HandleUnZip(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
zipFilePath := filepath.Join(basePath, path)
|
zipFilePath := filepath.Join(basePath, path)
|
||||||
destPath := filepath.Dir(zipFilePath)
|
destPath := filepath.Dir(zipFilePath)
|
||||||
err = Decompress(zipFilePath, destPath)
|
zipPath, err := Decompress(zipFilePath, destPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
libs.ErrorMsg(w, err.Error())
|
libs.ErrorMsg(w, err.Error())
|
||||||
return
|
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)
|
json.NewEncoder(w).Encode(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,53 @@ func Rmdir(basePath, dirPath string) error {
|
||||||
fullPath := filepath.Join(basePath, dirPath)
|
fullPath := filepath.Join(basePath, dirPath)
|
||||||
return os.RemoveAll(fullPath)
|
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
|
// CopyFile copies a file
|
||||||
func CopyFile(src, dst string) error {
|
func CopyFile(src, dst string) error {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Decompress 解压文件到指定目录,支持多种压缩格式
|
// Decompress 解压文件到指定目录,支持多种压缩格式
|
||||||
func Decompress(compressedFilePath, destPath string) error {
|
func Decompress(compressedFilePath, destPath string) (string, error) {
|
||||||
ext := filepath.Ext(compressedFilePath)
|
ext := filepath.Ext(compressedFilePath)
|
||||||
switch ext {
|
switch ext {
|
||||||
case ".zip":
|
case ".zip":
|
||||||
|
|
@ -23,7 +23,7 @@ func Decompress(compressedFilePath, destPath string) error {
|
||||||
case ".tar":
|
case ".tar":
|
||||||
tarFile, err := os.Open(compressedFilePath)
|
tarFile, err := os.Open(compressedFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer tarFile.Close()
|
defer tarFile.Close()
|
||||||
return Untar(tarFile, destPath)
|
return Untar(tarFile, destPath)
|
||||||
|
|
@ -32,12 +32,13 @@ func Decompress(compressedFilePath, destPath string) error {
|
||||||
case ".bz2":
|
case ".bz2":
|
||||||
return Untarbz2(compressedFilePath, destPath)
|
return Untarbz2(compressedFilePath, destPath)
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported compression format: %s", ext)
|
return "", fmt.Errorf("unsupported compression format: %s", ext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Untar 解压.tar文件到指定目录
|
// 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)
|
tarReader := tar.NewReader(reader)
|
||||||
for {
|
for {
|
||||||
header, err := tarReader.Next()
|
header, err := tarReader.Next()
|
||||||
|
|
@ -45,121 +46,147 @@ func Untar(reader io.Reader, destPath string) error {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if err != nil {
|
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)
|
target := filepath.Join(destPath, header.Name)
|
||||||
|
|
||||||
// Ensure that all directories in the path are created.
|
|
||||||
if header.Typeflag == tar.TypeDir {
|
if header.Typeflag == tar.TypeDir {
|
||||||
if !libs.PathExists(target) {
|
|
||||||
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
|
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
continue
|
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 {
|
if err := os.MkdirAll(filepath.Dir(target), os.ModePerm); err != nil {
|
||||||
return err
|
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))
|
outFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer outFile.Close()
|
defer outFile.Close()
|
||||||
|
|
||||||
if _, err := io.Copy(outFile, tarReader); err != nil {
|
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文件到指定目录
|
// Untargz 解压.tar.gz文件到指定目录
|
||||||
func Untargz(targzFilePath, destPath string) error {
|
func Untargz(targzFilePath, destPath string) (string, error) {
|
||||||
gzFile, err := os.Open(targzFilePath)
|
gzFile, err := os.Open(targzFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer gzFile.Close()
|
defer gzFile.Close()
|
||||||
|
|
||||||
gzReader, err := gzip.NewReader(gzFile)
|
gzReader, err := gzip.NewReader(gzFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer gzReader.Close()
|
defer gzReader.Close()
|
||||||
|
|
||||||
return Untar(gzReader, destPath) // 传递gzReader给Untar
|
return Untar(gzReader, destPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Untarbz2 解压.tar.bz2文件到指定目录
|
// Untarbz2 解压.tar.bz2文件到指定目录
|
||||||
func Untarbz2(tarbz2FilePath, destPath string) error {
|
func Untarbz2(tarbz2FilePath, destPath string) (string, error) {
|
||||||
bz2File, err := os.Open(tarbz2FilePath)
|
bz2File, err := os.Open(tarbz2FilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer bz2File.Close()
|
defer bz2File.Close()
|
||||||
|
|
||||||
bz2Reader := bzip2.NewReader(bz2File)
|
bz2Reader := bzip2.NewReader(bz2File)
|
||||||
return Untar(bz2Reader, destPath) // 传递bz2Reader给Untar
|
return Untar(bz2Reader, destPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecompressZip 解压zip文件到指定目录
|
// DecompressZip 解压zip文件到指定目录
|
||||||
func Unzip(zipFilePath, destPath string) error {
|
func Unzip(zipFilePath, destPath string) (string, error) {
|
||||||
r, err := zip.OpenReader(zipFilePath)
|
r, err := zip.OpenReader(zipFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer r.Close()
|
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 {
|
for _, f := range r.File {
|
||||||
rc, err := f.Open()
|
rc, err := f.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer rc.Close()
|
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 !libs.PathExists(filepath.Dir(path)) {
|
||||||
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if f.FileInfo().IsDir() {
|
if f.FileInfo().IsDir() {
|
||||||
// Create the directory itself if it doesn't exist yet.
|
// 创建目录
|
||||||
if !libs.PathExists(path) {
|
if !libs.PathExists(path) {
|
||||||
if err := os.Mkdir(path, f.Mode()); err != nil {
|
if err := os.Mkdir(path, f.Mode()); err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
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())
|
outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return "", err
|
||||||
}
|
}
|
||||||
defer outFile.Close()
|
defer outFile.Close()
|
||||||
|
|
||||||
// Write the contents of the zip file entry to the output file.
|
|
||||||
if _, err := io.Copy(outFile, rc); err != nil {
|
if _, err := io.Copy(outFile, rc); err != nil {
|
||||||
return err
|
return "", err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
// 返回完整的顶层目录路径
|
||||||
|
return topLevelDestPath, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsSupportedCompressionFormat 判断文件后缀是否为支持的压缩格式
|
// IsSupportedCompressionFormat 判断文件后缀是否为支持的压缩格式
|
||||||
|
|
@ -180,16 +207,13 @@ func IsSupportedCompressionFormat(filename string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandlerFile 根据文件后缀判断是否支持解压,支持则解压,否则移动文件
|
// HandlerFile 根据文件后缀判断是否支持解压,支持则解压,否则移动文件
|
||||||
func HandlerFile(filePath string, destDir string) error {
|
func HandlerFile(filePath string, destDir string) (string, error) {
|
||||||
// log.Printf("HandlerFile: %s", filePath)
|
// log.Printf("HandlerFile: %s", filePath)
|
||||||
// log.Printf("destDir: %s", destDir)
|
// log.Printf("destDir: %s", destDir)
|
||||||
if IsSupportedCompressionFormat(filePath) {
|
if IsSupportedCompressionFormat(filePath) {
|
||||||
// 移动文件
|
|
||||||
// newPath := filepath.Join(destDir, filepath.Base(filePath))
|
|
||||||
// return os.Rename(filePath, newPath)
|
|
||||||
// 解压文件
|
|
||||||
return Decompress(filePath, destDir)
|
return Decompress(filePath, destDir)
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("unsupported compression format: %s", filePath)
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
aead.dev/minisign v0.2.0 // indirect
|
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/go-ole/go-ole v1.2.6 // indirect
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
github.com/stretchr/testify v1.9.0 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
|
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
|
||||||
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
|
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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,9 @@ func HTTPError(w http.ResponseWriter, status int, message string) {
|
||||||
func ErrorMsg(w http.ResponseWriter, message string) {
|
func ErrorMsg(w http.ResponseWriter, message string) {
|
||||||
WriteJSONResponse(w, APIResponse{Message: message, Code: -1}, 200)
|
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) {
|
func SuccessMsg(w http.ResponseWriter, data any, message string) {
|
||||||
WriteJSONResponse(w, APIResponse{Message: message, Data: data, Code: 0}, 200)
|
WriteJSONResponse(w, APIResponse{Message: message, Data: data, Code: 0}, 200)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,8 @@ func MkDir(cmd Cmd) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func Unzip(cmd Cmd) error {
|
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 {
|
func Zip(cmd Cmd) error {
|
||||||
return files.Encompress(cmd.FilePath, cmd.Content)
|
return files.Encompress(cmd.FilePath, cmd.Content)
|
||||||
|
|
|
||||||
|
|
@ -2,155 +2,236 @@ package store
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"godo/files"
|
"godo/files"
|
||||||
"godo/libs"
|
"godo/libs"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/cavaliergopher/grab/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProgressReader struct {
|
const (
|
||||||
reader io.Reader
|
concurrency = 6 // 并发下载数
|
||||||
total int64
|
)
|
||||||
err error
|
|
||||||
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DownloadStatus struct {
|
type ReqBody struct {
|
||||||
|
DownloadUrl string `json:"url"`
|
||||||
|
PkgUrl string `json:"pkg"`
|
||||||
Name string `json:"name"`
|
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"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pr *ProgressReader) Read(p []byte) (n int, err error) {
|
|
||||||
n, err = pr.reader.Read(p)
|
|
||||||
pr.err = err
|
|
||||||
pr.total += int64(n)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
func DownloadHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
url := r.URL.Query().Get("url")
|
var reqBody ReqBody
|
||||||
log.Printf("Download url: %s", url)
|
err := json.NewDecoder(r.Body).Decode(&reqBody)
|
||||||
|
if err != nil {
|
||||||
// 获取下载目录,这里假设从请求参数中获取,如果没有则使用默认值
|
libs.ErrorMsg(w, "first Decode request body error")
|
||||||
|
return
|
||||||
|
}
|
||||||
downloadDir := libs.GetCacheDir()
|
downloadDir := libs.GetCacheDir()
|
||||||
if downloadDir == "" {
|
if downloadDir == "" {
|
||||||
downloadDir = "./downloads"
|
downloadDir = "./downloads"
|
||||||
}
|
}
|
||||||
|
if reqBody.Name == "" {
|
||||||
// 拼接完整的文件路径
|
libs.ErrorMsg(w, "the name is empty")
|
||||||
fileName := filepath.Base(url)
|
return
|
||||||
filePath := filepath.Join(downloadDir, fileName)
|
}
|
||||||
|
if reqBody.DownloadUrl == "" && reqBody.PkgUrl == "" {
|
||||||
// 开始下载
|
libs.ErrorMsg(w, "the url is empty")
|
||||||
resp, err := http.Get(url)
|
return
|
||||||
if err != nil {
|
}
|
||||||
log.Printf("Failed to get file: %v", err)
|
paths := []string{}
|
||||||
http.Error(w, "Failed to get file", http.StatusInternalServerError)
|
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
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// 检查文件是否已存在且大小一致
|
resp := client.Do(req)
|
||||||
if fileInfo, err := os.Stat(filePath); err == nil {
|
if fileInfo, err := os.Stat(filePath); err == nil {
|
||||||
if fileInfo.Size() == resp.ContentLength {
|
// 文件存在,检查文件大小是否与远程资源匹配
|
||||||
// 文件已存在且大小一致,无需下载
|
if fileInfo.Size() == resp.Size() { // 这里的req.Size需要从下载请求中获取,或通过其他方式预知
|
||||||
|
log.Printf("File %s already exists and is up-to-date.", filePath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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 {
|
||||||
|
libs.ErrorData(w, installInfo, "the install.json is error:"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
libs.SuccessMsg(w, installInfo, "install the app success!")
|
||||||
|
}
|
||||||
|
func HandlerZipFiles(paths []string, name string) error {
|
||||||
runDir := libs.GetRunDir()
|
runDir := libs.GetRunDir()
|
||||||
err := files.HandlerFile(filePath, runDir)
|
targetDir := filepath.Join(runDir, name)
|
||||||
|
downloadDir := libs.GetCacheDir()
|
||||||
|
for _, filePath := range paths {
|
||||||
|
zipPath, err := files.Decompress(filePath, downloadDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error moving file: %v", err)
|
return fmt.Errorf("decompress failed: %v", err)
|
||||||
}
|
}
|
||||||
libs.SuccessMsg(w, "success", "File already exists and is of correct size")
|
err = files.CopyResource(zipPath, targetDir)
|
||||||
return
|
|
||||||
} else {
|
|
||||||
// 重新打开响应体以便后续读取
|
|
||||||
resp.Body = http.NoBody
|
|
||||||
resp, err = http.Get(url)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to get file: %v", err)
|
return fmt.Errorf("copyResource failed")
|
||||||
http.Error(w, "Failed to get file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
err = os.RemoveAll(zipPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("removeAll failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建文件
|
|
||||||
file, err := os.Create(filePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to create file: %v", err)
|
|
||||||
http.Error(w, "Failed to create file", http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
// 使用ProgressReader来跟踪进度
|
return nil
|
||||||
pr := &ProgressReader{reader: resp.Body}
|
}
|
||||||
|
func trackProgress(w http.ResponseWriter, resp *grab.Response, md5url string) {
|
||||||
// 启动定时器来报告进度
|
defer func() {
|
||||||
ticker := time.NewTicker(200 * time.Millisecond)
|
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()
|
defer ticker.Stop()
|
||||||
|
|
||||||
flusher, ok := w.(http.Flusher)
|
flusher, ok := w.(http.Flusher)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("Streaming unsupported")
|
log.Printf("Streaming unsupported")
|
||||||
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
|
||||||
for {
|
for {
|
||||||
<-ticker.C
|
select {
|
||||||
rp := &DownloadStatus{
|
case <-ticker.C:
|
||||||
Name: fileName,
|
fp := FileProgress{
|
||||||
Path: filePath,
|
Progress: int(resp.Progress() * 100),
|
||||||
Url: url,
|
IsFinished: resp.IsComplete(),
|
||||||
Current: pr.total,
|
Total: resp.Size(),
|
||||||
Size: resp.ContentLength,
|
Current: resp.BytesComplete(),
|
||||||
Speed: 0, // 这里可以计算速度,但为了简化示例,我们暂时设为0
|
Status: "loading",
|
||||||
Progress: int(100 * (float64(pr.total) / float64(resp.ContentLength))),
|
|
||||||
Downloading: pr.err == nil && pr.total < resp.ContentLength,
|
|
||||||
Done: pr.total == resp.ContentLength,
|
|
||||||
}
|
}
|
||||||
if pr.err != nil || rp.Done {
|
//log.Printf("Progress: %v", fp)
|
||||||
rp.Downloading = false
|
if resp.IsComplete() && fp.Current == fp.Total {
|
||||||
//log.Printf("Download complete: %s", filePath)
|
fp.Status = "loaded"
|
||||||
runDir := libs.GetRunDir()
|
}
|
||||||
err := files.HandlerFile(filePath, runDir)
|
jsonBytes, err := json.Marshal(fp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error moving file: %v", err)
|
log.Printf("Error marshaling FileProgress to JSON: %v", err)
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if w != nil {
|
|
||||||
jsonBytes, err := json.Marshal(rp)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error marshaling DownloadStatus to JSON: %v", err)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
w.Write(jsonBytes)
|
if w != nil {
|
||||||
|
io.WriteString(w, string(jsonBytes))
|
||||||
w.Write([]byte("\n"))
|
w.Write([]byte("\n"))
|
||||||
flusher.Flush()
|
flusher.Flush()
|
||||||
} else {
|
} else {
|
||||||
log.Println("ResponseWriter is nil, cannot send progress")
|
log.Println("ResponseWriter is nil, cannot send progress")
|
||||||
}
|
}
|
||||||
}
|
if fp.Status == "loaded" {
|
||||||
}()
|
|
||||||
|
|
||||||
// 将响应体的内容写入文件
|
|
||||||
_, err = io.Copy(file, pr)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Failed to write file: %v", err)
|
|
||||||
http.Error(w, "Failed to write file", http.StatusInternalServerError)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
libs.SuccessMsg(w, "success", "Download complete")
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predefined filename for the icon
|
||||||
|
const iconFilename = "icon.png"
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,151 +18,193 @@ func InstallHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
libs.ErrorMsg(w, "the app name is empty!")
|
libs.ErrorMsg(w, "the app name is empty!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
exePath := GetExePath(pluginName)
|
installInfo, err := Installation(pluginName)
|
||||||
//log.Printf("the app path is %s", exePath)
|
if err != nil {
|
||||||
if !libs.PathExists(exePath) {
|
libs.ErrorData(w, installInfo, "the install.json is error:"+err.Error())
|
||||||
libs.ErrorMsg(w, "the app path is not exists!")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
libs.SuccessMsg(w, installInfo, "install the app success!")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Installation 处理安装逻辑
|
||||||
|
func Installation(pluginName string) (InstallInfo, error) {
|
||||||
|
var installInfo InstallInfo
|
||||||
|
exePath := GetExePath(pluginName)
|
||||||
|
if !libs.PathExists(exePath) {
|
||||||
|
return installInfo, fmt.Errorf("the app path is not exists")
|
||||||
|
}
|
||||||
installInfo, err := GetInstallInfo(pluginName)
|
installInfo, err := GetInstallInfo(pluginName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
libs.ErrorMsg(w, "the install.json is error:"+err.Error())
|
return installInfo, fmt.Errorf("the install.json is error: %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if len(installInfo.Dependencies) > 0 {
|
if len(installInfo.Dependencies) > 0 {
|
||||||
var needInstalls []Item
|
var needInstalls []Item
|
||||||
for _, item := range installInfo.Dependencies {
|
for _, item := range installInfo.Dependencies {
|
||||||
info, err := GetInstallInfo(item.Value.(string))
|
depInfo, err := GetInstallInfo(item.Value.(string))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
needInstalls = append(needInstalls, item)
|
needInstalls = append(needInstalls, item)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if info.Version != installInfo.Version {
|
if depInfo.Version != installInfo.Version {
|
||||||
needInstalls = append(needInstalls, item)
|
needInstalls = append(needInstalls, item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
libs.WriteJSONResponse(w, libs.APIResponse{Message: "you need install apps!", Data: needInstalls, Code: -1}, 200)
|
installInfo.Dependencies = needInstalls
|
||||||
return
|
return installInfo, fmt.Errorf("dependencies require installation")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the plugin name matches the install.json name
|
||||||
if pluginName != installInfo.Name {
|
if pluginName != installInfo.Name {
|
||||||
libs.ErrorMsg(w, "the app name must equal the install.json!")
|
return installInfo, fmt.Errorf("plugin name does not match install.json")
|
||||||
return
|
|
||||||
}
|
}
|
||||||
if !installInfo.NeedInstall {
|
// Copy static directory
|
||||||
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(storeFile)
|
|
||||||
if err != nil {
|
|
||||||
libs.ErrorMsg(w, "cant read the store.json!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//设置 info.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
|
|
||||||
}
|
|
||||||
replacePlaceholdersInCmds(&storeInfo)
|
|
||||||
storeInfo.Name = installInfo.Name
|
|
||||||
err = SaveInfoFile(storeInfo)
|
|
||||||
if err != nil {
|
|
||||||
libs.ErrorMsg(w, "the store info.json is error: "+err.Error())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//设置config
|
|
||||||
if libs.PathExists(storeInfo.Setting.ConfPath + ".tpl") {
|
|
||||||
err = SaveStoreConfig(storeInfo, exePath)
|
|
||||||
if err != nil {
|
|
||||||
libs.ErrorMsg(w, "save the config is error!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err = InstallStore(storeInfo)
|
|
||||||
if err != nil {
|
|
||||||
libs.ErrorMsg(w, "install the app is error!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//复制static目录
|
|
||||||
staticPath := filepath.Join(exePath, "static")
|
staticPath := filepath.Join(exePath, "static")
|
||||||
if libs.PathExists(staticPath) {
|
if libs.PathExists(staticPath) {
|
||||||
staticDir := libs.GetStaticDir()
|
staticDir := libs.GetStaticDir()
|
||||||
targetPath := filepath.Join(staticDir, pluginName)
|
targetPath := filepath.Join(staticDir, pluginName)
|
||||||
if !libs.PathExists(targetPath) {
|
if !libs.PathExists(targetPath) {
|
||||||
err = os.Rename(staticPath, targetPath)
|
if err := os.Rename(staticPath, targetPath); err != nil {
|
||||||
if err != nil {
|
return installInfo, fmt.Errorf("error copying static directory: %w", err)
|
||||||
log.Printf("copy the static is error! %v", err)
|
|
||||||
}
|
}
|
||||||
iconPath := filepath.Join(targetPath, storeInfo.Icon)
|
iconUrl, err := HandlerIcon(installInfo, targetPath)
|
||||||
if libs.PathExists(iconPath) {
|
if err == nil {
|
||||||
installInfo.Icon = "http://localhost:56780/static/" + pluginName + "/" + storeInfo.Icon
|
installInfo.Icon = iconUrl
|
||||||
|
err = SaveInstallInfo(installInfo)
|
||||||
|
if err != nil {
|
||||||
|
return installInfo, fmt.Errorf("error saving install.json: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
libs.SuccessMsg(w, installInfo, "install the app success!")
|
}
|
||||||
|
// Process store.json
|
||||||
|
storeFile := filepath.Join(exePath, "store.json")
|
||||||
|
if !libs.PathExists(storeFile) {
|
||||||
|
return installInfo, nil
|
||||||
|
}
|
||||||
|
var storeInfo StoreInfo
|
||||||
|
content, err := os.ReadFile(storeFile)
|
||||||
|
if err != nil {
|
||||||
|
return installInfo, fmt.Errorf("cannot read store.json: %w", err)
|
||||||
|
}
|
||||||
|
//设置 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 {
|
||||||
|
return installInfo, fmt.Errorf("error unmarshalling store.json: %w", err)
|
||||||
|
}
|
||||||
|
replacePlaceholdersInCmds(&storeInfo)
|
||||||
|
storeInfo.Name = installInfo.Name
|
||||||
|
|
||||||
|
// Save store info
|
||||||
|
if err := SaveInfoFile(storeInfo); err != nil {
|
||||||
|
return installInfo, fmt.Errorf("error saving store info.json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up configuration
|
||||||
|
if libs.PathExists(storeInfo.Setting.ConfPath + ".tpl") {
|
||||||
|
if err := SaveStoreConfig(storeInfo, exePath); err != nil {
|
||||||
|
return installInfo, fmt.Errorf("error saving config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install the store
|
||||||
|
if err := InstallStore(storeInfo); err != nil {
|
||||||
|
return installInfo, fmt.Errorf("error installing app: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return installInfo, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UnInstallHandler 处理卸载请求
|
||||||
func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
|
func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
pluginName := r.URL.Query().Get("name")
|
pluginName := r.URL.Query().Get("name")
|
||||||
if pluginName == "" {
|
if pluginName == "" {
|
||||||
libs.ErrorMsg(w, "the app name is empty!")
|
libs.ErrorMsg(w, "the app name is empty!")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf("uninstall the app %s", pluginName)
|
|
||||||
err := StopCmd(pluginName)
|
// Execute the uninstallation process
|
||||||
if err != nil {
|
if err := Uninstallation(pluginName); err != nil {
|
||||||
// libs.ErrorMsg(w, "stop the app is error!")
|
libs.ErrorMsg(w, err.Error())
|
||||||
// return
|
return
|
||||||
log.Printf("stop the app is error! %s", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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)
|
installInfo, err := GetInstallInfo(pluginName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
libs.ErrorMsg(w, "the install.json is error:"+err.Error())
|
return fmt.Errorf("error retrieving install.json: %w", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the app is in development mode
|
||||||
if installInfo.IsDev {
|
if installInfo.IsDev {
|
||||||
libs.SuccessMsg(w, "success", "uninstall the app success!")
|
return nil // No further action required for dev mode apps
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
//检查是否有其他应用依赖于它
|
||||||
|
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)
|
exePath := GetExePath(pluginName)
|
||||||
//log.Printf("the app path is %s", exePath)
|
|
||||||
if libs.PathExists(exePath) {
|
if libs.PathExists(exePath) {
|
||||||
err := os.RemoveAll(exePath)
|
if err := os.RemoveAll(exePath); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("error deleting the app: %w", err)
|
||||||
libs.ErrorMsg(w, "delete the app is error!")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove the static directory
|
||||||
staticDir := libs.GetStaticDir()
|
staticDir := libs.GetStaticDir()
|
||||||
staticPath := filepath.Join(staticDir, pluginName)
|
staticPath := filepath.Join(staticDir, pluginName)
|
||||||
if libs.PathExists(staticPath) {
|
if libs.PathExists(staticPath) {
|
||||||
err := os.RemoveAll(staticPath)
|
if err := os.RemoveAll(staticPath); err != nil {
|
||||||
if err != nil {
|
return fmt.Errorf("error deleting the static files: %w", err)
|
||||||
libs.ErrorMsg(w, "delete the static is error!")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
libs.SuccessMsg(w, "success", "uninstall the app success!")
|
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
func GetInstallInfo(pluginName string) (InstallInfo, error) {
|
func GetInstallPath(pluginName string) (string, error) {
|
||||||
var installInfo InstallInfo
|
|
||||||
exePath := GetExePath(pluginName)
|
exePath := GetExePath(pluginName)
|
||||||
installFile := filepath.Join(exePath, "install.json")
|
installFile := filepath.Join(exePath, "install.json")
|
||||||
if !libs.PathExists(installFile) {
|
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)
|
content, err := os.ReadFile(installFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return installInfo, err
|
return installInfo, err
|
||||||
|
|
@ -306,6 +348,21 @@ func SaveStoreInfo(storeInfo StoreInfo, infoFile string) error {
|
||||||
}
|
}
|
||||||
return nil
|
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 {
|
func SaveStoreConfig(storeInfo StoreInfo, exePath string) error {
|
||||||
content, err := os.ReadFile(storeInfo.Setting.ConfPath + ".tpl")
|
content, err := os.ReadFile(storeInfo.Setting.ConfPath + ".tpl")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -14,33 +14,57 @@ import (
|
||||||
|
|
||||||
func GetStoreListHandler(w http.ResponseWriter, r *http.Request) {
|
func GetStoreListHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
cate := r.URL.Query().Get("cate")
|
cate := r.URL.Query().Get("cate")
|
||||||
os := runtime.GOOS
|
list, err := GetInstallList(cate)
|
||||||
arch := runtime.GOARCH
|
if err != nil {
|
||||||
if cate == "" {
|
libs.ErrorMsg(w, err.Error())
|
||||||
libs.ErrorMsg(w, "cate is required")
|
|
||||||
return
|
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"
|
pluginUrl := "https://gitee.com/ruitao_admin/godoos-image/raw/master/store/" + os + "/" + arch + "/" + cate + ".json"
|
||||||
res, err := http.Get(pluginUrl)
|
res, err := http.Get(pluginUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
libs.ErrorMsg(w, err.Error())
|
return list, fmt.Errorf("failed to get plugin list: %v", err)
|
||||||
}
|
}
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
if res.StatusCode == 200 {
|
if res.StatusCode != 200 {
|
||||||
|
return list, fmt.Errorf("failed to get plugin list")
|
||||||
|
}
|
||||||
body, err := io.ReadAll(res.Body)
|
body, err := io.ReadAll(res.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
libs.ErrorMsg(w, err.Error())
|
return list, fmt.Errorf("failed to get plugin list: %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
var info interface{}
|
err = json.Unmarshal(body, &list)
|
||||||
err = json.Unmarshal(body, &info)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Error unmarshalling JSON:", err)
|
return list, fmt.Errorf("failed to get plugin list: %v", err)
|
||||||
return
|
|
||||||
}
|
}
|
||||||
json.NewEncoder(w).Encode(info)
|
return list, nil
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
func GetInstallInfoHandler(w http.ResponseWriter, r *http.Request) {
|
func GetInstallInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
name := r.URL.Query().Get("name")
|
name := r.URL.Query().Get("name")
|
||||||
|
|
@ -115,7 +139,10 @@ func GetStoreInfo(name string) (StoreInfo, error) {
|
||||||
if !libs.PathExists(infoPath) {
|
if !libs.PathExists(infoPath) {
|
||||||
return storeInfo, fmt.Errorf("process information for '%s' not found", name)
|
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)
|
content, err := os.ReadFile(infoPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return storeInfo, fmt.Errorf("failed to read info.json: %v", err)
|
return storeInfo, fmt.Errorf("failed to read info.json: %v", err)
|
||||||
|
|
@ -129,3 +156,21 @@ func GetStoreInfo(name string) (StoreInfo, error) {
|
||||||
}
|
}
|
||||||
return storeInfo, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,18 +4,24 @@ package store
|
||||||
// 包含应用程序的名称、下载地址、版本号等关键安装信息。
|
// 包含应用程序的名称、下载地址、版本号等关键安装信息。
|
||||||
type InstallInfo struct {
|
type InstallInfo struct {
|
||||||
Name string `json:"name"` // 应用程序名称。
|
Name string `json:"name"` // 应用程序名称。
|
||||||
URL string `json:"url"` // 应用程序下载地址。
|
URL string `json:"url"` // 应用程序下载地址或适配包的下载地址。
|
||||||
|
Pkg string `json:"pkg"` // 应用程序的官方下载地址。
|
||||||
WebUrl string `json:"webUrl"` // 应用程序的网页地址。
|
WebUrl string `json:"webUrl"` // 应用程序的网页地址。
|
||||||
IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
|
IsDev bool `json:"isDev"` // 标志位,表示是否为开发者版本。
|
||||||
NeedDownload bool `json:"needDownload"` // 标志位,表示是否需要下载。
|
|
||||||
NeedInstall bool `json:"needInstall"` // 标志位,表示是否需要安装。
|
|
||||||
Version string `json:"version"` // 应用程序的版本号。
|
Version string `json:"version"` // 应用程序的版本号。
|
||||||
Desc string `json:"desc"` // 应用程序的描述信息。
|
Desc string `json:"desc"` // 应用程序的描述信息。
|
||||||
Icon string `json:"icon"` // 应用程序的图标路径。
|
Icon string `json:"icon"` // 应用程序的图标路径。
|
||||||
CheckProgress bool `json:"checkProgress"` // 标志位,表示是否显示启动和停止。
|
HasStart bool `json:"hasStart"` // 标志位,表示是否显示启动和停止。
|
||||||
HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
|
HasRestart bool `json:"hasRestart"` // 标志位,表示安装后是否需要重启。
|
||||||
Setting bool `json:"setting"` // 标志位,表示是否需要配置。
|
Setting bool `json:"setting"` // 标志位,表示是否需要配置。
|
||||||
Dependencies []Item `json:"dependencies"` // 依赖项。
|
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 维护了应用程序商店的信息。
|
// StoreInfo 维护了应用程序商店的信息。
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// UploadHandler 处理上传的HTTP请求
|
// UploadHandler 处理上传的HTTP请求
|
||||||
|
|
@ -37,7 +36,6 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
}
|
}
|
||||||
cachePath := libs.GetCacheDir()
|
cachePath := libs.GetCacheDir()
|
||||||
baseName := filepath.Base(header.Filename)
|
baseName := filepath.Base(header.Filename)
|
||||||
filenameNoExt := strings.TrimSuffix(baseName, filepath.Ext(baseName))
|
|
||||||
|
|
||||||
savePath := filepath.Join(cachePath, baseName)
|
savePath := filepath.Join(cachePath, baseName)
|
||||||
|
|
||||||
|
|
@ -56,11 +54,39 @@ func UploadHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Error(w, "写入文件出错", http.StatusInternalServerError)
|
http.Error(w, "写入文件出错", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
runDir := libs.GetRunDir()
|
|
||||||
err = files.HandlerFile(savePath, runDir)
|
zipPath, err := files.Decompress(savePath, cachePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error moving file: %v", err)
|
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()
|
||||||
|
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 {
|
||||||
|
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…
Reference in a new issue