|
|
@@ -1,37 +1,344 @@
|
|
|
package main
|
|
|
|
|
|
import (
|
|
|
+ "archive/tar"
|
|
|
+ "compress/gzip"
|
|
|
"context"
|
|
|
+ "crypto/md5"
|
|
|
+ "encoding/hex"
|
|
|
+ "encoding/json"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "os"
|
|
|
+ "path"
|
|
|
+ "path/filepath"
|
|
|
+ "strings"
|
|
|
+ "sync"
|
|
|
+ "time"
|
|
|
|
|
|
+ "hnyfkj.com.cn/rtu/linux/baseapp"
|
|
|
"hnyfkj.com.cn/rtu/linux/utils/jsonrpc2"
|
|
|
)
|
|
|
|
|
|
+type PkgInfo struct {
|
|
|
+ FileName string `json:"fileName"`
|
|
|
+ FileSize int64 `json:"fileSize"`
|
|
|
+ FileMD5Hash string `json:"fileHash"`
|
|
|
+ UploadTime string `json:"uploadTime"`
|
|
|
+}
|
|
|
+
|
|
|
+type AppInfo struct {
|
|
|
+ Name string `json:"name"`
|
|
|
+ Version string `json:"version"`
|
|
|
+ InstallPath string `json:"installPath"`
|
|
|
+ LibraryPath string `json:"libraryPath"`
|
|
|
+ Executable string `json:"executable"`
|
|
|
+ ReadmeFile string `json:"readmeFile"`
|
|
|
+ Description string `json:"description"`
|
|
|
+}
|
|
|
+
|
|
|
+type InsInfo struct {
|
|
|
+ Installed bool `json:"installed"`
|
|
|
+ InstallTime string `json:"installTime"`
|
|
|
+}
|
|
|
+
|
|
|
+type AppIns struct {
|
|
|
+ Pkg PkgInfo `json:"pkg"` // 应用的安装包
|
|
|
+ App AppInfo `json:"app"` // 应用详细信息
|
|
|
+ Ins InsInfo `json:"ins"` // 应用是否安装
|
|
|
+}
|
|
|
+
|
|
|
+var appMutex sync.Mutex
|
|
|
+
|
|
|
+const ErrConflict jsonrpc2.ErrCode = -32099 // 资源访问冲突, 拒绝执行
|
|
|
+
|
|
|
// 导入用户安装包
|
|
|
-func pkgImport(
|
|
|
- ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
- return nil
|
|
|
+func pkgImport(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
+ appMutex.Lock()
|
|
|
+ defer appMutex.Unlock()
|
|
|
+
|
|
|
+ var params map[string]string
|
|
|
+
|
|
|
+ if err := json.Unmarshal(req.Params, ¶ms); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ file, ok := params["file"]
|
|
|
+ if !ok || file == "" {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "missing or empty 'file' parameter")
|
|
|
+ }
|
|
|
+
|
|
|
+ if !strings.HasSuffix(strings.ToLower(filepath.Base(file)), ".tar.gz") {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package format")
|
|
|
+ }
|
|
|
+
|
|
|
+ info, err := os.Stat(file)
|
|
|
+ if err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if !info.Mode().IsRegular() {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, "invalid install package file")
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := os.MkdirAll(userAppDir, 0755); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if hasImportedPackage(userAppDir) {
|
|
|
+ return jsonrpc2.BuildError(req, ErrConflict, "application package already exists")
|
|
|
+ }
|
|
|
+
|
|
|
+ app, err := readAppJSON(file)
|
|
|
+ if err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInvalidParams, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ hash, err := fileMD5(file)
|
|
|
+ if err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ savePkgFile := filepath.Join(userAppDir, filepath.Base(file))
|
|
|
+ if err := os.Rename(file, savePkgFile); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ appIns := AppIns{
|
|
|
+ Pkg: PkgInfo{
|
|
|
+ FileName: filepath.Base(file),
|
|
|
+ FileSize: info.Size(),
|
|
|
+ FileMD5Hash: hash,
|
|
|
+ UploadTime: time.Now().Format("2006-01-02 15:04:05"),
|
|
|
+ },
|
|
|
+ App: app,
|
|
|
+ Ins: InsInfo{
|
|
|
+ Installed: false,
|
|
|
+ },
|
|
|
+ }
|
|
|
+
|
|
|
+ data, err := json.MarshalIndent(appIns, "", " ")
|
|
|
+ if err != nil {
|
|
|
+ os.Remove(savePkgFile)
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ appInsFile := filepath.Join(userAppDir, "appins.json")
|
|
|
+ if err := writeFileAtomic(appInsFile, data, 0644); err != nil {
|
|
|
+ os.Remove(savePkgFile)
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ baseapp.Logger.Infof("[PkgImport] 导入用户应用安装包成功: %s", savePkgFile)
|
|
|
+
|
|
|
+ return jsonrpc2.BuildResponse(req, "success", nil)
|
|
|
}
|
|
|
|
|
|
// 删除用户安装包
|
|
|
-func pkgRemove(
|
|
|
- ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
- return nil
|
|
|
+func pkgRemove(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
+ appMutex.Lock()
|
|
|
+ defer appMutex.Unlock()
|
|
|
+
|
|
|
+ appInsFile := filepath.Join(userAppDir, "appins.json")
|
|
|
+
|
|
|
+ data, err := os.ReadFile(appInsFile)
|
|
|
+ if err == nil {
|
|
|
+ var appIns AppIns
|
|
|
+ if err := json.Unmarshal(data, &appIns); err == nil {
|
|
|
+ if appIns.Ins.Installed { // 用户应用已安装
|
|
|
+ return jsonrpc2.BuildError(req, ErrConflict, "application is installed, please uninstall it first")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if !os.IsNotExist(err) {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := os.RemoveAll(userAppDir); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := os.MkdirAll(userAppDir, 0755); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ baseapp.Logger.Infof("[PkgRemove] 删除用户应用安装包成功")
|
|
|
+
|
|
|
+ return jsonrpc2.BuildResponse(req, "success", nil)
|
|
|
}
|
|
|
|
|
|
// 安装包应用安装
|
|
|
-func appInstall(
|
|
|
- ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
+func appInstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
+ appMutex.Lock()
|
|
|
+ defer appMutex.Unlock()
|
|
|
+
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// 卸载已安装应用
|
|
|
-func appUninstall(
|
|
|
- ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
+func appUninstall(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
+ appMutex.Lock()
|
|
|
+ defer appMutex.Unlock()
|
|
|
+
|
|
|
return nil
|
|
|
}
|
|
|
|
|
|
// 安装包应用信息
|
|
|
-func getInstallInfo(
|
|
|
- ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
- return nil
|
|
|
+func getInstallInfo(ctx context.Context, req *jsonrpc2.Request) *jsonrpc2.Response {
|
|
|
+ appInsFile := filepath.Join(userAppDir, "appins.json")
|
|
|
+
|
|
|
+ data, err := os.ReadFile(appInsFile)
|
|
|
+ if err != nil {
|
|
|
+ if os.IsNotExist(err) {
|
|
|
+ return jsonrpc2.BuildResponse(req, nil, nil)
|
|
|
+ }
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ var appIns AppIns
|
|
|
+ if err := json.Unmarshal(data, &appIns); err != nil {
|
|
|
+ return jsonrpc2.BuildError(req, jsonrpc2.ErrInternal, err.Error())
|
|
|
+ }
|
|
|
+
|
|
|
+ return jsonrpc2.BuildResponse(req, appIns, nil)
|
|
|
+}
|
|
|
+
|
|
|
+func readAppJSON(file string) (AppInfo, error) {
|
|
|
+ var app AppInfo
|
|
|
+
|
|
|
+ f, err := os.Open(file)
|
|
|
+ if err != nil {
|
|
|
+ return app, err
|
|
|
+ }
|
|
|
+ defer f.Close()
|
|
|
+
|
|
|
+ gz, err := gzip.NewReader(f)
|
|
|
+ if err != nil {
|
|
|
+ return app, err
|
|
|
+ }
|
|
|
+ defer gz.Close()
|
|
|
+
|
|
|
+ tr := tar.NewReader(gz)
|
|
|
+
|
|
|
+ for {
|
|
|
+ header, err := tr.Next()
|
|
|
+ if err == io.EOF {
|
|
|
+ break
|
|
|
+ }
|
|
|
+
|
|
|
+ if err != nil {
|
|
|
+ return app, err
|
|
|
+ }
|
|
|
+
|
|
|
+ if header.Typeflag != tar.TypeReg {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if path.Base(header.Name) != "app.json" {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if header.Size > 2<<20 { // 2MB 上限
|
|
|
+ return app, fmt.Errorf("app.json 文件过大")
|
|
|
+ }
|
|
|
+
|
|
|
+ data, err := io.ReadAll(io.LimitReader(tr, header.Size))
|
|
|
+ if err != nil {
|
|
|
+ return app, err
|
|
|
+ }
|
|
|
+
|
|
|
+ err = json.Unmarshal(data, &app)
|
|
|
+ if err != nil {
|
|
|
+ return app, err
|
|
|
+ }
|
|
|
+
|
|
|
+ if app.Name == "" || app.Version == "" || app.InstallPath == "" ||
|
|
|
+ app.Executable == "" || app.ReadmeFile == "" {
|
|
|
+ return app, fmt.Errorf("app.json 参数不完整")
|
|
|
+ }
|
|
|
+
|
|
|
+ if !filepath.IsAbs(app.InstallPath) {
|
|
|
+ return app, fmt.Errorf("installPath必须是绝对路径")
|
|
|
+ }
|
|
|
+
|
|
|
+ clean := filepath.Clean(app.InstallPath)
|
|
|
+ if clean != app.InstallPath {
|
|
|
+ return app, fmt.Errorf("installPath路径非法")
|
|
|
+ }
|
|
|
+
|
|
|
+ return app, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return app, fmt.Errorf("安装包中未找到 app.json")
|
|
|
+}
|
|
|
+
|
|
|
+func fileMD5(file string) (string, error) {
|
|
|
+ f, err := os.Open(file)
|
|
|
+ if err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ defer f.Close()
|
|
|
+
|
|
|
+ h := md5.New()
|
|
|
+ if _, err := io.Copy(h, f); err != nil {
|
|
|
+ return "", err
|
|
|
+ }
|
|
|
+ return hex.EncodeToString(h.Sum(nil)), nil
|
|
|
+}
|
|
|
+
|
|
|
+func isRegularFile(name string) bool {
|
|
|
+ info, err := os.Stat(name)
|
|
|
+ if err != nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return info.Mode().IsRegular()
|
|
|
+}
|
|
|
+
|
|
|
+func hasImportedPackage(dir string) bool {
|
|
|
+ if isRegularFile(filepath.Join(dir, "appins.json")) {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+
|
|
|
+ entries, err := os.ReadDir(dir)
|
|
|
+ if err != nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ for _, entry := range entries {
|
|
|
+ if entry.IsDir() {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ if strings.HasSuffix(strings.ToLower(entry.Name()), ".tar.gz") {
|
|
|
+ return true
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+func writeFileAtomic(name string, data []byte, perm os.FileMode) error {
|
|
|
+ tmp := name + ".tmp"
|
|
|
+
|
|
|
+ defer os.Remove(tmp)
|
|
|
+
|
|
|
+ f, err := os.OpenFile(tmp, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ if _, err := f.Write(data); err != nil {
|
|
|
+ f.Close()
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := f.Sync(); err != nil {
|
|
|
+ f.Close()
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ if err := f.Close(); err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ return os.Rename(tmp, name)
|
|
|
}
|