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 { 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 { 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 { appMutex.Lock() defer appMutex.Unlock() return nil } // 卸载已安装应用 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 { 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) }