345 lines
11 KiB
Go
345 lines
11 KiB
Go
/*
|
|
* 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 answercmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/apache/answer/internal/base/conf"
|
|
"github.com/apache/answer/internal/base/path"
|
|
"github.com/apache/answer/internal/cli"
|
|
"github.com/apache/answer/internal/install"
|
|
"github.com/apache/answer/internal/migrations"
|
|
"github.com/apache/answer/plugin"
|
|
"github.com/segmentfault/pacman/log"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
// dataDirPath save all answer application data in this directory. like config file, upload file...
|
|
dataDirPath string
|
|
// dumpDataPath dump data path
|
|
dumpDataPath string
|
|
// place to build new answer
|
|
buildDir string
|
|
// plugins needed to build in answer application
|
|
buildWithPlugins []string
|
|
// build output path
|
|
buildOutput string
|
|
// This config is used to upgrade the database from a specific version manually.
|
|
// If you want to upgrade the database to version 1.1.0, you can use `answer upgrade -f v1.1.0`.
|
|
upgradeVersion string
|
|
// The fields that need to be set to the default value
|
|
configFields []string
|
|
// i18nSourcePath i18n from path
|
|
i18nSourcePath string
|
|
// i18nTargetPath i18n to path
|
|
i18nTargetPath string
|
|
// resetPasswordEmail user email for password reset
|
|
resetPasswordEmail string
|
|
// resetPasswordPassword new password for password reset
|
|
resetPasswordPassword string
|
|
)
|
|
|
|
func init() {
|
|
rootCmd.Version = fmt.Sprintf("%s\nrevision: %s\nbuild time: %s", Version, Revision, Time)
|
|
|
|
rootCmd.PersistentFlags().StringVarP(&dataDirPath, "data-path", "C", "/data/", "data path, eg: -C ./data/")
|
|
|
|
dumpCmd.Flags().StringVarP(&dumpDataPath, "path", "p", "./", "dump data path, eg: -p ./dump/data/")
|
|
|
|
buildCmd.Flags().StringSliceVarP(&buildWithPlugins, "with", "w", []string{}, "plugins needed to build")
|
|
|
|
buildCmd.Flags().StringVarP(&buildOutput, "output", "o", "", "build output path")
|
|
|
|
buildCmd.Flags().StringVarP(&buildDir, "build-dir", "b", "", "dir for build process")
|
|
|
|
upgradeCmd.Flags().StringVarP(&upgradeVersion, "from", "f", "", "upgrade from specific version, eg: -f v1.1.0")
|
|
|
|
configCmd.Flags().StringSliceVarP(&configFields, "with", "w", []string{}, "the fields that need to be set to the default value, eg: -w allow_password_login")
|
|
|
|
i18nCmd.Flags().StringVarP(&i18nSourcePath, "source", "s", "", "i18n source path, eg: -s ./i18n/source")
|
|
|
|
i18nCmd.Flags().StringVarP(&i18nTargetPath, "target", "t", "", "i18n target path, eg: -t ./i18n/target")
|
|
|
|
resetPasswordCmd.Flags().StringVarP(&resetPasswordEmail, "email", "e", "", "user email address")
|
|
resetPasswordCmd.Flags().StringVarP(&resetPasswordPassword, "password", "p", "", "new password (not recommended, will be recorded in shell history)")
|
|
|
|
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd, resetPasswordCmd} {
|
|
rootCmd.AddCommand(cmd)
|
|
}
|
|
}
|
|
|
|
var (
|
|
rootCmd = &cobra.Command{
|
|
Use: "answer",
|
|
Short: "Answer is a minimalist open source Q&A community.",
|
|
Long: `Answer is a minimalist open source Q&A community.
|
|
To run answer, use:
|
|
- 'answer init' to initialize the required environment.
|
|
- 'answer run' to launch application.`,
|
|
}
|
|
|
|
runCmd = &cobra.Command{
|
|
Use: "run",
|
|
Short: "Run Answer",
|
|
Long: `Start running Answer`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
path.FormatAllPath(dataDirPath)
|
|
fmt.Println("config file path: ", path.GetConfigFilePath())
|
|
fmt.Println("Answer is starting..........................")
|
|
runApp()
|
|
},
|
|
}
|
|
|
|
initCmd = &cobra.Command{
|
|
Use: "init",
|
|
Short: "Initialize Answer",
|
|
Long: `Initialize Answer with specified configuration`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
// check config file and database. if config file exists and database is already created, init done
|
|
cli.InstallAllInitialEnvironment(dataDirPath)
|
|
|
|
configFileExist := cli.CheckConfigFile(path.GetConfigFilePath())
|
|
if configFileExist {
|
|
fmt.Println("config file exists, try to read the config...")
|
|
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
|
if err != nil {
|
|
fmt.Println("read config failed: ", err.Error())
|
|
return
|
|
}
|
|
|
|
fmt.Println("config file read successfully, try to connect database...")
|
|
if cli.CheckDBTableExist(c.Data.Database) {
|
|
fmt.Println("connect to database successfully and table already exists, do nothing.")
|
|
return
|
|
}
|
|
}
|
|
|
|
// start installation server to install
|
|
install.Run(path.GetConfigFilePath())
|
|
},
|
|
}
|
|
|
|
upgradeCmd = &cobra.Command{
|
|
Use: "upgrade",
|
|
Short: "Upgrade Answer",
|
|
Long: `Upgrade Answer to the latest version`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
log.SetLogger(log.NewStdLogger(os.Stdout))
|
|
path.FormatAllPath(dataDirPath)
|
|
cli.InstallI18nBundle(true)
|
|
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
|
if err != nil {
|
|
fmt.Println("read config failed: ", err.Error())
|
|
return
|
|
}
|
|
if err = migrations.Migrate(c.Debug, c.Data.Database, c.Data.Cache, upgradeVersion); err != nil {
|
|
fmt.Println("migrate failed: ", err.Error())
|
|
return
|
|
}
|
|
fmt.Println("upgrade done")
|
|
},
|
|
}
|
|
|
|
dumpCmd = &cobra.Command{
|
|
Use: "dump",
|
|
Short: "Back up data",
|
|
Long: `Back up database into an SQL file`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
fmt.Println("Answer is backing up data")
|
|
path.FormatAllPath(dataDirPath)
|
|
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
|
if err != nil {
|
|
fmt.Println("read config failed: ", err.Error())
|
|
return
|
|
}
|
|
err = cli.DumpAllData(c.Data.Database, dumpDataPath)
|
|
if err != nil {
|
|
fmt.Println("dump failed: ", err.Error())
|
|
return
|
|
}
|
|
fmt.Println("Answer backed up the data successfully.")
|
|
},
|
|
}
|
|
|
|
checkCmd = &cobra.Command{
|
|
Use: "check",
|
|
Short: "Check the required environment",
|
|
Long: `Check if the current environment meets the startup requirements`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
path.FormatAllPath(dataDirPath)
|
|
fmt.Println("Start checking the required environment...")
|
|
if cli.CheckConfigFile(path.GetConfigFilePath()) {
|
|
fmt.Println("config file exists [✔]")
|
|
} else {
|
|
fmt.Println("config file not exists [x]")
|
|
}
|
|
|
|
if cli.CheckUploadDir() {
|
|
fmt.Println("upload directory exists [✔]")
|
|
} else {
|
|
fmt.Println("upload directory not exists [x]")
|
|
}
|
|
|
|
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
|
if err != nil {
|
|
fmt.Println("read config failed: ", err.Error())
|
|
return
|
|
}
|
|
|
|
if cli.CheckDBConnection(c.Data.Database) {
|
|
fmt.Println("db connection successfully [✔]")
|
|
} else {
|
|
fmt.Println("db connection failed [x]")
|
|
}
|
|
fmt.Println("check environment all done")
|
|
},
|
|
}
|
|
|
|
buildCmd = &cobra.Command{
|
|
Use: "build",
|
|
Short: "Build Answer with plugins",
|
|
Long: `Build a new Answer with plugins that you need`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
fmt.Printf("try to build a new answer with plugins:\n%s\n", strings.Join(buildWithPlugins, "\n"))
|
|
err := cli.BuildNewAnswer(buildDir, buildOutput, buildWithPlugins, cli.OriginalAnswerInfo{
|
|
Version: Version,
|
|
Revision: Revision,
|
|
Time: Time,
|
|
})
|
|
if err != nil {
|
|
fmt.Printf("build failed %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("build new answer successfully %s\n", buildOutput)
|
|
},
|
|
}
|
|
|
|
pluginCmd = &cobra.Command{
|
|
Use: "plugin",
|
|
Short: "Print all plugins packed in the binary",
|
|
Long: `Print all plugins packed in the binary`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
_ = plugin.CallBase(func(base plugin.Base) error {
|
|
info := base.Info()
|
|
fmt.Printf("%s[%s] made by %s\n", info.SlugName, info.Version, info.Author)
|
|
return nil
|
|
})
|
|
},
|
|
}
|
|
|
|
configCmd = &cobra.Command{
|
|
Use: "config",
|
|
Short: "Set some config to default value",
|
|
Long: `Set some config to default value`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
path.FormatAllPath(dataDirPath)
|
|
|
|
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
|
if err != nil {
|
|
fmt.Println("read config failed: ", err.Error())
|
|
return
|
|
}
|
|
|
|
field := &cli.ConfigField{}
|
|
fmt.Println(configFields)
|
|
if len(configFields) > 0 {
|
|
switch configFields[0] {
|
|
case "allow_password_login":
|
|
field.AllowPasswordLogin = true
|
|
case "deactivate_plugin":
|
|
if len(configFields) > 1 {
|
|
field.DeactivatePluginSlugName = configFields[1]
|
|
}
|
|
default:
|
|
fmt.Printf("field %s not support\n", configFields[0])
|
|
}
|
|
}
|
|
err = cli.SetDefaultConfig(c.Data.Database, c.Data.Cache, field)
|
|
if err != nil {
|
|
fmt.Println("set default config failed: ", err.Error())
|
|
} else {
|
|
fmt.Println("set default config successfully")
|
|
}
|
|
},
|
|
}
|
|
|
|
i18nCmd = &cobra.Command{
|
|
Use: "i18n",
|
|
Short: "Overwrite i18n files",
|
|
Long: `Merge i18n files from plugins to original i18n files. It will overwrite the original i18n files`,
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
if err := cli.ReplaceI18nFilesLocal(i18nTargetPath); err != nil {
|
|
fmt.Printf("replace i18n files failed %v\n", err)
|
|
} else {
|
|
fmt.Printf("replace i18n files successfully\n")
|
|
}
|
|
|
|
fmt.Printf("try to merge i18n files from %q to %q\n", i18nSourcePath, i18nTargetPath)
|
|
|
|
if err := cli.MergeI18nFilesLocal(i18nTargetPath, i18nSourcePath); err != nil {
|
|
fmt.Printf("merge i18n files failed %v\n", err)
|
|
} else {
|
|
fmt.Printf("merge i18n files successfully\n")
|
|
}
|
|
},
|
|
}
|
|
|
|
resetPasswordCmd = &cobra.Command{
|
|
Use: "passwd",
|
|
Aliases: []string{"password", "reset-password"},
|
|
Short: "Reset user password",
|
|
Long: "Reset user password by email address.",
|
|
Example: ` # Interactive mode (recommended, safest)
|
|
answer passwd -C ./answer-data
|
|
|
|
# Specify email only (will prompt for password securely)
|
|
answer passwd -C ./answer-data --email user@example.com
|
|
answer passwd -C ./answer-data -e user@example.com
|
|
|
|
# Specify email and password (NOT recommended, will be recorded in shell history)
|
|
answer passwd -C ./answer-data -e user@example.com -p newpassword123`,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
opts := &cli.ResetPasswordOptions{
|
|
Email: resetPasswordEmail,
|
|
Password: resetPasswordPassword,
|
|
}
|
|
if err := cli.ResetPassword(context.Background(), dataDirPath, opts); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
},
|
|
}
|
|
)
|
|
|
|
// Execute adds all child commands to the root command and sets flags appropriately.
|
|
// This is called by main(). It only needs to happen once to the rootCmd.
|
|
func Execute() {
|
|
err := rootCmd.Execute()
|
|
if err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|