Files
gitea/modules/setting/recovery.go
T
petru 471cfdd161
release-nightly / nightly-binary (push) Has been cancelled
release-nightly / nightly-container (push) Has been cancelled
Modified - [install] [backup] [database] [recovery] Consolidated database backup and installer recovery support.
- 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)
2026-06-01 03:56:03 +03:00

178 lines
4.9 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package setting
import (
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"code.gitea.io/gitea/modules/generate"
)
// start edit/add - by petru @ codex
const (
recoveryConfigFileName = ".gitea-recovery.ini"
recoveryUsedTokensFileName = ".gitea-recovery.used"
defaultRecoveryTokenTTLMinutes = 15
)
type RecoveryConfig struct {
Enabled bool
AllowedEmails string
TokenSecret string
TokenTTLMin int
BackupPath string
RepoRootPath string
SMTPAddr string
SMTPPort string
SMTPFrom string
SMTPUser string
SMTPPasswd string
}
func RecoveryConfigPath() string {
if CustomConf == "" {
return ""
}
return filepath.Join(filepath.Dir(CustomConf), recoveryConfigFileName)
}
func RecoveryUsedTokensPath() string {
if CustomConf == "" {
return ""
}
return filepath.Join(filepath.Dir(CustomConf), recoveryUsedTokensFileName)
}
func LoadRecoveryConfig() (*RecoveryConfig, error) {
recoveryPath := RecoveryConfigPath()
if recoveryPath == "" {
return nil, errors.New("setting.CustomConf is not set")
}
cfg, err := NewConfigProviderFromFile(recoveryPath)
if err != nil {
return nil, err
}
if cfg.IsLoadedFromEmpty() {
return nil, nil
}
recoverySec := cfg.Section("recovery")
backupSec := cfg.Section("backup")
mailerSec := cfg.Section("mailer")
recoveryCfg := &RecoveryConfig{
Enabled: ConfigSectionKeyBool(recoverySec, "ENABLED"),
AllowedEmails: ConfigSectionKeyString(recoverySec, "ALLOWED_EMAILS"),
TokenSecret: ConfigSectionKeyString(recoverySec, "TOKEN_SECRET"),
TokenTTLMin: recoverySec.Key("TOKEN_TTL_MINUTES").MustInt(defaultRecoveryTokenTTLMinutes),
BackupPath: ConfigSectionKeyString(backupSec, "PATH"),
RepoRootPath: ConfigSectionKeyString(cfg.Section("repository"), "ROOT"),
SMTPAddr: ConfigSectionKeyString(mailerSec, "SMTP_ADDR"),
SMTPPort: ConfigSectionKeyString(mailerSec, "SMTP_PORT"),
SMTPFrom: ConfigSectionKeyString(mailerSec, "FROM"),
SMTPUser: ConfigSectionKeyString(mailerSec, "USER"),
SMTPPasswd: ConfigSectionKeyString(mailerSec, "PASSWD"),
}
if recoveryCfg.TokenTTLMin <= 0 {
recoveryCfg.TokenTTLMin = defaultRecoveryTokenTTLMinutes
}
return recoveryCfg, nil
}
func (data *RecoveryConfig) AllowedEmailSlice() []string {
if data == nil {
return nil
}
parts := strings.Split(data.AllowedEmails, ",")
emails := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.ToLower(strings.TrimSpace(part))
if part != "" {
emails = append(emails, part)
}
}
return emails
}
func (data *RecoveryConfig) AllowsEmail(email string) bool {
email = strings.ToLower(strings.TrimSpace(email))
if email == "" {
return false
}
for _, allowed := range data.AllowedEmailSlice() {
if allowed == email {
return true
}
}
return false
}
func (data *RecoveryConfig) EffectiveTokenTTL() time.Duration {
if data == nil || data.TokenTTLMin <= 0 {
return time.Duration(defaultRecoveryTokenTTLMinutes) * time.Minute
}
return time.Duration(data.TokenTTLMin) * time.Minute
}
func SaveRecoveryConfig(data *RecoveryConfig) error {
recoveryPath := RecoveryConfigPath()
if recoveryPath == "" {
return errors.New("setting.CustomConf is not set")
}
if data == nil || !data.Enabled {
if err := os.Remove(recoveryPath); err != nil && !os.IsNotExist(err) {
return err
}
return nil
}
cfg, err := NewConfigProviderFromFile(recoveryPath)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(recoveryPath), 0o755); err != nil {
return err // edit/add - by petru @ codex
}
if strings.TrimSpace(data.TokenSecret) == "" {
tokenSecret, err := generate.NewSecretKey()
if err != nil {
return err
}
data.TokenSecret = tokenSecret
}
if data.TokenTTLMin <= 0 {
data.TokenTTLMin = defaultRecoveryTokenTTLMinutes
}
recoverySec := cfg.Section("recovery")
recoverySec.Key("ENABLED").SetValue(strconv.FormatBool(data.Enabled))
recoverySec.Key("ALLOWED_EMAILS").SetValue(strings.TrimSpace(data.AllowedEmails))
recoverySec.Key("TOKEN_SECRET").SetValue(strings.TrimSpace(data.TokenSecret))
recoverySec.Key("TOKEN_TTL_MINUTES").SetValue(strconv.Itoa(data.TokenTTLMin))
backupSec := cfg.Section("backup")
backupSec.Key("PATH").SetValue(strings.TrimSpace(data.BackupPath))
repoSec := cfg.Section("repository")
repoSec.Key("ROOT").SetValue(strings.TrimSpace(data.RepoRootPath))
mailerSec := cfg.Section("mailer")
mailerSec.Key("ENABLED").SetValue(strconv.FormatBool(strings.TrimSpace(data.SMTPAddr) != ""))
mailerSec.Key("SMTP_ADDR").SetValue(strings.TrimSpace(data.SMTPAddr))
mailerSec.Key("SMTP_PORT").SetValue(strings.TrimSpace(data.SMTPPort))
mailerSec.Key("FROM").SetValue(strings.TrimSpace(data.SMTPFrom))
mailerSec.Key("USER").SetValue(strings.TrimSpace(data.SMTPUser))
mailerSec.Key("PASSWD").SetValue(data.SMTPPasswd)
return cfg.Save()
}
// end edit/add - by petru @ codex