* Move install pages out of main macaron loop Signed-off-by: Andrew Thornton <art27@cantab.net> * Update templates/post-install.tmpl Co-authored-by: Lauris BH <lauris@nix.lv> * remove prefetch Signed-off-by: Andrew Thornton <art27@cantab.net>tags/v1.15.0-dev
@@ -19,6 +19,8 @@ import ( | |||||
"code.gitea.io/gitea/routers" | "code.gitea.io/gitea/routers" | ||||
"code.gitea.io/gitea/routers/routes" | "code.gitea.io/gitea/routers/routes" | ||||
"gitea.com/macaron/macaron" | |||||
context2 "github.com/gorilla/context" | context2 "github.com/gorilla/context" | ||||
"github.com/unknwon/com" | "github.com/unknwon/com" | ||||
"github.com/urfave/cli" | "github.com/urfave/cli" | ||||
@@ -114,6 +116,39 @@ func runWeb(ctx *cli.Context) error { | |||||
setting.WritePIDFile = true | setting.WritePIDFile = true | ||||
} | } | ||||
// Flag for port number in case first time run conflict. | |||||
if ctx.IsSet("port") { | |||||
if err := setPort(ctx.String("port")); err != nil { | |||||
return err | |||||
} | |||||
} | |||||
// Perform pre-initialization | |||||
needsInstall := routers.PreInstallInit(graceful.GetManager().HammerContext()) | |||||
if needsInstall { | |||||
m := routes.NewMacaron() | |||||
routes.RegisterInstallRoute(m) | |||||
err := listen(m, false) | |||||
select { | |||||
case <-graceful.GetManager().IsShutdown(): | |||||
<-graceful.GetManager().Done() | |||||
log.Info("PID: %d Gitea Web Finished", os.Getpid()) | |||||
log.Close() | |||||
return err | |||||
default: | |||||
} | |||||
} else { | |||||
NoInstallListener() | |||||
} | |||||
if setting.EnablePprof { | |||||
go func() { | |||||
log.Info("Starting pprof server on localhost:6060") | |||||
log.Info("%v", http.ListenAndServe("localhost:6060", nil)) | |||||
}() | |||||
} | |||||
log.Info("Global init") | |||||
// Perform global initialization | // Perform global initialization | ||||
routers.GlobalInit(graceful.GetManager().HammerContext()) | routers.GlobalInit(graceful.GetManager().HammerContext()) | ||||
@@ -121,41 +156,49 @@ func runWeb(ctx *cli.Context) error { | |||||
m := routes.NewMacaron() | m := routes.NewMacaron() | ||||
routes.RegisterRoutes(m) | routes.RegisterRoutes(m) | ||||
// Flag for port number in case first time run conflict. | |||||
if ctx.IsSet("port") { | |||||
setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, ctx.String("port"), 1) | |||||
setting.HTTPPort = ctx.String("port") | |||||
err := listen(m, true) | |||||
<-graceful.GetManager().Done() | |||||
log.Info("PID: %d Gitea Web Finished", os.Getpid()) | |||||
log.Close() | |||||
return err | |||||
} | |||||
switch setting.Protocol { | |||||
case setting.UnixSocket: | |||||
case setting.FCGI: | |||||
case setting.FCGIUnix: | |||||
default: | |||||
// Save LOCAL_ROOT_URL if port changed | |||||
cfg := ini.Empty() | |||||
if com.IsFile(setting.CustomConf) { | |||||
// Keeps custom settings if there is already something. | |||||
if err := cfg.Append(setting.CustomConf); err != nil { | |||||
return fmt.Errorf("Failed to load custom conf '%s': %v", setting.CustomConf, err) | |||||
} | |||||
} | |||||
func setPort(port string) error { | |||||
setting.AppURL = strings.Replace(setting.AppURL, setting.HTTPPort, port, 1) | |||||
setting.HTTPPort = port | |||||
defaultLocalURL := string(setting.Protocol) + "://" | |||||
if setting.HTTPAddr == "0.0.0.0" { | |||||
defaultLocalURL += "localhost" | |||||
} else { | |||||
defaultLocalURL += setting.HTTPAddr | |||||
switch setting.Protocol { | |||||
case setting.UnixSocket: | |||||
case setting.FCGI: | |||||
case setting.FCGIUnix: | |||||
default: | |||||
// Save LOCAL_ROOT_URL if port changed | |||||
cfg := ini.Empty() | |||||
if com.IsFile(setting.CustomConf) { | |||||
// Keeps custom settings if there is already something. | |||||
if err := cfg.Append(setting.CustomConf); err != nil { | |||||
return fmt.Errorf("Failed to load custom conf '%s': %v", setting.CustomConf, err) | |||||
} | } | ||||
defaultLocalURL += ":" + setting.HTTPPort + "/" | |||||
} | |||||
defaultLocalURL := string(setting.Protocol) + "://" | |||||
if setting.HTTPAddr == "0.0.0.0" { | |||||
defaultLocalURL += "localhost" | |||||
} else { | |||||
defaultLocalURL += setting.HTTPAddr | |||||
} | |||||
defaultLocalURL += ":" + setting.HTTPPort + "/" | |||||
cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) | |||||
cfg.Section("server").Key("LOCAL_ROOT_URL").SetValue(defaultLocalURL) | |||||
if err := cfg.SaveTo(setting.CustomConf); err != nil { | |||||
return fmt.Errorf("Error saving generated JWT Secret to custom config: %v", err) | |||||
} | |||||
if err := cfg.SaveTo(setting.CustomConf); err != nil { | |||||
return fmt.Errorf("Error saving generated JWT Secret to custom config: %v", err) | |||||
} | } | ||||
} | } | ||||
return nil | |||||
} | |||||
func listen(m *macaron.Macaron, handleRedirector bool) error { | |||||
listenAddr := setting.HTTPAddr | listenAddr := setting.HTTPAddr | ||||
if setting.Protocol != setting.UnixSocket && setting.Protocol != setting.FCGIUnix { | if setting.Protocol != setting.UnixSocket && setting.Protocol != setting.FCGIUnix { | ||||
listenAddr = net.JoinHostPort(listenAddr, setting.HTTPPort) | listenAddr = net.JoinHostPort(listenAddr, setting.HTTPPort) | ||||
@@ -166,37 +209,40 @@ func runWeb(ctx *cli.Context) error { | |||||
log.Info("LFS server enabled") | log.Info("LFS server enabled") | ||||
} | } | ||||
if setting.EnablePprof { | |||||
go func() { | |||||
log.Info("Starting pprof server on localhost:6060") | |||||
log.Info("%v", http.ListenAndServe("localhost:6060", nil)) | |||||
}() | |||||
} | |||||
var err error | var err error | ||||
switch setting.Protocol { | switch setting.Protocol { | ||||
case setting.HTTP: | case setting.HTTP: | ||||
NoHTTPRedirector() | |||||
if handleRedirector { | |||||
NoHTTPRedirector() | |||||
} | |||||
err = runHTTP("tcp", listenAddr, context2.ClearHandler(m)) | err = runHTTP("tcp", listenAddr, context2.ClearHandler(m)) | ||||
case setting.HTTPS: | case setting.HTTPS: | ||||
if setting.EnableLetsEncrypt { | if setting.EnableLetsEncrypt { | ||||
err = runLetsEncrypt(listenAddr, setting.Domain, setting.LetsEncryptDirectory, setting.LetsEncryptEmail, context2.ClearHandler(m)) | err = runLetsEncrypt(listenAddr, setting.Domain, setting.LetsEncryptDirectory, setting.LetsEncryptEmail, context2.ClearHandler(m)) | ||||
break | break | ||||
} | } | ||||
if setting.RedirectOtherPort { | |||||
go runHTTPRedirector() | |||||
} else { | |||||
NoHTTPRedirector() | |||||
if handleRedirector { | |||||
if setting.RedirectOtherPort { | |||||
go runHTTPRedirector() | |||||
} else { | |||||
NoHTTPRedirector() | |||||
} | |||||
} | } | ||||
err = runHTTPS("tcp", listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m)) | err = runHTTPS("tcp", listenAddr, setting.CertFile, setting.KeyFile, context2.ClearHandler(m)) | ||||
case setting.FCGI: | case setting.FCGI: | ||||
NoHTTPRedirector() | |||||
if handleRedirector { | |||||
NoHTTPRedirector() | |||||
} | |||||
err = runFCGI("tcp", listenAddr, context2.ClearHandler(m)) | err = runFCGI("tcp", listenAddr, context2.ClearHandler(m)) | ||||
case setting.UnixSocket: | case setting.UnixSocket: | ||||
NoHTTPRedirector() | |||||
if handleRedirector { | |||||
NoHTTPRedirector() | |||||
} | |||||
err = runHTTP("unix", listenAddr, context2.ClearHandler(m)) | err = runHTTP("unix", listenAddr, context2.ClearHandler(m)) | ||||
case setting.FCGIUnix: | case setting.FCGIUnix: | ||||
NoHTTPRedirector() | |||||
if handleRedirector { | |||||
NoHTTPRedirector() | |||||
} | |||||
err = runFCGI("unix", listenAddr, context2.ClearHandler(m)) | err = runFCGI("unix", listenAddr, context2.ClearHandler(m)) | ||||
default: | default: | ||||
log.Fatal("Invalid protocol: %s", setting.Protocol) | log.Fatal("Invalid protocol: %s", setting.Protocol) | ||||
@@ -206,8 +252,5 @@ func runWeb(ctx *cli.Context) error { | |||||
log.Critical("Failed to start server: %v", err) | log.Critical("Failed to start server: %v", err) | ||||
} | } | ||||
log.Info("HTTP Listener: %s Closed", listenAddr) | log.Info("HTTP Listener: %s Closed", listenAddr) | ||||
<-graceful.GetManager().Done() | |||||
log.Info("PID: %d Gitea Web Finished", os.Getpid()) | |||||
log.Close() | |||||
return nil | |||||
return err | |||||
} | } |
@@ -37,6 +37,12 @@ func NoMainListener() { | |||||
graceful.GetManager().InformCleanup() | graceful.GetManager().InformCleanup() | ||||
} | } | ||||
// NoInstallListener tells our cleanup routine that we will not be using a possibly provided listener | |||||
// for our install HTTP/HTTPS service | |||||
func NoInstallListener() { | |||||
graceful.GetManager().InformCleanup() | |||||
} | |||||
func runFCGI(network, listenAddr string, m http.Handler) error { | func runFCGI(network, listenAddr string, m http.Handler) error { | ||||
// This needs to handle stdin as fcgi point | // This needs to handle stdin as fcgi point | ||||
fcgiServer := graceful.NewServer(network, listenAddr) | fcgiServer := graceful.NewServer(network, listenAddr) | ||||
@@ -26,12 +26,6 @@ type ToggleOptions struct { | |||||
// Toggle returns toggle options as middleware | // Toggle returns toggle options as middleware | ||||
func Toggle(options *ToggleOptions) macaron.Handler { | func Toggle(options *ToggleOptions) macaron.Handler { | ||||
return func(ctx *Context) { | return func(ctx *Context) { | ||||
// Cannot view any page before installation. | |||||
if !setting.InstallLock { | |||||
ctx.Redirect(setting.AppSubURL + "/install") | |||||
return | |||||
} | |||||
isAPIPath := auth.IsAPIPath(ctx.Req.URL.Path) | isAPIPath := auth.IsAPIPath(ctx.Req.URL.Path) | ||||
// Check prohibit login users. | // Check prohibit login users. | ||||
@@ -31,7 +31,7 @@ const ( | |||||
// | // | ||||
// If you add an additional place you must increment this number | // If you add an additional place you must increment this number | ||||
// and add a function to call manager.InformCleanup if it's not going to be used | // and add a function to call manager.InformCleanup if it's not going to be used | ||||
const numberOfServersToCreate = 3 | |||||
const numberOfServersToCreate = 4 | |||||
// Manager represents the graceful server manager interface | // Manager represents the graceful server manager interface | ||||
var manager *Manager | var manager *Manager | ||||
@@ -162,7 +162,7 @@ func (srv *Server) Serve(serve ServeFunction) error { | |||||
srv.setState(stateTerminate) | srv.setState(stateTerminate) | ||||
GetManager().ServerDone() | GetManager().ServerDone() | ||||
// use of closed means that the listeners are closed - i.e. we should be shutting down - return nil | // use of closed means that the listeners are closed - i.e. we should be shutting down - return nil | ||||
if err != nil && strings.Contains(err.Error(), "use of closed") { | |||||
if err == nil || strings.Contains(err.Error(), "use of closed") || strings.Contains(err.Error(), "http: Server closed") { | |||||
return nil | return nil | ||||
} | } | ||||
return err | return err | ||||
@@ -117,9 +117,46 @@ func InitLocales() { | |||||
}) | }) | ||||
} | } | ||||
// PreInstallInit preloads the configuration to check if we need to run install | |||||
func PreInstallInit(ctx context.Context) bool { | |||||
setting.NewContext() | |||||
if !setting.InstallLock { | |||||
log.Trace("AppPath: %s", setting.AppPath) | |||||
log.Trace("AppWorkPath: %s", setting.AppWorkPath) | |||||
log.Trace("Custom path: %s", setting.CustomPath) | |||||
log.Trace("Log path: %s", setting.LogRootPath) | |||||
log.Trace("Preparing to run install page") | |||||
InitLocales() | |||||
if setting.EnableSQLite3 { | |||||
log.Info("SQLite3 Supported") | |||||
} | |||||
setting.InitDBConfig() | |||||
svg.Init() | |||||
} | |||||
return !setting.InstallLock | |||||
} | |||||
// PostInstallInit rereads the settings and starts up the database | |||||
func PostInstallInit(ctx context.Context) { | |||||
setting.NewContext() | |||||
setting.InitDBConfig() | |||||
if setting.InstallLock { | |||||
if err := initDBEngine(ctx); err == nil { | |||||
log.Info("ORM engine initialization successful!") | |||||
} else { | |||||
log.Fatal("ORM engine initialization failed: %v", err) | |||||
} | |||||
svg.Init() | |||||
} | |||||
} | |||||
// GlobalInit is for global configuration reload-able. | // GlobalInit is for global configuration reload-able. | ||||
func GlobalInit(ctx context.Context) { | func GlobalInit(ctx context.Context) { | ||||
setting.NewContext() | setting.NewContext() | ||||
if !setting.InstallLock { | |||||
log.Fatal("Gitea is not installed") | |||||
} | |||||
if err := git.Init(ctx); err != nil { | if err := git.Init(ctx); err != nil { | ||||
log.Fatal("Git module init failed: %v", err) | log.Fatal("Git module init failed: %v", err) | ||||
} | } | ||||
@@ -134,59 +171,50 @@ func GlobalInit(ctx context.Context) { | |||||
NewServices() | NewServices() | ||||
if setting.InstallLock { | |||||
highlight.NewContext() | |||||
external.RegisterParsers() | |||||
markup.Init() | |||||
if err := initDBEngine(ctx); err == nil { | |||||
log.Info("ORM engine initialization successful!") | |||||
} else { | |||||
log.Fatal("ORM engine initialization failed: %v", err) | |||||
} | |||||
highlight.NewContext() | |||||
external.RegisterParsers() | |||||
markup.Init() | |||||
if err := initDBEngine(ctx); err == nil { | |||||
log.Info("ORM engine initialization successful!") | |||||
} else { | |||||
log.Fatal("ORM engine initialization failed: %v", err) | |||||
} | |||||
if err := models.InitOAuth2(); err != nil { | |||||
log.Fatal("Failed to initialize OAuth2 support: %v", err) | |||||
} | |||||
if err := models.InitOAuth2(); err != nil { | |||||
log.Fatal("Failed to initialize OAuth2 support: %v", err) | |||||
} | |||||
models.NewRepoContext() | |||||
models.NewRepoContext() | |||||
// Booting long running goroutines. | |||||
cron.NewContext() | |||||
issue_indexer.InitIssueIndexer(false) | |||||
code_indexer.Init() | |||||
if err := stats_indexer.Init(); err != nil { | |||||
log.Fatal("Failed to initialize repository stats indexer queue: %v", err) | |||||
} | |||||
mirror_service.InitSyncMirrors() | |||||
webhook.InitDeliverHooks() | |||||
if err := pull_service.Init(); err != nil { | |||||
log.Fatal("Failed to initialize test pull requests queue: %v", err) | |||||
} | |||||
if err := task.Init(); err != nil { | |||||
log.Fatal("Failed to initialize task scheduler: %v", err) | |||||
} | |||||
eventsource.GetManager().Init() | |||||
// Booting long running goroutines. | |||||
cron.NewContext() | |||||
issue_indexer.InitIssueIndexer(false) | |||||
code_indexer.Init() | |||||
if err := stats_indexer.Init(); err != nil { | |||||
log.Fatal("Failed to initialize repository stats indexer queue: %v", err) | |||||
} | } | ||||
mirror_service.InitSyncMirrors() | |||||
webhook.InitDeliverHooks() | |||||
if err := pull_service.Init(); err != nil { | |||||
log.Fatal("Failed to initialize test pull requests queue: %v", err) | |||||
} | |||||
if err := task.Init(); err != nil { | |||||
log.Fatal("Failed to initialize task scheduler: %v", err) | |||||
} | |||||
eventsource.GetManager().Init() | |||||
if setting.EnableSQLite3 { | if setting.EnableSQLite3 { | ||||
log.Info("SQLite3 Supported") | log.Info("SQLite3 Supported") | ||||
} | } | ||||
checkRunMode() | checkRunMode() | ||||
// Now because Install will re-run GlobalInit once it has set InstallLock | |||||
// we can't tell if the ssh port will remain unused until that's done. | |||||
// However, see FIXME comment in install.go | |||||
if setting.InstallLock { | |||||
if setting.SSH.StartBuiltinServer { | |||||
ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) | |||||
log.Info("SSH server started on %s:%d. Cipher list (%v), key exchange algorithms (%v), MACs (%v)", setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) | |||||
} else { | |||||
ssh.Unused() | |||||
} | |||||
} | |||||
if setting.InstallLock { | |||||
sso.Init() | |||||
if setting.SSH.StartBuiltinServer { | |||||
ssh.Listen(setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) | |||||
log.Info("SSH server started on %s:%d. Cipher list (%v), key exchange algorithms (%v), MACs (%v)", setting.SSH.ListenHost, setting.SSH.ListenPort, setting.SSH.ServerCiphers, setting.SSH.ServerKeyExchanges, setting.SSH.ServerMACs) | |||||
} else { | |||||
ssh.Unused() | |||||
} | } | ||||
sso.Init() | |||||
svg.Init() | svg.Init() | ||||
} | } |
@@ -5,7 +5,7 @@ | |||||
package routers | package routers | ||||
import ( | import ( | ||||
"errors" | |||||
"net/http" | |||||
"os" | "os" | ||||
"os/exec" | "os/exec" | ||||
"path/filepath" | "path/filepath" | ||||
@@ -27,13 +27,15 @@ import ( | |||||
const ( | const ( | ||||
// tplInstall template for installation page | // tplInstall template for installation page | ||||
tplInstall base.TplName = "install" | |||||
tplInstall base.TplName = "install" | |||||
tplPostInstall base.TplName = "post-install" | |||||
) | ) | ||||
// InstallInit prepare for rendering installation page | // InstallInit prepare for rendering installation page | ||||
func InstallInit(ctx *context.Context) { | func InstallInit(ctx *context.Context) { | ||||
if setting.InstallLock { | if setting.InstallLock { | ||||
ctx.NotFound("Install", errors.New("Installation is prohibited")) | |||||
ctx.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login") | |||||
ctx.HTML(200, tplPostInstall) | |||||
return | return | ||||
} | } | ||||
@@ -357,7 +359,8 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { | |||||
return | return | ||||
} | } | ||||
GlobalInit(graceful.GetManager().HammerContext()) | |||||
// Re-read settings | |||||
PostInstallInit(ctx.Req.Context()) | |||||
// Create admin account | // Create admin account | ||||
if len(form.AdminName) > 0 { | if len(form.AdminName) > 0 { | ||||
@@ -380,6 +383,11 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { | |||||
u, _ = models.GetUserByName(u.Name) | u, _ = models.GetUserByName(u.Name) | ||||
} | } | ||||
days := 86400 * setting.LogInRememberDays | |||||
ctx.SetCookie(setting.CookieUserName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) | |||||
ctx.SetSuperSecureCookie(base.EncodeMD5(u.Rands+u.Passwd), | |||||
setting.CookieRememberName, u.Name, days, setting.AppSubURL, setting.SessionConfig.Domain, setting.SessionConfig.Secure, true) | |||||
// Auto-login for admin | // Auto-login for admin | ||||
if err = ctx.Session.Set("uid", u.ID); err != nil { | if err = ctx.Session.Set("uid", u.ID); err != nil { | ||||
ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) | ctx.RenderWithErr(ctx.Tr("install.save_config_failed", err), tplInstall, &form) | ||||
@@ -397,12 +405,18 @@ func InstallPost(ctx *context.Context, form auth.InstallForm) { | |||||
} | } | ||||
log.Info("First-time run install finished!") | log.Info("First-time run install finished!") | ||||
// FIXME: This isn't really enough to completely take account of new configuration | |||||
// We should really be restarting: | |||||
// - On windows this is probably just a simple restart | |||||
// - On linux we can't just use graceful.RestartProcess() everything that was passed in on LISTEN_FDS | |||||
// (active or not) needs to be passed out and everything new passed out too. | |||||
// This means we need to prevent the cleanup goroutine from running prior to the second GlobalInit | |||||
ctx.Flash.Success(ctx.Tr("install.install_success")) | ctx.Flash.Success(ctx.Tr("install.install_success")) | ||||
ctx.Redirect(form.AppURL + "user/login") | |||||
ctx.Header().Add("Refresh", "1; url="+setting.AppURL+"user/login") | |||||
ctx.HTML(200, tplPostInstall) | |||||
// Now get the http.Server from this request and shut it down | |||||
// NB: This is not our hammerable graceful shutdown this is http.Server.Shutdown | |||||
srv := ctx.Req.Context().Value(http.ServerContextKey).(*http.Server) | |||||
go func() { | |||||
if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil { | |||||
log.Error("Unable to shutdown the install server! Error: %v", err) | |||||
} | |||||
}() | |||||
} | } |
@@ -301,6 +301,15 @@ func NewMacaron() *macaron.Macaron { | |||||
return m | return m | ||||
} | } | ||||
// RegisterInstallRoute registers the install routes | |||||
func RegisterInstallRoute(m *macaron.Macaron) { | |||||
m.Combo("/", routers.InstallInit).Get(routers.Install). | |||||
Post(binding.BindIgnErr(auth.InstallForm{}), routers.InstallPost) | |||||
m.NotFound(func(ctx *context.Context) { | |||||
ctx.Redirect(setting.AppURL, 302) | |||||
}) | |||||
} | |||||
// RegisterRoutes routes routes to Macaron | // RegisterRoutes routes routes to Macaron | ||||
func RegisterRoutes(m *macaron.Macaron) { | func RegisterRoutes(m *macaron.Macaron) { | ||||
reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true}) | reqSignIn := context.Toggle(&context.ToggleOptions{SignInRequired: true}) | ||||
@@ -10,7 +10,7 @@ | |||||
<p>{{.i18n.Tr "install.docker_helper" "https://docs.gitea.io/en-us/install-with-docker/" | Safe}}</p> | <p>{{.i18n.Tr "install.docker_helper" "https://docs.gitea.io/en-us/install-with-docker/" | Safe}}</p> | ||||
<form class="ui form" action="{{AppSubUrl}}/install" method="post"> | |||||
<form class="ui form" action="{{AppSubUrl}}/" method="post"> | |||||
<!-- Database Settings --> | <!-- Database Settings --> | ||||
<h4 class="ui dividing header">{{.i18n.Tr "install.db_title"}}</h4> | <h4 class="ui dividing header">{{.i18n.Tr "install.db_title"}}</h4> | ||||
<p>{{.i18n.Tr "install.requite_db_desc"}}</p> | <p>{{.i18n.Tr "install.requite_db_desc"}}</p> | ||||
@@ -307,4 +307,5 @@ | |||||
</div> | </div> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<img style="display: none" src="{{StaticUrlPrefix}}/img/loading.png"/> | |||||
{{template "base/footer" .}} | {{template "base/footer" .}} |
@@ -0,0 +1,24 @@ | |||||
{{template "base/head" .}} | |||||
<div class="install"> | |||||
<div class="ui container"> | |||||
<div class="ui grid"> | |||||
<div class="sixteen wide column content"> | |||||
<div class="home"> | |||||
<div class="ui stackable middle very relaxed page grid"> | |||||
<div id="repo_migrating" class="sixteen wide center aligned centered column"> | |||||
<div> | |||||
<img src="{{StaticUrlPrefix}}/img/loading.png"/> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
<div class="ui stackable middle very relaxed page grid"> | |||||
<div class="sixteen wide center aligned centered column"> | |||||
<p><a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a></p> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
{{template "base/footer" .}} |