Refactor path & config system (#25330) (#25416)

Backport #25330

# The problem

There were many "path tricks":

* By default, Gitea uses its program directory as its work path
* Gitea tries to use the "work path" to guess its "custom path" and
"custom conf (app.ini)"
* Users might want to use other directories as work path
* The non-default work path should be passed to Gitea by GITEA_WORK_DIR
or "--work-path"
* But some Gitea processes are started without these values
    * The "serv" process started by OpenSSH server
    * The CLI sub-commands started by site admin
* The paths are guessed by SetCustomPathAndConf again and again
* The default values of "work path / custom path / custom conf" can be
changed when compiling

# The solution

* Use `InitWorkPathAndCommonConfig` to handle these path tricks, and use
test code to cover its behaviors.
* When Gitea's web server runs, write the WORK_PATH to "app.ini", this
value must be the most correct one, because if this value is not right,
users would find that the web UI doesn't work and then they should be
able to fix it.
* Then all other sub-commands can use the WORK_PATH in app.ini to
initialize their paths.
* By the way, when Gitea starts for git protocol, it shouldn't output
any log, otherwise the git protocol gets broken and client blocks
forever.

The "work path" priority is: WORK_PATH in app.ini > cmd arg --work-path
> env var GITEA_WORK_DIR > builtin default

The "app.ini" searching order is: cmd arg --config > cmd arg "work path
/ custom path" > env var "work path / custom path" > builtin default


## ⚠️ BREAKING

If your instance's "work path / custom path / custom conf" doesn't meet
the requirements (eg: work path must be absolute), Gitea will report a
fatal error and exit. You need to set these values according to the
error log.
This commit is contained in:
wxiaoguang 2023-06-23 00:27:18 +08:00 committed by GitHub
parent 734fd93f59
commit 061b68e995
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 651 additions and 460 deletions

2
.gitignore vendored
View File

@ -53,8 +53,6 @@ cpu.out
/bin /bin
/dist /dist
/custom/* /custom/*
!/custom/conf
/custom/conf/*
!/custom/conf/app.example.ini !/custom/conf/app.example.ini
/data /data
/indexers /indexers

View File

@ -42,7 +42,7 @@ func runGenerateActionsRunnerToken(c *cli.Context) error {
ctx, cancel := installSignals() ctx, cancel := installSignals()
defer cancel() defer cancel()
setting.Init(&setting.Options{}) setting.MustInstalled()
scope := c.String("scope") scope := c.String("scope")

View File

@ -58,7 +58,7 @@ func confirm() (bool, error) {
} }
func initDB(ctx context.Context) error { func initDB(ctx context.Context) error {
setting.Init(&setting.Options{}) setting.MustInstalled()
setting.LoadDBSetting() setting.LoadDBSetting()
setting.InitSQLLoggersForCli(log.INFO) setting.InitSQLLoggersForCli(log.INFO)

View File

@ -91,7 +91,7 @@ func runRecreateTable(ctx *cli.Context) error {
golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info)) golog.SetOutput(log.LoggerToWriter(log.GetLogger(log.DEFAULT).Info))
debug := ctx.Bool("debug") debug := ctx.Bool("debug")
setting.Init(&setting.Options{}) setting.MustInstalled()
setting.LoadDBSetting() setting.LoadDBSetting()
if debug { if debug {

View File

@ -182,7 +182,7 @@ func runDump(ctx *cli.Context) error {
} }
fileName += "." + outType fileName += "." + outType
} }
setting.Init(&setting.Options{}) setting.MustInstalled()
// make sure we are logging to the console no matter what the configuration tells us do to // make sure we are logging to the console no matter what the configuration tells us do to
// FIXME: don't use CfgProvider directly // FIXME: don't use CfgProvider directly

View File

@ -99,11 +99,6 @@ type assetFile struct {
func initEmbeddedExtractor(c *cli.Context) error { func initEmbeddedExtractor(c *cli.Context) error {
setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr) setupConsoleLogger(log.ERROR, log.CanColorStderr, os.Stderr)
// Read configuration file
setting.Init(&setting.Options{
AllowEmpty: true,
})
patterns, err := compileCollectPatterns(c.Args()) patterns, err := compileCollectPatterns(c.Args())
if err != nil { if err != nil {
return err return err

View File

@ -16,7 +16,7 @@ func runSendMail(c *cli.Context) error {
ctx, cancel := installSignals() ctx, cancel := installSignals()
defer cancel() defer cancel()
setting.Init(&setting.Options{}) setting.MustInstalled()
if err := argsSet(c, "title"); err != nil { if err := argsSet(c, "title"); err != nil {
return err return err

View File

@ -51,7 +51,7 @@ func runRestoreRepository(c *cli.Context) error {
ctx, cancel := installSignals() ctx, cancel := installSignals()
defer cancel() defer cancel()
setting.Init(&setting.Options{}) setting.MustInstalled()
var units []string var units []string
if s := c.String("units"); s != "" { if s := c.String("units"); s != "" {
units = strings.Split(s, ",") units = strings.Split(s, ",")

View File

@ -61,7 +61,7 @@ func setup(ctx context.Context, debug bool) {
} else { } else {
setupConsoleLogger(log.FATAL, false, os.Stderr) setupConsoleLogger(log.FATAL, false, os.Stderr)
} }
setting.Init(&setting.Options{}) setting.MustInstalled()
if debug { if debug {
setting.RunMode = "dev" setting.RunMode = "dev"
} }

View File

@ -101,6 +101,110 @@ func createPIDFile(pidPath string) {
} }
} }
func serveInstall(ctx *cli.Context) error {
log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith)
log.Info("App path: %s", setting.AppPath)
log.Info("Work path: %s", setting.AppWorkPath)
log.Info("Custom path: %s", setting.CustomPath)
log.Info("Config file: %s", setting.CustomConf)
log.Info("Prepare to run install page")
routers.InitWebInstallPage(graceful.GetManager().HammerContext())
// Flag for port number in case first time run conflict
if ctx.IsSet("port") {
if err := setPort(ctx.String("port")); err != nil {
return err
}
}
if ctx.IsSet("install-port") {
if err := setPort(ctx.String("install-port")); err != nil {
return err
}
}
c := install.Routes()
err := listen(c, false)
if err != nil {
log.Critical("Unable to open listener for installer. Is Gitea already running?")
graceful.GetManager().DoGracefulShutdown()
}
select {
case <-graceful.GetManager().IsShutdown():
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
log.GetManager().Close()
return err
default:
}
return nil
}
func serveInstalled(ctx *cli.Context) error {
setting.InitCfgProvider(setting.CustomConf)
setting.LoadCommonSettings()
setting.MustInstalled()
log.Info("Gitea version: %s%s", setting.AppVer, setting.AppBuiltWith)
log.Info("App path: %s", setting.AppPath)
log.Info("Work path: %s", setting.AppWorkPath)
log.Info("Custom path: %s", setting.CustomPath)
log.Info("Config file: %s", setting.CustomConf)
log.Info("Run mode: %s", setting.RunMode)
log.Info("Prepare to run web server")
if setting.AppWorkPathMismatch {
log.Error("WORK_PATH from config %q doesn't match other paths from environment variables or command arguments. "+
"Only WORK_PATH in config should be set and used. Please remove the other outdated work paths from environment variables and command arguments", setting.CustomConf)
}
rootCfg := setting.CfgProvider
if rootCfg.Section("").Key("WORK_PATH").String() == "" {
saveCfg, err := rootCfg.PrepareSaving()
if err != nil {
log.Error("Unable to prepare saving WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err)
} else {
rootCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
saveCfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
if err = saveCfg.Save(); err != nil {
log.Error("Unable to update WORK_PATH=%s to config %q: %v\nYou must set it manually, otherwise there might be bugs when accessing the git repositories.", setting.AppWorkPath, setting.CustomConf, err)
}
}
}
routers.InitWebInstalled(graceful.GetManager().HammerContext())
// We check that AppDataPath exists here (it should have been created during installation)
// We can't check it in `InitWebInstalled`, because some integration tests
// use cmd -> InitWebInstalled, but the AppDataPath doesn't exist during those tests.
if _, err := os.Stat(setting.AppDataPath); err != nil {
log.Fatal("Can not find APP_DATA_PATH %q", setting.AppDataPath)
}
// Override the provided port number within the configuration
if ctx.IsSet("port") {
if err := setPort(ctx.String("port")); err != nil {
return err
}
}
// Set up Chi routes
c := routers.NormalRoutes(graceful.GetManager().HammerContext())
err := listen(c, true)
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
log.GetManager().Close()
return err
}
func servePprof() {
http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
log.Info("Starting pprof server on localhost:6060")
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
finished()
}
func runWeb(ctx *cli.Context) error { func runWeb(ctx *cli.Context) error {
if ctx.Bool("verbose") { if ctx.Bool("verbose") {
setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout) setupConsoleLogger(log.TRACE, log.CanColorStdout, os.Stdout)
@ -128,75 +232,19 @@ func runWeb(ctx *cli.Context) error {
createPIDFile(ctx.String("pid")) createPIDFile(ctx.String("pid"))
} }
// Perform pre-initialization if !setting.InstallLock {
needsInstall := install.PreloadSettings(graceful.GetManager().HammerContext()) if err := serveInstall(ctx); err != nil {
if needsInstall {
// Flag for port number in case first time run conflict
if ctx.IsSet("port") {
if err := setPort(ctx.String("port")); err != nil {
return err return err
} }
}
if ctx.IsSet("install-port") {
if err := setPort(ctx.String("install-port")); err != nil {
return err
}
}
c := install.Routes()
err := listen(c, false)
if err != nil {
log.Critical("Unable to open listener for installer. Is Gitea already running?")
graceful.GetManager().DoGracefulShutdown()
}
select {
case <-graceful.GetManager().IsShutdown():
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
log.GetManager().Close()
return err
default:
}
} else { } else {
NoInstallListener() NoInstallListener()
} }
if setting.EnablePprof { if setting.EnablePprof {
go func() { go servePprof()
http.DefaultServeMux.Handle("/debug/fgprof", fgprof.Handler())
_, _, finished := process.GetManager().AddTypedContext(context.Background(), "Web: PProf Server", process.SystemProcessType, true)
// The pprof server is for debug purpose only, it shouldn't be exposed on public network. At the moment it's not worth to introduce a configurable option for it.
log.Info("Starting pprof server on localhost:6060")
log.Info("Stopped pprof server: %v", http.ListenAndServe("localhost:6060", nil))
finished()
}()
} }
log.Info("Global init") return serveInstalled(ctx)
// Perform global initialization
setting.Init(&setting.Options{})
routers.GlobalInitInstalled(graceful.GetManager().HammerContext())
// We check that AppDataPath exists here (it should have been created during installation)
// We can't check it in `GlobalInitInstalled`, because some integration tests
// use cmd -> GlobalInitInstalled, but the AppDataPath doesn't exist during those tests.
if _, err := os.Stat(setting.AppDataPath); err != nil {
log.Fatal("Can not find APP_DATA_PATH '%s'", setting.AppDataPath)
}
// Override the provided port number within the configuration
if ctx.IsSet("port") {
if err := setPort(ctx.String("port")); err != nil {
return err
}
}
// Set up Chi routes
c := routers.NormalRoutes(graceful.GetManager().HammerContext())
err := listen(c, true)
<-graceful.GetManager().Done()
log.Info("PID: %d Gitea Web Finished", os.Getpid())
log.GetManager().Close()
return err
} }
func setPort(port string) error { func setPort(port string) error {

View File

@ -81,8 +81,6 @@ func main() {
}, },
} }
app.Action = runEnvironmentToIni app.Action = runEnvironmentToIni
setting.SetCustomPathAndConf("", "", "")
err := app.Run(os.Args) err := app.Run(os.Args)
if err != nil { if err != nil {
log.Fatal("Failed to run app with %s: %v", os.Args, err) log.Fatal("Failed to run app with %s: %v", os.Args, err)
@ -90,12 +88,13 @@ func main() {
} }
func runEnvironmentToIni(c *cli.Context) error { func runEnvironmentToIni(c *cli.Context) error {
providedCustom := c.String("custom-path") setting.InitWorkPathAndCommonConfig(os.Getenv, setting.ArgWorkPathAndCustomConf{
providedConf := c.String("config") WorkPath: c.String("work-path"),
providedWorkPath := c.String("work-path") CustomPath: c.String("custom-path"),
setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath) CustomConf: c.String("config"),
})
cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil { if err != nil {
log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err) log.Fatal("Failed to load custom conf '%s': %v", setting.CustomConf, err)
} }

171
main.go
View File

@ -33,30 +33,58 @@ var (
Tags = "" Tags = ""
// MakeVersion holds the current Make version if built with make // MakeVersion holds the current Make version if built with make
MakeVersion = "" MakeVersion = ""
originalAppHelpTemplate = ""
originalCommandHelpTemplate = ""
originalSubcommandHelpTemplate = ""
) )
func init() { func init() {
setting.AppVer = Version setting.AppVer = Version
setting.AppBuiltWith = formatBuiltWith() setting.AppBuiltWith = formatBuiltWith()
setting.AppStartTime = time.Now().UTC() setting.AppStartTime = time.Now().UTC()
}
// Grab the original help templates // cmdHelp is our own help subcommand with more information
originalAppHelpTemplate = cli.AppHelpTemplate // test cases:
originalCommandHelpTemplate = cli.CommandHelpTemplate // ./gitea help
originalSubcommandHelpTemplate = cli.SubcommandHelpTemplate // ./gitea -h
// ./gitea web help
// ./gitea web -h (due to cli lib limitation, this won't call our cmdHelp, so no extra info)
// ./gitea admin help auth
// ./gitea -c /tmp/app.ini -h
// ./gitea -c /tmp/app.ini help
// ./gitea help -c /tmp/app.ini
// GITEA_WORK_DIR=/tmp ./gitea help
// GITEA_WORK_DIR=/tmp ./gitea help --work-path /tmp/other
// GITEA_WORK_DIR=/tmp ./gitea help --config /tmp/app-other.ini
var cmdHelp = cli.Command{
Name: "help",
Aliases: []string{"h"},
Usage: "Shows a list of commands or help for one command",
ArgsUsage: "[command]",
Action: func(c *cli.Context) (err error) {
args := c.Args()
if args.Present() {
err = cli.ShowCommandHelp(c, args.First())
} else {
err = cli.ShowAppHelp(c)
}
_, _ = fmt.Fprintf(c.App.Writer, `
DEFAULT CONFIGURATION:
AppPath: %s
WorkPath: %s
CustomPath: %s
ConfigFile: %s
`, setting.AppPath, setting.AppWorkPath, setting.CustomPath, setting.CustomConf)
return err
},
} }
func main() { func main() {
app := cli.NewApp() app := cli.NewApp()
app.Name = "Gitea" app.Name = "Gitea"
app.Usage = "A painless self-hosted Git service" app.Usage = "A painless self-hosted Git service"
app.Description = `By default, gitea will start serving using the webserver with no app.Description = `By default, Gitea will start serving using the web-server with no argument, which can alternatively be run by running the subcommand "web".`
arguments - which can alternatively be run by running the subcommand web.`
app.Version = Version + formatBuiltWith() app.Version = Version + formatBuiltWith()
app.EnableBashCompletion = true
app.Commands = []cli.Command{ app.Commands = []cli.Command{
cmd.CmdWeb, cmd.CmdWeb,
cmd.CmdServ, cmd.CmdServ,
@ -77,118 +105,83 @@ arguments - which can alternatively be run by running the subcommand web.`
cmd.CmdRestoreRepository, cmd.CmdRestoreRepository,
cmd.CmdActions, cmd.CmdActions,
} }
// Now adjust these commands to add our global configuration options
// First calculate the default paths and set the AppHelpTemplates in this context
setting.SetCustomPathAndConf("", "", "")
setAppHelpTemplates()
// default configuration flags // default configuration flags
defaultFlags := []cli.Flag{ globalFlags := []cli.Flag{
cli.HelpFlag,
cli.StringFlag{ cli.StringFlag{
Name: "custom-path, C", Name: "custom-path, C",
Value: setting.CustomPath, Usage: "Set custom path (defaults to '{WorkPath}/custom')",
Usage: "Custom path file path",
}, },
cli.StringFlag{ cli.StringFlag{
Name: "config, c", Name: "config, c",
Value: setting.CustomConf, Value: setting.CustomConf,
Usage: "Custom configuration file path", Usage: "Set custom config file (defaults to '{WorkPath}/custom/conf/app.ini')",
}, },
cli.VersionFlag,
cli.StringFlag{ cli.StringFlag{
Name: "work-path, w", Name: "work-path, w",
Value: setting.AppWorkPath, Usage: "Set Gitea's working path (defaults to the Gitea's binary directory)",
Usage: "Set the gitea working path",
}, },
} }
// Set the default to be equivalent to cmdWeb and add the default flags // Set the default to be equivalent to cmdWeb and add the default flags
app.Flags = append(app.Flags, globalFlags...)
app.Flags = append(app.Flags, cmd.CmdWeb.Flags...) app.Flags = append(app.Flags, cmd.CmdWeb.Flags...)
app.Flags = append(app.Flags, defaultFlags...) app.Action = prepareWorkPathAndCustomConf(cmd.CmdWeb.Action)
app.Action = cmd.CmdWeb.Action app.HideHelp = true // use our own help action to show helps (with more information like default config)
app.Commands = append(app.Commands, cmdHelp)
// Add functions to set these paths and these flags to the commands
app.Before = establishCustomPath
for i := range app.Commands { for i := range app.Commands {
setFlagsAndBeforeOnSubcommands(&app.Commands[i], defaultFlags, establishCustomPath) prepareSubcommands(&app.Commands[i], globalFlags)
} }
app.EnableBashCompletion = true
err := app.Run(os.Args) err := app.Run(os.Args)
if err != nil { if err != nil {
log.Fatal("Failed to run app with %s: %v", os.Args, err) _, _ = fmt.Fprintf(app.Writer, "\nFailed to run with %s: %v\n", os.Args, err)
} }
log.GetManager().Close() log.GetManager().Close()
} }
func setFlagsAndBeforeOnSubcommands(command *cli.Command, defaultFlags []cli.Flag, before cli.BeforeFunc) { func prepareSubcommands(command *cli.Command, defaultFlags []cli.Flag) {
command.Flags = append(command.Flags, defaultFlags...) command.Flags = append(command.Flags, defaultFlags...)
command.Before = establishCustomPath command.Action = prepareWorkPathAndCustomConf(command.Action)
command.HideHelp = true
if command.Name != "help" {
command.Subcommands = append(command.Subcommands, cmdHelp)
}
for i := range command.Subcommands { for i := range command.Subcommands {
setFlagsAndBeforeOnSubcommands(&command.Subcommands[i], defaultFlags, before) prepareSubcommands(&command.Subcommands[i], defaultFlags)
} }
} }
func establishCustomPath(ctx *cli.Context) error { // prepareWorkPathAndCustomConf wraps the Action to prepare the work path and custom config
var providedCustom string // It can't use "Before", because each level's sub-command's Before will be called one by one, so the "init" would be done multiple times
var providedConf string func prepareWorkPathAndCustomConf(a any) func(ctx *cli.Context) error {
var providedWorkPath string if a == nil {
currentCtx := ctx
for {
if len(providedCustom) != 0 && len(providedConf) != 0 && len(providedWorkPath) != 0 {
break
}
if currentCtx == nil {
break
}
if currentCtx.IsSet("custom-path") && len(providedCustom) == 0 {
providedCustom = currentCtx.String("custom-path")
}
if currentCtx.IsSet("config") && len(providedConf) == 0 {
providedConf = currentCtx.String("config")
}
if currentCtx.IsSet("work-path") && len(providedWorkPath) == 0 {
providedWorkPath = currentCtx.String("work-path")
}
currentCtx = currentCtx.Parent()
}
setting.SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath)
setAppHelpTemplates()
if ctx.IsSet("version") {
cli.ShowVersion(ctx)
os.Exit(0)
}
return nil return nil
}
func setAppHelpTemplates() {
cli.AppHelpTemplate = adjustHelpTemplate(originalAppHelpTemplate)
cli.CommandHelpTemplate = adjustHelpTemplate(originalCommandHelpTemplate)
cli.SubcommandHelpTemplate = adjustHelpTemplate(originalSubcommandHelpTemplate)
}
func adjustHelpTemplate(originalTemplate string) string {
overridden := ""
if _, ok := os.LookupEnv("GITEA_CUSTOM"); ok {
overridden = "(GITEA_CUSTOM)"
} }
action := a.(func(*cli.Context) error)
return fmt.Sprintf(`%s return func(ctx *cli.Context) error {
DEFAULT CONFIGURATION: var args setting.ArgWorkPathAndCustomConf
CustomPath: %s %s curCtx := ctx
CustomConf: %s for curCtx != nil {
AppPath: %s if curCtx.IsSet("work-path") && args.WorkPath == "" {
AppWorkPath: %s args.WorkPath = curCtx.String("work-path")
}
`, originalTemplate, setting.CustomPath, overridden, setting.CustomConf, setting.AppPath, setting.AppWorkPath) if curCtx.IsSet("custom-path") && args.CustomPath == "" {
args.CustomPath = curCtx.String("custom-path")
}
if curCtx.IsSet("config") && args.CustomConf == "" {
args.CustomConf = curCtx.String("config")
}
curCtx = curCtx.Parent()
}
setting.InitWorkPathAndCommonConfig(os.Getenv, args)
if ctx.Bool("help") {
return cmdHelp.Action.(func(ctx *cli.Context) error)(ctx)
}
return action(ctx)
}
} }
func formatBuiltWith() string { func formatBuiltWith() string {

View File

@ -147,9 +147,9 @@ func MainTest(m *testing.M) {
os.Exit(1) os.Exit(1)
} }
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
setting.AppDataPath = tmpDataPath setting.AppDataPath = tmpDataPath
setting.SetCustomPathAndConf("", "", "")
unittest.InitSettings() unittest.InitSettings()
if err = git.InitFull(context.Background()); err != nil { if err = git.InitFull(context.Background()); err != nil {
fmt.Printf("Unable to InitFull: %v\n", err) fmt.Printf("Unable to InitFull: %v\n", err)

View File

@ -42,12 +42,14 @@ func fatalTestError(fmtStr string, args ...interface{}) {
os.Exit(1) os.Exit(1)
} }
// InitSettings initializes config provider and load common setttings for tests // InitSettings initializes config provider and load common settings for tests
func InitSettings(extraConfigs ...string) { func InitSettings(extraConfigs ...string) {
setting.Init(&setting.Options{ if setting.CustomConf == "" {
AllowEmpty: true, setting.CustomConf = filepath.Join(setting.CustomPath, "conf/app-unittest-tmp.ini")
ExtraConfig: strings.Join(extraConfigs, "\n"), _ = os.Remove(setting.CustomConf)
}) }
setting.InitCfgProvider(setting.CustomConf, strings.Join(extraConfigs, "\n"))
setting.LoadCommonSettings()
if err := setting.PrepareAppDataPath(); err != nil { if err := setting.PrepareAppDataPath(); err != nil {
log.Fatalf("Can not prepare APP_DATA_PATH: %v", err) log.Fatalf("Can not prepare APP_DATA_PATH: %v", err)
@ -69,7 +71,7 @@ type TestOptions struct {
// MainTest a reusable TestMain(..) function for unit tests that need to use a // MainTest a reusable TestMain(..) function for unit tests that need to use a
// test database. Creates the test database, and sets necessary settings. // test database. Creates the test database, and sets necessary settings.
func MainTest(m *testing.M, testOpts *TestOptions) { func MainTest(m *testing.M, testOpts *TestOptions) {
setting.SetCustomPathAndConf("", "", "") setting.CustomPath = filepath.Join(testOpts.GiteaRootPath, "custom")
InitSettings() InitSettings()
var err error var err error

View File

@ -28,7 +28,7 @@ type Check struct {
} }
func initDBSkipLogger(ctx context.Context) error { func initDBSkipLogger(ctx context.Context) error {
setting.Init(&setting.Options{}) setting.MustInstalled()
setting.LoadDBSetting() setting.LoadDBSetting()
if err := db.InitEngine(ctx); err != nil { if err := db.InitEngine(ctx); err != nil {
return fmt.Errorf("db.InitEngine: %w", err) return fmt.Errorf("db.InitEngine: %w", err)

View File

@ -66,7 +66,7 @@ func checkConfigurationFiles(ctx context.Context, logger log.Logger, autofix boo
return err return err
} }
setting.Init(&setting.Options{}) setting.MustInstalled()
configurationFiles := []configurationFile{ configurationFiles := []configurationFile{
{"Configuration File Path", setting.CustomConf, false, true, false}, {"Configuration File Path", setting.CustomConf, false, true, false},

View File

@ -10,6 +10,7 @@ import (
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/emoji" "code.gitea.io/gitea/modules/emoji"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
@ -28,9 +29,7 @@ var localMetas = map[string]string{
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
setting.Init(&setting.Options{ unittest.InitSettings()
AllowEmpty: true,
})
if err := git.InitSimple(context.Background()); err != nil { if err := git.InitSimple(context.Background()); err != nil {
log.Fatal("git init failed, err: %v", err) log.Fatal("git init failed, err: %v", err)
} }

View File

@ -9,6 +9,7 @@ import (
"strings" "strings"
"testing" "testing"
"code.gitea.io/gitea/models/unittest"
"code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/markup" "code.gitea.io/gitea/modules/markup"
@ -33,9 +34,7 @@ var localMetas = map[string]string{
} }
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
setting.Init(&setting.Options{ unittest.InitSettings()
AllowEmpty: true,
})
if err := git.InitSimple(context.Background()); err != nil { if err := git.InitSimple(context.Background()); err != nil {
log.Fatal("git init failed, err: %v", err) log.Fatal("git init failed, err: %v", err)
} }

View File

@ -55,15 +55,15 @@ type ConfigProvider interface {
DisableSaving() DisableSaving()
PrepareSaving() (ConfigProvider, error) PrepareSaving() (ConfigProvider, error)
IsLoadedFromEmpty() bool
} }
type iniConfigProvider struct { type iniConfigProvider struct {
opts *Options file string
ini *ini.File ini *ini.File
disableSaving bool disableSaving bool // disable the "Save" method because the config options could be polluted
loadedFromEmpty bool // whether the file has not existed previously
newFile bool // whether the file has not existed previously
} }
type iniConfigSection struct { type iniConfigSection struct {
@ -183,52 +183,42 @@ func NewConfigProviderFromData(configContent string) (ConfigProvider, error) {
cfg.NameMapper = ini.SnackCase cfg.NameMapper = ini.SnackCase
return &iniConfigProvider{ return &iniConfigProvider{
ini: cfg, ini: cfg,
newFile: true, loadedFromEmpty: true,
}, nil }, nil
} }
type Options struct {
CustomConf string // the ini file path
AllowEmpty bool // whether not finding configuration files is allowed
ExtraConfig string
DisableLoadCommonSettings bool // only used by "Init()", not used by "NewConfigProvider()"
}
// NewConfigProviderFromFile load configuration from file. // NewConfigProviderFromFile load configuration from file.
// NOTE: do not print any log except error. // NOTE: do not print any log except error.
func NewConfigProviderFromFile(opts *Options) (ConfigProvider, error) { func NewConfigProviderFromFile(file string, extraConfigs ...string) (ConfigProvider, error) {
cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "}) cfg := ini.Empty(ini.LoadOptions{KeyValueDelimiterOnWrite: " = "})
newFile := true loadedFromEmpty := true
if opts.CustomConf != "" { if file != "" {
isFile, err := util.IsFile(opts.CustomConf) isFile, err := util.IsFile(file)
if err != nil { if err != nil {
return nil, fmt.Errorf("unable to check if %s is a file. Error: %v", opts.CustomConf, err) return nil, fmt.Errorf("unable to check if %q is a file. Error: %v", file, err)
} }
if isFile { if isFile {
if err := cfg.Append(opts.CustomConf); err != nil { if err = cfg.Append(file); err != nil {
return nil, fmt.Errorf("failed to load custom conf '%s': %v", opts.CustomConf, err) return nil, fmt.Errorf("failed to load config file %q: %v", file, err)
} }
newFile = false loadedFromEmpty = false
} }
} }
if newFile && !opts.AllowEmpty { if len(extraConfigs) > 0 {
return nil, fmt.Errorf("unable to find configuration file: %q, please ensure you are running in the correct environment or set the correct configuration file with -c", CustomConf) for _, s := range extraConfigs {
} if err := cfg.Append([]byte(s)); err != nil {
if opts.ExtraConfig != "" {
if err := cfg.Append([]byte(opts.ExtraConfig)); err != nil {
return nil, fmt.Errorf("unable to append more config: %v", err) return nil, fmt.Errorf("unable to append more config: %v", err)
} }
} }
}
cfg.NameMapper = ini.SnackCase cfg.NameMapper = ini.SnackCase
return &iniConfigProvider{ return &iniConfigProvider{
opts: opts, file: file,
ini: cfg, ini: cfg,
newFile: newFile, loadedFromEmpty: loadedFromEmpty,
}, nil }, nil
} }
@ -266,20 +256,17 @@ func (p *iniConfigProvider) Save() error {
if p.disableSaving { if p.disableSaving {
return errDisableSaving return errDisableSaving
} }
filename := p.opts.CustomConf filename := p.file
if filename == "" { if filename == "" {
if !p.opts.AllowEmpty { return fmt.Errorf("config file path must not be empty")
return fmt.Errorf("custom config path must not be empty")
} }
return nil if p.loadedFromEmpty {
}
if p.newFile {
if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil { if err := os.MkdirAll(filepath.Dir(filename), os.ModePerm); err != nil {
return fmt.Errorf("failed to create '%s': %v", filename, err) return fmt.Errorf("failed to create %q: %v", filename, err)
} }
} }
if err := p.ini.SaveTo(filename); err != nil { if err := p.ini.SaveTo(filename); err != nil {
return fmt.Errorf("failed to save '%s': %v", filename, err) return fmt.Errorf("failed to save %q: %v", filename, err)
} }
// Change permissions to be more restrictive // Change permissions to be more restrictive
@ -313,11 +300,14 @@ func (p *iniConfigProvider) DisableSaving() {
// it makes the "Save" outputs a lot of garbage options // it makes the "Save" outputs a lot of garbage options
// After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped. // After the INI package gets refactored, no "MustXxx" pollution, this workaround can be dropped.
func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) { func (p *iniConfigProvider) PrepareSaving() (ConfigProvider, error) {
cfgFile := p.opts.CustomConf if p.file == "" {
if cfgFile == "" {
return nil, errors.New("no config file to save") return nil, errors.New("no config file to save")
} }
return NewConfigProviderFromFile(p.opts) return NewConfigProviderFromFile(p.file)
}
func (p *iniConfigProvider) IsLoadedFromEmpty() bool {
return p.loadedFromEmpty
} }
func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) { func mustMapSetting(rootCfg ConfigProvider, sectionName string, setting any) {
@ -357,7 +347,7 @@ func NewConfigProviderForLocale(source any, others ...any) (ConfigProvider, erro
iniFile.BlockMode = false iniFile.BlockMode = false
return &iniConfigProvider{ return &iniConfigProvider{
ini: iniFile, ini: iniFile,
newFile: true, loadedFromEmpty: true,
}, nil }, nil
} }

View File

@ -67,13 +67,14 @@ key = 123
} }
func TestNewConfigProviderFromFile(t *testing.T) { func TestNewConfigProviderFromFile(t *testing.T) {
_, err := NewConfigProviderFromFile(&Options{CustomConf: "no-such.ini", AllowEmpty: false}) cfg, err := NewConfigProviderFromFile("no-such.ini")
assert.ErrorContains(t, err, "unable to find configuration file") assert.NoError(t, err)
assert.True(t, cfg.IsLoadedFromEmpty())
// load non-existing file and save // load non-existing file and save
testFile := t.TempDir() + "/test.ini" testFile := t.TempDir() + "/test.ini"
testFile1 := t.TempDir() + "/test1.ini" testFile1 := t.TempDir() + "/test1.ini"
cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) cfg, err = NewConfigProviderFromFile(testFile)
assert.NoError(t, err) assert.NoError(t, err)
sec, _ := cfg.NewSection("foo") sec, _ := cfg.NewSection("foo")
@ -91,7 +92,7 @@ func TestNewConfigProviderFromFile(t *testing.T) {
assert.Equal(t, "[foo]\nk1 = a\nk2 = b\n", string(bs)) assert.Equal(t, "[foo]\nk1 = a\nk2 = b\n", string(bs))
// load existing file and save // load existing file and save
cfg, err = NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) cfg, err = NewConfigProviderFromFile(testFile)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "a", cfg.Section("foo").Key("k1").String()) assert.Equal(t, "a", cfg.Section("foo").Key("k1").String())
sec, _ = cfg.NewSection("bar") sec, _ = cfg.NewSection("bar")
@ -123,7 +124,7 @@ func TestNewConfigProviderForLocale(t *testing.T) {
func TestDisableSaving(t *testing.T) { func TestDisableSaving(t *testing.T) {
testFile := t.TempDir() + "/test.ini" testFile := t.TempDir() + "/test.ini"
_ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644) _ = os.WriteFile(testFile, []byte("k1=a\nk2=b"), 0o644)
cfg, err := NewConfigProviderFromFile(&Options{CustomConf: testFile, AllowEmpty: true}) cfg, err := NewConfigProviderFromFile(testFile)
assert.NoError(t, err) assert.NoError(t, err)
cfg.DisableSaving() cfg.DisableSaving()

191
modules/setting/path.go Normal file
View File

@ -0,0 +1,191 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
"code.gitea.io/gitea/modules/log"
)
var (
// AppPath represents the path to the gitea binary
AppPath string
// AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR.
// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
// It is used as the base path for several other paths.
AppWorkPath string
CustomPath string // Custom directory path. Env: GITEA_CUSTOM
CustomConf string
appWorkPathBuiltin string
customPathBuiltin string
customConfBuiltin string
AppWorkPathMismatch bool
)
func getAppPath() (string, error) {
var appPath string
var err error
if IsWindows && filepath.IsAbs(os.Args[0]) {
appPath = filepath.Clean(os.Args[0])
} else {
appPath, err = exec.LookPath(os.Args[0])
}
if err != nil {
if !errors.Is(err, exec.ErrDot) {
return "", err
}
appPath, err = filepath.Abs(os.Args[0])
}
if err != nil {
return "", err
}
appPath, err = filepath.Abs(appPath)
if err != nil {
return "", err
}
// Note: (legacy code) we don't use path.Dir here because it does not handle case which path starts with two "/" in Windows: "//psf/Home/..."
return strings.ReplaceAll(appPath, "\\", "/"), err
}
func init() {
var err error
if AppPath, err = getAppPath(); err != nil {
log.Fatal("Failed to get app path: %v", err)
}
if AppWorkPath == "" {
AppWorkPath = filepath.Dir(AppPath)
}
appWorkPathBuiltin = AppWorkPath
customPathBuiltin = CustomPath
customConfBuiltin = CustomConf
}
type ArgWorkPathAndCustomConf struct {
WorkPath string
CustomPath string
CustomConf string
}
type stringWithDefault struct {
Value string
IsSet bool
}
func (s *stringWithDefault) Set(v string) {
s.Value = v
s.IsSet = true
}
// InitWorkPathAndCommonConfig will set AppWorkPath, CustomPath and CustomConf, init default config provider by CustomConf and load common settings,
func InitWorkPathAndCommonConfig(getEnvFn func(name string) string, args ArgWorkPathAndCustomConf) {
tryAbsPath := func(paths ...string) string {
s := paths[len(paths)-1]
for i := len(paths) - 2; i >= 0; i-- {
if filepath.IsAbs(s) {
break
}
s = filepath.Join(paths[i], s)
}
return s
}
var err error
tmpWorkPath := stringWithDefault{Value: appWorkPathBuiltin}
if tmpWorkPath.Value == "" {
tmpWorkPath.Value = filepath.Dir(AppPath)
}
tmpCustomPath := stringWithDefault{Value: customPathBuiltin}
if tmpCustomPath.Value == "" {
tmpCustomPath.Value = "custom"
}
tmpCustomConf := stringWithDefault{Value: customConfBuiltin}
if tmpCustomConf.Value == "" {
tmpCustomConf.Value = "conf/app.ini"
}
readFromEnv := func() {
envWorkPath := getEnvFn("GITEA_WORK_DIR")
if envWorkPath != "" {
tmpWorkPath.Set(envWorkPath)
if !filepath.IsAbs(tmpWorkPath.Value) {
log.Fatal("GITEA_WORK_DIR (work path) must be absolute path")
}
}
envCustomPath := getEnvFn("GITEA_CUSTOM")
if envCustomPath != "" {
tmpCustomPath.Set(envCustomPath)
if !filepath.IsAbs(tmpCustomPath.Value) {
log.Fatal("GITEA_CUSTOM (custom path) must be absolute path")
}
}
}
readFromArgs := func() {
if args.WorkPath != "" {
tmpWorkPath.Set(args.WorkPath)
if !filepath.IsAbs(tmpWorkPath.Value) {
log.Fatal("--work-path must be absolute path")
}
}
if args.CustomPath != "" {
tmpCustomPath.Set(args.CustomPath) // if it is not abs, it will be based on work-path, it shouldn't happen
if !filepath.IsAbs(tmpCustomPath.Value) {
log.Error("--custom-path must be absolute path")
}
}
if args.CustomConf != "" {
tmpCustomConf.Set(args.CustomConf)
if !filepath.IsAbs(tmpCustomConf.Value) {
// the config path can be relative to the real current working path
if tmpCustomConf.Value, err = filepath.Abs(tmpCustomConf.Value); err != nil {
log.Fatal("Failed to get absolute path of config %q: %v", tmpCustomConf.Value, err)
}
}
}
}
readFromEnv()
readFromArgs()
if !tmpCustomConf.IsSet {
tmpCustomConf.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value, tmpCustomConf.Value))
}
// only read the config but do not load/init anything more, because the AppWorkPath and CustomPath are not ready
InitCfgProvider(tmpCustomConf.Value)
configWorkPath := ConfigSectionKeyString(CfgProvider.Section(""), "WORK_PATH")
if configWorkPath != "" {
if !filepath.IsAbs(configWorkPath) {
log.Fatal("WORK_PATH in %q must be absolute path", configWorkPath)
}
configWorkPath = filepath.Clean(configWorkPath)
if tmpWorkPath.Value != "" && (getEnvFn("GITEA_WORK_DIR") != "" || args.WorkPath != "") {
fi1, err1 := os.Stat(tmpWorkPath.Value)
fi2, err2 := os.Stat(configWorkPath)
if err1 != nil || err2 != nil || !os.SameFile(fi1, fi2) {
AppWorkPathMismatch = true
}
}
tmpWorkPath.Set(configWorkPath)
}
tmpCustomPath.Set(tryAbsPath(tmpWorkPath.Value, tmpCustomPath.Value))
AppWorkPath = tmpWorkPath.Value
CustomPath = tmpCustomPath.Value
CustomConf = tmpCustomConf.Value
LoadCommonSettings()
}

View File

@ -0,0 +1,151 @@
// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
type envVars map[string]string
func (e envVars) Getenv(key string) string {
return e[key]
}
func TestInitWorkPathAndCommonConfig(t *testing.T) {
testInit := func(defaultWorkPath, defaultCustomPath, defaultCustomConf string) {
AppWorkPathMismatch = false
AppWorkPath = defaultWorkPath
appWorkPathBuiltin = defaultWorkPath
CustomPath = defaultCustomPath
customPathBuiltin = defaultCustomPath
CustomConf = defaultCustomConf
customConfBuiltin = defaultCustomConf
}
fp := filepath.Join
tmpDir := t.TempDir()
dirFoo := fp(tmpDir, "foo")
dirBar := fp(tmpDir, "bar")
dirXxx := fp(tmpDir, "xxx")
dirYyy := fp(tmpDir, "yyy")
t.Run("Default", func(t *testing.T) {
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
assert.Equal(t, dirFoo, AppWorkPath)
assert.Equal(t, fp(dirFoo, "custom"), CustomPath)
assert.Equal(t, fp(dirFoo, "custom/conf/app.ini"), CustomConf)
})
t.Run("WorkDir(env)", func(t *testing.T) {
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{})
assert.Equal(t, dirBar, AppWorkPath)
assert.Equal(t, fp(dirBar, "custom"), CustomPath)
assert.Equal(t, fp(dirBar, "custom/conf/app.ini"), CustomConf)
})
t.Run("WorkDir(env,arg)", func(t *testing.T) {
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirXxx})
assert.Equal(t, dirXxx, AppWorkPath)
assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
assert.Equal(t, fp(dirXxx, "custom/conf/app.ini"), CustomConf)
})
t.Run("CustomPath(env)", func(t *testing.T) {
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{})
assert.Equal(t, dirFoo, AppWorkPath)
assert.Equal(t, fp(dirBar, "custom1"), CustomPath)
assert.Equal(t, fp(dirBar, "custom1/conf/app.ini"), CustomConf)
})
t.Run("CustomPath(env,arg)", func(t *testing.T) {
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": fp(dirBar, "custom1")}.Getenv, ArgWorkPathAndCustomConf{CustomPath: "custom2"})
assert.Equal(t, dirFoo, AppWorkPath)
assert.Equal(t, fp(dirFoo, "custom2"), CustomPath)
assert.Equal(t, fp(dirFoo, "custom2/conf/app.ini"), CustomConf)
})
t.Run("CustomConf", func(t *testing.T) {
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: "app1.ini"})
assert.Equal(t, dirFoo, AppWorkPath)
cwd, _ := os.Getwd()
assert.Equal(t, fp(cwd, "app1.ini"), CustomConf)
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: fp(dirBar, "app1.ini")})
assert.Equal(t, dirFoo, AppWorkPath)
assert.Equal(t, fp(dirBar, "app1.ini"), CustomConf)
})
t.Run("CustomConfOverrideWorkPath", func(t *testing.T) {
iniWorkPath := fp(tmpDir, "app-workpath.ini")
_ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644)
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
assert.Equal(t, dirXxx, AppWorkPath)
assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
assert.Equal(t, iniWorkPath, CustomConf)
assert.False(t, AppWorkPathMismatch)
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirBar}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
assert.Equal(t, dirXxx, AppWorkPath)
assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
assert.Equal(t, iniWorkPath, CustomConf)
assert.True(t, AppWorkPathMismatch)
testInit(dirFoo, "", "")
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{WorkPath: dirBar, CustomConf: iniWorkPath})
assert.Equal(t, dirXxx, AppWorkPath)
assert.Equal(t, fp(dirXxx, "custom"), CustomPath)
assert.Equal(t, iniWorkPath, CustomConf)
assert.True(t, AppWorkPathMismatch)
})
t.Run("Builtin", func(t *testing.T) {
testInit(dirFoo, dirBar, dirXxx)
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
assert.Equal(t, dirFoo, AppWorkPath)
assert.Equal(t, dirBar, CustomPath)
assert.Equal(t, dirXxx, CustomConf)
testInit(dirFoo, "custom1", "cfg.ini")
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{})
assert.Equal(t, dirFoo, AppWorkPath)
assert.Equal(t, fp(dirFoo, "custom1"), CustomPath)
assert.Equal(t, fp(dirFoo, "custom1/cfg.ini"), CustomConf)
testInit(dirFoo, "custom1", "cfg.ini")
InitWorkPathAndCommonConfig(envVars{"GITEA_WORK_DIR": dirYyy}.Getenv, ArgWorkPathAndCustomConf{})
assert.Equal(t, dirYyy, AppWorkPath)
assert.Equal(t, fp(dirYyy, "custom1"), CustomPath)
assert.Equal(t, fp(dirYyy, "custom1/cfg.ini"), CustomConf)
testInit(dirFoo, "custom1", "cfg.ini")
InitWorkPathAndCommonConfig(envVars{"GITEA_CUSTOM": dirYyy}.Getenv, ArgWorkPathAndCustomConf{})
assert.Equal(t, dirFoo, AppWorkPath)
assert.Equal(t, dirYyy, CustomPath)
assert.Equal(t, fp(dirYyy, "cfg.ini"), CustomConf)
iniWorkPath := fp(tmpDir, "app-workpath.ini")
_ = os.WriteFile(iniWorkPath, []byte("WORK_PATH="+dirXxx), 0o644)
testInit(dirFoo, "custom1", "cfg.ini")
InitWorkPathAndCommonConfig(envVars{}.Getenv, ArgWorkPathAndCustomConf{CustomConf: iniWorkPath})
assert.Equal(t, dirXxx, AppWorkPath)
assert.Equal(t, fp(dirXxx, "custom1"), CustomPath)
assert.Equal(t, iniWorkPath, CustomConf)
})
}

View File

@ -61,6 +61,7 @@ var (
AssetVersion string AssetVersion string
// Server settings // Server settings
Protocol Scheme Protocol Scheme
UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"` UseProxyProtocol bool // `ini:"USE_PROXY_PROTOCOL"`
ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"` ProxyProtocolTLSBridging bool //`ini:"PROXY_PROTOCOL_TLS_BRIDGING"`
@ -324,7 +325,6 @@ func loadServerFrom(rootCfg ConfigProvider) {
StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour) StaticCacheTime = sec.Key("STATIC_CACHE_TIME").MustDuration(6 * time.Hour)
AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data")) AppDataPath = sec.Key("APP_DATA_PATH").MustString(path.Join(AppWorkPath, "data"))
if !filepath.IsAbs(AppDataPath) { if !filepath.IsAbs(AppDataPath) {
log.Info("The provided APP_DATA_PATH: %s is not absolute - it will be made absolute against the work path: %s", AppDataPath, AppWorkPath)
AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath)) AppDataPath = filepath.ToSlash(filepath.Join(AppWorkPath, AppDataPath))
} }

View File

@ -5,12 +5,8 @@
package setting package setting
import ( import (
"errors"
"fmt" "fmt"
"os" "os"
"os/exec"
"path"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"time" "time"
@ -28,19 +24,9 @@ var (
// AppStartTime store time gitea has started // AppStartTime store time gitea has started
AppStartTime time.Time AppStartTime time.Time
// AppPath represents the path to the gitea binary
AppPath string
// AppWorkPath is the "working directory" of Gitea. It maps to the environment variable GITEA_WORK_DIR.
// If that is not set it is the default set here by the linker or failing that the directory of AppPath.
//
// AppWorkPath is used as the base path for several other paths.
AppWorkPath string
// Other global setting objects // Other global setting objects
CfgProvider ConfigProvider CfgProvider ConfigProvider
CustomPath string // Custom directory path
CustomConf string
RunMode string RunMode string
RunUser string RunUser string
IsProd bool IsProd bool
@ -51,62 +37,6 @@ var (
IsInTesting = false IsInTesting = false
) )
func getAppPath() (string, error) {
var appPath string
var err error
if IsWindows && filepath.IsAbs(os.Args[0]) {
appPath = filepath.Clean(os.Args[0])
} else {
appPath, err = exec.LookPath(os.Args[0])
}
if err != nil {
if !errors.Is(err, exec.ErrDot) {
return "", err
}
appPath, err = filepath.Abs(os.Args[0])
}
if err != nil {
return "", err
}
appPath, err = filepath.Abs(appPath)
if err != nil {
return "", err
}
// Note: we don't use path.Dir here because it does not handle case
// which path starts with two "/" in Windows: "//psf/Home/..."
return strings.ReplaceAll(appPath, "\\", "/"), err
}
func getWorkPath(appPath string) string {
workPath := AppWorkPath
if giteaWorkPath, ok := os.LookupEnv("GITEA_WORK_DIR"); ok {
workPath = giteaWorkPath
}
if len(workPath) == 0 {
i := strings.LastIndex(appPath, "/")
if i == -1 {
workPath = appPath
} else {
workPath = appPath[:i]
}
}
workPath = strings.ReplaceAll(workPath, "\\", "/")
if !filepath.IsAbs(workPath) {
log.Info("Provided work path %s is not absolute - will be made absolute against the current working directory", workPath)
absPath, err := filepath.Abs(workPath)
if err != nil {
log.Error("Unable to absolute %s against the current working directory %v. Will absolute against the AppPath %s", workPath, err, appPath)
workPath = filepath.Join(appPath, workPath)
} else {
workPath = absPath
}
}
return strings.ReplaceAll(workPath, "\\", "/")
}
func init() { func init() {
IsWindows = runtime.GOOS == "windows" IsWindows = runtime.GOOS == "windows"
if AppVer == "" { if AppVer == "" {
@ -116,12 +46,6 @@ func init() {
// We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically // We can rely on log.CanColorStdout being set properly because modules/log/console_windows.go comes before modules/setting/setting.go lexicographically
// By default set this logger at Info - we'll change it later, but we need to start with something. // By default set this logger at Info - we'll change it later, but we need to start with something.
log.SetConsoleLogger(log.DEFAULT, "console", log.INFO) log.SetConsoleLogger(log.DEFAULT, "console", log.INFO)
var err error
if AppPath, err = getAppPath(); err != nil {
log.Fatal("Failed to get app path: %v", err)
}
AppWorkPath = getWorkPath(AppPath)
} }
// IsRunUserMatchCurrentUser returns false if configured run user does not match // IsRunUserMatchCurrentUser returns false if configured run user does not match
@ -137,36 +61,6 @@ func IsRunUserMatchCurrentUser(runUser string) (string, bool) {
return currentUser, runUser == currentUser return currentUser, runUser == currentUser
} }
// SetCustomPathAndConf will set CustomPath and CustomConf with reference to the
// GITEA_CUSTOM environment variable and with provided overrides before stepping
// back to the default
func SetCustomPathAndConf(providedCustom, providedConf, providedWorkPath string) {
if len(providedWorkPath) != 0 {
AppWorkPath = filepath.ToSlash(providedWorkPath)
}
if giteaCustom, ok := os.LookupEnv("GITEA_CUSTOM"); ok {
CustomPath = giteaCustom
}
if len(providedCustom) != 0 {
CustomPath = providedCustom
}
if len(CustomPath) == 0 {
CustomPath = path.Join(AppWorkPath, "custom")
} else if !filepath.IsAbs(CustomPath) {
CustomPath = path.Join(AppWorkPath, CustomPath)
}
if len(providedConf) != 0 {
CustomConf = providedConf
}
if len(CustomConf) == 0 {
CustomConf = path.Join(CustomPath, "conf/app.ini")
} else if !filepath.IsAbs(CustomConf) {
CustomConf = path.Join(CustomPath, CustomConf)
log.Warn("Using 'custom' directory as relative origin for configuration file: '%s'", CustomConf)
}
}
// PrepareAppDataPath creates app data directory if necessary // PrepareAppDataPath creates app data directory if necessary
func PrepareAppDataPath() error { func PrepareAppDataPath() error {
// FIXME: There are too many calls to MkdirAll in old code. It is incorrect. // FIXME: There are too many calls to MkdirAll in old code. It is incorrect.
@ -196,20 +90,23 @@ func PrepareAppDataPath() error {
return nil return nil
} }
func Init(opts *Options) { func InitCfgProvider(file string, extraConfigs ...string) {
if opts.CustomConf == "" {
opts.CustomConf = CustomConf
}
var err error var err error
CfgProvider, err = NewConfigProviderFromFile(opts) if CfgProvider, err = NewConfigProviderFromFile(file, extraConfigs...); err != nil {
log.Fatal("Unable to init config provider from %q: %v", file, err)
}
CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls CfgProvider.DisableSaving() // do not allow saving the CfgProvider into file, it will be polluted by the "MustXxx" calls
if err != nil { }
log.Fatal("newConfigProviderFromFile[%v]: %v", opts, err)
func MustInstalled() {
if !InstallLock {
log.Fatal(`Unable to load config file for a installed Gitea instance, you should either use "--config" to set your config file (app.ini), or run "gitea web" command to install Gitea.`)
} }
if !opts.DisableLoadCommonSettings { }
func LoadCommonSettings() {
if err := loadCommonSettingsFrom(CfgProvider); err != nil { if err := loadCommonSettingsFrom(CfgProvider); err != nil {
log.Fatal("loadCommonSettingsFrom[%v]: %v", opts, err) log.Fatal("Unable to load settings from config: %v", err)
}
} }
} }

View File

@ -90,10 +90,11 @@ func (w *testLoggerWriterCloser) Reset() {
// PrintCurrentTest prints the current test to os.Stdout // PrintCurrentTest prints the current test to os.Stdout
func PrintCurrentTest(t testing.TB, skip ...int) func() { func PrintCurrentTest(t testing.TB, skip ...int) func() {
t.Helper()
start := time.Now() start := time.Now()
actualSkip := 1 actualSkip := 1
if len(skip) > 0 { if len(skip) > 0 {
actualSkip = skip[0] actualSkip = skip[0] + 1
} }
_, filename, line, _ := runtime.Caller(actualSkip) _, filename, line, _ := runtime.Caller(actualSkip)

View File

@ -28,7 +28,6 @@ import (
"code.gitea.io/gitea/modules/system" "code.gitea.io/gitea/modules/system"
"code.gitea.io/gitea/modules/templates" "code.gitea.io/gitea/modules/templates"
"code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
actions_router "code.gitea.io/gitea/routers/api/actions" actions_router "code.gitea.io/gitea/routers/api/actions"
packages_router "code.gitea.io/gitea/routers/api/packages" packages_router "code.gitea.io/gitea/routers/api/packages"
@ -101,21 +100,16 @@ func syncAppConfForGit(ctx context.Context) error {
return nil return nil
} }
// GlobalInitInstalled is for global installed configuration. func InitWebInstallPage(ctx context.Context) {
func GlobalInitInstalled(ctx context.Context) { translation.InitLocales(ctx)
if !setting.InstallLock { setting.LoadSettingsForInstall()
log.Fatal("Gitea is not installed") mustInit(svg.Init)
} }
// InitWebInstalled is for global installed configuration.
func InitWebInstalled(ctx context.Context) {
mustInitCtx(ctx, git.InitFull) mustInitCtx(ctx, git.InitFull)
log.Info("Gitea Version: %s%s", setting.AppVer, setting.AppBuiltWith) log.Info("Git version: %s (home: %s)", git.VersionInfo(), git.HomeDir())
log.Info("Git Version: %s (home: %s)", git.VersionInfo(), git.HomeDir())
log.Info("AppPath: %s", setting.AppPath)
log.Info("AppWorkPath: %s", setting.AppWorkPath)
log.Info("Custom path: %s", setting.CustomPath)
log.Info("Log path: %s", setting.Log.RootPath)
log.Info("Configuration file: %s", setting.CustomConf)
log.Info("Run Mode: %s", util.ToTitleCase(setting.RunMode))
// Setup i18n // Setup i18n
translation.InitLocales(ctx) translation.InitLocales(ctx)

View File

@ -32,6 +32,7 @@ import (
"code.gitea.io/gitea/modules/util" "code.gitea.io/gitea/modules/util"
"code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/modules/web"
"code.gitea.io/gitea/modules/web/middleware" "code.gitea.io/gitea/modules/web/middleware"
"code.gitea.io/gitea/routers/common"
"code.gitea.io/gitea/services/forms" "code.gitea.io/gitea/services/forms"
"gitea.com/go-chi/session" "gitea.com/go-chi/session"
@ -368,11 +369,16 @@ func SubmitInstall(ctx *context.Context) {
} }
// Save settings. // Save settings.
cfg, err := setting.NewConfigProviderFromFile(&setting.Options{CustomConf: setting.CustomConf, AllowEmpty: true}) cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
if err != nil { if err != nil {
log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err) log.Error("Failed to load custom conf '%s': %v", setting.CustomConf, err)
} }
cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
cfg.Section("").Key("WORK_PATH").SetValue(setting.AppWorkPath)
cfg.Section("").Key("RUN_MODE").SetValue("prod")
cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String()) cfg.Section("database").Key("DB_TYPE").SetValue(setting.Database.Type.String())
cfg.Section("database").Key("HOST").SetValue(setting.Database.Host) cfg.Section("database").Key("HOST").SetValue(setting.Database.Host)
cfg.Section("database").Key("NAME").SetValue(setting.Database.Name) cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)
@ -383,9 +389,7 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("database").Key("PATH").SetValue(setting.Database.Path) cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful
cfg.Section("").Key("APP_NAME").SetValue(form.AppName)
cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath) cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
cfg.Section("").Key("RUN_USER").SetValue(form.RunUser)
cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain) cfg.Section("server").Key("SSH_DOMAIN").SetValue(form.Domain)
cfg.Section("server").Key("DOMAIN").SetValue(form.Domain) cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort) cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
@ -447,8 +451,6 @@ func SubmitInstall(ctx *context.Context) {
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress)) cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(fmt.Sprint(form.NoReplyAddress))
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(fmt.Sprint(form.EnableUpdateChecker)) cfg.Section("cron.update_checker").Key("ENABLED").SetValue(fmt.Sprint(form.EnableUpdateChecker))
cfg.Section("").Key("RUN_MODE").SetValue("prod")
cfg.Section("session").Key("PROVIDER").SetValue("file") cfg.Section("session").Key("PROVIDER").SetValue("file")
cfg.Section("log").Key("MODE").MustString("console") cfg.Section("log").Key("MODE").MustString("console")
@ -511,7 +513,13 @@ func SubmitInstall(ctx *context.Context) {
// ---- All checks are passed // ---- All checks are passed
// Reload settings (and re-initialize database connection) // Reload settings (and re-initialize database connection)
reloadSettings(ctx) setting.InitCfgProvider(setting.CustomConf)
setting.LoadCommonSettings()
setting.MustInstalled()
setting.LoadDBSetting()
if err := common.InitDBEngine(ctx); err != nil {
log.Fatal("ORM engine initialization failed: %v", err)
}
// Create admin account // Create admin account
if len(form.AdminName) > 0 { if len(form.AdminName) > 0 {

View File

@ -1,51 +0,0 @@
// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package install
import (
"context"
"code.gitea.io/gitea/modules/log"
"code.gitea.io/gitea/modules/setting"
"code.gitea.io/gitea/modules/svg"
"code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/routers/common"
)
// PreloadSettings preloads the configuration to check if we need to run install
func PreloadSettings(ctx context.Context) bool {
setting.Init(&setting.Options{
AllowEmpty: true,
})
if !setting.InstallLock {
log.Info("AppPath: %s", setting.AppPath)
log.Info("AppWorkPath: %s", setting.AppWorkPath)
log.Info("Custom path: %s", setting.CustomPath)
log.Info("Log path: %s", setting.Log.RootPath)
log.Info("Configuration file: %s", setting.CustomConf)
log.Info("Prepare to run install page")
translation.InitLocales(ctx)
if setting.EnableSQLite3 {
log.Info("SQLite3 is supported")
}
setting.LoadSettingsForInstall()
_ = svg.Init()
}
return !setting.InstallLock
}
// reloadSettings reloads the existing settings and starts up the database
func reloadSettings(ctx context.Context) {
setting.Init(&setting.Options{})
setting.LoadDBSetting()
if setting.InstallLock {
if err := common.InitDBEngine(ctx); err == nil {
log.Info("ORM engine initialization successful!")
} else {
log.Fatal("ORM engine initialization failed: %v", err)
}
}
}

View File

@ -8,7 +8,6 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"strings" "strings"
@ -167,20 +166,6 @@ func Config(ctx *context.Context) {
ctx.Data["SessionConfig"] = sessionCfg ctx.Data["SessionConfig"] = sessionCfg
ctx.Data["Git"] = setting.Git ctx.Data["Git"] = setting.Git
type envVar struct {
Name, Value string
}
envVars := map[string]*envVar{}
if len(os.Getenv("GITEA_WORK_DIR")) > 0 {
envVars["GITEA_WORK_DIR"] = &envVar{"GITEA_WORK_DIR", os.Getenv("GITEA_WORK_DIR")}
}
if len(os.Getenv("GITEA_CUSTOM")) > 0 {
envVars["GITEA_CUSTOM"] = &envVar{"GITEA_CUSTOM", os.Getenv("GITEA_CUSTOM")}
}
ctx.Data["EnvVars"] = envVars
ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate ctx.Data["AccessLogTemplate"] = setting.Log.AccessLogTemplate
ctx.Data["LogSQL"] = setting.Database.LogSQL ctx.Data["LogSQL"] = setting.Database.LogSQL

View File

@ -46,15 +46,6 @@
<dd>{{.ScriptType}}</dd> <dd>{{.ScriptType}}</dd>
<dt>{{.locale.Tr "admin.config.reverse_auth_user"}}</dt> <dt>{{.locale.Tr "admin.config.reverse_auth_user"}}</dt>
<dd>{{.ReverseProxyAuthUser}}</dd> <dd>{{.ReverseProxyAuthUser}}</dd>
{{if .EnvVars}}
<div class="ui divider"></div>
{{range .EnvVars}}
<dt>{{.Name}}</dt>
<dd>{{.Value}}</dd>
{{end}}
{{end}}
</dl> </dl>
</div> </div>

View File

@ -42,7 +42,10 @@ func InitTest(requireGitea bool) {
if giteaRoot == "" { if giteaRoot == "" {
exitf("Environment variable $GITEA_ROOT not set") exitf("Environment variable $GITEA_ROOT not set")
} }
setting.IsInTesting = true
setting.AppWorkPath = giteaRoot setting.AppWorkPath = giteaRoot
setting.CustomPath = filepath.Join(setting.AppWorkPath, "custom")
if requireGitea { if requireGitea {
giteaBinary := "gitea" giteaBinary := "gitea"
if setting.IsWindows { if setting.IsWindows {
@ -53,7 +56,6 @@ func InitTest(requireGitea bool) {
exitf("Could not find gitea binary at %s", setting.AppPath) exitf("Could not find gitea binary at %s", setting.AppPath)
} }
} }
giteaConf := os.Getenv("GITEA_CONF") giteaConf := os.Getenv("GITEA_CONF")
if giteaConf == "" { if giteaConf == "" {
// By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger. // By default, use sqlite.ini for testing, then IDE like GoLand can start the test process with debugger.
@ -66,16 +68,12 @@ func InitTest(requireGitea bool) {
exitf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify`) exitf(`sqlite3 requires: import _ "github.com/mattn/go-sqlite3" or -tags sqlite,sqlite_unlock_notify`)
} }
} }
setting.IsInTesting = true
if !path.IsAbs(giteaConf) { if !path.IsAbs(giteaConf) {
setting.CustomConf = path.Join(giteaRoot, giteaConf) setting.CustomConf = filepath.Join(giteaRoot, giteaConf)
} else { } else {
setting.CustomConf = giteaConf setting.CustomConf = giteaConf
} }
setting.SetCustomPathAndConf("", "", "")
unittest.InitSettings() unittest.InitSettings()
setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master" setting.Repository.DefaultBranch = "master" // many test code still assume that default branch is called "master"
_ = util.RemoveAll(repo_module.LocalCopyPath()) _ = util.RemoveAll(repo_module.LocalCopyPath())
@ -175,7 +173,7 @@ func InitTest(requireGitea bool) {
defer db.Close() defer db.Close()
} }
routers.GlobalInitInstalled(graceful.GetManager().HammerContext()) routers.InitWebInstalled(graceful.GetManager().HammerContext())
} }
func PrepareTestEnv(t testing.TB, skip ...int) func() { func PrepareTestEnv(t testing.TB, skip ...int) func() {
@ -240,10 +238,12 @@ func PrepareTestEnv(t testing.TB, skip ...int) func() {
} }
func PrintCurrentTest(t testing.TB, skip ...int) func() { func PrintCurrentTest(t testing.TB, skip ...int) func() {
if len(skip) == 1 { t.Helper()
skip = []int{skip[0] + 1} actualSkip := 1
if len(skip) > 0 {
actualSkip = skip[0] + 1
} }
return testlogger.PrintCurrentTest(t, skip...) return testlogger.PrintCurrentTest(t, actualSkip)
} }
// Printf takes a format and args and prints the string to os.Stdout // Printf takes a format and args and prints the string to os.Stdout