Browse Source

add zip and unzip

master
godo 10 months ago
parent
commit
389cffec76
  1. 19
      README.md
  2. BIN
      frontend/public/image/store/banner1.jpg
  3. BIN
      frontend/public/image/store/banner2.jpg
  4. BIN
      frontend/public/image/store/banner3.jpg
  5. BIN
      frontend/public/image/store/mysql.png
  6. BIN
      frontend/public/image/store/nginx.png
  7. BIN
      frontend/public/image/store/notepad.ico
  8. BIN
      frontend/public/image/store/php.png
  9. BIN
      frontend/public/image/store/phpmyadmin.png
  10. BIN
      frontend/public/image/store/redis.png
  11. 49
      frontend/src/assets/store.json
  12. 2
      frontend/src/components/store/AppItem.vue
  13. 62
      frontend/src/components/store/Store.vue
  14. 10
      godo/cmd/main.go
  15. 165
      godo/files/unzip.go
  16. 219
      godo/files/zip.go
  17. 2
      godo/go.mod
  18. 4
      godo/go.sum
  19. 2
      godo/localchat/client.go
  20. 6
      godo/localchat/server.go
  21. 65
      godo/progress/checkactive.go
  22. 97
      godo/progress/forward.go
  23. 2
      godo/progress/linux.go
  24. 13
      godo/progress/map.go
  25. 11
      godo/progress/progress.go
  26. 137
      godo/progress/start.go
  27. 26
      godo/progress/stop.go
  28. 2
      godo/progress/windows.go
  29. 203
      godo/store/download.go
  30. 401
      godo/store/install.go
  31. 2
      godo/store/store.go
  32. 2
      godo/sys/update.go

19
README.md

@ -15,7 +15,7 @@
### 安装
### 📚 安装
[![Windows][Windows-image]][Windows-url]
[![MacOS][MacOS-image]][MacOS-url]
@ -41,7 +41,7 @@
</div>
## 亮点
## 💝 亮点
- ***无需联网使用,全开源***
- ***零配置,无需注册,下载即用***
- ***零污染,无插件依赖***
@ -49,17 +49,17 @@
- ***可无限扩展,支持自定义应用***
- ***golang开发后端,低资源消耗和高性能***
## 开发进程
## 🚧 开发进程
- 2024年7月,发布v1.0.0版本,发布后,项目进入第二阶段。
## 第二阶段目标
## 🏭 第二阶段目标
- [x] 完善的应用商店体系
- [x] 完善开发文档
- [x] 支持远程存储
- [x] 完成企业客户端
## 功能说明和预览
## 功能说明和预览
### 一、系统桌面
- 精仿windows风格
@ -159,7 +159,7 @@ pnpm build
wails build
# wails build -nsis -upx //you need install nsis and upx
```
## 感谢
## ❤️ 感谢
- [element-plus](http://element-plus.org/)
- [vue3](https://v3.cn.vuejs.org/)
- [wails](https://wails.io/)
@ -168,7 +168,7 @@ wails build
- [mind-map](https://github.com/wanglin2/mind-map)
- [canvas-editor](https://github.com/Hufe921/canvas-editor)
## 关联项目
## 💕 关联项目
- [godooa](https://gitee.com/ruitao_admin/gdoa)
- [gododb](https://gitee.com/ruitao_admin/gododb)
@ -182,3 +182,8 @@ wails build
- 如用于商业活动或二次开发后发售,请购买相关版权
- 不提供私下维护工作,如有bug请 [issures](https://gitee.com/ruitao_admin/gododbs) 提交
- 请尊重作者的劳动成果
## 💌 支持作者
如果觉得不错,或者已经在使用了,希望你可以去
<a target="_blank" href="https://gitee.com/ruitao_admin/godoos">Gitee</a> 帮我点个 ⭐ Star,这将是对我极大的鼓励与支持。

BIN
frontend/public/image/store/banner1.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

BIN
frontend/public/image/store/banner2.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 155 KiB

BIN
frontend/public/image/store/banner3.jpg

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

BIN
frontend/public/image/store/mysql.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
frontend/public/image/store/nginx.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
frontend/public/image/store/notepad.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
frontend/public/image/store/php.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 987 B

BIN
frontend/public/image/store/phpmyadmin.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

BIN
frontend/public/image/store/redis.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

49
frontend/src/assets/store.json

@ -1,36 +1,21 @@
[
{
"name": "在线工具",
"url": "https://tool.lu/",
"icon": "/image/store/tools.png"
},
{
"name": "石墨文档",
"url": "https://shimo.im/desktop",
"icon": "/image/store/shimo.png"
},
{
"name": "在线节拍器",
"url": "https://metronome.tooltool.net/?utm_source=xinquji",
"icon": "/image/store/jpq.png"
},
{
"name": "查看月食",
"url": "https://nadc.china-vo.org/eclipse/",
"icon": "/image/store/yueshi.png"
},
{
"name": "蒸 気 機",
"url": "https://magiconch.com/vaporwave/?from=home",
"icon": "/image/store/zqj.png"
},
{
"name": "vtb 大数据",
"url": "https://vtbs.moe/",
"icon": "/image/store/vtb.png"
},
{
"name": "更多应用",
"desc": "更多应用敬请期待,如果您有合适的应用,可以点击开始菜单,意见反馈取得联系"
"name": "mysql5.7",
"url": "https://gitee.com/ruitao_admin/godoos-mysql5.7/releases/download/mysql5.7/mysql-windows-5.7.7z",
"needDownload": true,
"needInstall": true,
"version": "5.7.37",
"icon": "https://img0.baidu.com/it/u=1087008535,3349487719&fm=253&fmt=auto&app=138&f=PNG?w=969&h=500",
"checkProgress": true,
"hasRestart": true,
"setting": [
{
"path": "setting.html",
"lang":{
"cn-zh": "设置",
"en": "Setting"
}
}
]
}
]

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

@ -17,7 +17,7 @@
<button @click="uninstall?.(item)">卸载</button>
</div>
<div v-else>
<button v-if="item?.url || item?.path" @click="install?.(item)">安装</button>
<button v-if="item?.url" @click="install?.(item)">安装</button>
</div>
</div>
</div>

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

@ -3,8 +3,8 @@ import { inject, onMounted, ref } from "vue";
import { System } from "@/system";
import {notifySuccess, notifyError} from "@/util/msg";
import { t } from "@/i18n";
//import storeList from "@/assets/store.json";
import { getSystemKey, setSystemKey } from "@/system/config";
import storeInitList from "@/assets/store.json";
import { getSystemKey, setSystemKey, parseJson } from "@/system/config";
const sys: any = inject<System>("system");
const currentCateId = ref(0)
const currentTitle = ref(t("store.hots"))
@ -14,6 +14,7 @@ const categoryIcon = ['HomeFilled', 'Odometer', 'Postcard', 'TrendCharts', 'Scho
const isready = ref(false);
const installed = getSystemKey("intstalledPlugins");
const apiUrl = getSystemKey("apiUrl");
const installedList: any = ref(installed);
const storeList: any = ref([])
onMounted(async() => {
@ -22,14 +23,16 @@ onMounted(async() => {
});
async function getList(){
if(currentCate.value == 'add')return;
const storeUrl = apiUrl + '/system/storeList?cate=' + currentCate.value
fetch(storeUrl).then(res => {
res.json().then(data => {
storeList.value = data
})
}).catch(() => {
notifyError(t("store.errorList"))
})
storeList.value = storeInitList
// const apiUrl = getSystemKey("apiUrl");
// const storeUrl = apiUrl + '/system/storeList?cate=' + currentCate.value
// fetch(storeUrl).then(res => {
// res.json().then(data => {
// storeList.value = data
// })
// }).catch(() => {
// notifyError(t("store.errorList"))
// })
isready.value = true;
}
async function changeCate(index: number, item: string) {
@ -44,8 +47,14 @@ function setCache() {
sys.refershAppList();
}, 1000);
}
function install(item: any) {
//console.log(item)
async function install(item: any) {
if(item.needDownload){
await download(item)
}
if(item.needInstall){
}
if(item.url){
sys.fs.writeFile(
`${sys._options.userLocation}Desktop/${item.name}.url`,
@ -64,6 +73,35 @@ function uninstall(item: any) {
delete installedList.value[installedList.value.indexOf(item.name)];
setCache();
}
async function download(item: any) {
//console.log(item)
const completion = await fetch(apiUrl + '/store/download?url=' + item.url)
if(!completion.ok){
notifyError(t("store.downloadError"))
}
//console.log(completion)
const reader:any = completion.body?.getReader();
if (!reader) {
notifyError(t("store.cantStream"));
}
while (true) {
const { done, value } = await reader?.read();
if (done) {
break;
}
// console.log(value)
const json = await new TextDecoder().decode(value);
//console.log(json)
const res = parseJson(json)
console.log(res)
if(res){
if(res.done) {
notifySuccess(t("store.downloadSuccess"))
break;
}
}
}
}
</script>
<template>
<div class="outer">

10
godo/cmd/main.go

@ -6,6 +6,7 @@ import (
"godo/libs"
"godo/localchat"
"godo/progress"
"godo/store"
"godo/sys"
"log"
"net/http"
@ -41,13 +42,16 @@ func OsStart() {
progressRouter.HandleFunc("/list", progress.Status).Methods(http.MethodGet)
progressRouter.HandleFunc("/listport", progress.ListPortsHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/killport", progress.KillPortHandler).Methods(http.MethodGet)
progressRouter.HandleFunc("/app/{name}", progress.ForwardRequest).Methods(http.MethodGet, http.MethodPost)
progressRouter.HandleFunc("/app/{name}/{subpath:.*}", progress.ForwardRequest).Methods(http.MethodGet, http.MethodPost)
router.HandleFunc("/system/updateInfo", sys.GetUpdateUrlHandler).Methods(http.MethodGet)
router.HandleFunc("/system/update", sys.UpdateAppHandler).Methods(http.MethodGet)
router.HandleFunc("/system/storeList", sys.GetStoreInfoHandler).Methods(http.MethodGet)
router.HandleFunc("/system/setting", sys.HandleSetConfig).Methods(http.MethodPost)
router.HandleFunc("/store/list", store.GetStoreInfoHandler).Methods(http.MethodGet)
router.HandleFunc("/store/download", store.DownloadHandler).Methods(http.MethodGet)
router.HandleFunc("/store/install", store.InstallHandler).Methods(http.MethodGet)
router.HandleFunc("/store/uninstall", store.UnInstallHandler).Methods(http.MethodGet)
router.HandleFunc("/files/info", files.HandleSystemInfo).Methods(http.MethodGet)
router.HandleFunc("/file/read", files.HandleReadDir).Methods(http.MethodGet)
router.HandleFunc("/file/stat", files.HandleStat).Methods(http.MethodGet)

165
godo/files/unzip.go

@ -0,0 +1,165 @@
package files
import (
"compress/bzip2"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"archive/tar"
"archive/zip"
)
// Decompress 解压文件到指定目录,支持多种压缩格式
func Decompress(compressedFilePath, destPath string) error {
ext := filepath.Ext(compressedFilePath)
switch ext {
case ".zip":
return Unzip(compressedFilePath, destPath)
case ".tar":
tarFile, err := os.Open(compressedFilePath)
if err != nil {
return err
}
defer tarFile.Close()
return Untar(tarFile, destPath)
case ".gz":
return Untargz(compressedFilePath, destPath)
case ".bz2":
return Untarbz2(compressedFilePath, destPath)
default:
return fmt.Errorf("unsupported compression format: %s", ext)
}
}
// Untar 解压.tar文件到指定目录
func Untar(reader io.Reader, destPath string) error {
tarReader := tar.NewReader(reader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
target := filepath.Join(destPath, header.Name)
if header.Typeflag == tar.TypeDir {
if err := os.MkdirAll(target, os.FileMode(header.Mode)); err != nil {
return err
}
continue
}
outFile, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR|os.O_TRUNC, os.FileMode(header.Mode))
if err != nil {
return err
}
defer outFile.Close()
if _, err := io.Copy(outFile, tarReader); err != nil {
return err
}
}
return nil
}
// Untargz 解压.tar.gz文件到指定目录
func Untargz(targzFilePath, destPath string) error {
gzFile, err := os.Open(targzFilePath)
if err != nil {
return err
}
defer gzFile.Close()
gzReader, err := gzip.NewReader(gzFile)
if err != nil {
return err
}
defer gzReader.Close()
return Untar(gzReader, destPath) // 传递gzReader给Untar
}
// Untarbz2 解压.tar.bz2文件到指定目录
func Untarbz2(tarbz2FilePath, destPath string) error {
bz2File, err := os.Open(tarbz2FilePath)
if err != nil {
return err
}
defer bz2File.Close()
bz2Reader := bzip2.NewReader(bz2File)
return Untar(bz2Reader, destPath) // 传递bz2Reader给Untar
}
// DecompressZip 解压zip文件到指定目录
func Unzip(zipFilePath, destPath string) error {
r, err := zip.OpenReader(zipFilePath)
if err != nil {
return err
}
defer r.Close()
for _, f := range r.File {
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()
path := filepath.Join(destPath, f.Name)
if f.FileInfo().IsDir() {
os.MkdirAll(path, f.Mode())
continue
}
os.MkdirAll(filepath.Dir(path), f.Mode())
outFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err
}
defer outFile.Close()
_, err = io.Copy(outFile, rc)
if err != nil {
return err
}
}
return nil
}
// IsSupportedCompressionFormat 判断文件后缀是否为支持的压缩格式
func IsSupportedCompressionFormat(filename string) bool {
supportedFormats := map[string]bool{
".zip": true,
".tar": true,
".gz": true,
".bz2": true,
}
for format := range supportedFormats {
if strings.HasSuffix(strings.ToLower(filename), format) {
return true
}
}
return false
}
// HandlerFile 根据文件后缀判断是否支持解压,支持则解压,否则移动文件
func HandlerFile(filePath string, destDir string) error {
if !IsSupportedCompressionFormat(filePath) {
// 移动文件
newPath := filepath.Join(destDir, filepath.Base(filePath))
return os.Rename(filePath, newPath)
}
// 解压文件
return Decompress(filePath, destDir)
}

219
godo/files/zip.go

@ -0,0 +1,219 @@
package files
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// 压缩文件到指定的目录
func zipCompress(folderPath, zipFilePath string) error {
zipFile, err := os.Create(zipFilePath)
if err != nil {
return err
}
defer zipFile.Close()
zipWriter := zip.NewWriter(zipFile)
defer zipWriter.Close()
info, err := os.Stat(folderPath)
if err != nil {
return err
}
var baseDir string
if info.IsDir() {
baseDir = filepath.Base(folderPath)
}
filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
relPath, err := filepath.Rel(folderPath, path)
if err != nil {
return err
}
if baseDir != "" {
header.Name = filepath.Join(baseDir, relPath)
} else {
header.Name = relPath
}
if info.IsDir() {
header.Name += "/"
} else {
header.Method = zip.Deflate
}
writer, err := zipWriter.CreateHeader(header)
if err != nil {
return err
}
if info.IsDir() {
return nil
}
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(writer, file)
return err
})
return nil
}
// IsSupportedCompressionFormat 判断文件后缀是否为支持的压缩格式
func IsSupportedFormat(filename string) bool {
supportedFormats := map[string]bool{
".zip": true,
".tar": true,
".gz": true,
".bz2": true,
// 添加其他支持的复合格式
}
for format := range supportedFormats {
if strings.HasSuffix(strings.ToLower(filename), format) {
return true
}
}
return false
}
// CompressFileOrFolder 压缩文件或文件夹到指定的压缩文件
func CompressFileOrFolder(sourcePath, compressedFilePath string) error {
if IsSupportedFormat(compressedFilePath) {
return Encompress(sourcePath, compressedFilePath)
}
// 如果压缩文件格式不受支持,则返回错误或采取其他措施
return fmt.Errorf("unsupported compression format for file: %s", compressedFilePath)
}
// Encompress 压缩文件或文件夹到指定的压缩格式
func Encompress(sourcePath, compressedFilePath string) error {
switch filepath.Ext(compressedFilePath) {
case ".zip":
return zipCompress(sourcePath, compressedFilePath)
case ".tar":
return tarCompress(sourcePath, compressedFilePath)
case ".gz":
return tarGzCompress(sourcePath, compressedFilePath)
default:
return fmt.Errorf("unsupported compression format: %s", filepath.Ext(compressedFilePath))
}
}
// tarCompress 使用 tar 格式压缩文件或文件夹
func tarCompress(folderPath, tarFilePath string) error {
// 打开目标文件
tarFile, err := os.Create(tarFilePath)
if err != nil {
return err
}
defer tarFile.Close()
// 创建 tar.Writer
tarWriter := tar.NewWriter(tarFile)
defer tarWriter.Close()
// 遍历文件夹并添加到 tar 文件
err = filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tarWriter, file)
if err != nil {
return err
}
}
return nil
})
return err
}
// tarGzCompress 使用 tar.gz 格式压缩文件或文件夹
func tarGzCompress(folderPath, tarGzFilePath string) error {
// 打开目标文件
gzFile, err := os.Create(tarGzFilePath)
if err != nil {
return err
}
defer gzFile.Close()
// 创建 gzip.Writer
gzWriter := gzip.NewWriter(gzFile)
defer gzWriter.Close()
// 创建 tar.Writer
tarWriter := tar.NewWriter(gzWriter)
defer tarWriter.Close()
// 遍历文件夹并添加到 tar 文件
err = filepath.Walk(folderPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, "")
if err != nil {
return err
}
if err := tarWriter.WriteHeader(header); err != nil {
return err
}
if !info.IsDir() {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
_, err = io.Copy(tarWriter, file)
if err != nil {
return err
}
}
return nil
})
return err
}

2
godo/go.mod

@ -3,11 +3,9 @@ module godo
go 1.22.5
require (
github.com/cavaliergopher/grab/v3 v3.0.1
github.com/fsnotify/fsnotify v1.7.0
github.com/gorilla/mux v1.8.1
github.com/minio/selfupdate v0.6.0
github.com/pkg/errors v0.9.1
github.com/shirou/gopsutil v3.21.11+incompatible
)

4
godo/go.sum

@ -1,7 +1,5 @@
aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=
aead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=
github.com/cavaliergopher/grab/v3 v3.0.1 h1:4z7TkBfmPjmLAAmkkAZNX/6QJ1nNFdv3SdIHXju0Fr4=
github.com/cavaliergopher/grab/v3 v3.0.1/go.mod h1:1U/KNnD+Ft6JJiYoYBAimKH2XrYptb8Kl3DFGmsjpq4=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
@ -12,8 +10,6 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=
github.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=

2
godo/localchat/client.go

@ -42,7 +42,7 @@ func DiscoverServers() {
conn.Close()
continue
}
log.Printf("Sending message: %+v", msg)
//log.Printf("Sending message: %+v", msg)
_, err = conn.Write(jsonData)
if err != nil {
fmt.Println(err)

6
godo/localchat/server.go

@ -27,10 +27,10 @@ func StartServiceDiscovery() {
for {
n, addr, err := conn.ReadFromUDP(buffer)
if err != nil {
log.Printf("Error reading from UDP: %v", err)
log.Printf("Error reading from UDP: %v,addr:%v", err, addr)
continue
}
fmt.Printf("Received message: %s from %s\n", buffer[:n], addr)
//fmt.Printf("Received message: %s from %s\n", buffer[:n], addr)
var udpMsg UdpMessage
err = json.Unmarshal(buffer[:n], &udpMsg)
@ -38,7 +38,7 @@ func StartServiceDiscovery() {
fmt.Printf("Error unmarshalling JSON: %v\n", err)
continue
}
log.Printf("Get message: %+v", udpMsg)
//log.Printf("Get message: %+v", udpMsg)
OnlineUsers[udpMsg.IP] = udpMsg
}
}

65
godo/progress/checkactive.go

@ -2,10 +2,14 @@ package progress
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"runtime"
"strings"
"sync"
"syscall"
"time"
)
@ -34,37 +38,46 @@ func checkInactiveProcesses() {
defer processesMu.RUnlock()
for name, p := range processes {
// 检查进程是否还在运行
pid := p.Cmd.Process.Pid
_, err := os.FindProcess(pid)
if err != nil {
p.Running = false
// 进程已不存在,跳过后续检查
continue
}
// 如果进程还在运行,检查/ping接口
if p.Running {
resp, err := http.Get(p.PingURL)
if err != nil {
p.Running = false
// 进程未响应
log.Printf("Error checking ping for process %s: %v", name, err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
if os.IsNotExist(err) {
p.Running = false
// 进程未正常响应,停止进程
if err := p.Cmd.Process.Signal(os.Kill); err != nil {
log.Printf("Failed to stop process %s before restart: %v", name, err)
continue
}
log.Printf("Process %s (PID: %d) not found.", name, pid)
} else {
// 进程正常响应,更新LastPingAt并继续检查下一个进程
p.LastPing = time.Now()
log.Printf("Error finding process %s (PID: %d): %v", name, pid, err)
}
continue
}
// 进程仍然运行,更新LastPing
p.LastPing = time.Now()
}
}
func IsProcessRunning(appName string) (bool, error) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "windows":
cmd = exec.Command("powershell", "Get-Process", appName)
case "linux":
fallthrough
case "darwin":
cmd = exec.Command("pgrep", "-f", appName)
default:
return false, fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
var output []byte
var err error
if cmd.SysProcAttr == nil {
cmd.SysProcAttr = &syscall.SysProcAttr{}
}
cmd.SysProcAttr.HideWindow = true // For Windows to hide the console window
if output, err = cmd.CombinedOutput(); err != nil {
return false, fmt.Errorf("error checking process: %w", err)
}
return len(strings.TrimSpace(string(output))) > 0, nil
}

97
godo/progress/forward.go

@ -1,97 +0,0 @@
package progress
import (
"fmt"
"log"
"net/http"
"net/url"
"path"
"time"
"github.com/gorilla/mux"
)
func ForwardRequest(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
proxyName := vars["name"] // 获取代理名称
log.Printf("Forwarding request to proxy %s", proxyName)
log.Printf("processes: %+v", processes)
cmd, ok := processes[proxyName]
if !ok || !cmd.Running {
// Start the process again
err := ExecuteScript(proxyName)
if err != nil {
respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to restart process %s: %v", proxyName, err))
return
}
time.Sleep(1 * time.Second)
}
if cmd.PingURL == "" {
respondWithError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to get url %s", proxyName))
return
}
// 构造目标基础URL,假设proxyName直接对应目标服务的基础路径
targetBaseURLStr := cmd.PingURL
targetBaseURL, err := url.Parse(targetBaseURLStr)
if err != nil {
respondWithError(w, http.StatusInternalServerError, "Failed to parse target base URL")
return
}
// 获取请求的子路径
subpath := r.URL.Path[len("/"+proxyName):]
targetURL := &url.URL{
Scheme: targetBaseURL.Scheme,
Host: targetBaseURL.Host,
Path: path.Clean(path.Join(targetBaseURL.Path, subpath)), // 使用path.Clean处理路径
RawQuery: r.URL.RawQuery, // 正确编码查询参数
}
log.Printf("Forwarding request to %s", targetURL)
// 执行重定向
http.Redirect(w, r, targetURL.String(), http.StatusTemporaryRedirect)
/*
// 保留原请求的路径和查询字符串,构建完整的目标URL
// 如果没有子路径,直接使用基础URL
// 如果有子路径,将其附加到基础URL
client := http.DefaultClient
req, err := http.NewRequest(r.Method, targetURL.String(), r.Body)
if err != nil {
respondWithError(w, http.StatusBadRequest, "Failed to create request: "+err.Error())
return
}
copyHeader(req.Header, r.Header)
resp, err := client.Do(req)
if err != nil {
respondWithError(w, http.StatusServiceUnavailable, "Failed to forward request: "+err.Error())
return
}
defer resp.Body.Close()
// 将响应头复制到w.Header()
for k, vv := range resp.Header {
for _, v := range vv {
w.Header().Add(k, v)
}
}
w.WriteHeader(resp.StatusCode)
if _, err := io.Copy(w, resp.Body); err != nil {
log.Printf("Error copying response body: %v", err)
}*/
}
// 复制源Header到目标Header
// func copyHeader(dst, src http.Header) {
// for k, vv := range src {
// for _, v := range vv {
// dst.Add(k, v)
// }
// }
// }

2
godo/progress/linux.go

@ -7,6 +7,6 @@ import (
"os/exec"
)
func setHideConsoleCursor(cmd *exec.Cmd) *exec.Cmd {
func SetHideConsoleCursor(cmd *exec.Cmd) *exec.Cmd {
return cmd
}

13
godo/progress/map.go

@ -5,15 +5,4 @@ type ProcessInfo struct {
PingPath string // 增加ping或健康检查路径
}
var ProcessInfoMap = map[string]ProcessInfo{
"model": {Port: "56711", PingPath: "ping"},
"goconv": {Port: "56712", PingPath: "ping"},
"file": {Port: "56713", PingPath: "ping"},
"knowledge": {Port: "56714", PingPath: "ping"},
//"llmcpu": {Port: "56715", PingPath: "healthz"},
"ollama": {Port: "56715", PingPath: ""},
"sd": {Port: "56716", PingPath: "ping"},
"voice": {Port: "56717", PingPath: "ping"},
"localchat": {Port: "56718", PingPath: "ping"},
"llmgpu": {Port: "56719", PingPath: "healthz"},
}
var ProcessInfoMap = map[string]ProcessInfo{}

11
godo/progress/progress.go

@ -14,7 +14,7 @@ type Process struct {
Running bool
ExitCode int
Cmd *exec.Cmd
PingURL string // 新增字段,存储每个进程的/ping URL
Waiting bool
LastPing time.Time
}
@ -26,16 +26,13 @@ var (
func respondWithError(w http.ResponseWriter, code int, message string) {
http.Error(w, message, code)
}
func registerProcess(name, pingURL string, cmdstr *exec.Cmd) {
func RegisterProcess(name string, cmdstr *exec.Cmd) {
processesMu.Lock()
defer processesMu.Unlock()
log.Printf("pingurl is: %s", pingURL) // 更改这里的格式化字符串
processes[name] = &Process{
Name: name,
Running: true,
PingURL: pingURL,
Cmd: cmdstr,
}
}
@ -54,10 +51,10 @@ func Status(w http.ResponseWriter, r *http.Request) {
if cmd.Cmd.ProcessState != nil && cmd.Cmd.ProcessState.Exited() {
cmd.Running = false
// 进程已经退出
ps = append(ps, Process{Name: name, Running: false, PingURL: cmd.PingURL, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()})
ps = append(ps, Process{Name: name, Running: false, Waiting: cmd.Waiting, LastPing: cmd.LastPing, ExitCode: cmd.Cmd.ProcessState.ExitCode()})
} else {
// 进程仍在运行
ps = append(ps, Process{Name: name, Running: true, PingURL: cmd.PingURL, LastPing: cmd.LastPing})
ps = append(ps, Process{Name: name, Running: true, Waiting: cmd.Waiting, LastPing: cmd.LastPing})
}
}

137
godo/progress/start.go

@ -2,17 +2,9 @@ package progress
import (
"fmt"
"godo/libs"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"github.com/gorilla/mux"
"github.com/pkg/errors"
)
func StartProcess(w http.ResponseWriter, r *http.Request) {
@ -20,17 +12,6 @@ func StartProcess(w http.ResponseWriter, r *http.Request) {
name := vars["name"]
processesMu.Lock()
defer processesMu.Unlock()
cmd, ok := processes[name]
if ok && !cmd.Running {
err := cmd.Cmd.Start()
if err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
fmt.Fprintf(w, "failed to start process %s: %v", name, err)
} else {
w.WriteHeader(http.StatusOK)
}
return
}
err := ExecuteScript(name)
if err != nil {
@ -42,27 +23,15 @@ func StartProcess(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Process %s started.", name)
}
func ExecuteStartAll() error {
log.Println("Starting all processes...")
userExeDir := libs.GetRunDir()
userFis, err := os.ReadDir(userExeDir)
if err != nil {
return fmt.Errorf("failed to read user scripts directory: %w", err)
}
processesMu.Lock()
defer processesMu.Unlock()
var names []string
for _, userFi := range userFis {
if userFi.IsDir() && !strings.HasPrefix(userFi.Name(), ".") {
names = append(names, userFi.Name())
for name, cmd := range processes {
if err := cmd.Cmd.Start(); err != nil {
return fmt.Errorf("failed to stop process %s: %v", name, err)
}
}
err = batchExecuteScript(names)
if err != nil {
return fmt.Errorf("failed to start processes: %w", err)
}
return nil
}
func StartAll(w http.ResponseWriter, r *http.Request) {
@ -74,19 +43,6 @@ func StartAll(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "All processes started.")
}
func batchExecuteScript(names []string) error {
for _, name := range names {
// 检查processes中是否存在相同的name
if _, exists := processes[name]; exists {
log.Printf("Process with name %s already exists", name)
continue
}
if err := ExecuteScript(name); err != nil {
return errors.Wrapf(err, "failed to script: %v", err)
}
}
return nil
}
// ExecuteScript 执行指定名称的脚本。
// 参数:
@ -94,51 +50,50 @@ func batchExecuteScript(names []string) error {
// 返回值:
// 返回可能遇到的错误,如果执行成功,则返回nil。
func ExecuteScript(name string) error {
// 确保name存在于ProcessInfoMap中
info, ok := ProcessInfoMap[name]
if !ok {
cmd, exists := processes[name]
if !exists {
return fmt.Errorf("process information for '%s' not found", name)
}
// 根据操作系统添加.exe后缀
binaryExt := ""
if runtime.GOOS == "windows" {
binaryExt = ".exe"
}
scriptName := name + binaryExt
exeDir := libs.GetRunDir() // 获取执行程序的目录
scriptPath := filepath.Join(exeDir, name, scriptName) // 拼接脚本的完整路径
// 检查脚本文件的存在性
if !PathExists(scriptPath) {
return fmt.Errorf("script file %s does not exist", scriptPath)
if cmd.Running {
return fmt.Errorf("process %s is already running", name)
}
// 设置并启动脚本执行命令
var cmd *exec.Cmd
cmd = exec.Command(scriptPath)
if runtime.GOOS == "windows" {
// 在Windows上,通过设置CreationFlags来隐藏窗口
cmd = setHideConsoleCursor(cmd)
if err := cmd.Cmd.Start(); err != nil {
return fmt.Errorf("failed to start process %s: %v", name, err)
}
go func() {
// 启动脚本命令并返回可能的错误
if err := cmd.Start(); err != nil {
log.Printf("failed to start process %s: %v", name, err)
return
}
pingURL := fmt.Sprintf("http://localhost:%s/%s", info.Port, info.PingPath)
registerProcess(name, pingURL, cmd)
}()
cmd.Running = true
return nil
}
func PathExists(dir string) bool {
_, err := os.Stat(dir)
if err == nil {
return true
} else if os.IsNotExist(err) {
return false
} else if os.IsExist(err) {
return true
} else {
return false
}
// 确保name存在于ProcessInfoMap中
// _, ok := ProcessInfoMap[name]
// if !ok {
// return fmt.Errorf("process information for '%s' not found", name)
// }
// // 根据操作系统添加.exe后缀
// binaryExt := ""
// if runtime.GOOS == "windows" {
// binaryExt = ".exe"
// }
// scriptName := name + binaryExt
// exeDir := libs.GetRunDir() // 获取执行程序的目录
// scriptPath := filepath.Join(exeDir, name, scriptName) // 拼接脚本的完整路径
// // 检查脚本文件的存在性
// if !libs.PathExists(scriptPath) {
// return fmt.Errorf("script file %s does not exist", scriptPath)
// }
// // 设置并启动脚本执行命令
// var cmd *exec.Cmd
// cmd = exec.Command(scriptPath)
// if runtime.GOOS == "windows" {
// // 在Windows上,通过设置CreationFlags来隐藏窗口
// cmd = SetHideConsoleCursor(cmd)
// }
// go func() {
// // 启动脚本命令并返回可能的错误
// if err := cmd.Start(); err != nil {
// log.Printf("failed to start process %s: %v", name, err)
// return
// }
// RegisterProcess(name, cmd)
// }()
// return nil
}

26
godo/progress/stop.go

@ -8,23 +8,27 @@ import (
"github.com/gorilla/mux"
)
func StopProcess(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
func StopCmd(name string) error {
cmd, ok := processes[name]
if !ok {
respondWithError(w, http.StatusNotFound, "Process not found")
return
return fmt.Errorf("Process not found")
}
// 停止进程并更新status
if err := cmd.Cmd.Process.Kill(); err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
return fmt.Errorf("failed to kill process: %v", err)
}
//delete(processes, name) // 更新status,表示进程已停止
cmd.Running = false
return nil
}
func StopProcess(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
name := vars["name"]
err := StopCmd(name)
if err != nil {
respondWithError(w, http.StatusInternalServerError, err.Error())
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, "Process %s stopped.", name)
}
@ -34,7 +38,7 @@ func StopAllHandler() error {
for name, cmd := range processes {
if err := cmd.Cmd.Process.Signal(os.Interrupt); err != nil {
return fmt.Errorf("Failed to stop process %s: %v", name, err)
return fmt.Errorf("failed to stop process %s: %v", name, err)
}
}
return nil

2
godo/progress/windows.go

@ -9,7 +9,7 @@ import (
"syscall"
)
func setHideConsoleCursor(cmd *exec.Cmd) *exec.Cmd {
func SetHideConsoleCursor(cmd *exec.Cmd) *exec.Cmd {
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
return cmd
}

203
godo/store/download.go

@ -1,159 +1,156 @@
package store
import (
"context"
"encoding/json"
"godo/files"
"godo/libs"
"io"
"log"
"net/http"
"os"
"path/filepath"
"sync"
"time"
"github.com/cavaliergopher/grab/v3"
)
type ProgressReader struct {
reader io.Reader
total int64
err error
}
type DownloadStatus struct {
resp *grab.Response
cancel context.CancelFunc
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 float64 `json:"progress"`
Progress int `json:"progress"`
Downloading bool `json:"downloading"`
Done bool `json:"done"`
}
const (
concurrency = 6 // 并发下载数
)
// var downloads = make(map[string]*grab.Response)
var downloadsMutex sync.Mutex
var downloadList map[string]*DownloadStatus
func existsInDownloadList(url string) bool {
_, ok := downloadList[url]
return ok
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 PauseDownload(url string) {
downloadsMutex.Lock()
defer downloadsMutex.Unlock()
ds, ok := downloadList[url]
if ds.Url == url && ok {
if ds.cancel != nil {
ds.cancel()
}
ds.resp = nil
ds.Downloading = false
ds.Speed = 0
func DownloadHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
log.Printf("Download url: %s", url)
// 获取下载目录,这里假设从请求参数中获取,如果没有则使用默认值
downloadDir := libs.GetCacheDir()
if downloadDir == "" {
downloadDir = "./downloads"
}
}
// 拼接完整的文件路径
fileName := filepath.Base(url)
filePath := filepath.Join(downloadDir, fileName)
func ContinueDownload(url string) {
ds, ok := downloadList[url]
if ds.Url == url && ok {
if !ds.Downloading && ds.resp == nil && !ds.Done {
ds.Downloading = true
// 开始下载
resp, err := http.Get(url)
if err != nil {
log.Printf("Failed to get file: %v", err)
http.Error(w, "Failed to get file", http.StatusInternalServerError)
return
}
defer resp.Body.Close()
req, err := grab.NewRequest(ds.Path, ds.Url)
// 检查文件是否已存在且大小一致
if fileInfo, err := os.Stat(filePath); err == nil {
if fileInfo.Size() == resp.ContentLength {
// 文件已存在且大小一致,无需下载
runDir := libs.GetRunDir()
err := files.HandlerFile(filePath, runDir)
if err != nil {
ds.Downloading = false
return
log.Printf("Error moving file: %v", err)
}
ctx, cancel := context.WithCancel(context.Background())
ds.cancel = cancel
req = req.WithContext(ctx)
client := grab.NewClient()
client.HTTPClient = &http.Client{
Transport: &http.Transport{
MaxIdleConnsPerHost: concurrency, // 设置并发连接数
},
}
//resp := grab.DefaultClient.Do(req)
resp := client.Do(req)
if resp != nil && resp.HTTPResponse != nil &&
resp.HTTPResponse.StatusCode >= 200 && resp.HTTPResponse.StatusCode < 300 {
ds.resp = resp
} else {
ds.Downloading = false
libs.SuccessMsg(w, "success", "File already exists and is of correct size")
return
} else {
// 重新打开响应体以便后续读取
resp.Body = http.NoBody
resp, err = http.Get(url)
if err != nil {
log.Printf("Failed to get file: %v", err)
http.Error(w, "Failed to get file", http.StatusInternalServerError)
return
}
}
}
}
func Download(url string) {
chacheDir := libs.GetCacheDir()
absPath := filepath.Join(chacheDir, filepath.Base(url))
if !existsInDownloadList(url) {
downloadList[url] = &DownloadStatus{
resp: nil,
Name: filepath.Base(url),
Path: absPath,
Url: url,
Downloading: false,
}
}
ContinueDownload(url)
}
func GetDownload(url string) *DownloadStatus {
downloadsMutex.Lock()
defer downloadsMutex.Unlock()
ds, ok := downloadList[url]
if ds.resp != nil && ok {
ds.Current = ds.resp.BytesComplete()
ds.Size = ds.resp.Size()
ds.Speed = ds.resp.BytesPerSecond()
ds.Progress = 100 * ds.resp.Progress()
ds.Downloading = !ds.resp.IsComplete()
ds.Done = ds.resp.Progress() == 1
if !ds.Downloading {
ds.resp = nil
}
}
if ds.Done {
delete(downloadList, url)
// 创建文件
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
}
return ds
}
func DownloadHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
Download(url)
ticker := time.NewTicker(500 * time.Millisecond)
defer file.Close()
// 使用ProgressReader来跟踪进度
pr := &ProgressReader{reader: resp.Body}
// 启动定时器来报告进度
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
flusher, ok := w.(http.Flusher)
if !ok {
log.Printf("Streaming unsupported")
http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
return
}
go func() {
for {
<-ticker.C
ds := GetDownload(url)
jsonBytes, err := json.Marshal(ds)
if err != nil {
log.Printf("Error marshaling FileProgress to JSON: %v", err)
continue
rp := &DownloadStatus{
Name: fileName,
Path: filePath,
Url: url,
Current: pr.total,
Size: resp.ContentLength,
Speed: 0, // 这里可以计算速度,但为了简化示例,我们暂时设为0
Progress: int(100 * (float64(pr.total) / float64(resp.ContentLength))),
Downloading: pr.err == nil && pr.total < resp.ContentLength,
Done: pr.total == resp.ContentLength,
}
if pr.err != nil || rp.Done {
rp.Downloading = false
//log.Printf("Download complete: %s", filePath)
runDir := libs.GetRunDir()
err := files.HandlerFile(filePath, runDir)
if err != nil {
log.Printf("Error moving file: %v", err)
}
break
}
if w != nil {
io.WriteString(w, string(jsonBytes))
jsonBytes, err := json.Marshal(rp)
if err != nil {
log.Printf("Error marshaling DownloadStatus to JSON: %v", err)
continue
}
w.Write(jsonBytes)
w.Write([]byte("\n"))
flusher.Flush()
} else {
log.Println("ResponseWriter is nil, cannot send progress")
}
if ds.Done {
return
}
}
}()
// 将响应体的内容写入文件
_, 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
}
libs.SuccessMsg(w, "success", "Download complete")
}

401
godo/store/install.go

@ -0,0 +1,401 @@
package store
import (
"encoding/json"
"fmt"
"godo/files"
"godo/libs"
"godo/progress"
"log"
"net/http"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
)
type StoreInfo struct {
Name string `json:"name"`
URL string `json:"url"`
NeedDownload bool `json:"needDownload"`
Icon string `json:"icon"`
Setting Setting `json:"setting"`
Config map[string]any `json:"config"`
Cmds map[string][]Cmd `json:"cmds"`
Install Install `json:"install"`
Start Install `json:"start"`
}
type Setting struct {
BinPath string `json:"binPath"`
ConfPath string `json:"confPath"`
DataDir string `json:"dataDir"`
LogDir string `json:"logDir"`
}
type Item struct {
Name string `json:"name"`
Value any `json:"value"`
}
type Cmd struct {
Name string `json:"name"`
FilePath string `json:"filePath,omitempty"`
Content string `json:"content,omitempty"`
BinPath string `json:"binPath,omitempty"`
Cmds []string `json:"cmds,omitempty"`
Waiting bool `json:"waiting"`
Envs []Item `json:"envs"`
}
type Install struct {
Envs []Item `json:"envs"`
Cmds []string `json:"cmds"`
}
func InstallHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Query().Get("name")
if pluginName == "" {
libs.ErrorMsg(w, "the app name is empty!")
return
}
exeDir := libs.GetRunDir()
exePath := filepath.Join(exeDir, pluginName)
//log.Printf("the app path is %s", exePath)
if !libs.PathExists(exePath) {
libs.ErrorMsg(w, "the app path is not exists!")
return
}
installFile := filepath.Join(exePath, "install.json")
if !libs.PathExists(installFile) {
libs.ErrorMsg(w, "the install.json is not exists!")
return
}
var storeInfo StoreInfo
content, err := os.ReadFile(installFile)
if err != nil {
libs.ErrorMsg(w, "cant read the install.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 install.json is error: "+err.Error())
return
}
replacePlaceholdersInCmds(&storeInfo)
infoFile := filepath.Join(exePath, "info.json")
err = SaveStoreInfo(storeInfo, infoFile)
if err != nil {
libs.ErrorMsg(w, "save the info.json is 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")
if libs.PathExists(staticPath) {
staticDir := libs.GetStaticDir()
targetPath := filepath.Join(staticDir, pluginName)
err = os.Rename(staticPath, targetPath)
if err != nil {
libs.ErrorMsg(w, "copy the static is error!")
return
}
}
libs.SuccessMsg(w, "success", "install the app success!")
}
func UnInstallHandler(w http.ResponseWriter, r *http.Request) {
pluginName := r.URL.Query().Get("name")
if pluginName == "" {
libs.ErrorMsg(w, "the app name is empty!")
return
}
err := progress.StopCmd(pluginName)
if err != nil {
libs.ErrorMsg(w, "stop the app is error!")
return
}
exeDir := libs.GetRunDir()
exePath := filepath.Join(exeDir, pluginName)
//log.Printf("the app path is %s", exePath)
if libs.PathExists(exePath) {
err := os.RemoveAll(exePath)
if err != nil {
libs.ErrorMsg(w, "delete the app is error!")
return
}
}
staticDir := libs.GetStaticDir()
staticPath := filepath.Join(staticDir, pluginName)
if libs.PathExists(staticPath) {
err := os.RemoveAll(staticPath)
if err != nil {
libs.ErrorMsg(w, "delete the static is error!")
return
}
}
libs.SuccessMsg(w, "success", "uninstall the app success!")
}
func convertValueToString(value any) string {
var strValue string
switch v := value.(type) {
case string:
strValue = v
case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64:
strValue = fmt.Sprintf("%v", v)
default:
strValue = fmt.Sprintf("%v", v) // 处理其他类型,例如布尔型或更复杂的类型
}
return strValue
}
func InstallStore(storeInfo StoreInfo) error {
err := SetEnvs(storeInfo.Install.Envs)
if err != nil {
return fmt.Errorf("failed to set install environment variable %s: %w", storeInfo.Name, err)
}
if len(storeInfo.Install.Cmds) > 0 {
for _, cmdKey := range storeInfo.Install.Cmds {
if _, ok := storeInfo.Cmds[cmdKey]; ok {
// 如果命令存在,你可以进一步处理 cmds
RunCmds(storeInfo, cmdKey)
}
}
}
return nil
}
func RunCmds(storeInfo StoreInfo, cmdKey string) error {
cmds := storeInfo.Cmds[cmdKey]
for _, cmd := range cmds {
if cmd.Name == "stop" {
runStop(storeInfo)
}
if cmd.Name == "start" {
runStart(storeInfo)
}
if cmd.Name == "restart" {
runRestart(storeInfo)
}
if cmd.Name == "exec" {
runExec(storeInfo, cmd)
}
if cmd.Name == "writeFile" {
err := WriteFile(cmd)
if err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
}
if cmd.Name == "deleteFile" {
err := DeleteFile(cmd)
if err != nil {
return fmt.Errorf("failed to delete file: %w", err)
}
}
if cmd.Name == "unzip" {
err := Unzip(cmd)
if err != nil {
return fmt.Errorf("failed to unzip file: %w", err)
}
}
if cmd.Name == "zip" {
err := Zip(cmd)
if err != nil {
return fmt.Errorf("failed to unzip file: %w", err)
}
}
if cmd.Name == "mkdir" {
err := MkDir(cmd)
if err != nil {
return fmt.Errorf("failed to create directory: %w", err)
}
}
}
return nil
}
func runStop(storeInfo StoreInfo) error {
return progress.StopCmd(storeInfo.Name)
}
func runStart(storeInfo StoreInfo) error {
err := SetEnvs(storeInfo.Start.Envs)
if err != nil {
return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
}
if len(storeInfo.Start.Cmds) > 0 {
if !libs.PathExists(storeInfo.Setting.BinPath) {
return fmt.Errorf("script file %s does not exist", storeInfo.Setting.BinPath)
}
cmd := exec.Command(storeInfo.Setting.BinPath, storeInfo.Start.Cmds...)
if runtime.GOOS == "windows" {
// 在Windows上,通过设置CreationFlags来隐藏窗口
cmd = progress.SetHideConsoleCursor(cmd)
}
// 启动脚本命令并返回可能的错误
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start process %s: %w", storeInfo.Name, err)
}
progress.RegisterProcess(storeInfo.Name, cmd)
}
return nil
}
func runRestart(storeInfo StoreInfo) error {
err := runStop(storeInfo)
if err != nil {
return fmt.Errorf("failed to stop process %s: %w", storeInfo.Name, err)
}
return runStart(storeInfo)
}
func runExec(storeInfo StoreInfo, cmdParam Cmd) error {
err := SetEnvs(cmdParam.Envs)
if err != nil {
return fmt.Errorf("failed to set start environment variable %s: %w", storeInfo.Name, err)
}
log.Printf("bin path:%v", cmdParam.BinPath)
log.Printf("cmds:%v", cmdParam.Cmds)
cmd := exec.Command(cmdParam.BinPath, cmdParam.Cmds...)
if runtime.GOOS == "windows" {
// 在Windows上,通过设置CreationFlags来隐藏窗口
cmd = progress.SetHideConsoleCursor(cmd)
}
if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to run exec process %s: %w", storeInfo.Name, err)
}
if cmdParam.Waiting {
if err = cmd.Wait(); err != nil {
return fmt.Errorf("failed to wait for exec process %s: %w", storeInfo.Name, err)
}
}
log.Printf("run exec process %s, name is %s", storeInfo.Name, cmdParam.Name)
return nil
}
func WriteFile(cmd Cmd) error {
if cmd.FilePath != "" {
content := cmd.Content
if content != "" {
err := os.WriteFile(cmd.FilePath, []byte(content), 0644)
if err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
}
}
return nil
}
func DeleteFile(cmd Cmd) error {
if cmd.FilePath != "" {
err := os.Remove(cmd.FilePath)
if err != nil {
return fmt.Errorf("failed to delete file: %w", err)
}
}
return nil
}
func MkDir(cmd Cmd) error {
err := os.MkdirAll(cmd.FilePath, os.ModePerm)
if err != nil {
return fmt.Errorf("failed to make dir: %w", err)
}
return nil
}
func Unzip(cmd Cmd) error {
return files.Decompress(cmd.FilePath, cmd.Content)
}
func Zip(cmd Cmd) error {
return files.Encompress(cmd.FilePath, cmd.Content)
}
func SetEnvs(envs []Item) error {
if len(envs) > 0 {
for _, item := range envs {
strValue := convertValueToString(item.Value)
if strValue != "" {
err := os.Setenv(item.Name, strValue)
if err != nil {
return fmt.Errorf("failed to set environment variable %s: %w", item.Name, err)
}
}
}
}
return nil
}
func SaveStoreInfo(storeInfo StoreInfo, infoFile string) error {
// 使用 json.MarshalIndent 直接获取内容的字节切片
content, err := json.MarshalIndent(storeInfo, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal reqBodies to JSON: %w", err)
}
if err := os.WriteFile(infoFile, content, 0644); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
return nil
}
func SaveStoreConfig(storeInfo StoreInfo, exePath string) error {
content, err := os.ReadFile(storeInfo.Setting.ConfPath + ".tpl")
if err != nil {
return fmt.Errorf("failed to read tpl file: %w", err)
}
contentstr := strings.ReplaceAll(string(content), "{exePath}", exePath)
contentstr = ChangeConfig(storeInfo, contentstr)
if err := os.WriteFile(storeInfo.Setting.ConfPath, []byte(contentstr), 0644); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}
return nil
}
func ChangeConfig(storeInfo StoreInfo, contentstr string) string {
pattern := regexp.MustCompile(`\{(\w+)\}`)
for key, value := range storeInfo.Config {
strValue := convertValueToString(value)
contentstr = pattern.ReplaceAllStringFunc(contentstr, func(match string) string {
if strings.Trim(match, "{}") == key {
return strValue
}
return match
})
}
return contentstr
}
func replacePlaceholdersInCmds(storeInfo *StoreInfo) {
// 创建一个正则表达式模式,用于匹配形如{key}的占位符
pattern := regexp.MustCompile(`\{(\w+)\}`)
// 遍历Config中的所有键值对
for key, value := range storeInfo.Config {
// 遍历Cmds中的所有命令组
for cmdGroupName, cmds := range storeInfo.Cmds {
// 遍历每个命令组中的所有Cmd
for i, cmd := range cmds {
strValue := convertValueToString(value)
// 使用正则表达式替换Content中的占位符
storeInfo.Cmds[cmdGroupName][i].Content = pattern.ReplaceAllStringFunc(
cmd.Content,
func(match string) string {
// 检查占位符是否与Config中的键匹配
if strings.Trim(match, "{}") == key {
return strValue
}
return match
},
)
}
}
}
}

2
godo/sys/store.go → godo/store/store.go

@ -1,4 +1,4 @@
package sys
package store
import (
"encoding/json"

2
godo/sys/update.go

@ -61,7 +61,7 @@ func UpdateAppHandler(w http.ResponseWriter, r *http.Request) {
defer resp.Body.Close()
pr := &ProgressReader{reader: resp.Body}
ticker := time.NewTicker(250 * time.Millisecond)
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
flusher, ok := w.(http.Flusher)
if !ok {

Loading…
Cancel
Save