diff --git a/internal/web/actions/default/setup/detectDB.go b/internal/web/actions/default/setup/detectDB.go
index aba1f4fc..2c113cbf 100644
--- a/internal/web/actions/default/setup/detectDB.go
+++ b/internal/web/actions/default/setup/detectDB.go
@@ -9,6 +9,7 @@ import (
"github.com/iwind/TeaGo/maps"
"net"
"os"
+ "runtime"
"strings"
"time"
)
@@ -63,10 +64,11 @@ func (this *DetectDBAction) RunPost(params struct{}) {
}
this.Data["localDB"] = maps.Map{
- "host": localHost,
- "port": localPort,
- "username": localUsername,
- "password": localPassword,
+ "host": localHost,
+ "port": localPort,
+ "username": localUsername,
+ "password": localPassword,
+ "canInstall": runtime.GOOS == "linux" && runtime.GOARCH == "amd64" && os.Getgid() == 0,
}
this.Success()
diff --git a/internal/web/actions/default/setup/init.go b/internal/web/actions/default/setup/init.go
index 24693662..bf91f26a 100644
--- a/internal/web/actions/default/setup/init.go
+++ b/internal/web/actions/default/setup/init.go
@@ -1,6 +1,9 @@
package setup
-import "github.com/iwind/TeaGo"
+import (
+ "github.com/TeaOSLab/EdgeAdmin/internal/web/actions/default/setup/mysql"
+ "github.com/iwind/TeaGo"
+)
func init() {
TeaGo.BeforeStart(func(server *TeaGo.Server) {
@@ -15,6 +18,8 @@ func init() {
Post("/status", new(StatusAction)).
Post("/detectDB", new(DetectDBAction)).
Post("/checkLocalIP", new(CheckLocalIPAction)).
+ GetPost("/mysql/installPopup", new(mysql.InstallPopupAction)).
+ Post("/mysql/installLogs", new(mysql.InstallLogsAction)).
EndAll()
})
}
diff --git a/internal/web/actions/default/setup/mysql/installLogs.go b/internal/web/actions/default/setup/mysql/installLogs.go
new file mode 100644
index 00000000..b13f6606
--- /dev/null
+++ b/internal/web/actions/default/setup/mysql/installLogs.go
@@ -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()
+}
diff --git a/internal/web/actions/default/setup/mysql/installPopup.go b/internal/web/actions/default/setup/mysql/installPopup.go
new file mode 100644
index 00000000..19473d19
--- /dev/null
+++ b/internal/web/actions/default/setup/mysql/installPopup.go
@@ -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()
+}
diff --git a/internal/web/actions/default/setup/mysql/mysqlinstallers/mysql_installer.go b/internal/web/actions/default/setup/mysql/mysqlinstallers/mysql_installer.go
new file mode 100644
index 00000000..35cee27d
--- /dev/null
+++ b/internal/web/actions/default/setup/mysql/mysqlinstallers/mysql_installer.go
@@ -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(`
MySQL Community Server ([\d.]+)
`)
+ 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")
+}
diff --git a/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/cmd.go b/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/cmd.go
new file mode 100644
index 00000000..b80fe895
--- /dev/null
+++ b/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/cmd.go
@@ -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
+}
diff --git a/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/logger.go b/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/logger.go
new file mode 100644
index 00000000..0b1a496f
--- /dev/null
+++ b/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/logger.go
@@ -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
+ }
+ }
+}
diff --git a/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/proc.go b/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/proc.go
new file mode 100644
index 00000000..af391232
--- /dev/null
+++ b/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/proc.go
@@ -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
+}
diff --git a/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/writer_progress.go b/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/writer_progress.go
new file mode 100644
index 00000000..7f896bdf
--- /dev/null
+++ b/internal/web/actions/default/setup/mysql/mysqlinstallers/utils/writer_progress.go
@@ -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))
+}
diff --git a/web/views/@default/setup/@install.css b/web/views/@default/setup/@install.css
deleted file mode 100644
index b60d0616..00000000
--- a/web/views/@default/setup/@install.css
+++ /dev/null
@@ -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 */
\ No newline at end of file
diff --git a/web/views/@default/setup/@install.css.map b/web/views/@default/setup/@install.css.map
deleted file mode 100644
index 9ec79dcb..00000000
--- a/web/views/@default/setup/@install.css.map
+++ /dev/null
@@ -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"}
\ No newline at end of file
diff --git a/web/views/@default/setup/@install.less b/web/views/@default/setup/@install.less
deleted file mode 100644
index 176224f7..00000000
--- a/web/views/@default/setup/@install.less
+++ /dev/null
@@ -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;
-}
\ No newline at end of file
diff --git a/web/views/@default/setup/index.css b/web/views/@default/setup/index.css
index 51126112..32221ce2 100644
--- a/web/views/@default/setup/index.css
+++ b/web/views/@default/setup/index.css
@@ -16,6 +16,9 @@
.install-box .button.disabled {
float: right;
}
+.install-box table td {
+ vertical-align: top;
+}
.install-box table td.title {
width: 10em;
}
diff --git a/web/views/@default/setup/index.css.map b/web/views/@default/setup/index.css.map
index 09f3eb4e..9c3cbd3b 100644
--- a/web/views/@default/setup/index.css.map
+++ b/web/views/@default/setup/index.css.map
@@ -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"}
\ No newline at end of file
+{"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"}
\ No newline at end of file
diff --git a/web/views/@default/setup/index.html b/web/views/@default/setup/index.html
index d4159689..b047a858 100644
--- a/web/views/@default/setup/index.html
+++ b/web/views/@default/setup/index.html
@@ -152,6 +152,7 @@
+
diff --git a/web/views/@default/setup/index.js b/web/views/@default/setup/index.js
index 84d9b7f3..97154512 100644
--- a/web/views/@default/setup/index.js
+++ b/web/views/@default/setup/index.js
@@ -57,7 +57,7 @@ Tea.context(function () {
// 数据库
this.dbInfo = {}
- this.localDB = {"host": "", "port": "", "username": "", "port": "", "isLocal": true}
+ this.localDB = {"host": "", "port": "", "username": "", "port": "", "isLocal": true, "canInstall": false}
this.localDBHost = ""
this.dbRequesting = false
@@ -172,4 +172,17 @@ Tea.context(function () {
}, 1000)
})
}
+
+ /**
+ * MySQL
+ */
+ this.installMySQL = function () {
+ let that = this
+ teaweb.popup("/setup/mysql/installPopup", {
+ height: "28em",
+ onClose: function () {
+ that.detectDB()
+ }
+ })
+ }
})
\ No newline at end of file
diff --git a/web/views/@default/setup/index.less b/web/views/@default/setup/index.less
index d2f72e89..f4520bd2 100644
--- a/web/views/@default/setup/index.less
+++ b/web/views/@default/setup/index.less
@@ -1 +1,90 @@
-@import "@install";
\ No newline at end of file
+.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;
+}
\ No newline at end of file
diff --git a/web/views/@default/setup/mysql/installPopup.css b/web/views/@default/setup/mysql/installPopup.css
new file mode 100644
index 00000000..16c19a36
--- /dev/null
+++ b/web/views/@default/setup/mysql/installPopup.css
@@ -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 */
\ No newline at end of file
diff --git a/web/views/@default/setup/mysql/installPopup.css.map b/web/views/@default/setup/mysql/installPopup.css.map
new file mode 100644
index 00000000..6d6941c3
--- /dev/null
+++ b/web/views/@default/setup/mysql/installPopup.css.map
@@ -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"}
\ No newline at end of file
diff --git a/web/views/@default/setup/mysql/installPopup.html b/web/views/@default/setup/mysql/installPopup.html
new file mode 100644
index 00000000..f3585d01
--- /dev/null
+++ b/web/views/@default/setup/mysql/installPopup.html
@@ -0,0 +1,74 @@
+
+
+
+
+
+ 安装GoEdge管理系统
+
+
+
+ {$TEA.VUE}
+
+
+
+
+
+
+
+
+
+
+
方法1:使用命令安装MySQL
+
+
+ | 可以在你要安装MySQL的服务器上运行以下命令(目前仅支持Linux系统和X86架构服务器,安装后的MySQL版本是8.x.x): |
+
+
+ | sudo sh -c "$(wget https://goedge.cn/install-mysql.sh -O -)" |
+
+
+
+
方法2:在本机自动安装MySQL
+
+
+ | 如果你想在当前管理系统所在服务器上安装MySQL,可以点击下面的按钮自动开始尝试安装,如果安装不成功,请自行安装(建议仅在小流量应用场景或测试期间使用此功能): |
+
+
+ |
+
+
+ |
+
+
+ | 安装结果 |
+
+
+ 安装成功,请使用记事本或其他工具记录下面MySQL信息,防止以后忘记:
+ 安装目录:{{result.dir}}
+ 用户:{{result.user}}
+ 密码:{{result.password}}
+
+
+
+
+
+
+ 安装失败:{{result.err}}
+
+
+ 请将以上错误信息报告给开发者,并改用其他方式安装MySQL。
+
+ |
+
+
+ | 安装过程 |
+
+
+ |
+
+
+
+
+
\ No newline at end of file
diff --git a/web/views/@default/setup/mysql/installPopup.js b/web/views/@default/setup/mysql/installPopup.js
new file mode 100644
index 00000000..b11fb3b2
--- /dev/null
+++ b/web/views/@default/setup/mysql/installPopup.js
@@ -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()
+ }
+})
\ No newline at end of file
diff --git a/web/views/@default/setup/mysql/installPopup.less b/web/views/@default/setup/mysql/installPopup.less
new file mode 100644
index 00000000..7f9fba60
--- /dev/null
+++ b/web/views/@default/setup/mysql/installPopup.less
@@ -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;
+}
\ No newline at end of file