471cfdd161
- 1 - Add: Gitea now creates timestamped database backup bundles under `[backup].PATH`, exposes the backup schedule in the installer, and surfaces the `database_backup` cron task in admin monitoring. - 2 - Add: installed instances now use `.gitea-installed` and `.gitea-recovery.ini` to enter email-gated recovery instead of falling back to public install mode when configuration or database access is broken. - 3 - Mod: the installer recovery flow now covers backup-bundle restore, bundled or manual `app.ini` reuse, uploaded SQL/GZ database restores, and repository-filesystem recovery with source-specific validation, confirmations, and preserved launcher state. - 4 - Fix: recovery now restores bundled `app.ini` snapshots when needed, discovers backup bundles from both the active backup path and persisted `.gitea-recovery.ini` path, and preserves SMTP and other rebuilt settings correctly when `app.ini` is missing or incomplete. - 5 - Fix: recovery validation and restore handling now accept either a selected backup bundle or an uploaded SQL/GZ dump, keep sensitive secrets and existing `LFS_JWT_SECRET` when appropriate, clear SQLite restore targets before import, and complete the post-install handoff without redirect loops. - 6 - Mod: fresh installs now default recovery email authorization to enabled with first-admin fallback, and the install/recovery UI, styling, and EN/RO wording were refined to match the final launcher behavior. Co-Authored-By: petru @ codex (GPT-5) <codex@openai.com> (cherry picked from commit 9879caf2292691b0cb521d12e6fee924b066bae2)
2322 lines
88 KiB
Go
2322 lines
88 KiB
Go
// Copyright 2014 The Gogs Authors. All rights reserved.
|
|
// Copyright 2021 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package install
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
std_context "context"
|
|
"errors"
|
|
"fmt"
|
|
templatehtml "html/template"
|
|
"image"
|
|
"io"
|
|
"net/http"
|
|
"net/mail"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
_ "image/png"
|
|
|
|
"code.gitea.io/gitea/models/db"
|
|
db_install "code.gitea.io/gitea/models/db/install"
|
|
user_model "code.gitea.io/gitea/models/user"
|
|
"code.gitea.io/gitea/modules/auth/password/hash"
|
|
"code.gitea.io/gitea/modules/generate"
|
|
"code.gitea.io/gitea/modules/graceful"
|
|
"code.gitea.io/gitea/modules/log"
|
|
"code.gitea.io/gitea/modules/optional"
|
|
"code.gitea.io/gitea/modules/setting"
|
|
"code.gitea.io/gitea/modules/templates"
|
|
"code.gitea.io/gitea/modules/timeutil"
|
|
"code.gitea.io/gitea/modules/typesniffer"
|
|
"code.gitea.io/gitea/modules/web"
|
|
"code.gitea.io/gitea/modules/web/middleware"
|
|
"code.gitea.io/gitea/routers/common"
|
|
auth_service "code.gitea.io/gitea/services/auth"
|
|
"code.gitea.io/gitea/services/context"
|
|
dbbackup_service "code.gitea.io/gitea/services/dbbackup"
|
|
"code.gitea.io/gitea/services/forms"
|
|
"code.gitea.io/gitea/services/mailer"
|
|
"code.gitea.io/gitea/services/versioned_migration"
|
|
)
|
|
|
|
const (
|
|
// tplInstall template for installation page
|
|
tplInstall templates.TplName = "install"
|
|
tplPostInstall templates.TplName = "post-install"
|
|
|
|
registrationModeAdminOnly = "admin_only"
|
|
registrationModeLocalOnly = "local_only"
|
|
registrationModeExternalOnly = "external_only"
|
|
registrationModeLocalAndExternal = "local_and_external"
|
|
|
|
installBrandingMaxFileSize = int64(1 << 20)
|
|
installBrandingMinPNGEdge = 64
|
|
installAppINIImportMaxSize = int64(1 << 20)
|
|
|
|
recoveryModeExistingDatabase = "existing_database"
|
|
recoveryModeDatabaseBackup = "database_backup"
|
|
recoveryModeRepositoryFileSystem = "repository_filesystem"
|
|
)
|
|
|
|
type installBrandingAssetSpec struct {
|
|
FormField string
|
|
TargetName string
|
|
LabelKey string
|
|
MimeType string
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
type installBackupChoice struct {
|
|
ID string
|
|
Label string
|
|
HasAppINI bool
|
|
HasDatabase bool
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
var installBrandingAssetSpecs = []installBrandingAssetSpec{
|
|
{FormField: "logo_svg", TargetName: "logo.svg", LabelKey: "install.branding.logo_svg", MimeType: typesniffer.MimeTypeImageSvg},
|
|
{FormField: "logo_png", TargetName: "logo.png", LabelKey: "install.branding.logo_png", MimeType: "image/png"},
|
|
{FormField: "loading_png", TargetName: "loading.png", LabelKey: "install.branding.loading_png", MimeType: "image/png"},
|
|
{FormField: "favicon_svg", TargetName: "favicon.svg", LabelKey: "install.branding.favicon_svg", MimeType: typesniffer.MimeTypeImageSvg},
|
|
{FormField: "favicon_png", TargetName: "favicon.png", LabelKey: "install.branding.favicon_png", MimeType: "image/png"},
|
|
}
|
|
|
|
var installSMTPUnquotedDisplayNamePattern = regexp.MustCompile(`^[A-Za-z0-9][A-Za-z0-9 ._-]*$`)
|
|
|
|
// getSupportedDbTypeNames returns a slice for supported database types and names. The slice is used to keep the order
|
|
func getSupportedDbTypeNames() (dbTypeNames []map[string]string) {
|
|
for _, t := range setting.SupportedDatabaseTypes {
|
|
dbTypeNames = append(dbTypeNames, map[string]string{"type": t, "name": setting.DatabaseTypeNames[t]})
|
|
}
|
|
return dbTypeNames
|
|
}
|
|
|
|
func resolveInstallDefaultLanguage(defaultLanguage string) string {
|
|
if slices.Contains(setting.Langs, defaultLanguage) {
|
|
return defaultLanguage
|
|
}
|
|
if len(setting.Langs) > 0 {
|
|
return setting.Langs[0]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func reorderInstallLanguages(defaultLanguage string) []string {
|
|
defaultLanguage = resolveInstallDefaultLanguage(defaultLanguage)
|
|
if defaultLanguage == "" {
|
|
return append([]string(nil), setting.Langs...)
|
|
}
|
|
|
|
langs := []string{defaultLanguage}
|
|
for _, lang := range setting.Langs {
|
|
if lang != defaultLanguage {
|
|
langs = append(langs, lang)
|
|
}
|
|
}
|
|
return langs
|
|
}
|
|
|
|
func resolveInstallRegistrationMode(disableRegistration, allowOnlyInternalRegistration, allowOnlyExternalRegistration bool) string {
|
|
switch {
|
|
case disableRegistration:
|
|
return registrationModeAdminOnly
|
|
case allowOnlyExternalRegistration:
|
|
return registrationModeExternalOnly
|
|
case allowOnlyInternalRegistration:
|
|
return registrationModeLocalOnly
|
|
default:
|
|
return registrationModeLocalAndExternal
|
|
}
|
|
}
|
|
|
|
func isLocalRegistrationMode(mode string) bool {
|
|
return mode == registrationModeLocalOnly || mode == registrationModeLocalAndExternal
|
|
}
|
|
|
|
func isExternalRegistrationMode(mode string) bool {
|
|
return mode == registrationModeExternalOnly || mode == registrationModeLocalAndExternal
|
|
}
|
|
|
|
func normalizeInstallRegistrationOptions(form *forms.InstallForm) {
|
|
if form.RegistrationMode == "" {
|
|
form.RegistrationMode = resolveInstallRegistrationMode(form.DisableRegistration, form.AllowOnlyInternalRegistration, form.AllowOnlyExternalRegistration)
|
|
}
|
|
|
|
switch form.RegistrationMode {
|
|
case registrationModeAdminOnly:
|
|
form.DisableRegistration = true
|
|
form.AllowOnlyInternalRegistration = false
|
|
form.AllowOnlyExternalRegistration = false
|
|
case registrationModeLocalOnly:
|
|
form.DisableRegistration = false
|
|
form.AllowOnlyInternalRegistration = true
|
|
form.AllowOnlyExternalRegistration = false
|
|
case registrationModeExternalOnly:
|
|
form.DisableRegistration = false
|
|
form.AllowOnlyInternalRegistration = false
|
|
form.AllowOnlyExternalRegistration = true
|
|
default:
|
|
form.RegistrationMode = registrationModeLocalAndExternal
|
|
form.DisableRegistration = false
|
|
form.AllowOnlyInternalRegistration = false
|
|
form.AllowOnlyExternalRegistration = false
|
|
}
|
|
if form.AdminCreatedAccountMode == "" {
|
|
if form.RegistrationMode == registrationModeAdminOnly {
|
|
form.AdminCreatedAccountMode = setting.AdminCreatedAccountModeInvite
|
|
} else {
|
|
form.AdminCreatedAccountMode = setting.AdminCreatedAccountModeLocal
|
|
}
|
|
}
|
|
if form.AdminCreatedAccountMode != setting.AdminCreatedAccountModeInvite {
|
|
form.AdminCreatedAccountMode = setting.AdminCreatedAccountModeLocal
|
|
}
|
|
|
|
if !isLocalRegistrationMode(form.RegistrationMode) {
|
|
form.RegisterConfirm = false
|
|
form.RegisterManualConfirm = false
|
|
form.EnableCaptcha = false
|
|
}
|
|
if form.RegisterManualConfirm {
|
|
form.RegisterConfirm = true
|
|
}
|
|
|
|
if !isExternalRegistrationMode(form.RegistrationMode) {
|
|
form.EnableOpenIDSignIn = false
|
|
form.EnableOpenIDSignUp = false
|
|
}
|
|
if form.EnableOpenIDSignUp {
|
|
form.EnableOpenIDSignIn = true
|
|
}
|
|
}
|
|
|
|
func collectInstallBrandingAssets(ctx *context.Context, useSharedAssets bool) (map[string][]byte, error) {
|
|
uploads := make(map[string][]byte)
|
|
|
|
for _, spec := range installBrandingAssetSpecs {
|
|
file, header, err := ctx.Req.FormFile(spec.FormField)
|
|
if errors.Is(err, http.ErrMissingFile) {
|
|
continue
|
|
}
|
|
if err != nil {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_read_failed", ctx.Tr(spec.LabelKey), err)))
|
|
}
|
|
data, err := func() ([]byte, error) {
|
|
defer file.Close()
|
|
|
|
if header.Size > installBrandingMaxFileSize {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_too_big", ctx.Tr(spec.LabelKey), installBrandingMaxFileSize/1024)))
|
|
}
|
|
|
|
data, err := io.ReadAll(io.LimitReader(file, installBrandingMaxFileSize+1))
|
|
if err != nil {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_read_failed", ctx.Tr(spec.LabelKey), err)))
|
|
}
|
|
if int64(len(data)) > installBrandingMaxFileSize {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_too_big", ctx.Tr(spec.LabelKey), installBrandingMaxFileSize/1024)))
|
|
}
|
|
|
|
return data, nil
|
|
}()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sniffedType := typesniffer.DetectContentType(data)
|
|
switch spec.MimeType {
|
|
case typesniffer.MimeTypeImageSvg:
|
|
if !sniffedType.IsSvgImage() {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "SVG")))
|
|
}
|
|
case "image/png":
|
|
if sniffedType.GetMimeType() != "image/png" {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "PNG")))
|
|
}
|
|
|
|
cfg, format, err := image.DecodeConfig(bytes.NewReader(data))
|
|
if err != nil || format != "png" {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_invalid_type", ctx.Tr(spec.LabelKey), "PNG")))
|
|
}
|
|
if cfg.Width != cfg.Height {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_png_square", ctx.Tr(spec.LabelKey))))
|
|
}
|
|
if cfg.Width < installBrandingMinPNGEdge || cfg.Height < installBrandingMinPNGEdge {
|
|
return nil, errors.New(string(ctx.Tr("install.branding.upload_png_too_small", ctx.Tr(spec.LabelKey), installBrandingMinPNGEdge, installBrandingMinPNGEdge)))
|
|
}
|
|
}
|
|
|
|
uploads[spec.TargetName] = data
|
|
}
|
|
|
|
if useSharedAssets {
|
|
if logoSVG, ok := uploads["logo.svg"]; ok {
|
|
uploads["favicon.svg"] = logoSVG
|
|
}
|
|
if logoPNG, ok := uploads["logo.png"]; ok {
|
|
uploads["favicon.png"] = logoPNG
|
|
}
|
|
}
|
|
|
|
return uploads, nil
|
|
}
|
|
|
|
func saveInstallBrandingAssets(uploads map[string][]byte) error {
|
|
if len(uploads) == 0 {
|
|
return nil
|
|
}
|
|
|
|
customImgPath := filepath.Join(setting.CustomPath, "public", "assets", "img")
|
|
if err := os.MkdirAll(customImgPath, os.ModePerm); err != nil {
|
|
return err
|
|
}
|
|
|
|
for targetName, data := range uploads {
|
|
if err := os.WriteFile(filepath.Join(customImgPath, targetName), data, 0o644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if logoSVG, ok := uploads["logo.svg"]; ok {
|
|
if err := os.WriteFile(filepath.Join(customImgPath, "gitea.svg"), logoSVG, 0o644); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func getInstallProgressLogo() string {
|
|
customImgPath := filepath.Join(setting.CustomPath, "public", "assets", "img")
|
|
if _, err := os.Stat(filepath.Join(customImgPath, "loading.png")); err == nil {
|
|
return "loading.png"
|
|
}
|
|
if _, err := os.Stat(filepath.Join(customImgPath, "logo.svg")); err == nil {
|
|
return "logo.svg"
|
|
}
|
|
if _, err := os.Stat(filepath.Join(customImgPath, "logo.png")); err == nil {
|
|
return "logo.png"
|
|
}
|
|
return "loading.png"
|
|
}
|
|
|
|
func installContexter() func(next http.Handler) http.Handler {
|
|
return context.ContexterInstallPage(map[string]any{
|
|
"DbTypeNames": getSupportedDbTypeNames(),
|
|
"AppINIImportMaxSizeKB": installAppINIImportMaxSize / 1024,
|
|
"BrandingMaxFileSizeKB": installBrandingMaxFileSize / 1024,
|
|
"BrandingMinPNGEdge": installBrandingMinPNGEdge,
|
|
"EnvConfigKeys": setting.CollectEnvConfigKeys(),
|
|
"CustomConfFile": setting.CustomConf,
|
|
"PasswordHashAlgorithms": hash.RecommendedHashAlgorithms,
|
|
})
|
|
}
|
|
|
|
func newInstallFormFromSettings() (forms.InstallForm, string) {
|
|
form := forms.InstallForm{}
|
|
form.BrandingUseSharedAssets = true
|
|
// Database settings
|
|
form.DbHost = setting.Database.Host
|
|
form.DbUser = setting.Database.User
|
|
form.DbPasswd = setting.Database.Passwd
|
|
form.DbName = setting.Database.Name
|
|
form.DbPath = setting.Database.Path
|
|
form.DbSchema = setting.Database.Schema
|
|
form.SSLMode = setting.Database.SSLMode
|
|
|
|
curDBType := setting.Database.Type.String()
|
|
if !slices.Contains(setting.SupportedDatabaseTypes, curDBType) {
|
|
curDBType = "mysql"
|
|
}
|
|
|
|
// Application general settings
|
|
form.AppName = setting.AppName
|
|
form.DefaultLanguage = resolveInstallDefaultLanguage("")
|
|
form.RepoRootPath = setting.RepoRootPath
|
|
form.LFSRootPath = setting.LFS.Storage.Path
|
|
form.RunUser = setting.RunUser
|
|
form.Domain = setting.Domain
|
|
form.SSHPort = setting.SSH.Port
|
|
form.HTTPPort = setting.HTTPPort
|
|
form.AppURL = setting.AppURL
|
|
form.LogRootPath = setting.Log.RootPath
|
|
|
|
// E-mail service settings
|
|
if setting.MailService != nil {
|
|
form.SMTPAddr = setting.MailService.SMTPAddr
|
|
form.SMTPPort = setting.MailService.SMTPPort
|
|
form.SMTPFrom = setting.MailService.From
|
|
form.SMTPUser = setting.MailService.User
|
|
form.SMTPPasswd = setting.MailService.Passwd
|
|
}
|
|
populateInstallSMTPFromFields(&form)
|
|
form.RegisterConfirm = setting.Service.RegisterEmailConfirm
|
|
form.RegisterManualConfirm = setting.Service.RegisterManualConfirm
|
|
form.MailNotify = setting.Service.EnableNotifyMail
|
|
if setting.MailService == nil && !setting.Service.EnableNotifyMail {
|
|
form.MailNotify = true
|
|
}
|
|
|
|
form.RegistrationMode = resolveInstallRegistrationMode(setting.Service.DisableRegistration, setting.Service.AllowOnlyInternalRegistration, setting.Service.AllowOnlyExternalRegistration)
|
|
form.AdminCreatedAccountMode = setting.Service.AdminCreatedAccountMode
|
|
form.EnableOpenIDSignIn = setting.Service.EnableOpenIDSignIn
|
|
form.EnableOpenIDSignUp = setting.Service.EnableOpenIDSignUp
|
|
form.DisableRegistration = setting.Service.DisableRegistration
|
|
form.AllowOnlyInternalRegistration = setting.Service.AllowOnlyInternalRegistration
|
|
form.AllowOnlyExternalRegistration = setting.Service.AllowOnlyExternalRegistration
|
|
form.EnableCaptcha = setting.Service.EnableCaptcha
|
|
form.RequireSignInView = setting.Service.RequireSignInViewStrict
|
|
form.DefaultKeepEmailPrivate = setting.Service.DefaultKeepEmailPrivate
|
|
form.DefaultAllowCreateOrganization = setting.Service.DefaultAllowCreateOrganization
|
|
form.DefaultEnableTimetracking = setting.Service.DefaultEnableTimetracking
|
|
form.NoReplyAddress = setting.Service.NoReplyAddress
|
|
form.PasswordAlgorithm = hash.ConfigHashAlgorithm(setting.PasswordHashAlgo)
|
|
form.AdminManagementPolicy = setting.Admin.AdminManagementPolicy
|
|
form.ReleaseMaxFiles = setting.Repository.Release.MaxFiles
|
|
form.ReleaseFileMaxSize = setting.Repository.Release.FileMaxSize
|
|
// start edit/add - by petru @ codex
|
|
form.BackupPath = setting.Backup.Path
|
|
form.BackupRetentionCount = setting.Backup.RetentionCount
|
|
form.BackupCompress = setting.Backup.Compress
|
|
form.BackupIncludeAppINISnapshot = setting.Backup.IncludeAppINISnapshot
|
|
backupCronSec := setting.CfgProvider.Section("cron.database_backup")
|
|
form.BackupEnabled = setting.ConfigSectionKeyBool(backupCronSec, "ENABLED", true)
|
|
form.BackupRunAtStart = setting.ConfigSectionKeyBool(backupCronSec, "RUN_AT_START", false)
|
|
form.BackupSchedule = setting.ConfigSectionKeyString(backupCronSec, "SCHEDULE", "@daily")
|
|
form.RecoveryEmailEnabled = !installCustomConfExists() // edit/add - by petru @ codex
|
|
if recoveryCfg, err := setting.LoadRecoveryConfig(); err == nil && recoveryCfg != nil {
|
|
form.RecoveryEmailEnabled = recoveryCfg.Enabled // edit/add - by petru @ codex
|
|
form.RecoveryAllowedEmails = recoveryCfg.AllowedEmails // edit/add - by petru @ codex
|
|
if !installCustomConfExists() && strings.TrimSpace(recoveryCfg.BackupPath) != "" {
|
|
form.BackupPath = recoveryCfg.BackupPath // edit/add - by petru @ codex
|
|
}
|
|
if !installCustomConfExists() && strings.TrimSpace(recoveryCfg.RepoRootPath) != "" {
|
|
form.RepoRootPath = recoveryCfg.RepoRootPath // edit/add - by petru @ codex
|
|
}
|
|
} else if err != nil {
|
|
log.Error("Unable to load recovery config %q: %v", setting.RecoveryConfigPath(), err) // edit/add - by petru @ codex
|
|
}
|
|
// end edit/add - by petru @ codex
|
|
form.AllowAdoptionOfUnadoptedRepositories = true // edit/add - by petru @ codex
|
|
form.AllowDeleteOfUnadoptedRepositories = true // edit/add - by petru @ codex
|
|
normalizeInstallRegistrationOptions(&form)
|
|
|
|
return form, curDBType
|
|
}
|
|
|
|
func assignInstallPageData(ctx *context.Context, form *forms.InstallForm, curDBType string, availableBackups []*installBackupChoice, backupDiscoveryErr error) []*installBackupChoice {
|
|
recoveryRequest := isRecoveryRequest(ctx.Req) // edit/add - by petru @ codex
|
|
installerSiteName := resolveInstallPageSiteName(form, recoveryRequest) // edit/add - by petru @ codex
|
|
if recoveryRequest {
|
|
ctx.RespHeader().Set("Cache-Control", "no-store, no-cache, must-revalidate") // edit/add - by petru @ codex
|
|
ctx.RespHeader().Set("Pragma", "no-cache") // edit/add - by petru @ codex
|
|
ctx.RespHeader().Set("Expires", "0") // edit/add - by petru @ codex
|
|
}
|
|
if form.DefaultLanguage == "" {
|
|
form.DefaultLanguage = resolveInstallDefaultLanguage("")
|
|
}
|
|
normalizeInstallRegistrationOptions(form)
|
|
if availableBackups == nil {
|
|
if discoveredBackups, err := listInstallRecoveryBackupChoices(form, recoveryRequest); err == nil {
|
|
availableBackups = discoveredBackups
|
|
ctx.Data["InstallRecoveryBackups"] = availableBackups // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoveryOptionDatabaseBackupAvailable"] = len(availableBackups) > 0
|
|
} else {
|
|
backupDiscoveryErr = err
|
|
ctx.Data["Err_BackupPath"] = true // edit/add - by petru @ codex
|
|
}
|
|
} else {
|
|
ctx.Data["InstallRecoveryBackups"] = availableBackups // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoveryOptionDatabaseBackupAvailable"] = len(availableBackups) > 0
|
|
}
|
|
if recoveryRequest {
|
|
hasExistingDatabase := false
|
|
if dbPreviouslyUsed, err := detectInstallExistingDatabase(ctx); err == nil && dbPreviouslyUsed {
|
|
hasExistingDatabase = true
|
|
}
|
|
setInstallRecoverySourceAvailability(ctx, hasExistingDatabase, availableBackups, hasInstallRepositoryFilesystemRecovery(form)) // edit/add - by petru @ codex
|
|
}
|
|
ctx.Data["CurDbType"] = curDBType
|
|
ctx.Data["InstallIsRecoveryRequest"] = recoveryRequest // edit/add - by petru @ codex
|
|
ctx.Data["InstallerSiteName"] = installerSiteName // edit/add - by petru @ codex
|
|
ctx.Data["Title"] = ctx.Tr("install.install_btn_confirm", ctx.Data["InstallerSiteName"]) // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoverySectionTitle"] = ctx.Tr("install.import_app_ini_title") // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoverySectionDesc"] = ctx.Tr("install.import_app_ini_desc") // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoveryLauncherTitle"] = ctx.Tr("install.recovery_reinstall_title") // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoveryProblemText"] = "" // edit/add - by petru @ codex
|
|
if recoveryRequest {
|
|
ctx.Data["InstallRecoverySectionTitle"] = ctx.Tr("install.recovery_title") // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoverySectionDesc"] = ctx.Tr("install.recovery_desc") // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoveryLauncherTitle"] = ctx.Tr("install.recovery_title") // edit/add - by petru @ codex
|
|
if !installCustomConfExists() {
|
|
if dbPreviouslyUsed, err := detectInstallExistingDatabase(ctx); err != nil || !dbPreviouslyUsed {
|
|
ctx.Data["InstallRecoveryProblemText"] = ctx.Tr("install.recovery_problem_missing_app_ini_and_database_unavailable") // edit/add - by petru @ codex
|
|
} else {
|
|
ctx.Data["InstallRecoveryProblemText"] = ctx.Tr("install.recovery_problem_missing_app_ini") // edit/add - by petru @ codex
|
|
}
|
|
} else if dbPreviouslyUsed, err := detectInstallExistingDatabase(ctx); err != nil || !dbPreviouslyUsed {
|
|
ctx.Data["InstallRecoveryProblemText"] = ctx.Tr("install.recovery_problem_database_unavailable") // edit/add - by petru @ codex
|
|
}
|
|
}
|
|
if recoveryRequest && backupDiscoveryErr != nil {
|
|
ctx.Data["InstallRecoveryProblemText"] = ctx.Tr("install.recovery_database_backup_discovery_failed", backupDiscoveryErr) // edit/add - by petru @ codex
|
|
}
|
|
if ctx.Flash != nil && ctx.Flash.ErrorMsg != "" {
|
|
ctx.Data["InstallRecoveryProblemText"] = ctx.Flash.ErrorMsg // edit/add - by petru @ codex
|
|
}
|
|
ctx.Data["InstallFormAction"] = installRouteURL("/", recoveryRequest) // edit/add - by petru @ codex
|
|
ctx.Data["InstallImportAction"] = installRouteURL("/import_app_ini", recoveryRequest) // edit/add - by petru @ codex
|
|
ctx.Data["InstallBackupAppINIImportAction"] = installRouteURL("/import_backup_app_ini", recoveryRequest) // edit/add - by petru @ codex
|
|
ctx.Data["InstallTestMailAction"] = installRouteURL("/test_mail", recoveryRequest) // edit/add - by petru @ codex
|
|
middleware.AssignForm(form, ctx.Data)
|
|
return availableBackups
|
|
}
|
|
|
|
func renderInstallPage(ctx *context.Context, form *forms.InstallForm, curDBType string) {
|
|
assignInstallPageData(ctx, form, curDBType, nil, nil) // edit/add - by petru @ codex
|
|
ctx.HTML(http.StatusOK, tplInstall)
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func installRouteURL(path string, recovery bool) string {
|
|
target := setting.AppSubURL + path
|
|
if !recovery {
|
|
return target
|
|
}
|
|
return target + "?" + recoveryRequestQueryKey + "=1"
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func recoveryInstallSiteName() string {
|
|
recoveryCfg, recoveryErr := setting.LoadRecoveryConfig()
|
|
if recoveryErr != nil || recoveryCfg == nil {
|
|
return ""
|
|
}
|
|
parsedFrom, parseErr := mail.ParseAddress(strings.TrimSpace(recoveryCfg.SMTPFrom))
|
|
if parseErr != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(parsedFrom.Name)
|
|
}
|
|
|
|
func resolveInstallPageSiteName(form *forms.InstallForm, recovery bool) string {
|
|
if form == nil {
|
|
return installMailerDisplayName("")
|
|
}
|
|
|
|
if recovery {
|
|
recoverySiteName := recoveryInstallSiteName()
|
|
if recoverySiteName != "" {
|
|
if !form.ImportedAppINI && installMailerDisplayName(form.AppName) == "Gitea" {
|
|
form.AppName = recoverySiteName
|
|
}
|
|
if resolvedSiteName := installMailerDisplayName(form.AppName); resolvedSiteName != "Gitea" {
|
|
return resolvedSiteName
|
|
}
|
|
return recoverySiteName
|
|
}
|
|
}
|
|
|
|
return installMailerDisplayName(form.AppName)
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
func resolveInstallBackupPath(backupPath string) string {
|
|
backupPath = strings.TrimSpace(backupPath)
|
|
if backupPath == "" {
|
|
return ""
|
|
}
|
|
if filepath.IsAbs(backupPath) {
|
|
return backupPath
|
|
}
|
|
return filepath.Join(setting.AppWorkPath, backupPath)
|
|
}
|
|
|
|
func installCustomConfExists() bool {
|
|
// start edit/add - by petru @ codex
|
|
if strings.TrimSpace(setting.CustomConf) == "" {
|
|
return false
|
|
}
|
|
_, err := os.Stat(setting.CustomConf)
|
|
return err == nil
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func normalizeInstallRecoveryEmails(raw string) (string, error) {
|
|
parts := strings.FieldsFunc(raw, func(r rune) bool {
|
|
return r == ',' || r == ';' || r == '\n' || r == '\r' || r == '\t'
|
|
})
|
|
if len(parts) == 0 {
|
|
return "", nil
|
|
}
|
|
|
|
normalized := make([]string, 0, len(parts))
|
|
seen := make(map[string]struct{}, len(parts))
|
|
for _, part := range parts {
|
|
addr, err := mail.ParseAddress(strings.TrimSpace(part))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if _, ok := seen[addr.Address]; ok {
|
|
continue
|
|
}
|
|
seen[addr.Address] = struct{}{}
|
|
normalized = append(normalized, addr.Address)
|
|
}
|
|
return strings.Join(normalized, ","), nil
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
func listInstallBackupChoices(backupPath, dbType string) ([]*installBackupChoice, error) {
|
|
backups, err := dbbackup_service.ListBackupsInPath(resolveInstallBackupPath(backupPath))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
choices := make([]*installBackupChoice, 0, len(backups))
|
|
for _, backup := range backups {
|
|
if dbType != "" && backup.DBType != dbType {
|
|
continue
|
|
}
|
|
choices = append(choices, &installBackupChoice{
|
|
ID: backup.ID,
|
|
Label: fmt.Sprintf("%s (%s)", backup.ID, backup.DBType),
|
|
HasAppINI: strings.TrimSpace(backup.AppINISnapshotFile) != "",
|
|
HasDatabase: strings.TrimSpace(backup.FileName) != "",
|
|
})
|
|
}
|
|
return choices, nil
|
|
}
|
|
|
|
func listInstallRecoveryBackupChoices(form *forms.InstallForm, recoveryRequest bool) ([]*installBackupChoice, error) {
|
|
// start edit/add - by petru @ codex
|
|
if !recoveryRequest {
|
|
return listInstallBackupChoices(form.BackupPath, form.DbType)
|
|
}
|
|
|
|
backupPaths := []string{form.BackupPath}
|
|
if recoveryCfg, err := setting.LoadRecoveryConfig(); err == nil && recoveryCfg != nil && strings.TrimSpace(recoveryCfg.BackupPath) != "" {
|
|
backupPaths = append(backupPaths, recoveryCfg.BackupPath)
|
|
}
|
|
|
|
seenPaths := make(map[string]struct{}, len(backupPaths))
|
|
seenChoices := make(map[string]struct{})
|
|
choices := make([]*installBackupChoice, 0)
|
|
for _, backupPath := range backupPaths {
|
|
resolvedPath := resolveInstallBackupPath(backupPath)
|
|
if resolvedPath == "" {
|
|
continue
|
|
}
|
|
if _, ok := seenPaths[resolvedPath]; ok {
|
|
continue
|
|
}
|
|
seenPaths[resolvedPath] = struct{}{}
|
|
|
|
pathChoices, err := listInstallBackupChoices(backupPath, "")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
for _, choice := range pathChoices {
|
|
choiceKey := choice.ID + "\x00" + choice.Label
|
|
if _, ok := seenChoices[choiceKey]; ok {
|
|
continue
|
|
}
|
|
seenChoices[choiceKey] = struct{}{}
|
|
choices = append(choices, choice)
|
|
}
|
|
}
|
|
|
|
slices.SortFunc(choices, func(a, b *installBackupChoice) int {
|
|
if a.ID > b.ID {
|
|
return -1
|
|
}
|
|
if a.ID < b.ID {
|
|
return 1
|
|
}
|
|
return 0
|
|
})
|
|
return choices, nil
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
func hasInstallRepositoryFilesystem(repoRoot string) bool {
|
|
entries, err := os.ReadDir(repoRoot)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
for _, entry := range entries {
|
|
if !entry.IsDir() {
|
|
continue
|
|
}
|
|
if strings.HasSuffix(entry.Name(), ".git") {
|
|
return true
|
|
}
|
|
|
|
ownerPath := filepath.Join(repoRoot, entry.Name())
|
|
ownerEntries, err := os.ReadDir(ownerPath)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
for _, ownerEntry := range ownerEntries {
|
|
if ownerEntry.IsDir() && strings.HasSuffix(ownerEntry.Name(), ".git") {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func hasInstallRepositoryFilesystemRecovery(form *forms.InstallForm) bool {
|
|
if form == nil {
|
|
return false
|
|
}
|
|
if !installCustomConfExists() && (!form.ImportedAppINI || form.BackupImportAppINI) {
|
|
return false
|
|
}
|
|
return hasInstallRepositoryFilesystem(form.RepoRootPath)
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
func setInstallRecoverySourceAvailability(ctx *context.Context, hasExistingDatabase bool, availableBackups []*installBackupChoice, hasRepositoryFilesystem bool) {
|
|
ctx.Data["InstallRecoveryOptionExistingDBAvailable"] = hasExistingDatabase
|
|
ctx.Data["InstallRecoveryOptionDatabaseBackupAvailable"] = len(availableBackups) > 0
|
|
ctx.Data["InstallRecoveryOptionRepositoryFilesystemAvailable"] = hasRepositoryFilesystem
|
|
ctx.Data["InstallRecoveryBackups"] = availableBackups
|
|
}
|
|
|
|
func installRecoveryConfirmationsAccepted(form *forms.InstallForm) bool {
|
|
return form.ReinstallConfirmFirst && form.ReinstallConfirmSecond && form.ReinstallConfirmThird
|
|
}
|
|
|
|
func mergeInstallRecoveryConfigSMTP(existing, next *setting.RecoveryConfig) *setting.RecoveryConfig {
|
|
// start edit/add - by petru @ codex
|
|
if next == nil {
|
|
return existing
|
|
}
|
|
if existing == nil {
|
|
return next
|
|
}
|
|
if strings.TrimSpace(next.SMTPAddr) == "" {
|
|
next.SMTPAddr = existing.SMTPAddr
|
|
}
|
|
if strings.TrimSpace(next.SMTPPort) == "" {
|
|
next.SMTPPort = existing.SMTPPort
|
|
}
|
|
if strings.TrimSpace(next.SMTPFrom) == "" {
|
|
next.SMTPFrom = existing.SMTPFrom
|
|
}
|
|
if strings.TrimSpace(next.SMTPUser) == "" {
|
|
next.SMTPUser = existing.SMTPUser
|
|
}
|
|
if next.SMTPPasswd == "" {
|
|
next.SMTPPasswd = existing.SMTPPasswd
|
|
}
|
|
return next
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
func shouldRestoreDatabaseFromBackup(recoveryMode string, dbPreviouslyUsed bool) bool {
|
|
// start edit/add - by petru @ codex
|
|
return recoveryMode == recoveryModeDatabaseBackup && !dbPreviouslyUsed
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
func shouldRequireBackupAppINISnapshot(recoveryMode string, dbPreviouslyUsed bool) bool {
|
|
// start edit/add - by petru @ codex
|
|
return recoveryMode == recoveryModeDatabaseBackup && dbPreviouslyUsed
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func hasInstallRecoveryAppINIConfigSource(form *forms.InstallForm, backupAppINICfg setting.ConfigProvider) bool {
|
|
if backupAppINICfg != nil {
|
|
return true
|
|
}
|
|
return form != nil && form.ImportedAppINI && !form.BackupImportAppINI
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
// start edit/add - by petru @ codex
|
|
func removeInstallSQLiteDatabaseArtifacts(dbPath string) error {
|
|
dbPath = strings.TrimSpace(dbPath)
|
|
if dbPath == "" {
|
|
return nil
|
|
}
|
|
for _, path := range []string{dbPath, dbPath + "-wal", dbPath + "-shm", dbPath + "-journal"} {
|
|
err := os.Remove(path)
|
|
if err == nil || errors.Is(err, os.ErrNotExist) {
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
func installUploadedDatabaseBackupProvided(ctx *context.Context) bool {
|
|
// start edit/add - by petru @ codex
|
|
file, header, err := ctx.Req.FormFile("database_backup_file")
|
|
if file != nil {
|
|
_ = file.Close()
|
|
}
|
|
if err != nil || header == nil {
|
|
return false
|
|
}
|
|
return strings.TrimSpace(header.Filename) != ""
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
func importUploadedDatabaseBackup(ctx *context.Context) error {
|
|
// start edit/add - by petru @ codex
|
|
file, header, err := ctx.Req.FormFile("database_backup_file")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
|
|
sqlPath, cleanup, err := prepareUploadedInstallDatabaseBackup(file, header.Filename)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cleanup()
|
|
|
|
return db.ImportDatabase(sqlPath)
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
func prepareUploadedInstallDatabaseBackup(reader io.Reader, filename string) (string, func(), error) {
|
|
// start edit/add - by petru @ codex
|
|
trimmedName := strings.TrimSpace(strings.ToLower(filename))
|
|
if trimmedName == "" {
|
|
return "", nil, errors.New("missing database backup file name")
|
|
}
|
|
|
|
tempInputFile, err := os.CreateTemp("", "gitea-install-db-upload-*")
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
inputPath := tempInputFile.Name()
|
|
cleanup := func() {
|
|
_ = os.Remove(inputPath)
|
|
}
|
|
if _, err := io.Copy(tempInputFile, reader); err != nil {
|
|
_ = tempInputFile.Close()
|
|
cleanup()
|
|
return "", nil, err
|
|
}
|
|
if err := tempInputFile.Close(); err != nil {
|
|
cleanup()
|
|
return "", nil, err
|
|
}
|
|
|
|
if strings.HasSuffix(trimmedName, ".gz") {
|
|
sqlFile, err := os.CreateTemp("", "gitea-install-db-upload-*.sql")
|
|
if err != nil {
|
|
cleanup()
|
|
return "", nil, err
|
|
}
|
|
sqlPath := sqlFile.Name()
|
|
gzipCleanup := func() {
|
|
cleanup()
|
|
_ = os.Remove(sqlPath)
|
|
}
|
|
|
|
inputFile, err := os.Open(inputPath)
|
|
if err != nil {
|
|
_ = sqlFile.Close()
|
|
gzipCleanup()
|
|
return "", nil, err
|
|
}
|
|
defer inputFile.Close()
|
|
|
|
gzipReader, err := gzip.NewReader(inputFile)
|
|
if err != nil {
|
|
_ = sqlFile.Close()
|
|
gzipCleanup()
|
|
return "", nil, err
|
|
}
|
|
defer gzipReader.Close()
|
|
|
|
if _, err := io.Copy(sqlFile, gzipReader); err != nil {
|
|
_ = sqlFile.Close()
|
|
gzipCleanup()
|
|
return "", nil, err
|
|
}
|
|
if err := sqlFile.Close(); err != nil {
|
|
gzipCleanup()
|
|
return "", nil, err
|
|
}
|
|
return sqlPath, gzipCleanup, nil
|
|
}
|
|
|
|
return inputPath, cleanup, nil
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
func importedDBType(dbType, fallback string) string {
|
|
dbType = strings.TrimSpace(strings.ToLower(dbType))
|
|
switch dbType {
|
|
case "sqlite":
|
|
dbType = "sqlite3" // edit/add - by petru @ codex
|
|
case "postgresql":
|
|
dbType = "postgres" // edit/add - by petru @ codex
|
|
}
|
|
if dbType != "" {
|
|
return dbType
|
|
}
|
|
return fallback
|
|
}
|
|
|
|
func importedInt(sec setting.ConfigSection, key string, fallback int) int {
|
|
cfgKey := setting.ConfigSectionKey(sec, key)
|
|
if cfgKey == nil {
|
|
return fallback
|
|
}
|
|
return cfgKey.MustInt(fallback)
|
|
}
|
|
|
|
func importedInt64(sec setting.ConfigSection, key string, fallback int64) int64 {
|
|
cfgKey := setting.ConfigSectionKey(sec, key)
|
|
if cfgKey == nil {
|
|
return fallback
|
|
}
|
|
return cfgKey.MustInt64(fallback)
|
|
}
|
|
|
|
func importedFirstLang(value, fallback string) string {
|
|
for _, lang := range strings.Split(value, ",") {
|
|
lang = strings.TrimSpace(lang)
|
|
if lang != "" {
|
|
return resolveInstallDefaultLanguage(lang)
|
|
}
|
|
}
|
|
return resolveInstallDefaultLanguage(fallback)
|
|
}
|
|
|
|
func populateInstallFormFromConfig(form *forms.InstallForm, cfg setting.ConfigProvider, fallbackDBType string) string {
|
|
rootSec := cfg.Section("")
|
|
dbSec := cfg.Section("database")
|
|
repoSec := cfg.Section("repository")
|
|
serverSec := cfg.Section("server")
|
|
lfsSec := cfg.Section("lfs")
|
|
logSec := cfg.Section("log")
|
|
mailerSec := cfg.Section("mailer")
|
|
serviceSec := cfg.Section("service")
|
|
openIDSec := cfg.Section("openid")
|
|
securitySec := cfg.Section("security")
|
|
adminSec := cfg.Section("admin")
|
|
i18nSec := cfg.Section("i18n")
|
|
repoReleaseSec := cfg.Section("repository.release")
|
|
backupSec := cfg.Section("backup") // edit/add - by petru @ codex
|
|
backupCronSec := cfg.Section("cron.database_backup") // edit/add - by petru @ codex
|
|
|
|
form.AppName = setting.ConfigSectionKeyString(rootSec, "APP_NAME", form.AppName)
|
|
form.RunUser = setting.ConfigSectionKeyString(rootSec, "RUN_USER", form.RunUser)
|
|
|
|
form.DbType = importedDBType(setting.ConfigSectionKeyString(dbSec, "DB_TYPE", fallbackDBType), fallbackDBType)
|
|
form.DbHost = setting.ConfigSectionKeyString(dbSec, "HOST", form.DbHost)
|
|
form.DbUser = setting.ConfigSectionKeyString(dbSec, "USER", form.DbUser)
|
|
form.DbPasswd = setting.ConfigSectionKeyString(dbSec, "PASSWD", form.DbPasswd)
|
|
form.DbName = setting.ConfigSectionKeyString(dbSec, "NAME", form.DbName)
|
|
form.DbSchema = setting.ConfigSectionKeyString(dbSec, "SCHEMA", form.DbSchema)
|
|
form.SSLMode = setting.ConfigSectionKeyString(dbSec, "SSL_MODE", form.SSLMode)
|
|
form.DbPath = setting.ConfigSectionKeyString(dbSec, "PATH", form.DbPath)
|
|
|
|
form.DefaultLanguage = importedFirstLang(setting.ConfigSectionKeyString(i18nSec, "LANGS"), form.DefaultLanguage)
|
|
form.RepoRootPath = setting.ConfigSectionKeyString(repoSec, "ROOT", form.RepoRootPath)
|
|
form.AllowAdoptionOfUnadoptedRepositories = setting.ConfigSectionKeyBool(repoSec, "ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES", form.AllowAdoptionOfUnadoptedRepositories) // edit/add - by petru @ codex
|
|
form.AllowDeleteOfUnadoptedRepositories = setting.ConfigSectionKeyBool(repoSec, "ALLOW_DELETE_OF_UNADOPTED_REPOSITORIES", form.AllowDeleteOfUnadoptedRepositories) // edit/add - by petru @ codex
|
|
form.LFSRootPath = setting.ConfigSectionKeyString(lfsSec, "PATH", form.LFSRootPath)
|
|
form.Domain = setting.ConfigSectionKeyString(serverSec, "DOMAIN", setting.ConfigSectionKeyString(serverSec, "SSH_DOMAIN", form.Domain))
|
|
form.SSHPort = importedInt(serverSec, "SSH_PORT", form.SSHPort)
|
|
if setting.ConfigSectionKeyBool(serverSec, "DISABLE_SSH") {
|
|
form.SSHPort = 0
|
|
}
|
|
form.HTTPPort = setting.ConfigSectionKeyString(serverSec, "HTTP_PORT", form.HTTPPort)
|
|
form.AppURL = setting.ConfigSectionKeyString(serverSec, "ROOT_URL", form.AppURL)
|
|
form.LogRootPath = setting.ConfigSectionKeyString(logSec, "ROOT_PATH", form.LogRootPath)
|
|
|
|
form.SMTPAddr = ""
|
|
form.SMTPPort = ""
|
|
form.SMTPFrom = ""
|
|
form.SMTPFromName = ""
|
|
form.SMTPFromAddress = ""
|
|
form.SMTPUser = ""
|
|
form.SMTPPasswd = ""
|
|
|
|
if setting.ConfigSectionKeyBool(mailerSec, "ENABLED") {
|
|
form.SMTPAddr = setting.ConfigSectionKeyString(mailerSec, "SMTP_ADDR", form.SMTPAddr)
|
|
form.SMTPPort = setting.ConfigSectionKeyString(mailerSec, "SMTP_PORT", form.SMTPPort)
|
|
form.SMTPFrom = setting.ConfigSectionKeyString(mailerSec, "FROM", form.SMTPFrom)
|
|
form.SMTPUser = setting.ConfigSectionKeyString(mailerSec, "USER", form.SMTPUser)
|
|
form.SMTPPasswd = setting.ConfigSectionKeyString(mailerSec, "PASSWD", form.SMTPPasswd)
|
|
}
|
|
populateInstallSMTPFromFields(form)
|
|
|
|
form.RegisterConfirm = setting.ConfigSectionKeyBool(serviceSec, "REGISTER_EMAIL_CONFIRM", form.RegisterConfirm)
|
|
form.RegisterManualConfirm = setting.ConfigSectionKeyBool(serviceSec, "REGISTER_MANUAL_CONFIRM", form.RegisterManualConfirm)
|
|
form.MailNotify = setting.ConfigSectionKeyBool(serviceSec, "ENABLE_NOTIFY_MAIL", form.MailNotify)
|
|
form.AdminCreatedAccountMode = setting.ConfigSectionKeyString(serviceSec, "ADMIN_CREATED_ACCOUNT_MODE", form.AdminCreatedAccountMode)
|
|
form.DisableRegistration = setting.ConfigSectionKeyBool(serviceSec, "DISABLE_REGISTRATION", form.DisableRegistration)
|
|
form.AllowOnlyInternalRegistration = setting.ConfigSectionKeyBool(serviceSec, "ALLOW_ONLY_INTERNAL_REGISTRATION", form.AllowOnlyInternalRegistration)
|
|
form.AllowOnlyExternalRegistration = setting.ConfigSectionKeyBool(serviceSec, "ALLOW_ONLY_EXTERNAL_REGISTRATION", form.AllowOnlyExternalRegistration)
|
|
form.RegistrationMode = ""
|
|
form.EnableCaptcha = setting.ConfigSectionKeyBool(serviceSec, "ENABLE_CAPTCHA", form.EnableCaptcha)
|
|
form.RequireSignInView = setting.ConfigSectionKeyBool(serviceSec, "REQUIRE_SIGNIN_VIEW", form.RequireSignInView)
|
|
form.DefaultKeepEmailPrivate = setting.ConfigSectionKeyBool(serviceSec, "DEFAULT_KEEP_EMAIL_PRIVATE", form.DefaultKeepEmailPrivate)
|
|
form.DefaultAllowCreateOrganization = setting.ConfigSectionKeyBool(serviceSec, "DEFAULT_ALLOW_CREATE_ORGANIZATION", form.DefaultAllowCreateOrganization)
|
|
form.DefaultEnableTimetracking = setting.ConfigSectionKeyBool(serviceSec, "DEFAULT_ENABLE_TIMETRACKING", form.DefaultEnableTimetracking)
|
|
form.NoReplyAddress = setting.ConfigSectionKeyString(serviceSec, "NO_REPLY_ADDRESS", form.NoReplyAddress)
|
|
|
|
form.EnableOpenIDSignIn = setting.ConfigSectionKeyBool(openIDSec, "ENABLE_OPENID_SIGNIN", form.EnableOpenIDSignIn)
|
|
form.EnableOpenIDSignUp = setting.ConfigSectionKeyBool(openIDSec, "ENABLE_OPENID_SIGNUP", form.EnableOpenIDSignUp)
|
|
form.EnableUpdateChecker = setting.ConfigSectionKeyBool(securitySec, "ENABLE_UPDATE_CHECKER", form.EnableUpdateChecker)
|
|
form.PasswordAlgorithm = setting.ConfigSectionKeyString(securitySec, "PASSWORD_HASH_ALGO", form.PasswordAlgorithm)
|
|
form.AdminManagementPolicy = setting.ConfigSectionKeyString(adminSec, "ADMIN_MANAGEMENT_POLICY", form.AdminManagementPolicy)
|
|
form.ReleaseMaxFiles = importedInt64(repoReleaseSec, "MAX_FILES", form.ReleaseMaxFiles)
|
|
form.ReleaseFileMaxSize = importedInt64(repoReleaseSec, "FILE_MAX_SIZE", form.ReleaseFileMaxSize)
|
|
// start edit/add - by petru @ codex
|
|
form.BackupPath = setting.ConfigSectionKeyString(backupSec, "PATH", form.BackupPath)
|
|
form.BackupRetentionCount = importedInt(backupSec, "RETENTION_COUNT", form.BackupRetentionCount)
|
|
form.BackupCompress = setting.ConfigSectionKeyBool(backupSec, "COMPRESS", form.BackupCompress)
|
|
form.BackupIncludeAppINISnapshot = setting.ConfigSectionKeyBool(backupSec, "INCLUDE_APP_INI_SNAPSHOT", form.BackupIncludeAppINISnapshot)
|
|
form.BackupEnabled = setting.ConfigSectionKeyBool(backupCronSec, "ENABLED", form.BackupEnabled)
|
|
form.BackupRunAtStart = setting.ConfigSectionKeyBool(backupCronSec, "RUN_AT_START", form.BackupRunAtStart)
|
|
form.BackupSchedule = setting.ConfigSectionKeyString(backupCronSec, "SCHEDULE", form.BackupSchedule)
|
|
// end edit/add - by petru @ codex
|
|
if form.ImportSensitiveSecrets {
|
|
form.ImportedLFSJWTSecret = readImportedSecretValue(serverSec, "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
|
form.ImportedInternalToken = readImportedSecretValue(securitySec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
|
form.ImportedOAuth2JWTSecret = readImportedSecretValue(cfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET")
|
|
}
|
|
normalizeInstallRegistrationOptions(form)
|
|
|
|
return form.DbType
|
|
}
|
|
|
|
func readImportedSecretValue(sec setting.ConfigSection, uriKey, verbatimKey string) string {
|
|
verbatim := setting.ConfigSectionKeyString(sec, verbatimKey)
|
|
if verbatim != "" {
|
|
return verbatim
|
|
}
|
|
|
|
uriValue := setting.ConfigSectionKeyString(sec, uriKey)
|
|
if uriValue == "" {
|
|
return ""
|
|
}
|
|
|
|
parsed, err := url.Parse(uriValue)
|
|
if err != nil || parsed.Scheme != "file" {
|
|
return ""
|
|
}
|
|
|
|
path := parsed.Path
|
|
if parsed.Opaque != "" {
|
|
path = parsed.Opaque
|
|
}
|
|
if parsed.Host != "" {
|
|
switch {
|
|
case len(parsed.Host) == 2 && parsed.Host[1] == ':':
|
|
path = parsed.Host + path
|
|
default:
|
|
path = "//" + parsed.Host + path
|
|
}
|
|
}
|
|
path, err = url.PathUnescape(path)
|
|
if err != nil || path == "" {
|
|
return ""
|
|
}
|
|
if runtime.GOOS == "windows" && strings.HasPrefix(path, "/") && len(path) > 2 && path[2] == ':' {
|
|
path = path[1:]
|
|
}
|
|
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(string(data))
|
|
}
|
|
|
|
func readImportedInstallConfig(ctx *context.Context) (setting.ConfigProvider, error) {
|
|
file, header, err := ctx.Req.FormFile("app_ini_file")
|
|
if errors.Is(err, http.ErrMissingFile) {
|
|
return nil, errors.New(string(ctx.Tr("install.import_app_ini_missing")))
|
|
}
|
|
if err != nil {
|
|
return nil, errors.New(string(ctx.Tr("install.import_app_ini_read_failed", err)))
|
|
}
|
|
defer file.Close()
|
|
|
|
if header.Size > installAppINIImportMaxSize {
|
|
return nil, errors.New(string(ctx.Tr("install.import_app_ini_too_big", installAppINIImportMaxSize/1024)))
|
|
}
|
|
|
|
data, err := io.ReadAll(io.LimitReader(file, installAppINIImportMaxSize+1))
|
|
if err != nil {
|
|
return nil, errors.New(string(ctx.Tr("install.import_app_ini_read_failed", err)))
|
|
}
|
|
if int64(len(data)) > installAppINIImportMaxSize {
|
|
return nil, errors.New(string(ctx.Tr("install.import_app_ini_too_big", installAppINIImportMaxSize/1024)))
|
|
}
|
|
|
|
cfg, err := setting.NewConfigProviderFromData(string(data))
|
|
if err != nil {
|
|
return nil, errors.New(string(ctx.Tr("install.import_app_ini_invalid", err)))
|
|
}
|
|
return cfg, nil
|
|
}
|
|
|
|
func populateInstallSensitiveSecretsFromConfig(form *forms.InstallForm, cfg setting.ConfigProvider) {
|
|
if !form.ImportSensitiveSecrets {
|
|
return
|
|
}
|
|
if form.ImportedLFSJWTSecret == "" {
|
|
form.ImportedLFSJWTSecret = readImportedSecretValue(cfg.Section("server"), "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
|
}
|
|
if form.ImportedInternalToken == "" {
|
|
form.ImportedInternalToken = readImportedSecretValue(cfg.Section("security"), "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
|
}
|
|
if form.ImportedOAuth2JWTSecret == "" {
|
|
form.ImportedOAuth2JWTSecret = readImportedSecretValue(cfg.Section("oauth2"), "JWT_SECRET_URI", "JWT_SECRET")
|
|
}
|
|
}
|
|
|
|
func applyInstallSensitiveSecretsToConfig(cfg setting.ConfigProvider, form *forms.InstallForm) error {
|
|
if form.LFSRootPath != "" {
|
|
if form.ImportSensitiveSecrets && form.ImportedLFSJWTSecret != "" {
|
|
cfg.Section("server").DeleteKey("LFS_JWT_SECRET_URI")
|
|
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(form.ImportedLFSJWTSecret)
|
|
} else if !cfg.Section("server").HasKey("LFS_JWT_SECRET") && !cfg.Section("server").HasKey("LFS_JWT_SECRET_URI") { // edit/add - by petru @ codex
|
|
_, lfsJwtSecret := generate.NewJwtSecretWithBase64()
|
|
cfg.Section("server").Key("LFS_JWT_SECRET").SetValue(lfsJwtSecret)
|
|
}
|
|
}
|
|
|
|
// the internal token could be read from INTERNAL_TOKEN or INTERNAL_TOKEN_URI (the file is guaranteed to be non-empty)
|
|
// if there is no InternalToken, generate one and save to security.INTERNAL_TOKEN
|
|
if form.ImportSensitiveSecrets && form.ImportedInternalToken != "" {
|
|
cfg.Section("security").DeleteKey("INTERNAL_TOKEN_URI")
|
|
cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(form.ImportedInternalToken)
|
|
} else if setting.InternalToken == "" {
|
|
internalToken, err := generate.NewInternalToken()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cfg.Section("security").Key("INTERNAL_TOKEN").SetValue(internalToken)
|
|
}
|
|
|
|
// FIXME: at the moment, no matter oauth2 is enabled or not, it must generate a "oauth2 JWT_SECRET"
|
|
// see the "loadOAuth2From" in "setting/oauth2.go"
|
|
if form.ImportSensitiveSecrets && form.ImportedOAuth2JWTSecret != "" {
|
|
cfg.Section("oauth2").DeleteKey("JWT_SECRET_URI")
|
|
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(form.ImportedOAuth2JWTSecret)
|
|
} else if !cfg.Section("oauth2").HasKey("JWT_SECRET") && !cfg.Section("oauth2").HasKey("JWT_SECRET_URI") {
|
|
_, jwtSecretBase64 := generate.NewJwtSecretWithBase64()
|
|
cfg.Section("oauth2").Key("JWT_SECRET").SetValue(jwtSecretBase64)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Install render installation page
|
|
func Install(ctx *context.Context) {
|
|
if setting.InstallLock { // edit/add - by petru @ codex
|
|
if isRecoveryRequest(ctx.Req) {
|
|
// start edit/add - by petru @ codex
|
|
if dbPreviouslyUsed, err := detectInstallExistingDatabase(ctx); err == nil && dbPreviouslyUsed {
|
|
ctx.Redirect(setting.AppSubURL+"/post-install", http.StatusSeeOther)
|
|
return
|
|
}
|
|
// end edit/add - by petru @ codex
|
|
} else {
|
|
InstallDone(ctx)
|
|
return
|
|
}
|
|
}
|
|
|
|
form, curDBType := newInstallFormFromSettings()
|
|
renderInstallPage(ctx, &form, curDBType)
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func applyInstallImportPreviewStateFromRequest(form *forms.InstallForm, ctx *context.Context) {
|
|
if form == nil {
|
|
return
|
|
}
|
|
|
|
form.ImportSensitiveSecrets = ctx.FormBool("import_sensitive_secrets")
|
|
form.ImportedAppINI = ctx.FormBool("imported_app_ini")
|
|
form.BackupRestoreID = ctx.FormString("backup_restore_id")
|
|
form.BackupImportAppINI = ctx.FormBool("backup_import_app_ini")
|
|
form.BackupRestoreDB = ctx.FormBool("backup_restore_db")
|
|
form.RecoveryMode = ctx.FormString("recovery_mode")
|
|
form.ReinstallConfirmFirst = ctx.FormBool("reinstall_confirm_first")
|
|
form.ReinstallConfirmSecond = ctx.FormBool("reinstall_confirm_second")
|
|
form.ReinstallConfirmThird = ctx.FormBool("reinstall_confirm_third")
|
|
form.ImportedLFSJWTSecret = ctx.FormString("imported_lfs_jwt_secret")
|
|
form.ImportedInternalToken = ctx.FormString("imported_internal_token")
|
|
form.ImportedOAuth2JWTSecret = ctx.FormString("imported_o_auth2_jwt_secret")
|
|
if backupPath := strings.TrimSpace(ctx.FormString("backup_path")); backupPath != "" {
|
|
form.BackupPath = backupPath
|
|
}
|
|
normalizeInstallRecoverySelectionState(form, false) // edit/add - by petru @ codex
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func applyImportedAppINIDefaults(form *forms.InstallForm) {
|
|
if form == nil {
|
|
return
|
|
}
|
|
|
|
form.ImportedAppINI = true
|
|
form.ImportSensitiveSecrets = true
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
// start edit/add - by petru @ codex
|
|
func normalizeInstallRecoverySelectionState(form *forms.InstallForm, uploadedDatabaseBackupProvided bool) {
|
|
if form == nil {
|
|
return
|
|
}
|
|
|
|
if form.BackupImportAppINI {
|
|
form.ImportSensitiveSecrets = true // edit/add - by petru @ codex
|
|
}
|
|
if uploadedDatabaseBackupProvided || (form.BackupRestoreDB && strings.TrimSpace(form.BackupRestoreID) != "") {
|
|
form.RecoveryMode = recoveryModeDatabaseBackup
|
|
}
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func hasSelectedInstallDatabaseBackupSource(form *forms.InstallForm, uploadedDatabaseBackupProvided bool) bool {
|
|
if form == nil {
|
|
return uploadedDatabaseBackupProvided
|
|
}
|
|
return uploadedDatabaseBackupProvided || strings.TrimSpace(form.BackupRestoreID) != ""
|
|
}
|
|
|
|
func defaultInstallRecoveryAllowedEmails(form *forms.InstallForm) string { // edit/add - by petru @ codex
|
|
if form == nil || strings.TrimSpace(form.RecoveryAllowedEmails) != "" {
|
|
return ""
|
|
}
|
|
return strings.TrimSpace(form.AdminEmail)
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
func ImportAppINI(ctx *context.Context) {
|
|
if setting.InstallLock && !isRecoveryRequest(ctx.Req) { // edit/add - by petru @ codex
|
|
InstallDone(ctx)
|
|
return
|
|
}
|
|
|
|
form, curDBType := newInstallFormFromSettings()
|
|
applyInstallImportPreviewStateFromRequest(&form, ctx) // edit/add - by petru @ codex
|
|
applyImportedAppINIDefaults(&form) // edit/add - by petru @ codex
|
|
cfg, err := readImportedInstallConfig(ctx)
|
|
if err != nil {
|
|
ctx.Data["CurDbType"] = curDBType
|
|
ctx.RenderWithErrDeprecated(err.Error(), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
curDBType = populateInstallFormFromConfig(&form, cfg, curDBType)
|
|
ctx.Flash.Success(ctx.Tr("install.import_app_ini_success"), true)
|
|
renderInstallPage(ctx, &form, curDBType)
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func ImportBackupAppINI(ctx *context.Context) {
|
|
if setting.InstallLock && !isRecoveryRequest(ctx.Req) {
|
|
InstallDone(ctx)
|
|
return
|
|
}
|
|
|
|
form, curDBType := newInstallFormFromSettings()
|
|
applyInstallImportPreviewStateFromRequest(&form, ctx)
|
|
if form.BackupRestoreID == "" {
|
|
ctx.Data["CurDbType"] = curDBType
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_database_backup_select_required"), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
cfg, err := dbbackup_service.LoadBackupAppINIConfig(resolveInstallBackupPath(form.BackupPath), form.BackupRestoreID)
|
|
if err != nil {
|
|
ctx.Data["CurDbType"] = curDBType
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_database_backup_restore_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
if cfg == nil {
|
|
ctx.Data["CurDbType"] = curDBType
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_import_app_ini_required"), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
applyBackupAppINIToInstallForm(&form, cfg)
|
|
ctx.Flash.Success(ctx.Tr("install.import_app_ini_success"), true)
|
|
renderInstallPage(ctx, &form, form.DbType)
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
func installMailerDisplayName(appName string) string {
|
|
appName = strings.TrimSpace(appName)
|
|
if appName == "" {
|
|
return "Gitea"
|
|
}
|
|
|
|
if siteName, _, found := strings.Cut(appName, ":"); found {
|
|
siteName = strings.TrimSpace(siteName)
|
|
if siteName != "" {
|
|
return siteName
|
|
}
|
|
}
|
|
|
|
if parts := strings.Fields(appName); len(parts) > 0 {
|
|
return parts[0]
|
|
}
|
|
|
|
return appName
|
|
}
|
|
|
|
func composeInstallSMTPFrom(appName, fromName, fromAddress string) (string, error) {
|
|
fromAddress = strings.TrimSpace(fromAddress)
|
|
if fromAddress == "" {
|
|
return "", errors.New("missing from address")
|
|
}
|
|
|
|
if _, err := mail.ParseAddress(fromAddress); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
fromName = strings.TrimSpace(fromName)
|
|
if fromName == "" {
|
|
fromName = installMailerDisplayName(appName)
|
|
}
|
|
|
|
if installSMTPUnquotedDisplayNamePattern.MatchString(fromName) {
|
|
return fromName + " <" + fromAddress + ">", nil
|
|
}
|
|
|
|
return (&mail.Address{
|
|
Name: fromName,
|
|
Address: fromAddress,
|
|
}).String(), nil
|
|
}
|
|
|
|
func populateInstallSMTPFromFields(form *forms.InstallForm) {
|
|
form.SMTPFromName = strings.TrimSpace(form.SMTPFromName)
|
|
form.SMTPFromAddress = strings.TrimSpace(form.SMTPFromAddress)
|
|
|
|
if form.SMTPFrom != "" {
|
|
if parsedFrom, err := mail.ParseAddress(form.SMTPFrom); err == nil {
|
|
if form.SMTPFromName == "" {
|
|
form.SMTPFromName = parsedFrom.Name
|
|
}
|
|
if form.SMTPFromAddress == "" {
|
|
form.SMTPFromAddress = parsedFrom.Address
|
|
}
|
|
} else if form.SMTPFromAddress == "" {
|
|
form.SMTPFromAddress = strings.TrimSpace(form.SMTPFrom)
|
|
}
|
|
}
|
|
|
|
if form.SMTPFromName == "" {
|
|
form.SMTPFromName = installMailerDisplayName(form.AppName)
|
|
}
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
func applyImportedAppINIRepositoryRecoveryDefaults(form *forms.InstallForm, dbPreviouslyUsed bool) {
|
|
if form.ImportedAppINI && !dbPreviouslyUsed && form.RecoveryMode == recoveryModeRepositoryFileSystem {
|
|
form.AllowAdoptionOfUnadoptedRepositories = true
|
|
form.AllowDeleteOfUnadoptedRepositories = true
|
|
}
|
|
}
|
|
|
|
func applyBackupAppINIToInstallForm(form *forms.InstallForm, cfg setting.ConfigProvider) {
|
|
// start edit/add - by petru @ codex
|
|
if form == nil || cfg == nil {
|
|
return
|
|
}
|
|
|
|
selectedRecoveryMode := form.RecoveryMode
|
|
selectedBackupRestoreID := form.BackupRestoreID
|
|
selectedBackupPath := form.BackupPath
|
|
selectedBackupImportAppINI := form.BackupImportAppINI
|
|
|
|
curDBType := importedDBType(form.DbType, form.DbType)
|
|
populateInstallFormFromConfig(form, cfg, curDBType)
|
|
applyImportedAppINIDefaults(form)
|
|
populateInstallSensitiveSecretsFromConfig(form, cfg)
|
|
|
|
form.RecoveryMode = selectedRecoveryMode
|
|
form.BackupRestoreID = selectedBackupRestoreID
|
|
form.BackupPath = selectedBackupPath
|
|
form.BackupImportAppINI = selectedBackupImportAppINI
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
func detectInstallExistingDatabase(ctx std_context.Context) (bool, error) {
|
|
// start edit/add - by petru @ codex
|
|
db.UnsetDefaultEngine()
|
|
defer db.UnsetDefaultEngine()
|
|
|
|
if err := db.InitEngine(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
if err := db_install.CheckDatabaseConnection(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
|
|
hasPostInstallationUser, err := db_install.HasPostInstallationUsers(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
dbMigrationVersion, err := db_install.GetMigrationVersion(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
return hasPostInstallationUser || dbMigrationVersion > 0, nil
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
// end edit/add - by petru @ codex
|
|
|
|
// TestMail checks the mail settings entered on the install page.
|
|
func TestMail(ctx *context.Context) {
|
|
email := strings.TrimSpace(ctx.FormString("test_mail_email"))
|
|
if email == "" {
|
|
email = strings.TrimSpace(ctx.FormString("admin_email"))
|
|
}
|
|
|
|
state := "success"
|
|
message := string(ctx.Tr("admin.config.test_mail_sent", email))
|
|
ok := true
|
|
|
|
mailService, err := buildInstallTestMailService(ctx)
|
|
if err != nil {
|
|
state = "failed"
|
|
message = err.Error()
|
|
ok = false
|
|
} else if err := mailer.SendTestMailWith(email, mailService); err != nil {
|
|
state = "failed"
|
|
message = string(ctx.Tr("admin.config.test_mail_failed", email, mailer.ShortTestMailError(err)))
|
|
ok = false
|
|
}
|
|
|
|
ctx.JSON(http.StatusOK, map[string]any{
|
|
"ok": ok,
|
|
"state": state,
|
|
"message": message,
|
|
"email": email,
|
|
})
|
|
}
|
|
|
|
func buildInstallTestMailService(ctx *context.Context) (*setting.Mailer, error) {
|
|
smtpAddr := strings.TrimSpace(ctx.FormString("smtp_addr"))
|
|
smtpPort := strings.TrimSpace(ctx.FormString("smtp_port"))
|
|
appName := strings.TrimSpace(ctx.FormString("app_name"))
|
|
smtpFromName := strings.TrimSpace(ctx.FormString("smtp_from_name"))
|
|
smtpFromAddress := strings.TrimSpace(ctx.FormString("smtp_from_address"))
|
|
|
|
if smtpAddr == "" {
|
|
return nil, errors.New(string(ctx.Tr("install.test_mail_missing_host")))
|
|
}
|
|
|
|
email := strings.TrimSpace(ctx.FormString("test_mail_email"))
|
|
if email == "" {
|
|
email = strings.TrimSpace(ctx.FormString("admin_email"))
|
|
}
|
|
if email == "" {
|
|
return nil, errors.New(string(ctx.Tr("install.test_mail_missing_recipient")))
|
|
}
|
|
if _, err := mail.ParseAddress(email); err != nil {
|
|
return nil, errors.New(string(ctx.Tr("form.email_invalid")))
|
|
}
|
|
|
|
smtpFrom, err := composeInstallSMTPFrom(appName, smtpFromName, smtpFromAddress)
|
|
if err != nil {
|
|
return nil, errors.New(string(ctx.Tr("install.smtp_from_invalid")))
|
|
}
|
|
|
|
parsedFrom, err := mail.ParseAddress(smtpFrom)
|
|
if err != nil {
|
|
return nil, errors.New(string(ctx.Tr("install.smtp_from_invalid")))
|
|
}
|
|
|
|
protocol := inferInstallMailProtocol(smtpAddr, smtpPort)
|
|
if smtpPort == "" {
|
|
switch protocol {
|
|
case "smtp":
|
|
smtpPort = "25"
|
|
case "smtp+starttls":
|
|
smtpPort = "587"
|
|
default:
|
|
smtpPort = "465"
|
|
}
|
|
}
|
|
|
|
return &setting.Mailer{
|
|
Name: installMailerDisplayName(appName),
|
|
From: smtpFrom,
|
|
FromName: parsedFrom.Name,
|
|
FromEmail: parsedFrom.Address,
|
|
Protocol: protocol,
|
|
SMTPAddr: smtpAddr,
|
|
SMTPPort: smtpPort,
|
|
User: strings.TrimSpace(ctx.FormString("smtp_user")),
|
|
Passwd: ctx.FormString("smtp_passwd"),
|
|
EnableHelo: true,
|
|
OverrideHeader: map[string][]string{},
|
|
}, nil
|
|
}
|
|
|
|
func inferInstallMailProtocol(smtpAddr, smtpPort string) string {
|
|
if strings.ContainsAny(smtpAddr, "/\\") {
|
|
return "smtp+unix"
|
|
}
|
|
switch smtpPort {
|
|
case "25":
|
|
return "smtp"
|
|
case "587":
|
|
return "smtp+starttls"
|
|
default:
|
|
return "smtps"
|
|
}
|
|
}
|
|
|
|
func checkDatabase(ctx *context.Context, form *forms.InstallForm, availableBackups []*installBackupChoice, uploadedDatabaseBackupProvided bool) (bool, bool) {
|
|
var err error
|
|
|
|
if (setting.Database.Type == "sqlite3") &&
|
|
len(setting.Database.Path) == 0 {
|
|
ctx.Data["Err_DbPath"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_db_path"), tplInstall, form)
|
|
return false, false
|
|
}
|
|
|
|
// Check if the user is trying to re-install in an installed database
|
|
db.UnsetDefaultEngine()
|
|
defer db.UnsetDefaultEngine()
|
|
|
|
if err = db.InitEngine(ctx); err != nil {
|
|
if strings.Contains(err.Error(), `Unknown database type: sqlite3`) {
|
|
ctx.Data["Err_DbType"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.sqlite3_not_available", "https://docs.gitea.com/installation/install-from-binary"), tplInstall, form)
|
|
} else {
|
|
ctx.Data["Err_DbSetting"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form)
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
err = db_install.CheckDatabaseConnection(ctx)
|
|
if err != nil {
|
|
ctx.Data["Err_DbSetting"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, form)
|
|
return false, false
|
|
}
|
|
|
|
hasPostInstallationUser, err := db_install.HasPostInstallationUsers(ctx)
|
|
if err != nil {
|
|
ctx.Data["Err_DbSetting"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_table", "user", err), tplInstall, form)
|
|
return false, false
|
|
}
|
|
dbMigrationVersion, err := db_install.GetMigrationVersion(ctx)
|
|
if err != nil {
|
|
ctx.Data["Err_DbSetting"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_table", "version", err), tplInstall, form)
|
|
return false, false
|
|
}
|
|
|
|
dbPreviouslyUsed := hasPostInstallationUser || dbMigrationVersion > 0 // edit/add - by petru @ codex
|
|
|
|
if hasPostInstallationUser && dbMigrationVersion > 0 {
|
|
log.Error("The database is likely to have been used by Gitea before, database migration version=%d", dbMigrationVersion)
|
|
setInstallRecoverySourceAvailability(ctx, true, availableBackups, false) // edit/add - by petru @ codex
|
|
if form.RecoveryMode == "" {
|
|
form.RecoveryMode = recoveryModeExistingDatabase
|
|
}
|
|
|
|
recoverySelectionValid := installRecoveryConfirmationsAccepted(form)
|
|
switch form.RecoveryMode {
|
|
case recoveryModeExistingDatabase:
|
|
// nothing extra
|
|
case recoveryModeDatabaseBackup:
|
|
recoverySelectionValid = recoverySelectionValid && hasSelectedInstallDatabaseBackupSource(form, uploadedDatabaseBackupProvided)
|
|
default:
|
|
recoverySelectionValid = false
|
|
}
|
|
if !recoverySelectionValid {
|
|
if form.RecoveryMode == recoveryModeDatabaseBackup && (len(availableBackups) > 0 || uploadedDatabaseBackupProvided) {
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
assignInstallPageData(ctx, form, form.DbType, availableBackups, nil) // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoveryProblemText"] = ctx.Tr("install.recovery_database_backup_error") // edit/add - by petru @ codex
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_database_backup_error"), tplInstall, form)
|
|
return false, dbPreviouslyUsed
|
|
}
|
|
ctx.Data["Err_DbInstalledBefore"] = true
|
|
assignInstallPageData(ctx, form, form.DbType, availableBackups, nil) // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoveryProblemText"] = ctx.Tr("install.reinstall_error") // edit/add - by petru @ codex
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.reinstall_error"), tplInstall, form)
|
|
return false, dbPreviouslyUsed
|
|
}
|
|
|
|
if form.RecoveryMode == recoveryModeDatabaseBackup {
|
|
log.Info("User chose database-backup recovery over a pre-existing Gitea database; the selected recovery configuration source will be applied")
|
|
return true, dbPreviouslyUsed
|
|
}
|
|
|
|
log.Info("User confirmed re-installation of Gitea into a pre-existing database")
|
|
}
|
|
|
|
if dbPreviouslyUsed {
|
|
log.Info("Gitea will be installed in a database with: hasPostInstallationUser=%v, dbMigrationVersion=%v", hasPostInstallationUser, dbMigrationVersion)
|
|
}
|
|
|
|
return true, dbPreviouslyUsed
|
|
}
|
|
|
|
// SubmitInstall response for submit install items
|
|
func SubmitInstall(ctx *context.Context) {
|
|
if setting.InstallLock && !isRecoveryRequest(ctx.Req) { // edit/add - by petru @ codex
|
|
InstallDone(ctx)
|
|
return
|
|
}
|
|
|
|
var err error
|
|
var backupAppINICfg setting.ConfigProvider // edit/add - by petru @ codex
|
|
|
|
form := *web.GetForm(ctx).(*forms.InstallForm)
|
|
recoveryRequest := isRecoveryRequest(ctx.Req) || ctx.FormBool("recovery_request") // edit/add - by petru @ codex
|
|
if recoveryRequest {
|
|
setting.AllowSQLite3CreateForInstall = true // edit/add - by petru @ codex
|
|
defer func() {
|
|
setting.AllowSQLite3CreateForInstall = false // edit/add - by petru @ codex
|
|
}()
|
|
}
|
|
form.ImportSensitiveSecrets = ctx.FormBool("import_sensitive_secrets")
|
|
form.ImportedAppINI = ctx.FormBool("imported_app_ini") // edit/add - by petru @ codex
|
|
form.BackupImportAppINI = ctx.FormBool("backup_import_app_ini") // edit/add - by petru @ codex
|
|
form.BackupRestoreDB = ctx.FormBool("backup_restore_db") // edit/add - by petru @ codex
|
|
form.ImportedLFSJWTSecret = ctx.FormString("imported_lfs_jwt_secret")
|
|
form.ImportedInternalToken = ctx.FormString("imported_internal_token")
|
|
form.ImportedOAuth2JWTSecret = ctx.FormString("imported_o_auth2_jwt_secret")
|
|
if form.ImportSensitiveSecrets && (form.ImportedLFSJWTSecret == "" || form.ImportedInternalToken == "" || form.ImportedOAuth2JWTSecret == "") {
|
|
if importedCfg, err := readImportedInstallConfig(ctx); err == nil {
|
|
populateInstallSensitiveSecretsFromConfig(&form, importedCfg)
|
|
}
|
|
}
|
|
missingAppINIRecovery := recoveryRequest && !form.ImportedAppINI && !installCustomConfExists() // edit/add - by petru @ codex
|
|
uploadedDatabaseBackupProvided := installUploadedDatabaseBackupProvided(ctx) // edit/add - by petru @ codex
|
|
normalizeInstallRecoverySelectionState(&form, uploadedDatabaseBackupProvided) // edit/add - by petru @ codex
|
|
renderInstallError := func(message templatehtml.HTML, availableBackups []*installBackupChoice, backupDiscoveryErr error) {
|
|
effectiveCurDBType := form.DbType
|
|
if effectiveCurDBType == "" {
|
|
effectiveCurDBType = setting.Database.Type.String()
|
|
}
|
|
assignInstallPageData(ctx, &form, effectiveCurDBType, availableBackups, backupDiscoveryErr) // edit/add - by petru @ codex
|
|
ctx.Data["InstallRecoveryProblemText"] = message // edit/add - by petru @ codex
|
|
ctx.RenderWithErrDeprecated(string(message), tplInstall, &form)
|
|
}
|
|
|
|
// fix form values
|
|
if form.AppURL != "" && form.AppURL[len(form.AppURL)-1] != '/' {
|
|
form.AppURL += "/"
|
|
}
|
|
if form.AdminManagementPolicy == "" {
|
|
form.AdminManagementPolicy = setting.AdminManagementPolicyGrantorOnly
|
|
}
|
|
form.DefaultLanguage = strings.TrimSpace(form.DefaultLanguage)
|
|
normalizeInstallRegistrationOptions(&form)
|
|
populateInstallSMTPFromFields(&form)
|
|
// start edit/add - by petru @ codex
|
|
if normalizedRecoveryEmails, err := normalizeInstallRecoveryEmails(form.RecoveryAllowedEmails); err != nil {
|
|
ctx.Data["Err_BackupRecovery"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_email_invalid_addresses", err), tplInstall, &form)
|
|
return
|
|
} else {
|
|
form.RecoveryAllowedEmails = normalizedRecoveryEmails // edit/add - by petru @ codex
|
|
}
|
|
if defaultRecoveryAllowedEmails := defaultInstallRecoveryAllowedEmails(&form); defaultRecoveryAllowedEmails != "" { // edit/add - by petru @ codex
|
|
form.RecoveryAllowedEmails = defaultRecoveryAllowedEmails // edit/add - by petru @ codex
|
|
}
|
|
if form.RecoveryEmailEnabled && !missingAppINIRecovery {
|
|
if strings.TrimSpace(form.SMTPAddr) == "" {
|
|
ctx.Data["Err_BackupRecovery"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_email_requires_smtp"), tplInstall, &form)
|
|
return
|
|
}
|
|
if form.RecoveryAllowedEmails == "" {
|
|
ctx.Data["Err_BackupRecovery"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_email_requires_addresses"), tplInstall, &form)
|
|
return
|
|
}
|
|
}
|
|
// end edit/add - by petru @ codex
|
|
ctx.Data["InstallerSiteName"] = resolveInstallPageSiteName(&form, isRecoveryRequest(ctx.Req)) // edit/add - by petru @ codex
|
|
|
|
if form.DefaultLanguage == "" {
|
|
form.DefaultLanguage = resolveInstallDefaultLanguage("")
|
|
} else if !slices.Contains(setting.Langs, form.DefaultLanguage) {
|
|
ctx.Data["Err_DefaultLanguage"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.lang_select_error"), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
ctx.Data["CurDbType"] = form.DbType
|
|
|
|
if ctx.HasError() {
|
|
ctx.Data["Err_SMTP"] = ctx.Data["Err_SMTPUser"] != nil
|
|
ctx.Data["Err_Registration"] = ctx.Data["Err_RegistrationMode"] != nil
|
|
ctx.Data["Err_Admin"] = ctx.Data["Err_AdminName"] != nil || ctx.Data["Err_AdminPasswd"] != nil || ctx.Data["Err_AdminEmail"] != nil
|
|
ctx.HTML(http.StatusOK, tplInstall)
|
|
return
|
|
}
|
|
|
|
if _, err = exec.LookPath("git"); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.test_git_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
// ---- Basic checks are passed, now test configuration.
|
|
|
|
// Test database setting.
|
|
setting.Database.Type = setting.DatabaseType(form.DbType)
|
|
setting.Database.Host = form.DbHost
|
|
setting.Database.User = form.DbUser
|
|
setting.Database.Passwd = form.DbPasswd
|
|
setting.Database.Name = form.DbName
|
|
setting.Database.Schema = form.DbSchema
|
|
setting.Database.SSLMode = form.SSLMode
|
|
setting.Database.Path = form.DbPath
|
|
setting.Database.LogSQL = !setting.IsProd
|
|
|
|
// start edit/add - by petru @ codex
|
|
if recoveryRequest && form.BackupRestoreID != "" && form.BackupImportAppINI {
|
|
backupAppINICfg, err = dbbackup_service.LoadBackupAppINIConfig(resolveInstallBackupPath(form.BackupPath), form.BackupRestoreID)
|
|
if err != nil {
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_restore_failed", err), nil, nil)
|
|
return
|
|
}
|
|
if backupAppINICfg != nil {
|
|
applyBackupAppINIToInstallForm(&form, backupAppINICfg)
|
|
setting.Database.Type = setting.DatabaseType(form.DbType)
|
|
setting.Database.Host = form.DbHost
|
|
setting.Database.User = form.DbUser
|
|
setting.Database.Passwd = form.DbPasswd
|
|
setting.Database.Name = form.DbName
|
|
setting.Database.Schema = form.DbSchema
|
|
setting.Database.SSLMode = form.SSLMode
|
|
setting.Database.Path = form.DbPath
|
|
setting.Database.LogSQL = !setting.IsProd
|
|
}
|
|
}
|
|
normalizeInstallRecoverySelectionState(&form, uploadedDatabaseBackupProvided) // edit/add - by petru @ codex
|
|
|
|
hasRepositoryFilesystem := hasInstallRepositoryFilesystemRecovery(&form) // edit/add - by petru @ codex
|
|
availableBackups, err := listInstallRecoveryBackupChoices(&form, recoveryRequest)
|
|
if err != nil {
|
|
ctx.Data["Err_BackupPath"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_discovery_failed", err), nil, err)
|
|
return
|
|
}
|
|
hasDatabaseBackups := len(availableBackups) > 0
|
|
normalizeInstallRecoverySelectionState(&form, uploadedDatabaseBackupProvided) // edit/add - by petru @ codex
|
|
if uploadedDatabaseBackupProvided && form.BackupRestoreID != "" && form.BackupRestoreDB {
|
|
// start edit/add - by petru @ codex
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_multiple_database_backups_error"), availableBackups, nil)
|
|
return
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
if (form.BackupImportAppINI || form.BackupRestoreDB) && !uploadedDatabaseBackupProvided && form.BackupRestoreID == "" {
|
|
// start edit/add - by petru @ codex
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_select_required"), availableBackups, nil)
|
|
return
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
if form.RecoveryMode == recoveryModeDatabaseBackup && !hasDatabaseBackups {
|
|
form.RecoveryMode = "" // edit/add - by petru @ codex
|
|
}
|
|
if missingAppINIRecovery && !hasDatabaseBackups && !hasRepositoryFilesystem {
|
|
ctx.Data["Err_BackupRecovery"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_import_app_ini_required"), tplInstall, &form)
|
|
return
|
|
}
|
|
existingDatabaseDetected := false // edit/add - by petru @ codex
|
|
if recoveryRequest && form.ImportedAppINI {
|
|
existingDatabaseDetected, err = detectInstallExistingDatabase(ctx)
|
|
if err == nil && existingDatabaseDetected {
|
|
setInstallRecoverySourceAvailability(ctx, true, availableBackups, false)
|
|
if form.RecoveryMode == "" {
|
|
form.RecoveryMode = recoveryModeExistingDatabase
|
|
}
|
|
|
|
recoverySelectionValid := installRecoveryConfirmationsAccepted(&form)
|
|
switch form.RecoveryMode {
|
|
case recoveryModeExistingDatabase:
|
|
// nothing extra
|
|
case recoveryModeDatabaseBackup:
|
|
recoverySelectionValid = recoverySelectionValid && hasSelectedInstallDatabaseBackupSource(&form, uploadedDatabaseBackupProvided)
|
|
default:
|
|
recoverySelectionValid = false
|
|
}
|
|
if !recoverySelectionValid {
|
|
if form.RecoveryMode == recoveryModeDatabaseBackup && (hasDatabaseBackups || uploadedDatabaseBackupProvided) {
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_error"), availableBackups, nil)
|
|
return
|
|
}
|
|
ctx.Data["Err_DbInstalledBefore"] = true
|
|
renderInstallError(ctx.Tr("install.reinstall_error"), availableBackups, nil)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
skipDatabasePreflight := false
|
|
if recoveryRequest && !existingDatabaseDetected && (hasRepositoryFilesystem || hasDatabaseBackups) {
|
|
setInstallRecoverySourceAvailability(ctx, false, availableBackups, hasRepositoryFilesystem)
|
|
if form.RecoveryMode == "" {
|
|
if hasDatabaseBackups {
|
|
form.RecoveryMode = recoveryModeDatabaseBackup
|
|
} else {
|
|
form.RecoveryMode = recoveryModeRepositoryFileSystem
|
|
}
|
|
}
|
|
|
|
recoverySelectionValid := installRecoveryConfirmationsAccepted(&form)
|
|
switch form.RecoveryMode {
|
|
case recoveryModeDatabaseBackup:
|
|
recoverySelectionValid = recoverySelectionValid && hasSelectedInstallDatabaseBackupSource(&form, uploadedDatabaseBackupProvided)
|
|
case recoveryModeRepositoryFileSystem:
|
|
// nothing extra
|
|
default:
|
|
recoverySelectionValid = false
|
|
}
|
|
|
|
if !recoverySelectionValid {
|
|
if form.RecoveryMode == recoveryModeDatabaseBackup && (hasDatabaseBackups || uploadedDatabaseBackupProvided) {
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_error"), availableBackups, nil)
|
|
return
|
|
}
|
|
ctx.Data["Err_RepositoryFilesystemRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_repository_filesystem_error"), availableBackups, nil)
|
|
return
|
|
}
|
|
|
|
if form.RecoveryMode == recoveryModeDatabaseBackup || form.RecoveryMode == recoveryModeRepositoryFileSystem {
|
|
skipDatabasePreflight = true
|
|
}
|
|
}
|
|
// end edit/add - by petru @ codex
|
|
|
|
dbPreviouslyUsed := false
|
|
if !skipDatabasePreflight {
|
|
dbOK, checkedPreviouslyUsed := checkDatabase(ctx, &form, availableBackups, uploadedDatabaseBackupProvided)
|
|
if !dbOK {
|
|
return
|
|
}
|
|
dbPreviouslyUsed = checkedPreviouslyUsed
|
|
}
|
|
|
|
if shouldRequireBackupAppINISnapshot(form.RecoveryMode, dbPreviouslyUsed) && !hasInstallRecoveryAppINIConfigSource(&form, backupAppINICfg) {
|
|
// start edit/add - by petru @ codex
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_app_ini_required"), availableBackups, nil)
|
|
return
|
|
// end edit/add - by petru @ codex
|
|
}
|
|
|
|
// Prepare AppDataPath, it is very important for Gitea
|
|
if err = setting.PrepareAppDataPath(); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_app_data_path", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
// Test repository root path.
|
|
form.RepoRootPath = strings.ReplaceAll(form.RepoRootPath, "\\", "/")
|
|
if err = os.MkdirAll(form.RepoRootPath, os.ModePerm); err != nil {
|
|
ctx.Data["Err_RepoRootPath"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_repo_path", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
if !dbPreviouslyUsed && (hasRepositoryFilesystem || hasDatabaseBackups) {
|
|
setInstallRecoverySourceAvailability(ctx, false, availableBackups, hasRepositoryFilesystem)
|
|
if form.RecoveryMode == "" {
|
|
if hasDatabaseBackups {
|
|
form.RecoveryMode = recoveryModeDatabaseBackup
|
|
} else {
|
|
form.RecoveryMode = recoveryModeRepositoryFileSystem
|
|
}
|
|
}
|
|
|
|
recoverySelectionValid := installRecoveryConfirmationsAccepted(&form)
|
|
switch form.RecoveryMode {
|
|
case recoveryModeDatabaseBackup:
|
|
recoverySelectionValid = recoverySelectionValid && hasSelectedInstallDatabaseBackupSource(&form, uploadedDatabaseBackupProvided)
|
|
case recoveryModeRepositoryFileSystem:
|
|
// nothing extra
|
|
default:
|
|
recoverySelectionValid = false
|
|
}
|
|
|
|
if !recoverySelectionValid {
|
|
if form.RecoveryMode == recoveryModeDatabaseBackup && (hasDatabaseBackups || uploadedDatabaseBackupProvided) {
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_error"), availableBackups, nil)
|
|
return
|
|
}
|
|
ctx.Data["Err_RepositoryFilesystemRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_repository_filesystem_error"), availableBackups, nil)
|
|
return
|
|
}
|
|
}
|
|
// end edit/add - by petru @ codex
|
|
|
|
// Test LFS root path if not empty, empty meaning disable LFS
|
|
if form.LFSRootPath != "" {
|
|
form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
|
|
if err := os.MkdirAll(form.LFSRootPath, os.ModePerm); err != nil {
|
|
ctx.Data["Err_LFSRootPath"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_lfs_path", err), tplInstall, &form)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Test log root path.
|
|
form.LogRootPath = strings.ReplaceAll(form.LogRootPath, "\\", "/")
|
|
if err = os.MkdirAll(form.LogRootPath, os.ModePerm); err != nil {
|
|
ctx.Data["Err_LogRootPath"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_log_root_path", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
// Check logic loophole between disable self-registration and no admin account.
|
|
if form.DisableRegistration && len(form.AdminName) == 0 {
|
|
ctx.Data["Err_Registration"] = true
|
|
ctx.Data["Err_Admin"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.no_admin_and_disable_registration"), tplInstall, form)
|
|
return
|
|
}
|
|
|
|
// Check admin user creation
|
|
if len(form.AdminName) > 0 {
|
|
// Ensure AdminName is valid
|
|
if err := user_model.IsUsableUsername(form.AdminName); err != nil {
|
|
ctx.Data["Err_Admin"] = true
|
|
ctx.Data["Err_AdminName"] = true
|
|
if db.IsErrNameReserved(err) {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_reserved"), tplInstall, form)
|
|
return
|
|
} else if db.IsErrNamePatternNotAllowed(err) {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_pattern_not_allowed"), tplInstall, form)
|
|
return
|
|
}
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_admin_name_is_invalid"), tplInstall, form)
|
|
return
|
|
}
|
|
// Check Admin email
|
|
if len(form.AdminEmail) == 0 {
|
|
ctx.Data["Err_Admin"] = true
|
|
ctx.Data["Err_AdminEmail"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_email"), tplInstall, form)
|
|
return
|
|
}
|
|
// Check admin password.
|
|
if len(form.AdminPasswd) == 0 {
|
|
ctx.Data["Err_Admin"] = true
|
|
ctx.Data["Err_AdminPasswd"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.err_empty_admin_password"), tplInstall, form)
|
|
return
|
|
}
|
|
if form.AdminPasswd != form.AdminConfirmPasswd {
|
|
ctx.Data["Err_Admin"] = true
|
|
ctx.Data["Err_AdminPasswd"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("form.password_not_match"), tplInstall, form)
|
|
return
|
|
}
|
|
}
|
|
|
|
brandingUploads, err := collectInstallBrandingAssets(ctx, form.BrandingUseSharedAssets)
|
|
if err != nil {
|
|
ctx.Data["Err_Branding"] = true
|
|
ctx.RenderWithErrDeprecated(err.Error(), tplInstall, form)
|
|
return
|
|
}
|
|
|
|
// start edit/add - by petru @ codex
|
|
if uploadedDatabaseBackupProvided || form.BackupRestoreDB || shouldRestoreDatabaseFromBackup(form.RecoveryMode, dbPreviouslyUsed) {
|
|
if setting.Database.Type.IsSQLite3() {
|
|
if err = removeInstallSQLiteDatabaseArtifacts(setting.Database.Path); err != nil {
|
|
ctx.Data["Err_DbSetting"] = true
|
|
renderInstallError(ctx.Tr("install.invalid_db_setting", err), availableBackups, nil)
|
|
return
|
|
}
|
|
}
|
|
if err = db.InitEngine(ctx); err != nil {
|
|
db.UnsetDefaultEngine()
|
|
ctx.Data["Err_DbSetting"] = true
|
|
renderInstallError(ctx.Tr("install.invalid_db_setting", err), availableBackups, nil)
|
|
return
|
|
}
|
|
if uploadedDatabaseBackupProvided {
|
|
if err = importUploadedDatabaseBackup(ctx); err != nil {
|
|
db.UnsetDefaultEngine()
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_restore_failed", err), availableBackups, nil)
|
|
return
|
|
}
|
|
} else if form.BackupRestoreDB || shouldRestoreDatabaseFromBackup(form.RecoveryMode, dbPreviouslyUsed) {
|
|
if _, err = dbbackup_service.RestoreDatabaseBackup(ctx, resolveInstallBackupPath(form.BackupPath), form.BackupRestoreID, form.DbType); err != nil {
|
|
db.UnsetDefaultEngine()
|
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
|
renderInstallError(ctx.Tr("install.recovery_database_backup_restore_failed", err), availableBackups, nil)
|
|
return
|
|
}
|
|
}
|
|
db.UnsetDefaultEngine()
|
|
} else {
|
|
// Init the engine with migration
|
|
if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil {
|
|
db.UnsetDefaultEngine()
|
|
ctx.Data["Err_DbSetting"] = true
|
|
renderInstallError(ctx.Tr("install.invalid_db_setting", err), availableBackups, nil)
|
|
return
|
|
}
|
|
}
|
|
// end edit/add - by petru @ codex
|
|
|
|
// Save settings.
|
|
cfg := backupAppINICfg // edit/add - by petru @ codex
|
|
if cfg == nil {
|
|
cfg, err = setting.NewConfigProviderFromFile(setting.CustomConf)
|
|
if err != nil {
|
|
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("HOST").SetValue(setting.Database.Host)
|
|
cfg.Section("database").Key("NAME").SetValue(setting.Database.Name)
|
|
cfg.Section("database").Key("USER").SetValue(setting.Database.User)
|
|
cfg.Section("database").Key("PASSWD").SetValue(setting.Database.Passwd)
|
|
cfg.Section("database").Key("SCHEMA").SetValue(setting.Database.Schema)
|
|
cfg.Section("database").Key("SSL_MODE").SetValue(setting.Database.SSLMode)
|
|
cfg.Section("database").Key("PATH").SetValue(setting.Database.Path)
|
|
cfg.Section("database").Key("LOG_SQL").SetValue("false") // LOG_SQL is rarely helpful
|
|
|
|
cfg.Section("repository").Key("ROOT").SetValue(form.RepoRootPath)
|
|
applyImportedAppINIRepositoryRecoveryDefaults(&form, dbPreviouslyUsed) // edit/add - by petru @ codex
|
|
cfg.Section("repository").Key("ALLOW_ADOPTION_OF_UNADOPTED_REPOSITORIES").SetValue(strconv.FormatBool(form.AllowAdoptionOfUnadoptedRepositories)) // edit/add - by petru @ codex
|
|
cfg.Section("repository").Key("ALLOW_DELETE_OF_UNADOPTED_REPOSITORIES").SetValue(strconv.FormatBool(form.AllowDeleteOfUnadoptedRepositories)) // edit/add - by petru @ codex
|
|
cfg.Section("repository.release").Key("MAX_FILES").SetValue(strconv.FormatInt(form.ReleaseMaxFiles, 10))
|
|
cfg.Section("repository.release").Key("FILE_MAX_SIZE").SetValue(strconv.FormatInt(form.ReleaseFileMaxSize, 10))
|
|
// start edit/add - by petru @ codex
|
|
cfg.Section("backup").Key("PATH").SetValue(form.BackupPath)
|
|
cfg.Section("backup").Key("RETENTION_COUNT").SetValue(strconv.Itoa(form.BackupRetentionCount))
|
|
cfg.Section("backup").Key("COMPRESS").SetValue(strconv.FormatBool(form.BackupCompress))
|
|
cfg.Section("backup").Key("INCLUDE_APP_INI_SNAPSHOT").SetValue(strconv.FormatBool(form.BackupIncludeAppINISnapshot))
|
|
cfg.Section("cron.database_backup").Key("ENABLED").SetValue(strconv.FormatBool(form.BackupEnabled))
|
|
cfg.Section("cron.database_backup").Key("RUN_AT_START").SetValue(strconv.FormatBool(form.BackupRunAtStart))
|
|
cfg.Section("cron.database_backup").Key("SCHEDULE").SetValue(form.BackupSchedule)
|
|
// end edit/add - by petru @ codex
|
|
cfg.Section("server").Key("SSH_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("ROOT_URL").SetValue(form.AppURL)
|
|
cfg.Section("server").Key("APP_DATA_PATH").SetValue(setting.AppDataPath)
|
|
|
|
if form.SSHPort == 0 {
|
|
cfg.Section("server").Key("DISABLE_SSH").SetValue("true")
|
|
} else {
|
|
cfg.Section("server").Key("DISABLE_SSH").SetValue("false")
|
|
cfg.Section("server").Key("SSH_PORT").SetValue(strconv.Itoa(form.SSHPort))
|
|
}
|
|
|
|
if form.LFSRootPath != "" {
|
|
cfg.Section("server").Key("LFS_START_SERVER").SetValue("true")
|
|
cfg.Section("lfs").Key("PATH").SetValue(form.LFSRootPath)
|
|
} else {
|
|
cfg.Section("server").Key("LFS_START_SERVER").SetValue("false")
|
|
}
|
|
|
|
if len(strings.TrimSpace(form.SMTPAddr)) > 0 {
|
|
form.SMTPFrom, err = composeInstallSMTPFrom(form.AppName, form.SMTPFromName, form.SMTPFromAddress)
|
|
if err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.smtp_from_invalid"), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
cfg.Section("mailer").Key("ENABLED").SetValue("true")
|
|
cfg.Section("mailer").Key("SMTP_ADDR").SetValue(form.SMTPAddr)
|
|
cfg.Section("mailer").Key("SMTP_PORT").SetValue(form.SMTPPort)
|
|
cfg.Section("mailer").Key("FROM").SetValue(form.SMTPFrom)
|
|
cfg.Section("mailer").Key("USER").SetValue(form.SMTPUser)
|
|
cfg.Section("mailer").Key("PASSWD").SetValue(form.SMTPPasswd)
|
|
} else {
|
|
cfg.Section("mailer").Key("ENABLED").SetValue("false")
|
|
}
|
|
registerEmailConfirm := form.RegisterConfirm && !form.RegisterManualConfirm && isLocalRegistrationMode(form.RegistrationMode)
|
|
registerManualConfirm := form.RegisterManualConfirm && isLocalRegistrationMode(form.RegistrationMode)
|
|
|
|
cfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").SetValue(strconv.FormatBool(registerEmailConfirm))
|
|
cfg.Section("service").Key("REGISTER_MANUAL_CONFIRM").SetValue(strconv.FormatBool(registerManualConfirm))
|
|
cfg.Section("service").Key("ENABLE_NOTIFY_MAIL").SetValue(strconv.FormatBool(form.MailNotify))
|
|
cfg.Section("service").Key("ADMIN_CREATED_ACCOUNT_MODE").SetValue(form.AdminCreatedAccountMode)
|
|
|
|
cfg.Section("openid").Key("ENABLE_OPENID_SIGNIN").SetValue(strconv.FormatBool(form.EnableOpenIDSignIn))
|
|
cfg.Section("openid").Key("ENABLE_OPENID_SIGNUP").SetValue(strconv.FormatBool(form.EnableOpenIDSignUp))
|
|
cfg.Section("service").Key("DISABLE_REGISTRATION").SetValue(strconv.FormatBool(form.DisableRegistration))
|
|
cfg.Section("service").Key("ALLOW_ONLY_INTERNAL_REGISTRATION").SetValue(strconv.FormatBool(form.AllowOnlyInternalRegistration))
|
|
cfg.Section("service").Key("ALLOW_ONLY_EXTERNAL_REGISTRATION").SetValue(strconv.FormatBool(form.AllowOnlyExternalRegistration))
|
|
cfg.Section("service").Key("ENABLE_CAPTCHA").SetValue(strconv.FormatBool(form.EnableCaptcha))
|
|
cfg.Section("service").Key("REQUIRE_SIGNIN_VIEW").SetValue(strconv.FormatBool(form.RequireSignInView))
|
|
cfg.Section("service").Key("DEFAULT_KEEP_EMAIL_PRIVATE").SetValue(strconv.FormatBool(form.DefaultKeepEmailPrivate))
|
|
cfg.Section("service").Key("DEFAULT_ALLOW_CREATE_ORGANIZATION").SetValue(strconv.FormatBool(form.DefaultAllowCreateOrganization))
|
|
cfg.Section("service").Key("DEFAULT_ENABLE_TIMETRACKING").SetValue(strconv.FormatBool(form.DefaultEnableTimetracking))
|
|
cfg.Section("service").Key("NO_REPLY_ADDRESS").SetValue(form.NoReplyAddress)
|
|
cfg.Section("cron.update_checker").Key("ENABLED").SetValue(strconv.FormatBool(form.EnableUpdateChecker))
|
|
cfg.Section("admin").Key("SUPER_ADMIN_ENABLED").SetValue("true")
|
|
cfg.Section("admin").Key("ADMIN_MANAGEMENT_POLICY").SetValue(form.AdminManagementPolicy)
|
|
cfg.Section("i18n").Key("LANGS").SetValue(strings.Join(reorderInstallLanguages(form.DefaultLanguage), ","))
|
|
|
|
cfg.Section("session").Key("PROVIDER").SetValue("file")
|
|
|
|
cfg.Section("log").Key("MODE").MustString("console")
|
|
cfg.Section("log").Key("LEVEL").SetValue(setting.Log.Level.String())
|
|
cfg.Section("log").Key("ROOT_PATH").SetValue(form.LogRootPath)
|
|
|
|
cfg.Section("repository.pull-request").Key("DEFAULT_MERGE_STYLE").SetValue("merge")
|
|
|
|
cfg.Section("repository.signing").Key("DEFAULT_TRUST_MODEL").SetValue("committer")
|
|
|
|
cfg.Section("security").Key("INSTALL_LOCK").SetValue("true")
|
|
if err = applyInstallSensitiveSecretsToConfig(cfg, &form); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.internal_token_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
// if there is already a SECRET_KEY, we should not overwrite it, otherwise the encrypted data will not be able to be decrypted
|
|
if setting.SecretKey == "" {
|
|
var secretKey string
|
|
if secretKey, err = generate.NewSecretKey(); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.secret_key_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
cfg.Section("security").Key("SECRET_KEY").SetValue(secretKey)
|
|
}
|
|
|
|
if len(form.PasswordAlgorithm) > 0 {
|
|
var algorithm *hash.PasswordHashAlgorithm
|
|
setting.PasswordHashAlgo, algorithm = hash.SetDefaultPasswordHashAlgorithm(form.PasswordAlgorithm)
|
|
if algorithm == nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_password_algorithm"), tplInstall, &form)
|
|
return
|
|
}
|
|
cfg.Section("security").Key("PASSWORD_HASH_ALGO").SetValue(form.PasswordAlgorithm)
|
|
}
|
|
|
|
log.Info("Save settings to custom config file %s", setting.CustomConf)
|
|
|
|
err = os.MkdirAll(filepath.Dir(setting.CustomConf), os.ModePerm)
|
|
if err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
setting.EnvironmentToConfig(cfg, os.Environ())
|
|
|
|
if err = cfg.SaveTo(setting.CustomConf); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
// start edit/add - by petru @ codex
|
|
recoveryCfgToSave := &setting.RecoveryConfig{
|
|
Enabled: form.RecoveryEmailEnabled,
|
|
AllowedEmails: form.RecoveryAllowedEmails,
|
|
BackupPath: form.BackupPath,
|
|
RepoRootPath: form.RepoRootPath,
|
|
SMTPAddr: form.SMTPAddr,
|
|
SMTPPort: form.SMTPPort,
|
|
SMTPFrom: form.SMTPFrom,
|
|
SMTPUser: form.SMTPUser,
|
|
SMTPPasswd: form.SMTPPasswd,
|
|
}
|
|
existingRecoveryCfg, recoveryCfgErr := setting.LoadRecoveryConfig()
|
|
if recoveryCfgErr == nil {
|
|
recoveryCfgToSave = mergeInstallRecoveryConfigSMTP(existingRecoveryCfg, recoveryCfgToSave) // edit/add - by petru @ codex
|
|
}
|
|
if err = setting.SaveRecoveryConfig(recoveryCfgToSave); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
// end edit/add - by petru @ codex
|
|
|
|
if err = saveInstallBrandingAssets(brandingUploads); err != nil {
|
|
ctx.Data["Err_Branding"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.branding.upload_save_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
// unset default engine before reload database setting
|
|
db.UnsetDefaultEngine()
|
|
|
|
// ---- All checks are passed
|
|
|
|
// Reload settings (and re-initialize database connection)
|
|
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
|
|
if len(form.AdminName) > 0 {
|
|
u := &user_model.User{
|
|
Name: form.AdminName,
|
|
Email: form.AdminEmail,
|
|
Passwd: form.AdminPasswd,
|
|
IsAdmin: true,
|
|
}
|
|
overwriteDefault := &user_model.CreateUserOverwriteOptions{
|
|
IsRestricted: optional.Some(false),
|
|
IsActive: optional.Some(true),
|
|
}
|
|
|
|
if err = user_model.CreateUser(ctx, u, &user_model.Meta{}, overwriteDefault); err != nil {
|
|
if !user_model.IsErrUserAlreadyExist(err) {
|
|
setting.InstallLock = false
|
|
ctx.Data["Err_AdminName"] = true
|
|
ctx.Data["Err_AdminEmail"] = true
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_admin_setting", err), tplInstall, &form)
|
|
return
|
|
}
|
|
log.Info("Admin account already exist")
|
|
u, _ = user_model.GetUserByName(ctx, u.Name)
|
|
}
|
|
|
|
nt, token, err := auth_service.CreateAuthTokenForUserID(ctx, u.ID)
|
|
if err != nil {
|
|
ctx.ServerError("CreateAuthTokenForUserID", err)
|
|
return
|
|
}
|
|
|
|
ctx.SetSiteCookie(setting.CookieRememberName, nt.ID+":"+token, setting.LogInRememberDays*timeutil.Day)
|
|
|
|
// Auto-login for admin
|
|
if err = ctx.Session.Set("uid", u.ID); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
if err = ctx.Session.Set("uname", u.Name); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
if err = ctx.Session.Release(); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
}
|
|
|
|
if err = setting.EnsureInstallSentinel(); err != nil {
|
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.save_config_failed", err), tplInstall, &form)
|
|
return
|
|
}
|
|
|
|
setting.ClearEnvConfigKeys()
|
|
log.Info("First-time run install finished!")
|
|
if isRecoveryRequest(ctx.Req) {
|
|
InstallDone(ctx) // edit/add - by petru @ codex
|
|
} else {
|
|
InstallDone(ctx)
|
|
}
|
|
|
|
go func() {
|
|
// Sleep for a while to make sure the user's browser has loaded the post-install page and its assets (images, css, js)
|
|
// What if this duration is not long enough? That's impossible -- if the user can't load the simple page in time, how could they install or use Gitea in the future ....
|
|
time.Sleep(3 * time.Second)
|
|
|
|
// 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.Value(http.ServerContextKey).(*http.Server)
|
|
if err := srv.Shutdown(graceful.GetManager().HammerContext()); err != nil {
|
|
log.Error("Unable to shutdown the install server! Error: %v", err)
|
|
}
|
|
|
|
// After the HTTP server for "install" shuts down, the `runWeb()` will continue to run the "normal" server
|
|
}()
|
|
}
|
|
|
|
// InstallDone shows the "post-install" page, makes it easier to develop the page.
|
|
// The name is not called as "PostInstall" to avoid misinterpretation as a handler for "POST /install"
|
|
func InstallDone(ctx *context.Context) { //nolint:revive // export stutter
|
|
recoveryRequest := isRecoveryRequest(ctx.Req) // edit/add - by petru @ codex
|
|
hasUsers := struct{ HasAnyUser, HasOnlyOneUser bool }{} // edit/add - by petru @ codex
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
log.Warn("InstallDone: unable to determine whether accounts exist: %v", r) // edit/add - by petru @ codex
|
|
}
|
|
}()
|
|
resolvedHasUsers, err := user_model.HasUsers(ctx)
|
|
if err != nil {
|
|
log.Warn("InstallDone: unable to determine whether accounts exist: %v", err) // edit/add - by petru @ codex
|
|
return
|
|
}
|
|
hasUsers = resolvedHasUsers
|
|
}()
|
|
ctx.Data["IsAccountCreated"] = hasUsers.HasAnyUser
|
|
ctx.Data["InstallProgressLogo"] = getInstallProgressLogo()
|
|
ctx.Data["InstallIsRecoveryRequest"] = recoveryRequest // edit/add - by petru @ codex
|
|
if recoveryRequest {
|
|
ctx.RespHeader().Set("Cache-Control", "no-store, no-cache, must-revalidate") // edit/add - by petru @ codex
|
|
ctx.RespHeader().Set("Pragma", "no-cache") // edit/add - by petru @ codex
|
|
ctx.RespHeader().Set("Expires", "0") // edit/add - by petru @ codex
|
|
}
|
|
ctx.HTML(http.StatusOK, tplPostInstall)
|
|
}
|