Before Width: | Height: | Size: 98 KiB |
Before Width: | Height: | Size: 155 KiB |
Before Width: | Height: | Size: 100 KiB |
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 987 B |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 4.3 KiB |
@ -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" |
|||
} |
|||
} |
|||
] |
|||
} |
|||
] |
@ -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) |
|||
} |
@ -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 |
|||
} |
@ -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)
|
|||
// }
|
|||
// }
|
|||
// }
|
@ -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") |
|||
} |
|||
|
@ -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 |
|||
}, |
|||
) |
|||
} |
|||
} |
|||
} |
|||
} |
@ -1,4 +1,4 @@ |
|||
package sys |
|||
package store |
|||
|
|||
import ( |
|||
"encoding/json" |