chore: import upstream snapshot with attribution
CI / Check and lint (push) Failing after 1s
Lint / Lint (ubuntu-latest) (push) Failing after 1s

This commit is contained in:
wehub-resource-sync
2026-07-13 12:22:32 +08:00
commit 39dbe3a57d
1131 changed files with 272709 additions and 0 deletions
+250
View File
@@ -0,0 +1,250 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package install
import (
"encoding/json"
"net/http"
"os"
"path/filepath"
"time"
"github.com/apache/answer/configs"
"github.com/apache/answer/internal/base/conf"
"github.com/apache/answer/internal/base/data"
"github.com/apache/answer/internal/base/handler"
"github.com/apache/answer/internal/base/path"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/base/translator"
"github.com/apache/answer/internal/cli"
"github.com/apache/answer/internal/migrations"
"github.com/apache/answer/internal/schema"
"github.com/gin-gonic/gin"
"github.com/jinzhu/copier"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/i18n"
"github.com/segmentfault/pacman/log"
)
// LangOptions get installation language options
// @Summary get installation language options
// @Description get installation language options
// @Tags Lang
// @Produce json
// @Success 200 {object} handler.RespBody{data=[]translator.LangOption}
// @Router /installation/language/options [get]
func LangOptions(ctx *gin.Context) {
handler.HandleResponse(ctx, nil, translator.LanguageOptions)
}
// GetLangMapping get installation language config mapping
// @Summary get installation language config mapping
// @Description get installation language config mapping
// @Tags Lang
// @Param lang query string true "installation language"
// @Produce json
// @Success 200 {object} handler.RespBody{}
// @Router /installation/language/config [get]
func GetLangMapping(ctx *gin.Context) {
t, err := translator.NewTranslator(&translator.I18n{BundleDir: path.I18nPath})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
lang := ctx.Query("lang")
trData, _ := t.Dump(i18n.Language(lang))
var resp map[string]any
_ = json.Unmarshal(trData, &resp)
handler.HandleResponse(ctx, nil, resp)
}
// CheckConfigFileAndRedirectToInstallPage if config file not exist try to redirect to install page
// @Summary if config file not exist try to redirect to install page
// @Description if config file not exist try to redirect to install page
// @Tags installation
// @Accept json
// @Produce json
// @Router / [get]
func CheckConfigFileAndRedirectToInstallPage(ctx *gin.Context) {
if cli.CheckConfigFile(confPath) {
ctx.Redirect(http.StatusFound, "/50x")
} else {
ctx.Redirect(http.StatusFound, "/install")
}
}
// CheckConfigFile check config file if exist when installation
// @Summary check config file if exist when installation
// @Description check config file if exist when installation
// @Tags installation
// @Accept json
// @Produce json
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
// @Router /installation/config-file/check [post]
func CheckConfigFile(ctx *gin.Context) {
resp := &CheckConfigFileResp{}
resp.ConfigFileExist = cli.CheckConfigFile(confPath)
if !resp.ConfigFileExist {
handler.HandleResponse(ctx, nil, resp)
return
}
allConfig, err := conf.ReadConfig(confPath)
if err != nil {
log.Error(err)
err = errors.BadRequest(reason.ReadConfigFailed)
handler.HandleResponse(ctx, err, nil)
return
}
resp.DBConnectionSuccess = cli.CheckDBConnection(allConfig.Data.Database)
if resp.DBConnectionSuccess {
resp.DbTableExist = cli.CheckDBTableExist(allConfig.Data.Database)
}
handler.HandleResponse(ctx, nil, resp)
}
// CheckDatabase check database if exist when installation
// @Summary check database if exist when installation
// @Description check database if exist when installation
// @Tags installation
// @Accept json
// @Produce json
// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq"
// @Success 200 {object} handler.RespBody{data=install.CheckConfigFileResp{}}
// @Router /installation/db/check [post]
func CheckDatabase(ctx *gin.Context) {
req := &CheckDatabaseReq{}
if handler.BindAndCheck(ctx, req) {
return
}
resp := &CheckDatabaseResp{}
dataConf := &data.Database{
Driver: req.DbType,
Connection: req.GetConnection(),
}
resp.ConnectionSuccess = cli.CheckDBConnection(dataConf)
if !resp.ConnectionSuccess {
handler.HandleResponse(ctx, errors.BadRequest(reason.DatabaseConnectionFailed), schema.ErrTypeAlert)
return
}
handler.HandleResponse(ctx, nil, resp)
}
// InitEnvironment init environment
// @Summary init environment
// @Description init environment
// @Tags installation
// @Accept json
// @Produce json
// @Param data body install.CheckDatabaseReq true "CheckDatabaseReq"
// @Success 200 {object} handler.RespBody{}
// @Router /installation/init [post]
func InitEnvironment(ctx *gin.Context) {
req := &CheckDatabaseReq{}
if handler.BindAndCheck(ctx, req) {
return
}
// check config file if exist
if cli.CheckConfigFile(confPath) {
log.Debug("config file already exists")
handler.HandleResponse(ctx, nil, nil)
return
}
if err := cli.InstallConfigFile(confPath); err != nil {
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), &InitEnvironmentResp{
Success: false,
CreateConfigFailed: true,
DefaultConfig: string(configs.Config),
ErrType: schema.ErrTypeAlert.ErrType,
})
return
}
c, err := conf.ReadConfig(confPath)
if err != nil {
log.Errorf("read config failed %s", err)
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
return
}
c.Data.Database.Driver = req.DbType
c.Data.Database.Connection = req.GetConnection()
c.Data.Cache.FilePath = filepath.Join(path.CacheDir, path.DefaultCacheFileName)
c.I18n.BundleDir = path.I18nPath
c.ServiceConfig.UploadPath = path.UploadFilePath
if err := conf.RewriteConfig(confPath, c); err != nil {
log.Errorf("rewrite config failed %s", err)
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
return
}
handler.HandleResponse(ctx, nil, nil)
}
// InitBaseInfo init base info
// @Summary init base info
// @Description init base info
// @Tags installation
// @Accept json
// @Produce json
// @Param data body install.InitBaseInfoReq true "InitBaseInfoReq"
// @Success 200 {object} handler.RespBody{}
// @Router /installation/base-info [post]
func InitBaseInfo(ctx *gin.Context) {
req := &InitBaseInfoReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.FormatSiteUrl()
c, err := conf.ReadConfig(confPath)
if err != nil {
log.Errorf("read config failed %s", err)
handler.HandleResponse(ctx, errors.BadRequest(reason.ReadConfigFailed), nil)
return
}
if cli.CheckDBTableExist(c.Data.Database) {
log.Warn("database is already initialized")
handler.HandleResponse(ctx, nil, nil)
return
}
engine, err := data.NewDB(false, c.Data.Database)
if err != nil {
log.Errorf("init database failed %s", err)
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallCreateTableFailed), nil)
}
inputData := &migrations.InitNeedUserInputData{}
_ = copier.Copy(inputData, req)
if err := migrations.NewMentor(ctx, engine, inputData).InitDB(); err != nil {
log.Error("init database error: ", err.Error())
handler.HandleResponse(ctx, errors.BadRequest(reason.InstallConfigFailed), schema.ErrTypeAlert)
return
}
handler.HandleResponse(ctx, nil, nil)
go func() {
time.Sleep(1 * time.Second)
os.Exit(0)
}()
}
+150
View File
@@ -0,0 +1,150 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package install
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"os"
"github.com/gin-gonic/gin"
"github.com/tidwall/gjson"
)
type Env struct {
AutoInstall string `json:"auto_install"`
DbType string `json:"db_type"`
DbUsername string `json:"db_username"`
DbPassword string `json:"db_password"`
DbHost string `json:"db_host"`
DbName string `json:"db_name"`
DbFile string `json:"db_file"`
Language string `json:"lang"`
SiteName string `json:"site_name"`
SiteURL string `json:"site_url"`
ContactEmail string `json:"contact_email"`
AdminName string `json:"name"`
AdminPassword string `json:"password"`
AdminEmail string `json:"email"`
LoginRequired bool `json:"login_required"`
ExternalContentDisplay string `json:"external_content_display"`
}
func TryToInstallByEnv() (installByEnv bool, err error) {
env := loadEnv()
if len(env.AutoInstall) == 0 {
return false, nil
}
fmt.Println("[auto-install] try to install by environment variable")
return true, initByEnv(env)
}
func loadEnv() (env *Env) {
return &Env{
AutoInstall: os.Getenv("AUTO_INSTALL"),
DbType: os.Getenv("DB_TYPE"),
DbUsername: os.Getenv("DB_USERNAME"),
DbPassword: os.Getenv("DB_PASSWORD"),
DbHost: os.Getenv("DB_HOST"),
DbName: os.Getenv("DB_NAME"),
DbFile: os.Getenv("DB_FILE"),
Language: os.Getenv("LANGUAGE"),
SiteName: os.Getenv("SITE_NAME"),
SiteURL: os.Getenv("SITE_URL"),
ContactEmail: os.Getenv("CONTACT_EMAIL"),
AdminName: os.Getenv("ADMIN_NAME"),
AdminPassword: os.Getenv("ADMIN_PASSWORD"),
AdminEmail: os.Getenv("ADMIN_EMAIL"),
ExternalContentDisplay: os.Getenv("EXTERNAL_CONTENT_DISPLAY"),
}
}
func initByEnv(env *Env) (err error) {
gin.SetMode(gin.TestMode)
if err = dbCheck(env); err != nil {
return err
}
if err = initConfigAndDb(env); err != nil {
return err
}
if err = initBaseInfo(env); err != nil {
return err
}
return nil
}
func dbCheck(env *Env) (err error) {
req := &CheckDatabaseReq{
DbType: env.DbType,
DbUsername: env.DbUsername,
DbPassword: env.DbPassword,
DbHost: env.DbHost,
DbName: env.DbName,
DbFile: env.DbFile,
}
return requestAPI(req, "POST", "/installation/db/check", CheckDatabase)
}
func initConfigAndDb(env *Env) (err error) {
req := &CheckDatabaseReq{
DbType: env.DbType,
DbUsername: env.DbUsername,
DbPassword: env.DbPassword,
DbHost: env.DbHost,
DbName: env.DbName,
DbFile: env.DbFile,
}
return requestAPI(req, "POST", "/installation/init", InitEnvironment)
}
func initBaseInfo(env *Env) (err error) {
req := &InitBaseInfoReq{
Language: env.Language,
SiteName: env.SiteName,
SiteURL: env.SiteURL,
ContactEmail: env.ContactEmail,
AdminName: env.AdminName,
AdminPassword: env.AdminPassword,
AdminEmail: env.AdminEmail,
LoginRequired: env.LoginRequired,
ExternalContentDisplay: env.ExternalContentDisplay,
}
return requestAPI(req, "POST", "/installation/base-info", InitBaseInfo)
}
func requestAPI(req any, method, url string, handlerFunc gin.HandlerFunc) error {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
body, _ := json.Marshal(req)
c.Request, _ = http.NewRequest(method, url, bytes.NewBuffer(body))
if method == "POST" {
c.Request.Header.Set("Content-Type", "application/json")
}
handlerFunc(c)
if w.Code != http.StatusOK {
return errors.New(gjson.Get(w.Body.String(), "msg").String())
}
return nil
}
+62
View File
@@ -0,0 +1,62 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package install
import (
"fmt"
"os"
"github.com/apache/answer/internal/base/path"
"github.com/apache/answer/internal/base/translator"
"github.com/joho/godotenv"
)
var (
port string
confPath = ""
)
func init() {
_ = godotenv.Load()
port = os.Getenv("INSTALL_PORT")
}
func Run(configPath string) {
confPath = configPath
// initialize translator for return internationalization error when installing.
_, err := translator.NewTranslator(&translator.I18n{BundleDir: path.I18nPath})
if err != nil {
panic(err)
}
// try to install by env
if installByEnv, err := TryToInstallByEnv(); installByEnv && err != nil {
fmt.Printf("[auto-install] try to init by env fail: %v\n", err)
}
installServer := NewInstallHTTPServer()
if len(port) == 0 {
port = "80"
}
fmt.Printf("[SUCCESS] answer installation service will run at: http://localhost:%s/install/ \n", port)
if err = installServer.Run(":" + port); err != nil {
panic(err)
}
}
+157
View File
@@ -0,0 +1,157 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package install
import (
"fmt"
"net/url"
"strings"
"github.com/apache/answer/internal/base/reason"
"github.com/apache/answer/internal/base/validator"
"github.com/apache/answer/pkg/checker"
"github.com/apache/answer/pkg/dir"
"github.com/segmentfault/pacman/errors"
"xorm.io/xorm/schemas"
)
// CheckConfigFileResp check config file if exist or not response
type CheckConfigFileResp struct {
ConfigFileExist bool `json:"config_file_exist"`
DBConnectionSuccess bool `json:"db_connection_success"`
DbTableExist bool `json:"db_table_exist"`
}
// CheckDatabaseReq check database
type CheckDatabaseReq struct {
DbType string `validate:"required,oneof=postgres sqlite3 mysql" json:"db_type"`
DbUsername string `json:"db_username"`
DbPassword string `json:"db_password"`
DbHost string `json:"db_host"`
DbName string `json:"db_name"`
DbFile string `json:"db_file"`
Ssl bool `json:"ssl_enabled"`
SslMode string `json:"ssl_mode"`
SslRootCert string `json:"ssl_root_cert"`
SslKey string `json:"ssl_key"`
SslCert string `json:"ssl_cert"`
}
// GetConnection get connection string
func (r *CheckDatabaseReq) GetConnection() string {
if r.DbType == string(schemas.SQLITE) {
return r.DbFile
}
if r.DbType == string(schemas.MYSQL) {
return fmt.Sprintf("%s:%s@tcp(%s)/%s",
r.DbUsername, r.DbPassword, r.DbHost, r.DbName)
}
if r.DbType == string(schemas.POSTGRES) {
host, port := parsePgSQLHostPort(r.DbHost)
switch {
case !r.Ssl:
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable",
host, port, r.DbUsername, r.DbPassword, r.DbName)
case r.SslMode == "require":
return fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
host, port, r.DbUsername, r.DbPassword, r.DbName, r.SslMode)
case r.SslMode == "verify-ca" || r.SslMode == "verify-full":
connection := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
host, port, r.DbUsername, r.DbPassword, r.DbName, r.SslMode)
if len(r.SslRootCert) > 0 && dir.CheckFileExist(r.SslRootCert) {
connection += fmt.Sprintf(" sslrootcert=%s", r.SslRootCert)
}
if len(r.SslCert) > 0 && dir.CheckFileExist(r.SslCert) {
connection += fmt.Sprintf(" sslcert=%s", r.SslCert)
}
if len(r.SslKey) > 0 && dir.CheckFileExist(r.SslKey) {
connection += fmt.Sprintf(" sslkey=%s", r.SslKey)
}
return connection
}
}
return ""
}
func parsePgSQLHostPort(dbHost string) (host string, port string) {
if strings.Contains(dbHost, ":") {
idx := strings.LastIndex(dbHost, ":")
host, port = dbHost[:idx], dbHost[idx+1:]
} else if len(dbHost) > 0 {
host = dbHost
}
if host == "" {
host = "127.0.0.1"
}
if port == "" {
port = "5432"
}
return host, port
}
// CheckDatabaseResp check database response
type CheckDatabaseResp struct {
ConnectionSuccess bool `json:"connection_success"`
}
// InitEnvironmentResp init environment response
type InitEnvironmentResp struct {
Success bool `json:"success"`
CreateConfigFailed bool `json:"create_config_failed"`
DefaultConfig string `json:"default_config"`
ErrType string `json:"err_type"`
}
// InitBaseInfoReq init base info request
type InitBaseInfoReq struct {
Language string `validate:"required,gt=0,lte=30" json:"lang"`
SiteName string `validate:"required,sanitizer,gt=0,lte=30" json:"site_name"`
SiteURL string `validate:"required,gt=0,lte=512,url" json:"site_url"`
ContactEmail string `validate:"required,email,gt=0,lte=500" json:"contact_email"`
AdminName string `validate:"required,gte=2,lte=30" json:"name"`
AdminPassword string `validate:"required,gte=8,lte=32" json:"password"`
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
LoginRequired bool `json:"login_required"`
ExternalContentDisplay string `validate:"required,oneof=always_display ask_before_display" json:"external_content_display"`
}
func (r *InitBaseInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
if checker.IsInvalidUsername(r.AdminName) {
errField := &validator.FormErrorField{
ErrorField: "name",
ErrorMsg: reason.UsernameInvalid,
}
errFields = append(errFields, errField)
return errFields, errors.BadRequest(reason.UsernameInvalid)
}
return
}
func (r *InitBaseInfoReq) FormatSiteUrl() {
parsedUrl, err := url.Parse(r.SiteURL)
if err != nil {
return
}
r.SiteURL = fmt.Sprintf("%s://%s", parsedUrl.Scheme, parsedUrl.Host)
if len(parsedUrl.Path) > 0 {
r.SiteURL += parsedUrl.Path
r.SiteURL = strings.TrimSuffix(r.SiteURL, "/")
}
}
+93
View File
@@ -0,0 +1,93 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package install
import (
"embed"
"fmt"
"io/fs"
"net/http"
"github.com/apache/answer/configs"
"github.com/apache/answer/internal/base/conf"
"github.com/apache/answer/ui"
"github.com/gin-gonic/gin"
"github.com/segmentfault/pacman/log"
"gopkg.in/yaml.v3"
)
const UIStaticPath = "build/static"
type _resource struct {
fs embed.FS
}
// Open to implement the interface by http.FS required
func (r *_resource) Open(name string) (fs.File, error) {
name = fmt.Sprintf(UIStaticPath+"/%s", name)
log.Debugf("open static path %s", name)
return r.fs.Open(name)
}
// NewInstallHTTPServer new install http server.
func NewInstallHTTPServer() *gin.Engine {
gin.SetMode(gin.ReleaseMode)
r := gin.New()
c := &conf.AllConfig{}
_ = yaml.Unmarshal(configs.Config, c)
r.GET("/healthz", func(ctx *gin.Context) { ctx.String(200, "OK") })
r.StaticFS(c.UI.BaseURL+"/static", http.FS(&_resource{
fs: ui.Build,
}))
// read default config file and extract ui config
installApi := r.Group("")
installApi.GET(c.UI.BaseURL+"/", CheckConfigFileAndRedirectToInstallPage)
installApi.GET(c.UI.BaseURL+"/install", WebPage)
installApi.GET(c.UI.BaseURL+"/50x", WebPage)
installApi.GET(c.UI.APIBaseURL+"/installation/language/config", GetLangMapping)
installApi.GET(c.UI.APIBaseURL+"/installation/language/options", LangOptions)
installApi.POST(c.UI.APIBaseURL+"/installation/db/check", CheckDatabase)
installApi.POST(c.UI.APIBaseURL+"/installation/config-file/check", CheckConfigFile)
installApi.POST(c.UI.APIBaseURL+"/installation/init", InitEnvironment)
installApi.POST(c.UI.APIBaseURL+"/installation/base-info", InitBaseInfo)
r.NoRoute(func(ctx *gin.Context) {
ctx.Redirect(http.StatusFound, "/50x")
})
return r
}
func WebPage(c *gin.Context) {
filePath := ""
var file []byte
var err error
filePath = "build/index.html"
c.Header("content-type", "text/html;charset=utf-8")
file, err = ui.Build.ReadFile(filePath)
if err != nil {
log.Error(err)
c.Status(http.StatusNotFound)
return
}
c.String(http.StatusOK, string(file))
}