tank/code/rest/install_controller.go
2022-03-18 22:17:32 +08:00

467 lines
13 KiB
Go

package rest
import (
"fmt"
"github.com/eyebluecn/tank/code/core"
"github.com/eyebluecn/tank/code/tool/builder"
"github.com/eyebluecn/tank/code/tool/i18n"
"github.com/eyebluecn/tank/code/tool/result"
"github.com/eyebluecn/tank/code/tool/third"
"github.com/eyebluecn/tank/code/tool/util"
"github.com/eyebluecn/tank/code/tool/uuid"
"github.com/glebarez/sqlite"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"gorm.io/gorm/schema"
"log"
"net/http"
"os"
"regexp"
"strconv"
"sync"
"time"
)
//install apis. Only when installing period can be visited.
type InstallController struct {
BaseController
uploadTokenDao *UploadTokenDao
downloadTokenDao *DownloadTokenDao
matterDao *MatterDao
matterService *MatterService
imageCacheDao *ImageCacheDao
imageCacheService *ImageCacheService
tableNames []interface{}
}
func (this *InstallController) Init() {
this.BaseController.Init()
b := core.CONTEXT.GetBean(this.uploadTokenDao)
if c, ok := b.(*UploadTokenDao); ok {
this.uploadTokenDao = c
}
b = core.CONTEXT.GetBean(this.downloadTokenDao)
if c, ok := b.(*DownloadTokenDao); ok {
this.downloadTokenDao = c
}
b = core.CONTEXT.GetBean(this.matterDao)
if c, ok := b.(*MatterDao); ok {
this.matterDao = c
}
b = core.CONTEXT.GetBean(this.matterService)
if c, ok := b.(*MatterService); ok {
this.matterService = c
}
b = core.CONTEXT.GetBean(this.imageCacheDao)
if c, ok := b.(*ImageCacheDao); ok {
this.imageCacheDao = c
}
b = core.CONTEXT.GetBean(this.imageCacheService)
if c, ok := b.(*ImageCacheService); ok {
this.imageCacheService = c
}
this.tableNames = []interface{}{
&Dashboard{},
&Bridge{},
&DownloadToken{},
&Footprint{},
&ImageCache{},
&Matter{},
&Preference{},
&Session{},
&Share{},
&UploadToken{},
&User{},
}
}
func (this *InstallController) RegisterRoutes() map[string]func(writer http.ResponseWriter, request *http.Request) {
routeMap := make(map[string]func(writer http.ResponseWriter, request *http.Request))
routeMap["/api/install/verify"] = this.Wrap(this.Verify, USER_ROLE_GUEST)
routeMap["/api/install/table/info/list"] = this.Wrap(this.TableInfoList, USER_ROLE_GUEST)
routeMap["/api/install/create/table"] = this.Wrap(this.CreateTable, USER_ROLE_GUEST)
routeMap["/api/install/admin/list"] = this.Wrap(this.AdminList, USER_ROLE_GUEST)
routeMap["/api/install/create/admin"] = this.Wrap(this.CreateAdmin, USER_ROLE_GUEST)
routeMap["/api/install/validate/admin"] = this.Wrap(this.ValidateAdmin, USER_ROLE_GUEST)
routeMap["/api/install/finish"] = this.Wrap(this.Finish, USER_ROLE_GUEST)
return routeMap
}
func (this *InstallController) openDbConnection(writer http.ResponseWriter, request *http.Request) *gorm.DB {
dbType := request.FormValue("dbType")
mysqlPortStr := request.FormValue("mysqlPort")
mysqlHost := request.FormValue("mysqlHost")
mysqlSchema := request.FormValue("mysqlSchema")
mysqlUsername := request.FormValue("mysqlUsername")
mysqlPassword := request.FormValue("mysqlPassword")
mysqlCharset := request.FormValue("mysqlCharset")
var mysqlPort int
if mysqlPortStr != "" {
tmp, err := strconv.Atoi(mysqlPortStr)
this.PanicError(err)
mysqlPort = tmp
}
//log config
dbLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // slow SQL 1s
LogLevel: logger.Silent, // log level. open when debug.
IgnoreRecordNotFoundError: true, // ignore ErrRecordNotFound
Colorful: false, // colorful print
},
)
//table name strategy
namingStrategy := core.CONFIG.NamingStrategy()
if dbType == "sqlite" {
var err error = nil
sqliteFolder := core.CONFIG.SqliteFolder() + "/tank.sqlite"
this.logger.Info("Connect Sqlite %s", sqliteFolder)
db, err := gorm.Open(sqlite.Open(sqliteFolder), &gorm.Config{Logger: dbLogger, NamingStrategy: namingStrategy})
if err != nil {
core.LOGGER.Panic("failed to connect sqlite database")
}
//sqlite lock issue. https://gist.github.com/mrnugget/0eda3b2b53a70fa4a894
phyDb, err := db.DB()
phyDb.SetMaxOpenConns(1)
return db
} else {
mysqlUrl := util.GetMysqlUrl(mysqlPort, mysqlHost, mysqlSchema, mysqlUsername, mysqlPassword, mysqlCharset)
this.logger.Info("Connect MySQL %s", mysqlUrl)
var err error = nil
db, err := gorm.Open(mysql.Open(mysqlUrl), &gorm.Config{Logger: dbLogger, NamingStrategy: namingStrategy})
this.PanicError(err)
return db
}
}
func (this *InstallController) closeDbConnection(db *gorm.DB) {
if db != nil {
sqlDB, err := db.DB()
if err != nil {
core.LOGGER.Error("occur error when get *sql.DB %s", err.Error())
}
err = sqlDB.Close()
if err != nil {
core.LOGGER.Error("occur error when closing db %s", err.Error())
}
}
}
// (tableName, exists, allFields, missingFields)
func (this *InstallController) getTableMeta(gormDb *gorm.DB, entity interface{}) (string, bool, []*InstallFieldInfo, []*InstallFieldInfo) {
//get all useful fields from model.
entitySchema, err := schema.Parse(entity, &sync.Map{}, core.CONFIG.NamingStrategy())
this.PanicError(err)
tableName := entitySchema.Table
var allFields = make([]*InstallFieldInfo, 0)
for _, field := range entitySchema.Fields {
allFields = append(allFields, &InstallFieldInfo{
Name: field.DBName,
DataType: string(field.DataType),
})
}
var missingFields = make([]*InstallFieldInfo, 0)
if !gormDb.Migrator().HasTable(tableName) {
missingFields = append(missingFields, allFields...)
return tableName, false, allFields, missingFields
} else {
for _, field := range allFields {
//tag with `gorm:"-"` will be ""
if field.Name != "" {
database := gormDb.Migrator().CurrentDatabase()
//if sqlite
if _, ok := gormDb.Dialector.(*sqlite.Dialector); ok {
if !gormDb.Migrator().HasColumn(tableName, field.Name) {
missingFields = append(missingFields, field)
}
} else {
if !third.MysqlMigratorHasColumn(gormDb, database, tableName, field.Name) {
missingFields = append(missingFields, field)
}
}
}
}
return tableName, true, allFields, missingFields
}
}
func (this *InstallController) getTableMetaList(db *gorm.DB) []*InstallTableInfo {
var installTableInfos []*InstallTableInfo
for _, iBase := range this.tableNames {
tableName, exist, allFields, missingFields := this.getTableMeta(db, iBase)
installTableInfos = append(installTableInfos, &InstallTableInfo{
Name: tableName,
TableExist: exist,
AllFields: allFields,
MissingFields: missingFields,
})
}
return installTableInfos
}
// validate table whether integrity. if not panic err.
func (this *InstallController) validateTableMetaList(tableInfoList []*InstallTableInfo) {
for _, tableInfo := range tableInfoList {
if tableInfo.TableExist {
if len(tableInfo.MissingFields) != 0 {
var strs []string
for _, v := range tableInfo.MissingFields {
strs = append(strs, v.Name)
}
panic(result.BadRequest(fmt.Sprintf("table %s miss the following fields %v", tableInfo.Name, strs)))
}
} else {
panic(result.BadRequest(tableInfo.Name + " table not exist"))
}
}
}
//Ping db.
func (this *InstallController) Verify(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
defer this.closeDbConnection(db)
this.logger.Info("Ping DB")
phyDb, err := db.DB()
this.PanicError(err)
err = phyDb.Ping()
this.PanicError(err)
return this.Success("OK")
}
func (this *InstallController) TableInfoList(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
defer this.closeDbConnection(db)
return this.Success(this.getTableMetaList(db))
}
func (this *InstallController) CreateTable(writer http.ResponseWriter, request *http.Request) *result.WebResult {
var installTableInfos []*InstallTableInfo
db := this.openDbConnection(writer, request)
defer this.closeDbConnection(db)
for _, iBase := range this.tableNames {
if _, ok := db.Dialector.(*sqlite.Dialector); ok {
//if sqlite. no need to set CHARSET.
err := db.AutoMigrate(iBase)
this.PanicError(err)
} else {
//use utf8 charset
mysqlCharset := request.FormValue("mysqlCharset")
err := db.Set("gorm:table_options", fmt.Sprintf("CHARSET=%s", mysqlCharset)).AutoMigrate(iBase)
this.PanicError(err)
}
//complete the missing fields or create table.
tableName, exist, allFields, missingFields := this.getTableMeta(db, iBase)
installTableInfos = append(installTableInfos, &InstallTableInfo{
Name: tableName,
TableExist: exist,
AllFields: allFields,
MissingFields: missingFields,
})
}
return this.Success(installTableInfos)
}
//get the list of admin.
func (this *InstallController) AdminList(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
defer this.closeDbConnection(db)
var wp = &builder.WherePair{}
wp = wp.And(&builder.WherePair{Query: "role = ?", Args: []interface{}{USER_ROLE_ADMINISTRATOR}})
var users []*User
db = db.Where(wp.Query, wp.Args...).Offset(0).Limit(10).Find(&users)
this.PanicError(db.Error)
return this.Success(users)
}
//create admin
func (this *InstallController) CreateAdmin(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
defer this.closeDbConnection(db)
adminUsername := request.FormValue("adminUsername")
adminPassword := request.FormValue("adminPassword")
//validate admin's username
if m, _ := regexp.MatchString(USERNAME_PATTERN, adminUsername); !m {
panic(result.BadRequestI18n(request, i18n.UsernameError))
}
if len(adminPassword) < 6 {
panic(result.BadRequest(`admin's password at least 6 chars'`))
}
//check whether duplicate
var count2 int64
db2 := db.Model(&User{}).Where("username = ?", adminUsername).Count(&count2)
this.PanicError(db2.Error)
if count2 > 0 {
panic(result.BadRequestI18n(request, i18n.UsernameExist, adminUsername))
}
user := &User{}
timeUUID, _ := uuid.NewV4()
user.Uuid = string(timeUUID.String())
user.CreateTime = time.Now()
user.UpdateTime = time.Now()
user.LastTime = time.Now()
user.Sort = time.Now().UnixNano() / 1e6
user.Role = USER_ROLE_ADMINISTRATOR
user.Username = adminUsername
user.Password = util.GetBcrypt(adminPassword)
user.SizeLimit = -1
user.Status = USER_STATUS_OK
db3 := db.Create(user)
this.PanicError(db3.Error)
return this.Success("OK")
}
//(if there is admin in db)Validate admin.
func (this *InstallController) ValidateAdmin(writer http.ResponseWriter, request *http.Request) *result.WebResult {
db := this.openDbConnection(writer, request)
defer this.closeDbConnection(db)
adminUsername := request.FormValue("adminUsername")
adminPassword := request.FormValue("adminPassword")
if adminUsername == "" {
panic(result.BadRequest(`admin's username cannot be null'`))
}
if len(adminPassword) < 6 {
panic(result.BadRequest(`admin's password at least 6 chars'`))
}
var existUsernameUser = &User{}
db = db.Where(&User{Username: adminUsername}).First(existUsernameUser)
if db.Error != nil {
panic(result.BadRequestI18n(request, i18n.UsernameNotExist, adminUsername))
}
if !util.MatchBcrypt(adminPassword, existUsernameUser.Password) {
panic(result.BadRequestI18n(request, i18n.UsernameOrPasswordError, adminUsername))
}
if existUsernameUser.Role != USER_ROLE_ADMINISTRATOR {
panic(result.BadRequestI18n(request, i18n.UsernameIsNotAdmin, adminUsername))
}
return this.Success("OK")
}
//Finish the installation
func (this *InstallController) Finish(writer http.ResponseWriter, request *http.Request) *result.WebResult {
dbType := request.FormValue("dbType")
mysqlPortStr := request.FormValue("mysqlPort")
mysqlHost := request.FormValue("mysqlHost")
mysqlSchema := request.FormValue("mysqlSchema")
mysqlUsername := request.FormValue("mysqlUsername")
mysqlPassword := request.FormValue("mysqlPassword")
mysqlCharset := request.FormValue("mysqlCharset")
var mysqlPort int
if mysqlPortStr != "" {
tmp, err := strconv.Atoi(mysqlPortStr)
this.PanicError(err)
mysqlPort = tmp
}
//Recheck the db connection
db := this.openDbConnection(writer, request)
defer this.closeDbConnection(db)
//Recheck the integrity of tables.
tableMetaList := this.getTableMetaList(db)
this.validateTableMetaList(tableMetaList)
//At least one admin
var count1 int64
db1 := db.Model(&User{}).Where("role = ?", USER_ROLE_ADMINISTRATOR).Count(&count1)
this.PanicError(db1.Error)
if count1 == 0 {
panic(result.BadRequest(`please config at least one admin user`))
}
//announce the config to write config to tank.json
core.CONFIG.FinishInstall(dbType, mysqlPort, mysqlHost, mysqlSchema, mysqlUsername, mysqlPassword, mysqlCharset)
//announce the context to broadcast the installation news to bean.
core.CONTEXT.InstallOk()
return this.Success("OK")
}