mirror of
https://github.com/TeaOSLab/EdgeAdmin.git
synced 2025-11-14 04:10:26 +08:00
安装过程中可以选择自动在本机安装MySQL
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/iwind/TeaGo/maps"
|
"github.com/iwind/TeaGo/maps"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -63,10 +64,11 @@ func (this *DetectDBAction) RunPost(params struct{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.Data["localDB"] = maps.Map{
|
this.Data["localDB"] = maps.Map{
|
||||||
"host": localHost,
|
"host": localHost,
|
||||||
"port": localPort,
|
"port": localPort,
|
||||||
"username": localUsername,
|
"username": localUsername,
|
||||||
"password": localPassword,
|
"password": localPassword,
|
||||||
|
"canInstall": runtime.GOOS == "linux" && runtime.GOARCH == "amd64" && os.Getgid() == 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
this.Success()
|
this.Success()
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package setup
|
package setup
|
||||||
|
|
||||||
import "github.com/iwind/TeaGo"
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql"
|
||||||
|
"github.com/iwind/TeaGo"
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
TeaGo.BeforeStart(func(server *TeaGo.Server) {
|
||||||
@@ -15,6 +18,8 @@ func init() {
|
|||||||
Post("/status", new(StatusAction)).
|
Post("/status", new(StatusAction)).
|
||||||
Post("/detectDB", new(DetectDBAction)).
|
Post("/detectDB", new(DetectDBAction)).
|
||||||
Post("/checkLocalIP", new(CheckLocalIPAction)).
|
Post("/checkLocalIP", new(CheckLocalIPAction)).
|
||||||
|
GetPost("/mysql/installPopup", new(mysql.InstallPopupAction)).
|
||||||
|
Post("/mysql/installLogs", new(mysql.InstallLogsAction)).
|
||||||
EndAll()
|
EndAll()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
17
internal/web/actions/default/setup/mysql/installLogs.go
Normal file
17
internal/web/actions/default/setup/mysql/installLogs.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstallLogsAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *InstallLogsAction) RunPost(params struct{}) {
|
||||||
|
this.Data["logs"] = utils.SharedLogger.ReadAll()
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
47
internal/web/actions/default/setup/mysql/installPopup.go
Normal file
47
internal/web/actions/default/setup/mysql/installPopup.go
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/actionutils"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InstallPopupAction struct {
|
||||||
|
actionutils.ParentAction
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *InstallPopupAction) RunGet(params struct{}) {
|
||||||
|
this.Show()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *InstallPopupAction) RunPost(params struct{}) {
|
||||||
|
// 清空日志
|
||||||
|
utils.SharedLogger.Reset()
|
||||||
|
|
||||||
|
this.Data["isOk"] = false
|
||||||
|
|
||||||
|
var installer = mysqlinstallers.NewMySQLInstaller()
|
||||||
|
var targetDir = "/usr/local/mysql"
|
||||||
|
xzFile, err := installer.Download()
|
||||||
|
if err != nil {
|
||||||
|
this.Data["err"] = "download failed: " + err.Error()
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = installer.InstallFromFile(xzFile, targetDir)
|
||||||
|
if err != nil {
|
||||||
|
this.Data["err"] = "install from '" + xzFile + "' failed: " + err.Error()
|
||||||
|
this.Success()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.Data["user"] = "root"
|
||||||
|
this.Data["password"] = installer.Password()
|
||||||
|
this.Data["dir"] = targetDir
|
||||||
|
this.Data["isOk"] = true
|
||||||
|
|
||||||
|
this.Success()
|
||||||
|
}
|
||||||
@@ -0,0 +1,612 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package mysqlinstallers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql/mysqlinstallers/utils"
|
||||||
|
timeutil "github.com/iwind/TeaGo/utils/time"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MySQLInstaller struct {
|
||||||
|
password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMySQLInstaller() *MySQLInstaller {
|
||||||
|
return &MySQLInstaller{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MySQLInstaller) InstallFromFile(xzFilePath string, targetDir string) error {
|
||||||
|
// check whether mysql already running
|
||||||
|
this.log("checking mysqld ...")
|
||||||
|
var oldPid = utils.FindPidWithName("mysqld")
|
||||||
|
if oldPid > 0 {
|
||||||
|
return errors.New("there is already a running mysql server process, pid: '" + strconv.Itoa(oldPid) + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
// check target dir
|
||||||
|
this.log("checking target dir '" + targetDir + "' ...")
|
||||||
|
_, err := os.Stat(targetDir)
|
||||||
|
if err == nil {
|
||||||
|
// check target dir
|
||||||
|
matches, _ := filepath.Glob(targetDir + "/*")
|
||||||
|
if len(matches) > 0 {
|
||||||
|
return errors.New("target dir '" + targetDir + "' already exists and not empty")
|
||||||
|
} else {
|
||||||
|
err = os.Remove(targetDir)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("clean target dir '" + targetDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check 'tar' command
|
||||||
|
this.log("checking 'tar' command ...")
|
||||||
|
var tarExe, _ = exec.LookPath("tar")
|
||||||
|
if len(tarExe) == 0 {
|
||||||
|
this.log("installing 'tar' command ...")
|
||||||
|
err = this.installTarCommand()
|
||||||
|
if err != nil {
|
||||||
|
this.log("WARN: failed to install 'tar' ...")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check commands
|
||||||
|
this.log("checking system commands ...")
|
||||||
|
var cmdList = []string{"tar" /** again **/, "chown", "sh"}
|
||||||
|
for _, cmd := range cmdList {
|
||||||
|
cmdPath, err := exec.LookPath(cmd)
|
||||||
|
if err != nil || len(cmdPath) == 0 {
|
||||||
|
return errors.New("could not find '" + cmd + "' command in this system")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
groupAddExe, err := this.lookupGroupAdd()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("could not find 'groupadd' command in this system")
|
||||||
|
}
|
||||||
|
|
||||||
|
userAddExe, err := this.lookupUserAdd()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("could not find 'useradd' command in this system")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ubuntu apt
|
||||||
|
aptExe, err := exec.LookPath("apt")
|
||||||
|
if err == nil && len(aptExe) > 0 {
|
||||||
|
for _, lib := range []string{"libaio1", "libncurses5"} {
|
||||||
|
this.log("checking " + lib + " ...")
|
||||||
|
var cmd = utils.NewCmd("apt", "-y", "install", lib)
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("install " + lib + " failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
} else { // yum
|
||||||
|
yumExe, err := exec.LookPath("yum")
|
||||||
|
if err == nil && len(yumExe) > 0 {
|
||||||
|
for _, lib := range []string{"libaio", "ncurses-libs", "ncurses-compat-libs"} {
|
||||||
|
var cmd = utils.NewCmd("yum", "-y", "install", lib)
|
||||||
|
_ = cmd.Run()
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create 'mysql' user group
|
||||||
|
this.log("checking 'mysql' user group ...")
|
||||||
|
{
|
||||||
|
data, err := os.ReadFile("/etc/group")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("check user group failed: " + err.Error())
|
||||||
|
}
|
||||||
|
if !bytes.Contains(data, []byte("\nmysql:")) {
|
||||||
|
var cmd = utils.NewCmd(groupAddExe, "mysql")
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("add 'mysql' user group failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create 'mysql' user
|
||||||
|
this.log("checking 'mysql' user ...")
|
||||||
|
{
|
||||||
|
data, err := os.ReadFile("/etc/passwd")
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("check user failed: " + err.Error())
|
||||||
|
}
|
||||||
|
if !bytes.Contains(data, []byte("\nmysql:")) {
|
||||||
|
var cmd *utils.Cmd
|
||||||
|
if strings.HasSuffix(userAddExe, "useradd") {
|
||||||
|
cmd = utils.NewCmd(userAddExe, "mysql", "-g", "mysql")
|
||||||
|
} else { // adduser
|
||||||
|
cmd = utils.NewCmd(userAddExe, "-S", "-G", "mysql", "mysql")
|
||||||
|
}
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("add 'mysql' user failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mkdir
|
||||||
|
{
|
||||||
|
var parentDir = filepath.Dir(targetDir)
|
||||||
|
stat, err := os.Stat(parentDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(parentDir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("try to create dir '" + parentDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("check dir '" + parentDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if !stat.IsDir() {
|
||||||
|
return errors.New("'" + parentDir + "' should be a directory")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check installer file .xz
|
||||||
|
this.log("checking installer file ...")
|
||||||
|
{
|
||||||
|
stat, err := os.Stat(xzFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("could not open the installer file: " + err.Error())
|
||||||
|
}
|
||||||
|
if stat.IsDir() {
|
||||||
|
return errors.New("'" + xzFilePath + "' not a valid file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var basename = filepath.Base(xzFilePath)
|
||||||
|
if !strings.HasSuffix(basename, ".xz") {
|
||||||
|
return errors.New("installer file should has '.xz' extension")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extract
|
||||||
|
this.log("extracting installer file ...")
|
||||||
|
var tmpDir = os.TempDir() + "/goedge-mysql-tmp"
|
||||||
|
{
|
||||||
|
_, err := os.Stat(tmpDir)
|
||||||
|
if err == nil {
|
||||||
|
err = os.RemoveAll(tmpDir)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("clean temporary directory '" + tmpDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = os.Mkdir(tmpDir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create temporary directory '" + tmpDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd("tar", "-xJvf", xzFilePath, "-C", tmpDir)
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("extract installer file '" + xzFilePath + "' failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create datadir
|
||||||
|
matches, err := filepath.Glob(tmpDir + "/mysql-*")
|
||||||
|
if err != nil || len(matches) == 0 {
|
||||||
|
return errors.New("could not find mysql installer directory from '" + tmpDir + "'")
|
||||||
|
}
|
||||||
|
var baseDir = matches[0]
|
||||||
|
var dataDir = baseDir + "/data"
|
||||||
|
_, err = os.Stat(dataDir)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.Mkdir(dataDir, 0777)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create data dir '" + dataDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return errors.New("check data dir '" + dataDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// chown datadir
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd("chown", "mysql:mysql", dataDir)
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("chown data dir '" + dataDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// create my.cnf
|
||||||
|
var myCnfFile = "/etc/my.cnf"
|
||||||
|
_, err = os.Stat(myCnfFile)
|
||||||
|
if err == nil {
|
||||||
|
// backup it
|
||||||
|
err = os.Rename(myCnfFile, "/etc/my.cnf."+timeutil.Format("YmdHis"))
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("backup '/etc/my.cnf' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// mysql server options https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html
|
||||||
|
var myCnfTemplate = this.createMyCnf(baseDir, dataDir)
|
||||||
|
err = os.WriteFile(myCnfFile, []byte(myCnfTemplate), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("write '" + myCnfFile + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize
|
||||||
|
this.log("initializing mysql ...")
|
||||||
|
var generatedPassword = ""
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd(baseDir+"/bin/mysqld", "--initialize", "--user=mysql")
|
||||||
|
cmd.WithStderr()
|
||||||
|
cmd.WithStdout()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("initialize failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// read from stdout
|
||||||
|
var match = regexp.MustCompile(`temporary password is.+:\s*(.+)`).FindStringSubmatch(cmd.Stdout())
|
||||||
|
if len(match) == 0 {
|
||||||
|
// read from stderr
|
||||||
|
match = regexp.MustCompile(`temporary password is.+:\s*(.+)`).FindStringSubmatch(cmd.Stderr())
|
||||||
|
|
||||||
|
if len(match) == 0 {
|
||||||
|
return errors.New("initialize successfully, but could not find generated password, please report to developer")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
generatedPassword = strings.TrimSpace(match[1])
|
||||||
|
|
||||||
|
// write password to file
|
||||||
|
var passwordFile = baseDir + "/generated-password.txt"
|
||||||
|
err = os.WriteFile(passwordFile, []byte(generatedPassword), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("write password failed: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// move to right place
|
||||||
|
this.log("moving files to target dir ...")
|
||||||
|
err = os.Rename(baseDir, targetDir)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("move '" + baseDir + "' to '" + targetDir + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
baseDir = targetDir
|
||||||
|
|
||||||
|
// change my.cnf
|
||||||
|
myCnfTemplate = this.createMyCnf(baseDir, baseDir+"/data")
|
||||||
|
err = os.WriteFile(myCnfFile, []byte(myCnfTemplate), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("create new '" + myCnfFile + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// start mysql
|
||||||
|
this.log("starting mysql ...")
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd(baseDir+"/bin/mysqld_safe", "--user=mysql")
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("start failed '" + cmd.String() + "': " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
// waiting for startup
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
_, err = net.Dial("tcp", "127.0.0.1:3306")
|
||||||
|
if err != nil {
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
// change password
|
||||||
|
newPassword, err := this.generatePassword()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("generate new password failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("changing mysql password ...")
|
||||||
|
var passwordSQL = "ALTER USER 'root'@'localhost' IDENTIFIED BY '" + newPassword + "';"
|
||||||
|
{
|
||||||
|
var cmd = utils.NewCmd("sh", "-c", baseDir+"/bin/mysql --user=root --password=\""+generatedPassword+"\" --execute=\""+passwordSQL+"\" --connect-expired-password")
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("change password failed: " + cmd.String() + ": " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.password = newPassword
|
||||||
|
var passwordFile = baseDir + "/generated-password.txt"
|
||||||
|
err = os.WriteFile(passwordFile, []byte(this.password), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("write generated file failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove temporary directory
|
||||||
|
_ = os.Remove(tmpDir)
|
||||||
|
|
||||||
|
// create link to 'mysql' client command
|
||||||
|
var clientExe = "/usr/local/bin/mysql"
|
||||||
|
_, err = os.Stat(clientExe)
|
||||||
|
if err != nil && os.IsNotExist(err) {
|
||||||
|
err = os.Symlink(baseDir+"/bin/mysql", clientExe)
|
||||||
|
if err == nil {
|
||||||
|
this.log("created symbolic link '" + clientExe + "' to '" + baseDir + "/bin/mysql'")
|
||||||
|
} else {
|
||||||
|
this.log("WARN: failed to create symbolic link '" + clientExe + "' to '" + baseDir + "/bin/mysql': " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// install service
|
||||||
|
// this is not required, so we ignore all errors
|
||||||
|
err = this.installService(baseDir)
|
||||||
|
if err != nil {
|
||||||
|
this.log("WARN: install service failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("finished")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MySQLInstaller) Download() (path string, err error) {
|
||||||
|
var client = &http.Client{}
|
||||||
|
|
||||||
|
// check latest version
|
||||||
|
this.log("checking mysql latest version ...")
|
||||||
|
var latestVersion = "8.0.31" // 默认版本
|
||||||
|
{
|
||||||
|
req, err := http.NewRequest(http.MethodGet, "https://dev.mysql.com/downloads/mysql/", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "curl/7.61.1")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("check latest version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode == http.StatusOK {
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("read latest version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
var reg = regexp.MustCompile(`<h1>MySQL Community Server ([\d.]+) </h1>`)
|
||||||
|
var matches = reg.FindSubmatch(data)
|
||||||
|
if len(matches) > 0 {
|
||||||
|
latestVersion = string(matches[1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.log("found version: v" + latestVersion)
|
||||||
|
|
||||||
|
// download
|
||||||
|
this.log("start downloading ...")
|
||||||
|
var downloadURL = "https://cdn.mysql.com/Downloads/MySQL-8.0/mysql-" + latestVersion + "-linux-glibc2.17-x86_64-minimal.tar.xz"
|
||||||
|
|
||||||
|
{
|
||||||
|
req, err := http.NewRequest(http.MethodGet, downloadURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36")
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("check latest version failed: " + err.Error())
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return "", errors.New("check latest version failed: invalid response code: " + strconv.Itoa(resp.StatusCode))
|
||||||
|
}
|
||||||
|
|
||||||
|
path = filepath.Base(downloadURL)
|
||||||
|
fp, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
|
||||||
|
if err != nil {
|
||||||
|
return "", errors.New("create download file '" + path + "' failed: " + err.Error())
|
||||||
|
}
|
||||||
|
var writer = utils.NewProgressWriter(fp, resp.ContentLength)
|
||||||
|
var ticker = time.NewTicker(1 * time.Second)
|
||||||
|
var done = make(chan bool, 1)
|
||||||
|
go func() {
|
||||||
|
var lastProgress float32 = -1
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
var progress = writer.Progress()
|
||||||
|
if lastProgress < 0 || progress-lastProgress > 0.1 || progress == 1 {
|
||||||
|
lastProgress = progress
|
||||||
|
this.log(fmt.Sprintf("%.2f%%", progress*100))
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
_, err = io.Copy(writer, resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
_ = fp.Close()
|
||||||
|
done <- true
|
||||||
|
return "", errors.New("download failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = fp.Close()
|
||||||
|
if err != nil {
|
||||||
|
done <- true
|
||||||
|
return "", errors.New("download failed: " + err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second) // waiting for progress printing
|
||||||
|
done <- true
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password get generated password
|
||||||
|
func (this *MySQLInstaller) Password() string {
|
||||||
|
return this.password
|
||||||
|
}
|
||||||
|
|
||||||
|
// create my.cnf content
|
||||||
|
func (this *MySQLInstaller) createMyCnf(baseDir string, dataDir string) string {
|
||||||
|
return `
|
||||||
|
[mysqld]
|
||||||
|
port=3306
|
||||||
|
basedir="` + baseDir + `"
|
||||||
|
datadir="` + dataDir + `"
|
||||||
|
|
||||||
|
max_connections=256
|
||||||
|
innodb_flush_log_at_trx_commit=2
|
||||||
|
max_prepared_stmt_count=65535
|
||||||
|
binlog_cache_size=1M
|
||||||
|
binlog_stmt_cache_size=1M
|
||||||
|
thread_cache_size=32
|
||||||
|
binlog_expire_logs_seconds=1209600
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
// generate random password
|
||||||
|
func (this *MySQLInstaller) generatePassword() (string, error) {
|
||||||
|
var p = make([]byte, 16)
|
||||||
|
n, err := rand.Read(p)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%x", p[:n]), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// print log
|
||||||
|
func (this *MySQLInstaller) log(message string) {
|
||||||
|
utils.SharedLogger.Push("[" + timeutil.Format("H:i:s") + "]" + message)
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy file
|
||||||
|
func (this *MySQLInstaller) installService(baseDir string) error {
|
||||||
|
_, err := exec.LookPath("systemctl")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
this.log("registering systemd service ...")
|
||||||
|
|
||||||
|
var desc = `### BEGIN INIT INFO
|
||||||
|
# Provides: mysql
|
||||||
|
# Required-Start: $local_fs $network $remote_fs
|
||||||
|
# Should-Start: ypbind nscd ldap ntpd xntpd
|
||||||
|
# Required-Stop: $local_fs $network $remote_fs
|
||||||
|
# Default-Start: 2 3 4 5
|
||||||
|
# Default-Stop: 0 1 6
|
||||||
|
# Short-Description: start and stop MySQL
|
||||||
|
# Description: MySQL is a very fast and reliable SQL database engine.
|
||||||
|
### END INIT INFO
|
||||||
|
|
||||||
|
[Unit]
|
||||||
|
Description=MySQL Service
|
||||||
|
Before=shutdown.target
|
||||||
|
After=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
Restart=always
|
||||||
|
RestartSec=1s
|
||||||
|
ExecStart=${BASE_DIR}/support-files/mysql.server start
|
||||||
|
ExecStop=${BASE_DIR}/support-files/mysql.server stop
|
||||||
|
ExecRestart=${BASE_DIR}/support-files/mysql.server restart
|
||||||
|
ExecStatus=${BASE_DIR}/support-files/mysql.server status
|
||||||
|
ExecReload=${BASE_DIR}/support-files/mysql.server reload
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target`
|
||||||
|
|
||||||
|
desc = strings.ReplaceAll(desc, "${BASE_DIR}", baseDir)
|
||||||
|
|
||||||
|
err = os.WriteFile("/etc/systemd/system/mysqld.service", []byte(desc), 0666)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = utils.NewTimeoutCmd(5*time.Second, "systemctl", "enable", "mysqld.service")
|
||||||
|
cmd.WithStderr()
|
||||||
|
err = cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("enable mysqld.service failed: " + cmd.Stderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// install 'tar' command automatically
|
||||||
|
func (this *MySQLInstaller) installTarCommand() error {
|
||||||
|
// yum
|
||||||
|
yumExe, err := exec.LookPath("yum")
|
||||||
|
if err == nil && len(yumExe) > 0 {
|
||||||
|
var cmd = utils.NewTimeoutCmd(10*time.Second, yumExe, "-y", "install", "tar")
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// apt
|
||||||
|
aptExe, err := exec.LookPath("apt")
|
||||||
|
if err == nil && len(aptExe) > 0 {
|
||||||
|
var cmd = utils.NewTimeoutCmd(10*time.Second, aptExe, "-y", "install", "tar")
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MySQLInstaller) lookupGroupAdd() (string, error) {
|
||||||
|
for _, cmd := range []string{"groupadd", "addgroup"} {
|
||||||
|
path, err := exec.LookPath(cmd)
|
||||||
|
if err == nil && len(path) > 0 {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *MySQLInstaller) lookupUserAdd() (string, error) {
|
||||||
|
for _, cmd := range []string{"useradd", "adduser"} {
|
||||||
|
path, err := exec.LookPath(cmd)
|
||||||
|
if err == nil && len(path) > 0 {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", errors.New("not found")
|
||||||
|
}
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cmd struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
env []string
|
||||||
|
dir string
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
timeout time.Duration
|
||||||
|
cancelFunc func()
|
||||||
|
|
||||||
|
captureStdout bool
|
||||||
|
captureStderr bool
|
||||||
|
|
||||||
|
stdout *bytes.Buffer
|
||||||
|
stderr *bytes.Buffer
|
||||||
|
|
||||||
|
rawCmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCmd(name string, args ...string) *Cmd {
|
||||||
|
return &Cmd{
|
||||||
|
name: name,
|
||||||
|
args: args,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeoutCmd(timeout time.Duration, name string, args ...string) *Cmd {
|
||||||
|
return (&Cmd{
|
||||||
|
name: name,
|
||||||
|
args: args,
|
||||||
|
}).WithTimeout(timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithTimeout(timeout time.Duration) *Cmd {
|
||||||
|
this.timeout = timeout
|
||||||
|
|
||||||
|
ctx, cancelFunc := context.WithTimeout(context.Background(), timeout)
|
||||||
|
this.ctx = ctx
|
||||||
|
this.cancelFunc = cancelFunc
|
||||||
|
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithStdout() *Cmd {
|
||||||
|
this.captureStdout = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithStderr() *Cmd {
|
||||||
|
this.captureStderr = true
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithEnv(env []string) *Cmd {
|
||||||
|
this.env = env
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) WithDir(dir string) *Cmd {
|
||||||
|
this.dir = dir
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Start() error {
|
||||||
|
var cmd = this.compose()
|
||||||
|
return cmd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Wait() error {
|
||||||
|
var cmd = this.compose()
|
||||||
|
return cmd.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Run() error {
|
||||||
|
if this.cancelFunc != nil {
|
||||||
|
defer this.cancelFunc()
|
||||||
|
}
|
||||||
|
|
||||||
|
var cmd = this.compose()
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) RawStdout() string {
|
||||||
|
if this.stdout != nil {
|
||||||
|
return this.stdout.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Stdout() string {
|
||||||
|
return strings.TrimSpace(this.RawStdout())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) RawStderr() string {
|
||||||
|
if this.stderr != nil {
|
||||||
|
return this.stderr.String()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Stderr() string {
|
||||||
|
return strings.TrimSpace(this.RawStderr())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) String() string {
|
||||||
|
if this.rawCmd != nil {
|
||||||
|
return this.rawCmd.String()
|
||||||
|
}
|
||||||
|
var newCmd = exec.Command(this.name, this.args...)
|
||||||
|
return newCmd.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) Process() *os.Process {
|
||||||
|
if this.rawCmd != nil {
|
||||||
|
return this.rawCmd.Process
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Cmd) compose() *exec.Cmd {
|
||||||
|
if this.rawCmd != nil {
|
||||||
|
return this.rawCmd
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.ctx != nil {
|
||||||
|
this.rawCmd = exec.CommandContext(this.ctx, this.name, this.args...)
|
||||||
|
} else {
|
||||||
|
this.rawCmd = exec.Command(this.name, this.args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.env != nil {
|
||||||
|
this.rawCmd.Env = this.env
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(this.dir) > 0 {
|
||||||
|
this.rawCmd.Dir = this.dir
|
||||||
|
}
|
||||||
|
|
||||||
|
if this.captureStdout {
|
||||||
|
this.stdout = &bytes.Buffer{}
|
||||||
|
this.rawCmd.Stdout = this.stdout
|
||||||
|
}
|
||||||
|
if this.captureStderr {
|
||||||
|
this.stderr = &bytes.Buffer{}
|
||||||
|
this.rawCmd.Stderr = this.stderr
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.rawCmd
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright 2023 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
var SharedLogger = NewLogger()
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
c chan string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLogger() *Logger {
|
||||||
|
return &Logger{
|
||||||
|
c: make(chan string, 1024),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Logger) Push(msg string) {
|
||||||
|
select {
|
||||||
|
case this.c <- msg:
|
||||||
|
default:
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Logger) ReadAll() (msgList []string) {
|
||||||
|
msgList = []string{}
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case msg := <-this.c:
|
||||||
|
msgList = append(msgList, msg)
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *Logger) Reset() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-this.c:
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ProcDir = "/proc"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FindPidWithName(name string) int {
|
||||||
|
// process name
|
||||||
|
commFiles, err := filepath.Glob(ProcDir + "/*/comm")
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, commFile := range commFiles {
|
||||||
|
data, err := os.ReadFile(commFile)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(string(data)) == name {
|
||||||
|
var pieces = strings.Split(commFile, "/")
|
||||||
|
var pid = pieces[len(pieces)-2]
|
||||||
|
pidInt, _ := strconv.Atoi(pid)
|
||||||
|
return pidInt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Copyright 2022 Liuxiangchao iwind.liu@gmail.com. All rights reserved. Official site: https://goedge.cn .
|
||||||
|
|
||||||
|
package utils
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type ProgressWriter struct {
|
||||||
|
rawWriter io.Writer
|
||||||
|
total int64
|
||||||
|
written int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProgressWriter(rawWriter io.Writer, total int64) *ProgressWriter {
|
||||||
|
return &ProgressWriter{
|
||||||
|
rawWriter: rawWriter,
|
||||||
|
total: total,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *ProgressWriter) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = this.rawWriter.Write(p)
|
||||||
|
this.written += int64(n)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (this *ProgressWriter) Progress() float32 {
|
||||||
|
if this.total <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return float32(float64(this.written) / float64(this.total))
|
||||||
|
}
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
.install-box {
|
|
||||||
width: 50em;
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -25em;
|
|
||||||
top: 1em;
|
|
||||||
bottom: 1em;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
.install-box .button.margin {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
.install-box .button.primary {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.install-box .button.disabled {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
.install-box table td.title {
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
.install-box .radio {
|
|
||||||
margin-right: 1em;
|
|
||||||
}
|
|
||||||
.install-box .radio label {
|
|
||||||
cursor: pointer !important;
|
|
||||||
font-size: 0.9em !important;
|
|
||||||
}
|
|
||||||
.install-box h3 {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
.install-box .content-box {
|
|
||||||
overflow-y: auto;
|
|
||||||
position: fixed;
|
|
||||||
top: 5em;
|
|
||||||
bottom: 5em;
|
|
||||||
left: 50%;
|
|
||||||
width: 50em;
|
|
||||||
padding-right: 1em;
|
|
||||||
margin-left: -25em;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
.install-box .content-box::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
.install-box .button-group {
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -25em;
|
|
||||||
z-index: 1;
|
|
||||||
width: 50em;
|
|
||||||
bottom: 1em;
|
|
||||||
}
|
|
||||||
.install-box .button-group button {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
.install-box .button-group .status-box {
|
|
||||||
position: absolute;
|
|
||||||
top: 3em;
|
|
||||||
left: 5em;
|
|
||||||
right: 5em;
|
|
||||||
bottom: 0;
|
|
||||||
text-align: center;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
.install-box::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
/*# sourceMappingURL=@install.css.map */
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"version":3,"sources":["@install.less"],"names":[],"mappings":"AAAA;EAIC,WAAA;EACA,eAAA;EACA,SAAA;EACA,kBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;;AAVD,YAYC,QAAO;EACN,eAAA;;AAbF,YAgBC,QAAO;EACN,YAAA;;AAjBF,YAoBC,QAAO;EACN,YAAA;;AArBF,YAwBC,MACC,GAAE;EACD,WAAA;;AA1BH,YA8BC;EACC,iBAAA;;AA/BF,YA8BC,OAGC;EACC,0BAAA;EACA,gBAAA;;AAnCH,YAuCC;EACC,mBAAA;;AAxCF,YA2CC;EACC,gBAAA;EACA,eAAA;EACA,QAAA;EACA,WAAA;EACA,SAAA;EACA,WAAA;EACA,kBAAA;EACA,kBAAA;EACA,UAAA;;AApDF,YAuDC,aAAY;EACX,UAAA;;AAxDF,YA2DC;EACC,eAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;EACA,WAAA;EACA,WAAA;;AAjEF,YA2DC,cAQC;EACC,WAAA;;AApEH,YA2DC,cAYC;EACC,kBAAA;EACA,QAAA;EACA,SAAA;EACA,UAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;;AAKH,YAAY;EACX,UAAA","file":"@install.css"}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
.install-box {
|
|
||||||
@width: 50em;
|
|
||||||
@half-width: 25em;
|
|
||||||
|
|
||||||
width: @width;
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -@half-width;
|
|
||||||
top: 1em;
|
|
||||||
bottom: 1em;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.button.margin {
|
|
||||||
margin-top: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.primary {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button.disabled {
|
|
||||||
float: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
table {
|
|
||||||
td.title {
|
|
||||||
width: 10em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio {
|
|
||||||
margin-right: 1em;
|
|
||||||
|
|
||||||
label {
|
|
||||||
cursor: pointer !important;
|
|
||||||
font-size: 0.9em !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-weight: normal;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-box {
|
|
||||||
overflow-y: auto;
|
|
||||||
position: fixed;
|
|
||||||
top: 5em;
|
|
||||||
bottom: 5em;
|
|
||||||
left: 50%;
|
|
||||||
width: @width;
|
|
||||||
padding-right: 1em;
|
|
||||||
margin-left: -@half-width;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.content-box::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.button-group {
|
|
||||||
position: fixed;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -@half-width;
|
|
||||||
z-index: 1;
|
|
||||||
width: @width;
|
|
||||||
bottom: 1em;
|
|
||||||
|
|
||||||
button {
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-box {
|
|
||||||
position: absolute;
|
|
||||||
top: 3em;
|
|
||||||
left: 5em;
|
|
||||||
right: 5em;
|
|
||||||
bottom: 0;
|
|
||||||
text-align: center;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.install-box::-webkit-scrollbar {
|
|
||||||
width: 4px;
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,9 @@
|
|||||||
.install-box .button.disabled {
|
.install-box .button.disabled {
|
||||||
float: right;
|
float: right;
|
||||||
}
|
}
|
||||||
|
.install-box table td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
.install-box table td.title {
|
.install-box table td.title {
|
||||||
width: 10em;
|
width: 10em;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"version":3,"sources":["@install.less"],"names":[],"mappings":"AAAA;EAIC,WAAA;EACA,eAAA;EACA,SAAA;EACA,kBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;;AAVD,YAYC,QAAO;EACN,eAAA;;AAbF,YAgBC,QAAO;EACN,YAAA;;AAjBF,YAoBC,QAAO;EACN,YAAA;;AArBF,YAwBC,MACC,GAAE;EACD,WAAA;;AA1BH,YA8BC;EACC,iBAAA;;AA/BF,YA8BC,OAGC;EACC,0BAAA;EACA,gBAAA;;AAnCH,YAuCC;EACC,mBAAA;;AAxCF,YA2CC;EACC,gBAAA;EACA,eAAA;EACA,QAAA;EACA,WAAA;EACA,SAAA;EACA,WAAA;EACA,kBAAA;EACA,kBAAA;EACA,UAAA;;AApDF,YAuDC,aAAY;EACX,UAAA;;AAxDF,YA2DC;EACC,eAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;EACA,WAAA;EACA,WAAA;;AAjEF,YA2DC,cAQC;EACC,WAAA;;AApEH,YA2DC,cAYC;EACC,kBAAA;EACA,QAAA;EACA,SAAA;EACA,UAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;;AAKH,YAAY;EACX,UAAA","file":"index.css"}
|
{"version":3,"sources":["index.less"],"names":[],"mappings":"AAAA;EAIC,WAAA;EACA,eAAA;EACA,SAAA;EACA,kBAAA;EACA,QAAA;EACA,WAAA;EACA,gBAAA;;AAVD,YAYC,QAAO;EACN,eAAA;;AAbF,YAgBC,QAAO;EACN,YAAA;;AAjBF,YAoBC,QAAO;EACN,YAAA;;AArBF,YAwBC,MACC;EACC,mBAAA;;AA1BH,YAwBC,MAKC,GAAE;EACD,WAAA;;AA9BH,YAkCC;EACC,iBAAA;;AAnCF,YAkCC,OAGC;EACC,0BAAA;EACA,gBAAA;;AAvCH,YA2CC;EACC,mBAAA;;AA5CF,YA+CC;EACC,gBAAA;EACA,eAAA;EACA,QAAA;EACA,WAAA;EACA,SAAA;EACA,WAAA;EACA,kBAAA;EACA,kBAAA;EACA,UAAA;;AAxDF,YA2DC,aAAY;EACX,UAAA;;AA5DF,YA+DC;EACC,eAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;EACA,WAAA;EACA,WAAA;;AArEF,YA+DC,cAQC;EACC,WAAA;;AAxEH,YA+DC,cAYC;EACC,kBAAA;EACA,QAAA;EACA,SAAA;EACA,UAAA;EACA,SAAA;EACA,kBAAA;EACA,UAAA;;AAKH,YAAY;EACX,UAAA","file":"index.css"}
|
||||||
@@ -152,6 +152,7 @@
|
|||||||
<input type="text" name="host" maxlength="100" placeholder="比如 192.168.1.100" style="width:16em" ref="dbHost" v-model="localDB.host" @change="checkDBIP" @input="localDB.isLocal = true"/>
|
<input type="text" name="host" maxlength="100" placeholder="比如 192.168.1.100" style="width:16em" ref="dbHost" v-model="localDB.host" @change="checkDBIP" @input="localDB.isLocal = true"/>
|
||||||
<p class="comment" v-if="localDBHost.length > 0 && localDBHost == localDB.host"><span class="blue">已经自动填入从当前服务器发现的MySQL数据库信息。</span></p>
|
<p class="comment" v-if="localDBHost.length > 0 && localDBHost == localDB.host"><span class="blue">已经自动填入从当前服务器发现的MySQL数据库信息。</span></p>
|
||||||
<p class="comment" v-if="!localDB.isLocal"><span class="red">当前地址不是局域网IP,可能会严重影响系统运行性能,请优先选择局域网IP。</span></p>
|
<p class="comment" v-if="!localDB.isLocal"><span class="red">当前地址不是局域网IP,可能会严重影响系统运行性能,请优先选择局域网IP。</span></p>
|
||||||
|
<p class="comment" v-if="localDB.host.length == 0 && localDB.canInstall">如果你还没有安装MySQL,可以 <a href="" @click.prevent="installMySQL">尝试自动安装</a> 。</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ Tea.context(function () {
|
|||||||
|
|
||||||
// 数据库
|
// 数据库
|
||||||
this.dbInfo = {}
|
this.dbInfo = {}
|
||||||
this.localDB = {"host": "", "port": "", "username": "", "port": "", "isLocal": true}
|
this.localDB = {"host": "", "port": "", "username": "", "port": "", "isLocal": true, "canInstall": false}
|
||||||
this.localDBHost = ""
|
this.localDBHost = ""
|
||||||
this.dbRequesting = false
|
this.dbRequesting = false
|
||||||
|
|
||||||
@@ -172,4 +172,17 @@ Tea.context(function () {
|
|||||||
}, 1000)
|
}, 1000)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MySQL
|
||||||
|
*/
|
||||||
|
this.installMySQL = function () {
|
||||||
|
let that = this
|
||||||
|
teaweb.popup("/setup/mysql/installPopup", {
|
||||||
|
height: "28em",
|
||||||
|
onClose: function () {
|
||||||
|
that.detectDB()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
})
|
})
|
||||||
@@ -1 +1,90 @@
|
|||||||
@import "@install";
|
.install-box {
|
||||||
|
@width: 50em;
|
||||||
|
@half-width: 25em;
|
||||||
|
|
||||||
|
width: @width;
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -@half-width;
|
||||||
|
top: 1em;
|
||||||
|
bottom: 1em;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.button.margin {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.primary {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button.disabled {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.title {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.radio {
|
||||||
|
margin-right: 1em;
|
||||||
|
|
||||||
|
label {
|
||||||
|
cursor: pointer !important;
|
||||||
|
font-size: 0.9em !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-box {
|
||||||
|
overflow-y: auto;
|
||||||
|
position: fixed;
|
||||||
|
top: 5em;
|
||||||
|
bottom: 5em;
|
||||||
|
left: 50%;
|
||||||
|
width: @width;
|
||||||
|
padding-right: 1em;
|
||||||
|
margin-left: -@half-width;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-box::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -@half-width;
|
||||||
|
z-index: 1;
|
||||||
|
width: @width;
|
||||||
|
bottom: 1em;
|
||||||
|
|
||||||
|
button {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-box {
|
||||||
|
position: absolute;
|
||||||
|
top: 3em;
|
||||||
|
left: 5em;
|
||||||
|
right: 5em;
|
||||||
|
bottom: 0;
|
||||||
|
text-align: center;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.install-box::-webkit-scrollbar {
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
32
web/views/@default/setup/mysql/installPopup.css
Normal file
32
web/views/@default/setup/mysql/installPopup.css
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
td.title {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
.result-box .row {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.result-box .button-box {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
.logs-box {
|
||||||
|
max-height: 12em;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
.logs-box .row {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
.logs-box::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
font-weight: normal !important;
|
||||||
|
}
|
||||||
|
.green {
|
||||||
|
color: #21ba45;
|
||||||
|
}
|
||||||
|
.red {
|
||||||
|
color: #db2828;
|
||||||
|
}
|
||||||
|
/*# sourceMappingURL=installPopup.css.map */
|
||||||
1
web/views/@default/setup/mysql/installPopup.css.map
Normal file
1
web/views/@default/setup/mysql/installPopup.css.map
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"version":3,"sources":["installPopup.less"],"names":[],"mappings":"AAAA;EACC,mBAAA;;AAGD,EAAE;EACD,WAAA;;AAGD,WACC;EACC,gBAAA;;AAFF,WAKC;EACC,eAAA;;AAIF;EACC,gBAAA;EACA,gBAAA;;AAFD,SAIC;EACC,gBAAA;;AAIF,SAAS;EACR,UAAA;;AAGD;EACC,8BAAA;;AAGD;EACC,cAAA;;AAGD;EACC,cAAA","file":"installPopup.css"}
|
||||||
74
web/views/@default/setup/mysql/installPopup.html
Normal file
74
web/views/@default/setup/mysql/installPopup.html
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
|
||||||
|
<link rel="shortcut icon" href="/images/favicon.png"/>
|
||||||
|
<title>安装GoEdge管理系统</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
|
||||||
|
<link rel="stylesheet" href="/_/@default/@layout_popup.css"/>
|
||||||
|
<link rel="stylesheet" type="text/css" href="/css/semantic.iframe.min.css?v=bRafhK" media="all"/>
|
||||||
|
{$TEA.VUE}
|
||||||
|
<link rel="stylesheet" type="text/css" href="/_/@default/@layout_override.css" media="all"/>
|
||||||
|
<script type="text/javascript" src="/js/md5.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/utils.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/sweetalert2/dist/sweetalert2.all.min.js"></script>
|
||||||
|
<script type="text/javascript" src="/js/components.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div class="margin"></div>
|
||||||
|
<h4>方法1:使用命令安装MySQL</h4>
|
||||||
|
<table class="ui table selectable">
|
||||||
|
<tr>
|
||||||
|
<td>可以在你要安装MySQL的服务器上运行以下命令<span class="grey">(目前仅支持Linux系统和X86架构服务器,安装后的MySQL版本是8.x.x)</span>:</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>sudo sh -c "$(wget https://goedge.cn/install-mysql.sh -O -)"</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h4>方法2:在本机自动安装MySQL</h4>
|
||||||
|
<table class="ui table selectable">
|
||||||
|
<tr>
|
||||||
|
<td colspan="2">如果你想在当前管理系统所在服务器上安装MySQL,可以点击下面的按钮自动开始尝试安装,如果安装不成功,请自行安装<span class="grey">(建议仅在小流量应用场景或测试期间使用此功能)</span>:</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-show="!result.isInstalling">
|
||||||
|
<td colspan="2">
|
||||||
|
<button class="ui button small" type="button" @click.prevent="install" v-if="!result.isInstalled">尝试在本机安装</button>
|
||||||
|
<button class="ui button small" type="button" @click.prevent="install" v-if="result.isInstalled && !result.isOk">重新尝试安装</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-show="result.isInstalled">
|
||||||
|
<td class="title">安装结果</td>
|
||||||
|
<td>
|
||||||
|
<div v-if="result.isOk" class="green result-box">
|
||||||
|
<div class="row">安装成功,请使用记事本或其他工具记录下面MySQL信息,防止以后忘记:</div>
|
||||||
|
<div class="row">安装目录:{{result.dir}}</div>
|
||||||
|
<div class="row">用户:{{result.user}}</div>
|
||||||
|
<div class="row">密码:{{result.password}}</div>
|
||||||
|
<div class="button-box">
|
||||||
|
<button class="ui button small" type="button" @click.prevent="finish">我已完成记录</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="!result.isOk" class="red">
|
||||||
|
<div>
|
||||||
|
安装失败:{{result.err}}
|
||||||
|
</div>
|
||||||
|
<div class="ui divider"></div>
|
||||||
|
<div>请将以上错误信息报告给开发者,并改用其他方式安装MySQL。</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-show="result.isInstalling || (result.isInstalled && !result.isOk)">
|
||||||
|
<td class="title">安装过程</td>
|
||||||
|
<td>
|
||||||
|
<div class="logs-box">
|
||||||
|
<div v-for="log in result.logs" class="row">{{log}}</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
65
web/views/@default/setup/mysql/installPopup.js
Normal file
65
web/views/@default/setup/mysql/installPopup.js
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
Tea.context(function () {
|
||||||
|
this.result = {
|
||||||
|
isInstalling: false,
|
||||||
|
isInstalled: false,
|
||||||
|
isOk: false,
|
||||||
|
err: "",
|
||||||
|
|
||||||
|
user: "",
|
||||||
|
password: "",
|
||||||
|
dir: "",
|
||||||
|
|
||||||
|
logs: []
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$delay(function () {
|
||||||
|
this.checkStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.install = function () {
|
||||||
|
this.result.isInstalling = true
|
||||||
|
this.result.isInstalled = false
|
||||||
|
this.result.logs = []
|
||||||
|
|
||||||
|
this.$post(".installPopup")
|
||||||
|
.timeout(3600)
|
||||||
|
.success(function (resp) {
|
||||||
|
this.result.isOk = resp.data.isOk
|
||||||
|
if (!resp.data.isOk) {
|
||||||
|
this.result.err = resp.data.err
|
||||||
|
} else {
|
||||||
|
this.result.user = resp.data.user
|
||||||
|
this.result.password = resp.data.password
|
||||||
|
this.result.dir = resp.data.dir
|
||||||
|
}
|
||||||
|
this.result.isInstalled = true
|
||||||
|
this.result.isInstalling = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.checkStatus = function () {
|
||||||
|
if (!this.result.isInstalling) {
|
||||||
|
this.$delay(function () {
|
||||||
|
this.checkStatus()
|
||||||
|
}, 1000)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$post(".installLogs")
|
||||||
|
.success(function (resp) {
|
||||||
|
let that = this
|
||||||
|
resp.data.logs.forEach(function (log) {
|
||||||
|
that.result.logs.unshift(log)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.done(function () {
|
||||||
|
this.$delay(function () {
|
||||||
|
this.checkStatus()
|
||||||
|
}, 2000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.finish = function () {
|
||||||
|
teaweb.closePopup()
|
||||||
|
}
|
||||||
|
})
|
||||||
42
web/views/@default/setup/mysql/installPopup.less
Normal file
42
web/views/@default/setup/mysql/installPopup.less
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
td.title {
|
||||||
|
width: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-box {
|
||||||
|
.row {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-box {
|
||||||
|
margin-top: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-box {
|
||||||
|
max-height: 12em;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.row {
|
||||||
|
line-height: 1.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.logs-box::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-weight: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.green {
|
||||||
|
color: #21ba45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.red {
|
||||||
|
color: #db2828;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user