Step-1 Modified - [install] [backup] [database] [recovery] Added periodic database backups and integrated them into the installer recovery flow.
- 1 - Add: Gitea now creates timestamped database backup bundles under `[backup].PATH`, with `manifest.json`, SQL dump output, optional gzip compression, optional `app.ini` snapshotting, and retention-based cleanup. - 2 - Mod: the installer now includes a `Database Backup` section under `Database Settings`, reads and writes the `[backup]` and `[cron.database_backup]` settings, and starts from an enabled-by-default daily backup schedule for installs or updates that do not already define backup cron values. - 3 - Mod: the existing installer recovery modal now works as a recovery-source chooser for existing databases, compatible database backup bundles, or repository-filesystem recovery, with source-specific warnings, confirmations, and preserved modal state across rerenders. - 4 - Add: installer-side database backup restore now detects compatible bundles from the configured backup path, lets the user choose a bundle, and imports the selected `database.sql` or `database.sql.gz` into the target database during reinstall recovery. - 5 - Mod: the admin maintenance dashboard and cron monitor now expose the `database_backup` task directly, including the configured schedule even before the task is manually enabled. - 6 - Fix: backup retention pruning no longer panics when `RETENTION_COUNT` is greater than the number of available backup directories.
This commit is contained in:
@@ -893,3 +893,11 @@ History search guidance:
|
|||||||
- 1 - Mod: `/-/admin/badges` now shows a delete button in each row that opens the badge delete modal directly from the table.
|
- 1 - Mod: `/-/admin/badges` now shows a delete button in each row that opens the badge delete modal directly from the table.
|
||||||
- 2 - Mod: `/-/admin/orgs` now shows a delete button in each row that opens an organization delete modal directly from the table.
|
- 2 - Mod: `/-/admin/orgs` now shows a delete button in each row that opens an organization delete modal directly from the table.
|
||||||
- 3 - Mod: the new table actions reuse the original delete modal layouts already used in badge edit and organization settings, and deleting an organization from `/-/admin/orgs` now returns to the admin organizations list instead of `/`.
|
- 3 - Mod: the new table actions reuse the original delete modal layouts already used in badge edit and organization settings, and deleting an organization from `/-/admin/orgs` now returns to the admin organizations list instead of `/`.
|
||||||
|
|
||||||
|
186 - [2026-05-24 19:46:12] - v1.27.0-dev-197-g406e6d0697 - Type: Step-1 Modified - [install] [backup] [database] [recovery] Added periodic database backups and integrated them into the installer recovery flow.
|
||||||
|
- 1 - Add: Gitea now creates timestamped database backup bundles under `[backup].PATH`, with `manifest.json`, SQL dump output, optional gzip compression, optional `app.ini` snapshotting, and retention-based cleanup.
|
||||||
|
- 2 - Mod: the installer now includes a `Database Backup` section under `Database Settings`, reads and writes the `[backup]` and `[cron.database_backup]` settings, and starts from an enabled-by-default daily backup schedule for installs or updates that do not already define backup cron values.
|
||||||
|
- 3 - Mod: the existing installer recovery modal now works as a recovery-source chooser for existing databases, compatible database backup bundles, or repository-filesystem recovery, with source-specific warnings, confirmations, and preserved modal state across rerenders.
|
||||||
|
- 4 - Add: installer-side database backup restore now detects compatible bundles from the configured backup path, lets the user choose a bundle, and imports the selected `database.sql` or `database.sql.gz` into the target database during reinstall recovery.
|
||||||
|
- 5 - Mod: the admin maintenance dashboard and cron monitor now expose the `database_backup` task directly, including the configured schedule even before the task is manually enabled.
|
||||||
|
- 6 - Fix: backup retention pruning no longer panics when `RETENTION_COUNT` is greater than the number of available backup directories.
|
||||||
|
|||||||
@@ -2108,6 +2108,22 @@ LEVEL = Info
|
|||||||
;; Empty means server's location setting
|
;; Empty means server's location setting
|
||||||
;DEFAULT_UI_LOCATION =
|
;DEFAULT_UI_LOCATION =
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;[backup]
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; start edit/add - by petru @ codex
|
||||||
|
;; Directory where periodic database backups are written.
|
||||||
|
;PATH = data/backups/db
|
||||||
|
;; Number of backups to keep. Set to 0 to disable pruning.
|
||||||
|
;RETENTION_COUNT = 7
|
||||||
|
;; Compress the SQL dump as `database.sql.gz`.
|
||||||
|
;COMPRESS = true
|
||||||
|
;; Save a copy of the current app.ini alongside the backup manifest.
|
||||||
|
;INCLUDE_APP_INI_SNAPSHOT = true
|
||||||
|
;; end edit/add - by petru @ codex
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;[cron]
|
;[cron]
|
||||||
@@ -2130,6 +2146,24 @@ LEVEL = Info
|
|||||||
;; Basic cron tasks - enabled by default
|
;; Basic cron tasks - enabled by default
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; Create database backups
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;[cron.database_backup]
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
;; start edit/add - by petru @ codex
|
||||||
|
;; Enable running the periodic database backup task.
|
||||||
|
;ENABLED = true
|
||||||
|
;; Run the database backup task when Gitea starts.
|
||||||
|
;RUN_AT_START = false
|
||||||
|
;; Whether to emit notice on successful execution too.
|
||||||
|
;NOTICE_ON_SUCCESS = false
|
||||||
|
;; Time interval for job to run.
|
||||||
|
;SCHEDULE = @daily
|
||||||
|
;; end edit/add - by petru @ codex
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; Clean up old repository archives
|
;; Clean up old repository archives
|
||||||
|
|||||||
@@ -31,3 +31,12 @@ func DumpDatabase(filePath, dbType string) error {
|
|||||||
}
|
}
|
||||||
return xormEngine.DumpTablesToFile(tbs, filePath)
|
return xormEngine.DumpTablesToFile(tbs, filePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
// ImportDatabase imports SQL statements from a file into the current default engine.
|
||||||
|
func ImportDatabase(filePath string) error {
|
||||||
|
_, err := xormEngine.ImportFile(filePath)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
var Backup = struct {
|
||||||
|
Path string
|
||||||
|
RetentionCount int
|
||||||
|
Compress bool
|
||||||
|
IncludeAppINISnapshot bool
|
||||||
|
}{
|
||||||
|
RetentionCount: 7,
|
||||||
|
Compress: true,
|
||||||
|
IncludeAppINISnapshot: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadBackupFrom(rootCfg ConfigProvider) {
|
||||||
|
sec := rootCfg.Section("backup")
|
||||||
|
|
||||||
|
Backup.Path = sec.Key("PATH").MustString(filepath.Join(AppDataPath, "backups", "db"))
|
||||||
|
if !filepath.IsAbs(Backup.Path) {
|
||||||
|
Backup.Path = filepath.Join(AppWorkPath, Backup.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
Backup.RetentionCount = sec.Key("RETENTION_COUNT").MustInt(7)
|
||||||
|
if Backup.RetentionCount < 0 {
|
||||||
|
Backup.RetentionCount = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Backup.Compress = sec.Key("COMPRESS").MustBool(true)
|
||||||
|
Backup.IncludeAppINISnapshot = sec.Key("INCLUDE_APP_INI_SNAPSHOT").MustBool(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package setting
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
func TestLoadBackupFrom(t *testing.T) {
|
||||||
|
oldAppWorkPath := AppWorkPath
|
||||||
|
oldAppDataPath := AppDataPath
|
||||||
|
oldBackup := Backup
|
||||||
|
defer func() {
|
||||||
|
AppWorkPath = oldAppWorkPath
|
||||||
|
AppDataPath = oldAppDataPath
|
||||||
|
Backup = oldBackup
|
||||||
|
}()
|
||||||
|
|
||||||
|
AppWorkPath = "/srv/gitea"
|
||||||
|
AppDataPath = "data"
|
||||||
|
|
||||||
|
cfg, err := NewConfigProviderFromData(`
|
||||||
|
[backup]
|
||||||
|
PATH = backups/custom-db
|
||||||
|
RETENTION_COUNT = -4
|
||||||
|
COMPRESS = false
|
||||||
|
INCLUDE_APP_INI_SNAPSHOT = false
|
||||||
|
`)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
loadBackupFrom(cfg)
|
||||||
|
|
||||||
|
assert.Equal(t, filepath.Join(AppWorkPath, "backups", "custom-db"), Backup.Path)
|
||||||
|
assert.Equal(t, 0, Backup.RetentionCount)
|
||||||
|
assert.False(t, Backup.Compress)
|
||||||
|
assert.False(t, Backup.IncludeAppINISnapshot)
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
@@ -135,6 +135,7 @@ func loadCommonSettingsFrom(cfg ConfigProvider) error {
|
|||||||
}
|
}
|
||||||
loadTimeFrom(cfg)
|
loadTimeFrom(cfg)
|
||||||
loadRepositoryFrom(cfg)
|
loadRepositoryFrom(cfg)
|
||||||
|
loadBackupFrom(cfg) // edit/add - by petru @ codex
|
||||||
if err := loadAvatarsFrom(cfg); err != nil {
|
if err := loadAvatarsFrom(cfg); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -266,11 +266,49 @@
|
|||||||
"install.ssl_mode": "SSL",
|
"install.ssl_mode": "SSL",
|
||||||
"install.path": "Path",
|
"install.path": "Path",
|
||||||
"install.sqlite_helper": "File path for the SQLite3 database.<br>Enter an absolute path if you run Gitea as a service.",
|
"install.sqlite_helper": "File path for the SQLite3 database.<br>Enter an absolute path if you run Gitea as a service.",
|
||||||
|
"install.database_backup_title": "Database Backup",
|
||||||
|
"install.database_backup_desc": "Configure periodic database backups that Gitea will generate for recovery and reinstall scenarios.",
|
||||||
|
"install.database_backup_enabled": "Enable periodic database backups",
|
||||||
|
"install.database_backup_enabled_helper": "Turn on the scheduled database backup cron task.",
|
||||||
|
"install.database_backup_run_at_start": "Run backup at startup",
|
||||||
|
"install.database_backup_run_at_start_helper": "Also create a backup once when Gitea starts, in addition to the scheduled runs.",
|
||||||
|
"install.database_backup_schedule": "Backup Schedule",
|
||||||
|
"install.database_backup_schedule_helper": "Cron descriptor or full cron expression, for example `@daily` or `@every 12h`.",
|
||||||
|
"install.database_backup_path": "Backup Path",
|
||||||
|
"install.database_backup_path_helper": "Directory where timestamped database backup bundles will be written.",
|
||||||
|
"install.database_backup_retention_count": "Retention Count",
|
||||||
|
"install.database_backup_retention_count_helper": "How many backups to keep. Set to `0` to disable pruning.",
|
||||||
|
"install.database_backup_compress": "Compress SQL dump",
|
||||||
|
"install.database_backup_compress_helper": "Store the database dump as `database.sql.gz` instead of a plain `.sql` file.",
|
||||||
|
"install.database_backup_include_app_ini_snapshot": "Include app.ini snapshot",
|
||||||
|
"install.database_backup_include_app_ini_snapshot_helper": "Save a copy of the current `app.ini` alongside each backup bundle.",
|
||||||
"install.reinstall_error": "You are trying to install into an existing Gitea database",
|
"install.reinstall_error": "You are trying to install into an existing Gitea database",
|
||||||
"install.reinstall_confirm_message": "Re-installing with an existing Gitea database can cause multiple problems. In most cases, you should use your existing \"app.ini\" to run Gitea. If you know what you are doing, confirm the following:",
|
"install.reinstall_confirm_message": "Re-installing with an existing Gitea database can cause multiple problems. In most cases, you should use your existing \"app.ini\" to run Gitea. If you know what you are doing, confirm the following:",
|
||||||
"install.reinstall_confirm_check_1": "The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP and mirrors may not function correctly. By checking this box, you confirm that the current app.ini file contains the correct SECRET_KEY.",
|
"install.reinstall_confirm_check_1": "The data encrypted by the SECRET_KEY in app.ini may be lost: users may not be able to log in with 2FA/OTP and mirrors may not function correctly. By checking this box, you confirm that the current app.ini file contains the correct SECRET_KEY.",
|
||||||
"install.reinstall_confirm_check_2": "The repositories and settings may need to be resynchronized. By checking this box, you confirm that you will resynchronize the hooks for the repositories and authorized_keys file manually. You confirm that you will ensure that repository and mirror settings are correct.",
|
"install.reinstall_confirm_check_2": "The repositories and settings may need to be resynchronized. By checking this box, you confirm that you will resynchronize the hooks for the repositories and authorized_keys file manually. You confirm that you will ensure that repository and mirror settings are correct.",
|
||||||
"install.reinstall_confirm_check_3": "You confirm that you are absolutely sure that this Gitea is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks.",
|
"install.reinstall_confirm_check_3": "You confirm that you are absolutely sure that this Gitea is running with the correct app.ini location and that you are sure that you have to re-install. You confirm that you acknowledge the above risks.",
|
||||||
|
"install.recovery_source_title": "Recovery source",
|
||||||
|
"install.recovery_source_existing_database": "Use existing database",
|
||||||
|
"install.recovery_source_existing_database_helper": "Continue with the currently configured Gitea database when it is already present and still usable.",
|
||||||
|
"install.recovery_source_database_backup": "Restore from database backup",
|
||||||
|
"install.recovery_source_database_backup_helper": "Continue with a new or empty database and restore it from a previously generated backup bundle that matches the selected database type.",
|
||||||
|
"install.recovery_source_database_backup_unavailable": "Database backup restore is not available in this installer yet.",
|
||||||
|
"install.recovery_source_repository_filesystem": "Recover from repository filesystem",
|
||||||
|
"install.recovery_source_repository_filesystem_helper": "Continue with a new database and recover repositories later from the existing repository directories on disk.",
|
||||||
|
"install.recovery_database_backup_error": "Compatible database backup bundles were detected for the selected database type",
|
||||||
|
"install.recovery_database_backup_message": "Restoring from a database backup can bring back most of the original installation state, but it will overwrite the target database contents. If you know what you are doing, confirm the following:",
|
||||||
|
"install.recovery_database_backup_select": "Database backup bundle",
|
||||||
|
"install.recovery_database_backup_select_helper": "Select the backup bundle that should be restored into the currently configured database.",
|
||||||
|
"install.recovery_database_backup_confirm_check_1": "You confirm that the selected backup belongs to this Gitea instance and matches the currently selected database type.",
|
||||||
|
"install.recovery_database_backup_confirm_check_2": "You confirm that the target database is new or empty and that its current contents may be overwritten by the restore process.",
|
||||||
|
"install.recovery_database_backup_confirm_check_3": "You confirm that you will verify the restored installation after setup and that you understand any data newer than the selected backup will not be recovered.",
|
||||||
|
"install.recovery_database_backup_discovery_failed": "Failed to inspect the configured database backup path: %v",
|
||||||
|
"install.recovery_database_backup_restore_failed": "Failed to restore the selected database backup: %v",
|
||||||
|
"install.recovery_repository_filesystem_error": "Existing repository data was detected in the configured repository root",
|
||||||
|
"install.recovery_repository_filesystem_message": "Recovering from the existing repository filesystem can restore repositories, but some account and database data may be lost. If you know what you are doing, confirm the following:",
|
||||||
|
"install.recovery_repository_filesystem_confirm_check_1": "You confirm that the current repository root points to the original repository filesystem you want to recover.",
|
||||||
|
"install.recovery_repository_filesystem_confirm_check_2": "You confirm that you understand this recovery path may lose database-only data such as accounts, sessions, issues, pull requests, comments, tokens, and notifications.",
|
||||||
|
"install.recovery_repository_filesystem_confirm_check_3": "You confirm that you will manually review and adopt the recovered repositories after installation, and that you accept the limits of filesystem-based recovery.",
|
||||||
"install.err_empty_db_path": "The SQLite3 database path cannot be empty.",
|
"install.err_empty_db_path": "The SQLite3 database path cannot be empty.",
|
||||||
"install.no_admin_and_disable_registration": "You cannot disable user self-registration without creating an administrator account.",
|
"install.no_admin_and_disable_registration": "You cannot disable user self-registration without creating an administrator account.",
|
||||||
"install.err_empty_admin_password": "The administrator password cannot be empty.",
|
"install.err_empty_admin_password": "The administrator password cannot be empty.",
|
||||||
@@ -3155,6 +3193,7 @@
|
|||||||
"admin.dashboard.gc_times": "GC Times",
|
"admin.dashboard.gc_times": "GC Times",
|
||||||
"admin.dashboard.delete_old_actions": "Delete all old activities from database",
|
"admin.dashboard.delete_old_actions": "Delete all old activities from database",
|
||||||
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
"admin.dashboard.delete_old_actions.started": "Deletion of all old activities from database started",
|
||||||
|
"admin.dashboard.database_backup": "Create a database backup",
|
||||||
"admin.dashboard.update_checker": "Update checker",
|
"admin.dashboard.update_checker": "Update checker",
|
||||||
"admin.dashboard.delete_old_system_notices": "Delete all old system notices from database",
|
"admin.dashboard.delete_old_system_notices": "Delete all old system notices from database",
|
||||||
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
"admin.dashboard.gc_lfs": "Garbage-collect LFS meta objects",
|
||||||
|
|||||||
@@ -266,11 +266,49 @@
|
|||||||
"install.ssl_mode": "SSL",
|
"install.ssl_mode": "SSL",
|
||||||
"install.path": "Cale Baza de Date",
|
"install.path": "Cale Baza de Date",
|
||||||
"install.sqlite_helper": "Calea fișierului pentru baza de date SQLite3.<br>Introduceți o cale absolută dacă rulați Gitea ca serviciu.",
|
"install.sqlite_helper": "Calea fișierului pentru baza de date SQLite3.<br>Introduceți o cale absolută dacă rulați Gitea ca serviciu.",
|
||||||
|
"install.database_backup_title": "Backup bază de date",
|
||||||
|
"install.database_backup_desc": "Configurează backupurile periodice ale bazei de date pe care Gitea le va genera pentru scenarii de recovery și reinstalare.",
|
||||||
|
"install.database_backup_enabled": "Activează backupurile periodice ale bazei de date",
|
||||||
|
"install.database_backup_enabled_helper": "Pornește taskul cron programat pentru backupul bazei de date.",
|
||||||
|
"install.database_backup_run_at_start": "Rulează backupul la pornire",
|
||||||
|
"install.database_backup_run_at_start_helper": "Creează și un backup la pornirea Gitea, pe lângă rulările programate.",
|
||||||
|
"install.database_backup_schedule": "Program backup",
|
||||||
|
"install.database_backup_schedule_helper": "Descriptor cron sau expresie cron completă, de exemplu `@daily` sau `@every 12h`.",
|
||||||
|
"install.database_backup_path": "Cale backup",
|
||||||
|
"install.database_backup_path_helper": "Directorul în care vor fi scrise bundle-urile versionate ale backupurilor bazei de date.",
|
||||||
|
"install.database_backup_retention_count": "Număr retenție",
|
||||||
|
"install.database_backup_retention_count_helper": "Câte backupuri să fie păstrate. Setează `0` pentru a dezactiva curățarea automată.",
|
||||||
|
"install.database_backup_compress": "Comprimă dumpul SQL",
|
||||||
|
"install.database_backup_compress_helper": "Salvează dumpul bazei de date ca `database.sql.gz` în loc de un fișier `.sql` simplu.",
|
||||||
|
"install.database_backup_include_app_ini_snapshot": "Include snapshot app.ini",
|
||||||
|
"install.database_backup_include_app_ini_snapshot_helper": "Salvează o copie a fișierului `app.ini` curent lângă fiecare bundle de backup.",
|
||||||
"install.reinstall_error": "Încercați să instalați într-o bază de date Gitea existentă",
|
"install.reinstall_error": "Încercați să instalați într-o bază de date Gitea existentă",
|
||||||
"install.reinstall_confirm_message": "Reinstalarea cu o bază de date Gitea existentă poate cauza mai multe probleme. În cele mai multe cazuri, ar trebui să utilizați „app.ini” existent pentru a rula Gitea. Dacă știți ce faceți, confirmați următoarele:",
|
"install.reinstall_confirm_message": "Reinstalarea cu o bază de date Gitea existentă poate cauza mai multe probleme. În cele mai multe cazuri, ar trebui să utilizați „app.ini” existent pentru a rula Gitea. Dacă știți ce faceți, confirmați următoarele:",
|
||||||
"install.reinstall_confirm_check_1": "Datele criptate de SECRET_KEY în app.ini se pot pierde: este posibil ca utilizatorii să nu se poată conecta cu 2FA/OTP și este posibil ca oglindirea să nu funcționeze corect. Bifând această casetă, confirmați că fișierul actual app.ini conține SECRET_KEY corect.",
|
"install.reinstall_confirm_check_1": "Datele criptate de SECRET_KEY în app.ini se pot pierde: este posibil ca utilizatorii să nu se poată conecta cu 2FA/OTP și este posibil ca oglindirea să nu funcționeze corect. Bifând această casetă, confirmați că fișierul actual app.ini conține SECRET_KEY corect.",
|
||||||
"install.reinstall_confirm_check_2": "Arhivele și setările ar putea avea nevoie să fie resincronizate. Bifând această casetă, confirmi că vei resincroniza manual cârligele pentru proiecte și fișierul authorized_keys. Confirmi că vei asigura că setările pentru proiecte și oglindire sunt corecte.",
|
"install.reinstall_confirm_check_2": "Arhivele și setările ar putea avea nevoie să fie resincronizate. Bifând această casetă, confirmi că vei resincroniza manual cârligele pentru proiecte și fișierul authorized_keys. Confirmi că vei asigura că setările pentru proiecte și oglindire sunt corecte.",
|
||||||
"install.reinstall_confirm_check_3": "Confirmă că ești absolut sigur că acest Gitea rulează cu locația corectă a app.ini și că ești sigur că trebuie să reinstalezi. Confirmă că înțelegi riscurile de mai sus.",
|
"install.reinstall_confirm_check_3": "Confirmă că ești absolut sigur că acest Gitea rulează cu locația corectă a app.ini și că ești sigur că trebuie să reinstalezi. Confirmă că înțelegi riscurile de mai sus.",
|
||||||
|
"install.recovery_source_title": "Sursa de recuperare",
|
||||||
|
"install.recovery_source_existing_database": "Folosește baza de date existentă",
|
||||||
|
"install.recovery_source_existing_database_helper": "Continuă cu baza de date Gitea configurată în prezent atunci când aceasta există deja și este încă utilizabilă.",
|
||||||
|
"install.recovery_source_database_backup": "Restaurează dintr-un backup al bazei de date",
|
||||||
|
"install.recovery_source_database_backup_helper": "Continuă cu o bază de date nouă sau goală și restaureaz-o dintr-un bundle de backup generat anterior, care se potrivește cu tipul de bază de date selectat.",
|
||||||
|
"install.recovery_source_database_backup_unavailable": "Restaurarea din backup-ul bazei de date nu este încă disponibilă în acest installer.",
|
||||||
|
"install.recovery_source_repository_filesystem": "Recuperează din structura de fișiere a arhivelor",
|
||||||
|
"install.recovery_source_repository_filesystem_helper": "Continuă cu o bază de date nouă și recuperează ulterior arhivele din directoarele de proiecte existente pe disc.",
|
||||||
|
"install.recovery_database_backup_error": "Au fost detectate bundle-uri de backup compatibile cu tipul de bază de date selectat",
|
||||||
|
"install.recovery_database_backup_message": "Restaurarea dintr-un backup al bazei de date poate readuce cea mai mare parte a stării originale a instalării, dar va suprascrie conținutul bazei de date țintă. Dacă știi ce faci, confirmă următoarele:",
|
||||||
|
"install.recovery_database_backup_select": "Bundle backup bază de date",
|
||||||
|
"install.recovery_database_backup_select_helper": "Selectează bundle-ul de backup care trebuie restaurat în baza de date configurată în prezent.",
|
||||||
|
"install.recovery_database_backup_confirm_check_1": "Confirmi că backupul selectat aparține acestei instanțe Gitea și corespunde tipului de bază de date selectat în prezent.",
|
||||||
|
"install.recovery_database_backup_confirm_check_2": "Confirmi că baza de date țintă este nouă sau goală și că procesul de restaurare îi poate suprascrie conținutul actual.",
|
||||||
|
"install.recovery_database_backup_confirm_check_3": "Confirmi că vei verifica instalarea restaurată după setup și că înțelegi că datele mai noi decât backupul selectat nu vor fi recuperate.",
|
||||||
|
"install.recovery_database_backup_discovery_failed": "Inspectarea căii configurate pentru backupurile bazei de date a eșuat: %v",
|
||||||
|
"install.recovery_database_backup_restore_failed": "Restaurarea backupului bazei de date selectat a eșuat: %v",
|
||||||
|
"install.recovery_repository_filesystem_error": "Au fost detectate date de proiect existente în calea configurată pentru arhive",
|
||||||
|
"install.recovery_repository_filesystem_message": "Recuperarea din structura de fișiere a arhivelor poate restaura proiectele, dar unele date de cont și din baza de date se pot pierde. Dacă știi ce faci, confirmă următoarele:",
|
||||||
|
"install.recovery_repository_filesystem_confirm_check_1": "Confirmi că calea actuală a arhivelor indică structura originală de proiecte pe care vrei să o recuperezi.",
|
||||||
|
"install.recovery_repository_filesystem_confirm_check_2": "Confirmi că înțelegi că această cale de recuperare poate pierde date existente doar în baza de date, cum ar fi conturi, sesiuni, tichete, pull request-uri, comentarii, tokenuri și notificări.",
|
||||||
|
"install.recovery_repository_filesystem_confirm_check_3": "Confirmi că vei verifica și adopta manual proiectele recuperate după instalare și că accepți limitele recuperării bazate pe structura de fișiere.",
|
||||||
"install.err_empty_db_path": "Calea bazei de date SQLite3 nu poate fi goală.",
|
"install.err_empty_db_path": "Calea bazei de date SQLite3 nu poate fi goală.",
|
||||||
"install.no_admin_and_disable_registration": "Nu poți dezactiva auto-înregistrarea utilizatorului fără a crea un cont de super administrator.",
|
"install.no_admin_and_disable_registration": "Nu poți dezactiva auto-înregistrarea utilizatorului fără a crea un cont de super administrator.",
|
||||||
"install.err_empty_admin_password": "Parola de super administrator nu poate fi goală.",
|
"install.err_empty_admin_password": "Parola de super administrator nu poate fi goală.",
|
||||||
@@ -3155,6 +3193,7 @@
|
|||||||
"admin.dashboard.gc_times": "Timpi GC",
|
"admin.dashboard.gc_times": "Timpi GC",
|
||||||
"admin.dashboard.delete_old_actions": "Șterge toate activitățile vechi din baza de date",
|
"admin.dashboard.delete_old_actions": "Șterge toate activitățile vechi din baza de date",
|
||||||
"admin.dashboard.delete_old_actions.started": "A început ștergerea tuturor activităților vechi din baza de date",
|
"admin.dashboard.delete_old_actions.started": "A început ștergerea tuturor activităților vechi din baza de date",
|
||||||
|
"admin.dashboard.database_backup": "Creează un backup al bazei de date",
|
||||||
"admin.dashboard.update_checker": "Verificare actualizări",
|
"admin.dashboard.update_checker": "Verificare actualizări",
|
||||||
"admin.dashboard.delete_old_system_notices": "Șterge toate notificările de sistem vechi, din baza de date",
|
"admin.dashboard.delete_old_system_notices": "Șterge toate notificările de sistem vechi, din baza de date",
|
||||||
"admin.dashboard.gc_lfs": "Meta-obiecte LFS de colectare a gunoiului",
|
"admin.dashboard.gc_lfs": "Meta-obiecte LFS de colectare a gunoiului",
|
||||||
|
|||||||
+199
-12
@@ -7,8 +7,8 @@ package install
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
_ "image/png"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
@@ -23,6 +23,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
_ "image/png"
|
||||||
|
|
||||||
"code.gitea.io/gitea/models/db"
|
"code.gitea.io/gitea/models/db"
|
||||||
db_install "code.gitea.io/gitea/models/db/install"
|
db_install "code.gitea.io/gitea/models/db/install"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
@@ -40,6 +42,7 @@ import (
|
|||||||
"code.gitea.io/gitea/routers/common"
|
"code.gitea.io/gitea/routers/common"
|
||||||
auth_service "code.gitea.io/gitea/services/auth"
|
auth_service "code.gitea.io/gitea/services/auth"
|
||||||
"code.gitea.io/gitea/services/context"
|
"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/forms"
|
||||||
"code.gitea.io/gitea/services/mailer"
|
"code.gitea.io/gitea/services/mailer"
|
||||||
"code.gitea.io/gitea/services/versioned_migration"
|
"code.gitea.io/gitea/services/versioned_migration"
|
||||||
@@ -58,6 +61,10 @@ const (
|
|||||||
installBrandingMaxFileSize = int64(1 << 20)
|
installBrandingMaxFileSize = int64(1 << 20)
|
||||||
installBrandingMinPNGEdge = 64
|
installBrandingMinPNGEdge = 64
|
||||||
installAppINIImportMaxSize = int64(1 << 20)
|
installAppINIImportMaxSize = int64(1 << 20)
|
||||||
|
|
||||||
|
recoveryModeExistingDatabase = "existing_database"
|
||||||
|
recoveryModeDatabaseBackup = "database_backup"
|
||||||
|
recoveryModeRepositoryFileSystem = "repository_filesystem"
|
||||||
)
|
)
|
||||||
|
|
||||||
type installBrandingAssetSpec struct {
|
type installBrandingAssetSpec struct {
|
||||||
@@ -67,6 +74,14 @@ type installBrandingAssetSpec struct {
|
|||||||
MimeType string
|
MimeType string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
type installBackupChoice struct {
|
||||||
|
ID string
|
||||||
|
Label string
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
var installBrandingAssetSpecs = []installBrandingAssetSpec{
|
var installBrandingAssetSpecs = []installBrandingAssetSpec{
|
||||||
{FormField: "logo_svg", TargetName: "logo.svg", LabelKey: "install.branding.logo_svg", MimeType: typesniffer.MimeTypeImageSvg},
|
{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: "logo_png", TargetName: "logo.png", LabelKey: "install.branding.logo_png", MimeType: "image/png"},
|
||||||
@@ -367,6 +382,16 @@ func newInstallFormFromSettings() (forms.InstallForm, string) {
|
|||||||
form.AdminManagementPolicy = setting.Admin.AdminManagementPolicy
|
form.AdminManagementPolicy = setting.Admin.AdminManagementPolicy
|
||||||
form.ReleaseMaxFiles = setting.Repository.Release.MaxFiles
|
form.ReleaseMaxFiles = setting.Repository.Release.MaxFiles
|
||||||
form.ReleaseFileMaxSize = setting.Repository.Release.FileMaxSize
|
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")
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
form.AllowAdoptionOfUnadoptedRepositories = true // edit/add - by petru @ codex
|
form.AllowAdoptionOfUnadoptedRepositories = true // edit/add - by petru @ codex
|
||||||
form.AllowDeleteOfUnadoptedRepositories = true // edit/add - by petru @ codex
|
form.AllowDeleteOfUnadoptedRepositories = true // edit/add - by petru @ codex
|
||||||
normalizeInstallRegistrationOptions(&form)
|
normalizeInstallRegistrationOptions(&form)
|
||||||
@@ -385,7 +410,80 @@ func renderInstallPage(ctx *context.Context, form *forms.InstallForm, curDBType
|
|||||||
ctx.HTML(http.StatusOK, tplInstall)
|
ctx.HTML(http.StatusOK, tplInstall)
|
||||||
}
|
}
|
||||||
|
|
||||||
func importedDBType(dbType string, fallback string) string {
|
// start 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 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return choices, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
|
func importedDBType(dbType, fallback string) string {
|
||||||
dbType = strings.TrimSpace(strings.ToLower(dbType))
|
dbType = strings.TrimSpace(strings.ToLower(dbType))
|
||||||
if slices.Contains(setting.SupportedDatabaseTypes, dbType) {
|
if slices.Contains(setting.SupportedDatabaseTypes, dbType) {
|
||||||
return dbType
|
return dbType
|
||||||
@@ -409,7 +507,7 @@ func importedInt64(sec setting.ConfigSection, key string, fallback int64) int64
|
|||||||
return cfgKey.MustInt64(fallback)
|
return cfgKey.MustInt64(fallback)
|
||||||
}
|
}
|
||||||
|
|
||||||
func importedFirstLang(value string, fallback string) string {
|
func importedFirstLang(value, fallback string) string {
|
||||||
for _, lang := range strings.Split(value, ",") {
|
for _, lang := range strings.Split(value, ",") {
|
||||||
lang = strings.TrimSpace(lang)
|
lang = strings.TrimSpace(lang)
|
||||||
if lang != "" {
|
if lang != "" {
|
||||||
@@ -433,6 +531,8 @@ func populateInstallFormFromConfig(form *forms.InstallForm, cfg setting.ConfigPr
|
|||||||
adminSec := cfg.Section("admin")
|
adminSec := cfg.Section("admin")
|
||||||
i18nSec := cfg.Section("i18n")
|
i18nSec := cfg.Section("i18n")
|
||||||
repoReleaseSec := cfg.Section("repository.release")
|
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.AppName = setting.ConfigSectionKeyString(rootSec, "APP_NAME", form.AppName)
|
||||||
form.RunUser = setting.ConfigSectionKeyString(rootSec, "RUN_USER", form.RunUser)
|
form.RunUser = setting.ConfigSectionKeyString(rootSec, "RUN_USER", form.RunUser)
|
||||||
@@ -499,6 +599,15 @@ func populateInstallFormFromConfig(form *forms.InstallForm, cfg setting.ConfigPr
|
|||||||
form.AdminManagementPolicy = setting.ConfigSectionKeyString(adminSec, "ADMIN_MANAGEMENT_POLICY", form.AdminManagementPolicy)
|
form.AdminManagementPolicy = setting.ConfigSectionKeyString(adminSec, "ADMIN_MANAGEMENT_POLICY", form.AdminManagementPolicy)
|
||||||
form.ReleaseMaxFiles = importedInt64(repoReleaseSec, "MAX_FILES", form.ReleaseMaxFiles)
|
form.ReleaseMaxFiles = importedInt64(repoReleaseSec, "MAX_FILES", form.ReleaseMaxFiles)
|
||||||
form.ReleaseFileMaxSize = importedInt64(repoReleaseSec, "FILE_MAX_SIZE", form.ReleaseFileMaxSize)
|
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 {
|
if form.ImportSensitiveSecrets {
|
||||||
form.ImportedLFSJWTSecret = readImportedSecretValue(serverSec, "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
form.ImportedLFSJWTSecret = readImportedSecretValue(serverSec, "LFS_JWT_SECRET_URI", "LFS_JWT_SECRET")
|
||||||
form.ImportedInternalToken = readImportedSecretValue(securitySec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
form.ImportedInternalToken = readImportedSecretValue(securitySec, "INTERNAL_TOKEN_URI", "INTERNAL_TOKEN")
|
||||||
@@ -652,7 +761,7 @@ func ImportAppINI(ctx *context.Context) {
|
|||||||
|
|
||||||
form, curDBType := newInstallFormFromSettings()
|
form, curDBType := newInstallFormFromSettings()
|
||||||
form.ImportSensitiveSecrets = true // edit/add - by petru @ codex
|
form.ImportSensitiveSecrets = true // edit/add - by petru @ codex
|
||||||
form.ImportedAppINI = true // edit/add - by petru @ codex
|
form.ImportedAppINI = true // edit/add - by petru @ codex
|
||||||
cfg, err := readImportedInstallConfig(ctx)
|
cfg, err := readImportedInstallConfig(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Data["CurDbType"] = curDBType
|
ctx.Data["CurDbType"] = curDBType
|
||||||
@@ -734,7 +843,7 @@ func populateInstallSMTPFromFields(form *forms.InstallForm) {
|
|||||||
|
|
||||||
// start edit/add - by petru @ codex
|
// start edit/add - by petru @ codex
|
||||||
func applyImportedAppINIRepositoryRecoveryDefaults(form *forms.InstallForm, dbPreviouslyUsed bool) {
|
func applyImportedAppINIRepositoryRecoveryDefaults(form *forms.InstallForm, dbPreviouslyUsed bool) {
|
||||||
if form.ImportedAppINI && !dbPreviouslyUsed {
|
if form.ImportedAppINI && !dbPreviouslyUsed && form.RecoveryMode == recoveryModeRepositoryFileSystem {
|
||||||
form.AllowAdoptionOfUnadoptedRepositories = true
|
form.AllowAdoptionOfUnadoptedRepositories = true
|
||||||
form.AllowDeleteOfUnadoptedRepositories = true
|
form.AllowDeleteOfUnadoptedRepositories = true
|
||||||
}
|
}
|
||||||
@@ -894,8 +1003,11 @@ func checkDatabase(ctx *context.Context, form *forms.InstallForm) (bool, bool) {
|
|||||||
|
|
||||||
if hasPostInstallationUser && dbMigrationVersion > 0 {
|
if hasPostInstallationUser && dbMigrationVersion > 0 {
|
||||||
log.Error("The database is likely to have been used by Gitea before, database migration version=%d", dbMigrationVersion)
|
log.Error("The database is likely to have been used by Gitea before, database migration version=%d", dbMigrationVersion)
|
||||||
confirmed := form.ReinstallConfirmFirst && form.ReinstallConfirmSecond && form.ReinstallConfirmThird
|
setInstallRecoverySourceAvailability(ctx, true, nil, false) // edit/add - by petru @ codex
|
||||||
if !confirmed {
|
if form.RecoveryMode == "" {
|
||||||
|
form.RecoveryMode = recoveryModeExistingDatabase
|
||||||
|
}
|
||||||
|
if form.RecoveryMode != recoveryModeExistingDatabase || !installRecoveryConfirmationsAccepted(form) {
|
||||||
ctx.Data["Err_DbInstalledBefore"] = true
|
ctx.Data["Err_DbInstalledBefore"] = true
|
||||||
ctx.RenderWithErrDeprecated(ctx.Tr("install.reinstall_error"), tplInstall, form)
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.reinstall_error"), tplInstall, form)
|
||||||
return false, dbPreviouslyUsed
|
return false, dbPreviouslyUsed
|
||||||
@@ -999,6 +1111,54 @@ func SubmitInstall(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
hasRepositoryFilesystem := form.ImportedAppINI && hasInstallRepositoryFilesystem(form.RepoRootPath)
|
||||||
|
availableBackups, err := listInstallBackupChoices(form.BackupPath, form.DbType)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Data["Err_BackupPath"] = true
|
||||||
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_database_backup_discovery_failed", err), tplInstall, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasDatabaseBackups := len(availableBackups) > 0
|
||||||
|
if form.BackupRestoreID == "" && hasDatabaseBackups {
|
||||||
|
form.BackupRestoreID = availableBackups[0].ID
|
||||||
|
}
|
||||||
|
if form.RecoveryMode == recoveryModeDatabaseBackup && !hasDatabaseBackups {
|
||||||
|
form.RecoveryMode = "" // 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 && form.BackupRestoreID != ""
|
||||||
|
case recoveryModeRepositoryFileSystem:
|
||||||
|
// nothing extra
|
||||||
|
default:
|
||||||
|
recoverySelectionValid = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !recoverySelectionValid {
|
||||||
|
if form.RecoveryMode == recoveryModeDatabaseBackup && hasDatabaseBackups {
|
||||||
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
||||||
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_database_backup_error"), tplInstall, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data["Err_RepositoryFilesystemRecovery"] = true
|
||||||
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_repository_filesystem_error"), tplInstall, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
// Test LFS root path if not empty, empty meaning disable LFS
|
// Test LFS root path if not empty, empty meaning disable LFS
|
||||||
if form.LFSRootPath != "" {
|
if form.LFSRootPath != "" {
|
||||||
form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
|
form.LFSRootPath = strings.ReplaceAll(form.LFSRootPath, "\\", "/")
|
||||||
@@ -1070,13 +1230,31 @@ func SubmitInstall(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init the engine with migration
|
// start edit/add - by petru @ codex
|
||||||
if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil {
|
if !dbPreviouslyUsed && form.RecoveryMode == recoveryModeDatabaseBackup {
|
||||||
|
if err = db.InitEngine(ctx); err != nil {
|
||||||
|
db.UnsetDefaultEngine()
|
||||||
|
ctx.Data["Err_DbSetting"] = true
|
||||||
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err = dbbackup_service.RestoreDatabaseBackup(ctx, resolveInstallBackupPath(form.BackupPath), form.BackupRestoreID, form.DbType); err != nil {
|
||||||
|
db.UnsetDefaultEngine()
|
||||||
|
ctx.Data["Err_DatabaseBackupRecovery"] = true
|
||||||
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.recovery_database_backup_restore_failed", err), tplInstall, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
db.UnsetDefaultEngine()
|
db.UnsetDefaultEngine()
|
||||||
ctx.Data["Err_DbSetting"] = true
|
} else {
|
||||||
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form)
|
// Init the engine with migration
|
||||||
return
|
if err = db.InitEngineWithMigration(ctx, versioned_migration.Migrate); err != nil {
|
||||||
|
db.UnsetDefaultEngine()
|
||||||
|
ctx.Data["Err_DbSetting"] = true
|
||||||
|
ctx.RenderWithErrDeprecated(ctx.Tr("install.invalid_db_setting", err), tplInstall, &form)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
// Save settings.
|
// Save settings.
|
||||||
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
cfg, err := setting.NewConfigProviderFromFile(setting.CustomConf)
|
||||||
@@ -1105,6 +1283,15 @@ func SubmitInstall(ctx *context.Context) {
|
|||||||
cfg.Section("repository").Key("ALLOW_DELETE_OF_UNADOPTED_REPOSITORIES").SetValue(strconv.FormatBool(form.AllowDeleteOfUnadoptedRepositories)) // 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("MAX_FILES").SetValue(strconv.FormatInt(form.ReleaseMaxFiles, 10))
|
||||||
cfg.Section("repository.release").Key("FILE_MAX_SIZE").SetValue(strconv.FormatInt(form.ReleaseFileMaxSize, 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("SSH_DOMAIN").SetValue(form.Domain)
|
||||||
cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
|
cfg.Section("server").Key("DOMAIN").SetValue(form.Domain)
|
||||||
cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
|
cfg.Section("server").Key("HTTP_PORT").SetValue(form.HTTPPort)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ package install
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
@@ -140,6 +141,17 @@ ADMIN_MANAGEMENT_POLICY = super_admin_only
|
|||||||
MAX_FILES = 20
|
MAX_FILES = 20
|
||||||
FILE_MAX_SIZE = 2048
|
FILE_MAX_SIZE = 2048
|
||||||
|
|
||||||
|
[backup]
|
||||||
|
PATH = /srv/gitea/backups/db
|
||||||
|
RETENTION_COUNT = 9
|
||||||
|
COMPRESS = false
|
||||||
|
INCLUDE_APP_INI_SNAPSHOT = false
|
||||||
|
|
||||||
|
[cron.database_backup]
|
||||||
|
ENABLED = true
|
||||||
|
RUN_AT_START = true
|
||||||
|
SCHEDULE = @every 12h
|
||||||
|
|
||||||
[i18n]
|
[i18n]
|
||||||
LANGS = de-DE,en-US
|
LANGS = de-DE,en-US
|
||||||
`)
|
`)
|
||||||
@@ -193,12 +205,35 @@ LANGS = de-DE,en-US
|
|||||||
assert.Equal(t, "super_admin_only", form.AdminManagementPolicy)
|
assert.Equal(t, "super_admin_only", form.AdminManagementPolicy)
|
||||||
assert.EqualValues(t, 20, form.ReleaseMaxFiles)
|
assert.EqualValues(t, 20, form.ReleaseMaxFiles)
|
||||||
assert.EqualValues(t, 2048, form.ReleaseFileMaxSize)
|
assert.EqualValues(t, 2048, form.ReleaseFileMaxSize)
|
||||||
|
assert.True(t, form.BackupEnabled)
|
||||||
|
assert.True(t, form.BackupRunAtStart)
|
||||||
|
assert.Equal(t, "@every 12h", form.BackupSchedule)
|
||||||
|
assert.Equal(t, "/srv/gitea/backups/db", form.BackupPath)
|
||||||
|
assert.Equal(t, 9, form.BackupRetentionCount)
|
||||||
|
assert.False(t, form.BackupCompress)
|
||||||
|
assert.False(t, form.BackupIncludeAppINISnapshot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
func TestNewInstallFormFromSettingsBackupDefaults(t *testing.T) {
|
||||||
|
cfg, err := setting.NewConfigProviderFromData("")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer test.MockVariableValue(&setting.CfgProvider, cfg)()
|
||||||
|
|
||||||
|
form, _ := newInstallFormFromSettings()
|
||||||
|
|
||||||
|
assert.True(t, form.BackupEnabled)
|
||||||
|
assert.False(t, form.BackupRunAtStart)
|
||||||
|
assert.Equal(t, "@daily", form.BackupSchedule)
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
// start edit/add - by petru @ codex
|
// start edit/add - by petru @ codex
|
||||||
func TestApplyImportedAppINIRepositoryRecoveryDefaults(t *testing.T) {
|
func TestApplyImportedAppINIRepositoryRecoveryDefaults(t *testing.T) {
|
||||||
form := forms.InstallForm{
|
form := forms.InstallForm{
|
||||||
ImportedAppINI: true,
|
ImportedAppINI: true,
|
||||||
|
RecoveryMode: recoveryModeRepositoryFileSystem,
|
||||||
AllowAdoptionOfUnadoptedRepositories: false,
|
AllowAdoptionOfUnadoptedRepositories: false,
|
||||||
AllowDeleteOfUnadoptedRepositories: false,
|
AllowDeleteOfUnadoptedRepositories: false,
|
||||||
}
|
}
|
||||||
@@ -209,6 +244,7 @@ func TestApplyImportedAppINIRepositoryRecoveryDefaults(t *testing.T) {
|
|||||||
|
|
||||||
form = forms.InstallForm{
|
form = forms.InstallForm{
|
||||||
ImportedAppINI: true,
|
ImportedAppINI: true,
|
||||||
|
RecoveryMode: recoveryModeRepositoryFileSystem,
|
||||||
AllowAdoptionOfUnadoptedRepositories: false,
|
AllowAdoptionOfUnadoptedRepositories: false,
|
||||||
AllowDeleteOfUnadoptedRepositories: false,
|
AllowDeleteOfUnadoptedRepositories: false,
|
||||||
}
|
}
|
||||||
@@ -218,6 +254,81 @@ func TestApplyImportedAppINIRepositoryRecoveryDefaults(t *testing.T) {
|
|||||||
assert.False(t, form.AllowDeleteOfUnadoptedRepositories)
|
assert.False(t, form.AllowDeleteOfUnadoptedRepositories)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyImportedAppINIRepositoryRecoveryDefaultsWithoutFilesystemRecoveryMode(t *testing.T) {
|
||||||
|
form := forms.InstallForm{
|
||||||
|
ImportedAppINI: true,
|
||||||
|
RecoveryMode: recoveryModeExistingDatabase,
|
||||||
|
AllowAdoptionOfUnadoptedRepositories: false,
|
||||||
|
AllowDeleteOfUnadoptedRepositories: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
applyImportedAppINIRepositoryRecoveryDefaults(&form, false)
|
||||||
|
assert.False(t, form.AllowAdoptionOfUnadoptedRepositories)
|
||||||
|
assert.False(t, form.AllowDeleteOfUnadoptedRepositories)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHasInstallRepositoryFilesystem(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
assert.False(t, hasInstallRepositoryFilesystem(tmpDir))
|
||||||
|
|
||||||
|
require.NoError(t, os.Mkdir(filepath.Join(tmpDir, "owner"), 0o755))
|
||||||
|
require.NoError(t, os.Mkdir(filepath.Join(tmpDir, "owner", "repo.git"), 0o755))
|
||||||
|
assert.True(t, hasInstallRepositoryFilesystem(tmpDir))
|
||||||
|
|
||||||
|
tmpDirFlat := t.TempDir()
|
||||||
|
require.NoError(t, os.Mkdir(filepath.Join(tmpDirFlat, "repo.git"), 0o755))
|
||||||
|
assert.True(t, hasInstallRepositoryFilesystem(tmpDirFlat))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveInstallBackupPath(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.AppWorkPath, "/srv/gitea")()
|
||||||
|
|
||||||
|
assert.Equal(t, "", resolveInstallBackupPath(""))
|
||||||
|
assert.Equal(t, "/srv/gitea/data/backups/db", resolveInstallBackupPath("data/backups/db"))
|
||||||
|
assert.Equal(t, "/var/lib/gitea/backups", resolveInstallBackupPath("/var/lib/gitea/backups"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListInstallBackupChoices(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
|
writeInstallBackupManifestForTest(t, tmpDir, "20260524-010101.000000001", "sqlite3", 100)
|
||||||
|
writeInstallBackupManifestForTest(t, tmpDir, "20260524-010102.000000002", "mysql", 200)
|
||||||
|
writeInstallBackupManifestForTest(t, tmpDir, "20260524-010103.000000003", "sqlite3", 300)
|
||||||
|
|
||||||
|
choices, err := listInstallBackupChoices(tmpDir, "sqlite3")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, choices, 2)
|
||||||
|
assert.Equal(t, "20260524-010103.000000003", choices[0].ID)
|
||||||
|
assert.Equal(t, "20260524-010101.000000001", choices[1].ID)
|
||||||
|
assert.Equal(t, "20260524-010103.000000003 (sqlite3)", choices[0].Label)
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
func writeInstallBackupManifestForTest(t *testing.T, rootPath, backupID, dbType string, createdUnix int64) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
backupDir := filepath.Join(rootPath, backupID)
|
||||||
|
require.NoError(t, os.MkdirAll(backupDir, 0o755))
|
||||||
|
|
||||||
|
manifestBytes, err := json.Marshal(map[string]any{
|
||||||
|
"schema_version": 1,
|
||||||
|
"id": backupID,
|
||||||
|
"created_unix": createdUnix,
|
||||||
|
"db_type": dbType,
|
||||||
|
"app_version": "dev",
|
||||||
|
"app_name": "Test",
|
||||||
|
"migration_version": 1,
|
||||||
|
"file_name": "database.sql.gz",
|
||||||
|
"file_sha256": "abc",
|
||||||
|
"file_size": 1,
|
||||||
|
"compressed": true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(backupDir, "manifest.json"), manifestBytes, 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
// end edit/add - by petru @ codex
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
func TestPopulateInstallFormFromConfigReplacesSMTPFromSplitFields(t *testing.T) {
|
func TestPopulateInstallFormFromConfigReplacesSMTPFromSplitFields(t *testing.T) {
|
||||||
|
|||||||
+1
-1
@@ -1071,7 +1071,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
|||||||
m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
|
m.Post("", web.Bind(forms.BlockUserForm{}), org.BlockedUsersPost)
|
||||||
})
|
})
|
||||||
m.Group("/repos", func() {
|
m.Group("/repos", func() {
|
||||||
m.Get("", org.Repos) // edit/add - by petru @ codex
|
m.Get("", org.Repos) // edit/add - by petru @ codex
|
||||||
m.Post("/unadopted", org.AdoptOrDeleteRepository) // edit/add - by petru @ codex
|
m.Post("/unadopted", org.AdoptOrDeleteRepository) // edit/add - by petru @ codex
|
||||||
})
|
})
|
||||||
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ func ListTasks() TaskTable {
|
|||||||
|
|
||||||
tTable := make([]*TaskTableRow, 0, len(tasks))
|
tTable := make([]*TaskTableRow, 0, len(tasks))
|
||||||
for _, task := range tasks {
|
for _, task := range tasks {
|
||||||
spec := "-"
|
spec := task.config.GetSchedule() // edit/add - by petru @ codex
|
||||||
var (
|
var (
|
||||||
next time.Time
|
next time.Time
|
||||||
prev time.Time
|
prev time.Time
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/updatechecker"
|
"code.gitea.io/gitea/modules/updatechecker"
|
||||||
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
asymkey_service "code.gitea.io/gitea/services/asymkey"
|
||||||
|
dbbackup_service "code.gitea.io/gitea/services/dbbackup" // edit/add - by petru @ codex
|
||||||
repo_service "code.gitea.io/gitea/services/repository"
|
repo_service "code.gitea.io/gitea/services/repository"
|
||||||
archiver_service "code.gitea.io/gitea/services/repository/archiver"
|
archiver_service "code.gitea.io/gitea/services/repository/archiver"
|
||||||
user_service "code.gitea.io/gitea/services/user"
|
user_service "code.gitea.io/gitea/services/user"
|
||||||
@@ -234,6 +235,21 @@ func registerRebuildIssueIndexer() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
func registerDatabaseBackup() {
|
||||||
|
RegisterTaskFatal("database_backup", &BaseConfig{
|
||||||
|
Enabled: true,
|
||||||
|
RunAtStart: false,
|
||||||
|
Schedule: "@daily",
|
||||||
|
NoticeOnSuccess: false,
|
||||||
|
}, func(ctx context.Context, _ *user_model.User, _ Config) error {
|
||||||
|
_, _, err := dbbackup_service.CreateDatabaseBackup(ctx)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
func initExtendedTasks() {
|
func initExtendedTasks() {
|
||||||
registerDeleteInactiveUsers()
|
registerDeleteInactiveUsers()
|
||||||
registerDeleteExpiredAccountRequests()
|
registerDeleteExpiredAccountRequests()
|
||||||
@@ -250,4 +266,5 @@ func initExtendedTasks() {
|
|||||||
registerDeleteOldSystemNotices()
|
registerDeleteOldSystemNotices()
|
||||||
registerGCLFS()
|
registerGCLFS()
|
||||||
registerRebuildIssueIndexer()
|
registerRebuildIssueIndexer()
|
||||||
|
registerDatabaseBackup() // edit/add - by petru @ codex
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,390 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package dbbackup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/models/db"
|
||||||
|
db_install "code.gitea.io/gitea/models/db/install"
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
const (
|
||||||
|
manifestFileName = "manifest.json"
|
||||||
|
databaseDumpFileName = "database.sql"
|
||||||
|
databaseDumpGzipFileName = "database.sql.gz"
|
||||||
|
appINISnapshotFileName = "app.ini"
|
||||||
|
backupManifestSchemaValue = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
type Manifest struct {
|
||||||
|
SchemaVersion int `json:"schema_version"`
|
||||||
|
ID string `json:"id"`
|
||||||
|
CreatedUnix int64 `json:"created_unix"`
|
||||||
|
DBType string `json:"db_type"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
AppName string `json:"app_name"`
|
||||||
|
MigrationVersion int64 `json:"migration_version"`
|
||||||
|
FileName string `json:"file_name"`
|
||||||
|
FileSHA256 string `json:"file_sha256"`
|
||||||
|
FileSize int64 `json:"file_size"`
|
||||||
|
Compressed bool `json:"compressed"`
|
||||||
|
AppINISnapshotFile string `json:"app_ini_snapshot_file,omitempty"`
|
||||||
|
|
||||||
|
dirPath string `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
ErrBackupNotFound = errors.New("database backup not found")
|
||||||
|
ErrBackupDBTypeMismatch = errors.New("database backup type mismatch")
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
)
|
||||||
|
|
||||||
|
func CreateDatabaseBackup(ctx context.Context) (*Manifest, string, error) {
|
||||||
|
createdAt := time.Now().UTC()
|
||||||
|
backupID := createdAt.Format("20060102-150405.000000000")
|
||||||
|
finalDir := filepath.Join(setting.Backup.Path, backupID)
|
||||||
|
tempDir := finalDir + ".tmp"
|
||||||
|
|
||||||
|
if err := os.MkdirAll(setting.Backup.Path, 0o700); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(tempDir, 0o700); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
success := false
|
||||||
|
defer func() {
|
||||||
|
if !success {
|
||||||
|
_ = os.RemoveAll(tempDir)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
dumpPath := filepath.Join(tempDir, databaseDumpFileName)
|
||||||
|
if err := db.DumpDatabase(dumpPath, setting.Database.Type.String()); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
backupFileName := databaseDumpFileName
|
||||||
|
finalDumpPath := dumpPath
|
||||||
|
if setting.Backup.Compress {
|
||||||
|
backupFileName = databaseDumpGzipFileName
|
||||||
|
finalDumpPath = filepath.Join(tempDir, backupFileName)
|
||||||
|
if err := gzipFile(dumpPath, finalDumpPath); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if err := os.Remove(dumpPath); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileSHA256, fileSize, err := fileDigestAndSize(finalDumpPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := &Manifest{
|
||||||
|
SchemaVersion: backupManifestSchemaValue,
|
||||||
|
ID: backupID,
|
||||||
|
CreatedUnix: createdAt.Unix(),
|
||||||
|
DBType: setting.Database.Type.String(),
|
||||||
|
AppVersion: setting.AppVer,
|
||||||
|
AppName: setting.AppName,
|
||||||
|
MigrationVersion: currentMigrationVersion(ctx),
|
||||||
|
FileName: backupFileName,
|
||||||
|
FileSHA256: fileSHA256,
|
||||||
|
FileSize: fileSize,
|
||||||
|
Compressed: setting.Backup.Compress,
|
||||||
|
}
|
||||||
|
|
||||||
|
if setting.Backup.IncludeAppINISnapshot {
|
||||||
|
copiedAppINI, err := copyAppINI(filepath.Join(tempDir, appINISnapshotFileName))
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if copiedAppINI {
|
||||||
|
manifest.AppINISnapshotFile = appINISnapshotFileName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := writeManifest(filepath.Join(tempDir, manifestFileName), manifest); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.Rename(tempDir, finalDir); err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
success = true
|
||||||
|
|
||||||
|
manifest.dirPath = finalDir
|
||||||
|
if err := PruneOldBackups(setting.Backup.RetentionCount); err != nil {
|
||||||
|
return manifest, finalDir, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return manifest, finalDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListBackups() ([]*Manifest, error) {
|
||||||
|
return ListBackupsInPath(setting.Backup.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
func ListBackupsInPath(backupPath string) ([]*Manifest, error) {
|
||||||
|
backupPath = strings.TrimSpace(backupPath)
|
||||||
|
if backupPath == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(backupPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
manifests := make([]*Manifest, 0, len(entries))
|
||||||
|
for _, entry := range entries {
|
||||||
|
if !entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
manifestPath := filepath.Join(backupPath, entry.Name(), manifestFileName)
|
||||||
|
manifest, err := readManifest(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
manifest.dirPath = filepath.Join(backupPath, entry.Name())
|
||||||
|
manifests = append(manifests, manifest)
|
||||||
|
}
|
||||||
|
|
||||||
|
slices.SortFunc(manifests, func(a, b *Manifest) int {
|
||||||
|
if a.CreatedUnix == b.CreatedUnix {
|
||||||
|
if a.ID > b.ID {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
if a.ID < b.ID {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if a.CreatedUnix > b.CreatedUnix {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
})
|
||||||
|
return manifests, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
|
func PruneOldBackups(retentionCount int) error {
|
||||||
|
if retentionCount <= 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
backups, err := ListBackups()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if retentionCount >= len(backups) {
|
||||||
|
return nil // edit/add - by petru @ codex
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, backup := range backups[retentionCount:] {
|
||||||
|
if backup.dirPath == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := os.RemoveAll(backup.dirPath); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func currentMigrationVersion(ctx context.Context) int64 {
|
||||||
|
version, err := db_install.GetMigrationVersion(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
func FindBackupInPath(backupPath, backupID string) (*Manifest, error) {
|
||||||
|
backups, err := ListBackupsInPath(backupPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, backup := range backups {
|
||||||
|
if backup.ID == backupID {
|
||||||
|
return backup, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, ErrBackupNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func RestoreDatabaseBackup(ctx context.Context, backupPath, backupID, targetDBType string) (*Manifest, error) {
|
||||||
|
backup, err := FindBackupInPath(backupPath, backupID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if targetDBType != "" && backup.DBType != targetDBType {
|
||||||
|
return nil, fmt.Errorf("%w: backup=%s target=%s", ErrBackupDBTypeMismatch, backup.DBType, targetDBType)
|
||||||
|
}
|
||||||
|
|
||||||
|
importPath, cleanup, err := prepareRestoreSQLFile(backup)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer cleanup()
|
||||||
|
|
||||||
|
if err := db.ImportDatabase(importPath); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
_ = ctx
|
||||||
|
return backup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareRestoreSQLFile(backup *Manifest) (string, func(), error) {
|
||||||
|
sourcePath := filepath.Join(backup.dirPath, backup.FileName)
|
||||||
|
if !backup.Compressed {
|
||||||
|
return sourcePath, func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
tempFile, err := os.CreateTemp("", "gitea-db-restore-*.sql")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
tempPath := tempFile.Name()
|
||||||
|
cleanup := func() {
|
||||||
|
_ = tempFile.Close()
|
||||||
|
_ = os.Remove(tempPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceFile, err := os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
cleanup()
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
gzipReader, err := gzip.NewReader(sourceFile)
|
||||||
|
if err != nil {
|
||||||
|
cleanup()
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
defer gzipReader.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(tempFile, gzipReader); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err = tempFile.Close(); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
return tempPath, cleanup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
|
|
||||||
|
func gzipFile(sourcePath, targetPath string) error {
|
||||||
|
sourceFile, err := os.Open(sourcePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
targetFile, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer targetFile.Close()
|
||||||
|
|
||||||
|
gzipWriter := gzip.NewWriter(targetFile)
|
||||||
|
if _, err := io.Copy(gzipWriter, sourceFile); err != nil {
|
||||||
|
_ = gzipWriter.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return gzipWriter.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func fileDigestAndSize(filePath string) (string, int64, error) {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hasher := sha256.New()
|
||||||
|
size, err := io.Copy(hasher, file)
|
||||||
|
if err != nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(hasher.Sum(nil)), size, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyAppINI(targetPath string) (bool, error) {
|
||||||
|
sourceFile, err := os.Open(setting.CustomConf)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer sourceFile.Close()
|
||||||
|
|
||||||
|
targetFile, err := os.Create(targetPath)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer targetFile.Close()
|
||||||
|
|
||||||
|
if _, err = io.Copy(targetFile, sourceFile); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeManifest(manifestPath string, manifest *Manifest) error {
|
||||||
|
manifestBytes, err := json.MarshalIndent(manifest, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
manifestBytes = append(manifestBytes, '\n')
|
||||||
|
return os.WriteFile(manifestPath, manifestBytes, 0o600)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readManifest(manifestPath string) (*Manifest, error) {
|
||||||
|
data, err := os.ReadFile(manifestPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := &Manifest{}
|
||||||
|
if err := json.Unmarshal(data, manifest); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if manifest.ID == "" || manifest.FileName == "" {
|
||||||
|
return nil, fmt.Errorf("invalid backup manifest: %s", manifestPath)
|
||||||
|
}
|
||||||
|
return manifest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Copyright 2026 The Gitea Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package dbbackup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"code.gitea.io/gitea/modules/test"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// start edit/add - by petru @ codex
|
||||||
|
func TestListBackups(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
defer test.MockVariableValue(&setting.Backup.Path, tmpDir)()
|
||||||
|
|
||||||
|
writeBackupManifestForTest(t, tmpDir, "20260524-010101.000000001", 100)
|
||||||
|
writeBackupManifestForTest(t, tmpDir, "20260524-010102.000000002", 200)
|
||||||
|
|
||||||
|
backups, err := ListBackups()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, backups, 2)
|
||||||
|
assert.Equal(t, "20260524-010102.000000002", backups[0].ID)
|
||||||
|
assert.Equal(t, "20260524-010101.000000001", backups[1].ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPruneOldBackups(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
defer test.MockVariableValue(&setting.Backup.Path, tmpDir)()
|
||||||
|
|
||||||
|
writeBackupManifestForTest(t, tmpDir, "20260524-010101.000000001", 100)
|
||||||
|
writeBackupManifestForTest(t, tmpDir, "20260524-010102.000000002", 200)
|
||||||
|
writeBackupManifestForTest(t, tmpDir, "20260524-010103.000000003", 300)
|
||||||
|
|
||||||
|
require.NoError(t, PruneOldBackups(2))
|
||||||
|
|
||||||
|
backups, err := ListBackups()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, backups, 2)
|
||||||
|
assert.Equal(t, int64(300), backups[0].CreatedUnix)
|
||||||
|
assert.Equal(t, int64(200), backups[1].CreatedUnix)
|
||||||
|
assert.NoDirExists(t, filepath.Join(tmpDir, "20260524-010101.000000001"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPruneOldBackupsRetentionGreaterThanBackupCount(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
defer test.MockVariableValue(&setting.Backup.Path, tmpDir)()
|
||||||
|
|
||||||
|
writeBackupManifestForTest(t, tmpDir, "20260524-010101.000000001", 100)
|
||||||
|
writeBackupManifestForTest(t, tmpDir, "20260524-010102.000000002", 200)
|
||||||
|
|
||||||
|
require.NoError(t, PruneOldBackups(7))
|
||||||
|
|
||||||
|
backups, err := ListBackups()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, backups, 2)
|
||||||
|
assert.DirExists(t, filepath.Join(tmpDir, "20260524-010101.000000001"))
|
||||||
|
assert.DirExists(t, filepath.Join(tmpDir, "20260524-010102.000000002"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeBackupManifestForTest(t *testing.T, rootPath, backupID string, createdUnix int64) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
backupDir := filepath.Join(rootPath, backupID)
|
||||||
|
require.NoError(t, os.MkdirAll(backupDir, 0o755))
|
||||||
|
|
||||||
|
manifestBytes, err := json.Marshal(&Manifest{
|
||||||
|
SchemaVersion: backupManifestSchemaValue,
|
||||||
|
ID: backupID,
|
||||||
|
CreatedUnix: createdUnix,
|
||||||
|
DBType: "sqlite3",
|
||||||
|
AppVersion: "dev",
|
||||||
|
AppName: "Test",
|
||||||
|
MigrationVersion: 1,
|
||||||
|
FileName: databaseDumpGzipFileName,
|
||||||
|
FileSHA256: "abc",
|
||||||
|
FileSize: 1,
|
||||||
|
Compressed: true,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, os.WriteFile(filepath.Join(backupDir, manifestFileName), manifestBytes, 0o644))
|
||||||
|
}
|
||||||
|
|
||||||
|
// end edit/add - by petru @ codex
|
||||||
@@ -50,6 +50,8 @@ type InstallForm struct {
|
|||||||
ImportedLFSJWTSecret string `form:"imported_lfs_jwt_secret"`
|
ImportedLFSJWTSecret string `form:"imported_lfs_jwt_secret"`
|
||||||
ImportedInternalToken string `form:"imported_internal_token"`
|
ImportedInternalToken string `form:"imported_internal_token"`
|
||||||
ImportedOAuth2JWTSecret string `form:"imported_o_auth2_jwt_secret"`
|
ImportedOAuth2JWTSecret string `form:"imported_o_auth2_jwt_secret"`
|
||||||
|
RecoveryMode string `binding:"In(,existing_database,database_backup,repository_filesystem)" locale:"install.recovery_source_title"` // edit/add - by petru @ codex
|
||||||
|
BackupRestoreID string // edit/add - by petru @ codex
|
||||||
RegisterConfirm bool
|
RegisterConfirm bool
|
||||||
RegisterManualConfirm bool
|
RegisterManualConfirm bool
|
||||||
MailNotify bool
|
MailNotify bool
|
||||||
@@ -74,8 +76,15 @@ type InstallForm struct {
|
|||||||
AdminManagementPolicy string `binding:"In(,super_admin_only,grantor_only,grantor_inheritance)" locale:"install.admin_management_policy"`
|
AdminManagementPolicy string `binding:"In(,super_admin_only,grantor_only,grantor_inheritance)" locale:"install.admin_management_policy"`
|
||||||
ReleaseMaxFiles int64
|
ReleaseMaxFiles int64
|
||||||
ReleaseFileMaxSize int64
|
ReleaseFileMaxSize int64
|
||||||
AllowAdoptionOfUnadoptedRepositories bool // edit/add - by petru @ codex
|
BackupEnabled bool // edit/add - by petru @ codex
|
||||||
AllowDeleteOfUnadoptedRepositories bool // edit/add - by petru @ codex
|
BackupRunAtStart bool // edit/add - by petru @ codex
|
||||||
|
BackupSchedule string // edit/add - by petru @ codex
|
||||||
|
BackupPath string // edit/add - by petru @ codex
|
||||||
|
BackupRetentionCount int // edit/add - by petru @ codex
|
||||||
|
BackupCompress bool // edit/add - by petru @ codex
|
||||||
|
BackupIncludeAppINISnapshot bool // edit/add - by petru @ codex
|
||||||
|
AllowAdoptionOfUnadoptedRepositories bool // edit/add - by petru @ codex
|
||||||
|
AllowDeleteOfUnadoptedRepositories bool // edit/add - by petru @ codex
|
||||||
|
|
||||||
AdminName string `binding:"OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"`
|
AdminName string `binding:"OmitEmpty;Username;MaxSize(30)" locale:"install.admin_name"`
|
||||||
AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"`
|
AdminPasswd string `binding:"OmitEmpty;MaxSize(255)" locale:"install.admin_password"`
|
||||||
|
|||||||
@@ -58,6 +58,12 @@
|
|||||||
<td>{{ctx.Locale.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td>
|
<td>{{ctx.Locale.Tr "admin.dashboard.delete_generated_repository_avatars"}}</td>
|
||||||
<td class="tw-text-right"><button type="submit" class="ui primary button" name="op" value="delete_generated_repository_avatars">{{svg "octicon-play"}} {{ctx.Locale.Tr "admin.dashboard.operation_run"}}</button></td>
|
<td class="tw-text-right"><button type="submit" class="ui primary button" name="op" value="delete_generated_repository_avatars">{{svg "octicon-play"}} {{ctx.Locale.Tr "admin.dashboard.operation_run"}}</button></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<!-- start edit/add - by petru @ codex -->
|
||||||
|
<tr>
|
||||||
|
<td>{{ctx.Locale.Tr "admin.dashboard.database_backup"}}</td>
|
||||||
|
<td class="tw-text-right"><button type="submit" class="ui primary button" name="op" value="database_backup">{{svg "octicon-play"}} {{ctx.Locale.Tr "admin.dashboard.operation_run"}}</button></td>
|
||||||
|
</tr>
|
||||||
|
<!-- end edit/add - by petru @ codex -->
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ctx.Locale.Tr "admin.dashboard.sync_repo_branches"}}</td>
|
<td>{{ctx.Locale.Tr "admin.dashboard.sync_repo_branches"}}</td>
|
||||||
<td class="tw-text-right"><button type="submit" class="ui primary button" name="op" value="sync_repo_branches">{{svg "octicon-play"}} {{ctx.Locale.Tr "admin.dashboard.operation_run"}}</button></td>
|
<td class="tw-text-right"><button type="submit" class="ui primary button" name="op" value="sync_repo_branches">{{svg "octicon-play"}} {{ctx.Locale.Tr "admin.dashboard.operation_run"}}</button></td>
|
||||||
|
|||||||
+259
-19
@@ -6,7 +6,7 @@
|
|||||||
{{ctx.Locale.Tr "install.title"}}
|
{{ctx.Locale.Tr "install.title"}}
|
||||||
</h3>
|
</h3>
|
||||||
<div class="ui attached segment">
|
<div class="ui attached segment">
|
||||||
{{if not .Err_DbInstalledBefore}}{{template "base/alert" .}}{{end}} <!-- edit/add - by petru @ codex -->
|
{{if not (or .Err_DbInstalledBefore .Err_RepositoryFilesystemRecovery .Err_DatabaseBackupRecovery)}}{{template "base/alert" .}}{{end}} <!-- edit/add - by petru @ codex -->
|
||||||
|
|
||||||
<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}</p>
|
<p>{{ctx.Locale.Tr "install.docker_helper" "https://docs.gitea.com/installation/install-with-docker"}}</p>
|
||||||
|
|
||||||
@@ -96,6 +96,57 @@
|
|||||||
<span class="help">{{ctx.Locale.Tr "install.sqlite_helper"}}</span>
|
<span class="help">{{ctx.Locale.Tr "install.sqlite_helper"}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<!-- start edit/add - by petru @ codex -->
|
||||||
|
<details class="optional field">
|
||||||
|
<summary class="right-content tw-py-2">
|
||||||
|
{{ctx.Locale.Tr "install.database_backup_title"}}
|
||||||
|
</summary>
|
||||||
|
<p>{{ctx.Locale.Tr "install.database_backup_desc"}}</p>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<label>{{ctx.Locale.Tr "install.database_backup_enabled"}}</label>
|
||||||
|
<input name="backup_enabled" type="checkbox" {{if .backup_enabled}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
<span class="help">{{ctx.Locale.Tr "install.database_backup_enabled_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<label>{{ctx.Locale.Tr "install.database_backup_run_at_start"}}</label>
|
||||||
|
<input name="backup_run_at_start" type="checkbox" {{if .backup_run_at_start}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
<span class="help">{{ctx.Locale.Tr "install.database_backup_run_at_start_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<label for="backup_schedule">{{ctx.Locale.Tr "install.database_backup_schedule"}}</label>
|
||||||
|
<input id="backup_schedule" name="backup_schedule" value="{{.backup_schedule}}" placeholder="@daily">
|
||||||
|
<span class="help">{{ctx.Locale.Tr "install.database_backup_schedule_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<label for="backup_path">{{ctx.Locale.Tr "install.database_backup_path"}}</label>
|
||||||
|
<input id="backup_path" name="backup_path" value="{{.backup_path}}">
|
||||||
|
<span class="help">{{ctx.Locale.Tr "install.database_backup_path_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<label for="backup_retention_count">{{ctx.Locale.Tr "install.database_backup_retention_count"}}</label>
|
||||||
|
<input id="backup_retention_count" name="backup_retention_count" type="number" min="0" value="{{.backup_retention_count}}">
|
||||||
|
<span class="help">{{ctx.Locale.Tr "install.database_backup_retention_count_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<label>{{ctx.Locale.Tr "install.database_backup_compress"}}</label>
|
||||||
|
<input name="backup_compress" type="checkbox" {{if .backup_compress}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
<span class="help">{{ctx.Locale.Tr "install.database_backup_compress_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="inline field">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<label>{{ctx.Locale.Tr "install.database_backup_include_app_ini_snapshot"}}</label>
|
||||||
|
<input name="backup_include_app_ini_snapshot" type="checkbox" {{if .backup_include_app_ini_snapshot}}checked{{end}}>
|
||||||
|
</div>
|
||||||
|
<span class="help">{{ctx.Locale.Tr "install.database_backup_include_app_ini_snapshot_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
<!-- end edit/add - by petru @ codex -->
|
||||||
|
|
||||||
<!-- General Settings -->
|
<!-- General Settings -->
|
||||||
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.general_title"}}</h4>
|
<h4 class="ui dividing header">{{ctx.Locale.Tr "install.general_title"}}</h4>
|
||||||
@@ -505,7 +556,7 @@
|
|||||||
{{$filePath := HTMLFormat `<span class="ui label">%s</span> <button class="btn interact-fg" data-clipboard-text="%s">%s</button>` .CustomConfFile .CustomConfFile $copyBtn}}
|
{{$filePath := HTMLFormat `<span class="ui label">%s</span> <button class="btn interact-fg" data-clipboard-text="%s">%s</button>` .CustomConfFile .CustomConfFile $copyBtn}}
|
||||||
{{ctx.Locale.Tr "install.config_write_file_prompt" $filePath}}
|
{{ctx.Locale.Tr "install.config_write_file_prompt" $filePath}}
|
||||||
</div>
|
</div>
|
||||||
{{if not .Err_DbInstalledBefore}} <!-- edit/add - by petru @ codex -->
|
{{if not (or .Err_DbInstalledBefore .Err_RepositoryFilesystemRecovery .Err_DatabaseBackupRecovery)}} <!-- edit/add - by petru @ codex -->
|
||||||
<div class="tw-mt-4 tw-mb-2 tw-text-center">
|
<div class="tw-mt-4 tw-mb-2 tw-text-center">
|
||||||
<button
|
<button
|
||||||
class="ui primary button js-install-confirm-button"
|
class="ui primary button js-install-confirm-button"
|
||||||
@@ -519,28 +570,141 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if .Err_DbInstalledBefore}}
|
{{if or .Err_DbInstalledBefore .Err_RepositoryFilesystemRecovery .Err_DatabaseBackupRecovery}}
|
||||||
<!-- start edit/add - by petru @ codex -->
|
<!-- start edit/add - by petru @ codex -->
|
||||||
<div class="ui small modal install-reinstall-confirm-modal js-install-reinstall-modal">
|
<div class="ui small modal install-reinstall-confirm-modal js-install-reinstall-modal">
|
||||||
<div class="ui negative message install-reinstall-alert">{{ctx.Locale.Tr "install.reinstall_error"}}</div>
|
<div
|
||||||
|
class="ui negative message install-reinstall-alert js-install-recovery-alert"
|
||||||
|
data-existing-database-text="{{ctx.Locale.Tr "install.reinstall_error"}}"
|
||||||
|
data-database-backup-text="{{ctx.Locale.Tr "install.recovery_database_backup_error"}}"
|
||||||
|
data-repository-filesystem-text="{{ctx.Locale.Tr "install.recovery_repository_filesystem_error"}}"
|
||||||
|
>{{ctx.Locale.Tr "install.reinstall_error"}}</div>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<p class="reinstall-message">{{ctx.Locale.Tr "install.reinstall_confirm_message"}}</p>
|
<div class="recovery-source-block">
|
||||||
<div class="reinstall-confirm">
|
<h5 class="ui dividing header">{{ctx.Locale.Tr "install.recovery_source_title"}}</h5>
|
||||||
<div class="ui checkbox">
|
<div class="recovery-source-option{{if .InstallRecoveryOptionExistingDBAvailable}} is-available{{end}}">
|
||||||
<input id="reinstall_confirm_first" name="reinstall_confirm_first" type="checkbox" form="install-form">
|
<div class="ui radio checkbox">
|
||||||
<label for="reinstall_confirm_first">{{ctx.Locale.Tr "install.reinstall_confirm_check_1"}}</label>
|
<input
|
||||||
|
id="recovery_mode_existing_database"
|
||||||
|
name="recovery_mode"
|
||||||
|
type="radio"
|
||||||
|
value="existing_database"
|
||||||
|
form="install-form"
|
||||||
|
{{if not .InstallRecoveryOptionExistingDBAvailable}}disabled{{end}}
|
||||||
|
{{if eq .recovery_mode "existing_database"}}checked{{end}}
|
||||||
|
>
|
||||||
|
<label for="recovery_mode_existing_database">{{ctx.Locale.Tr "install.recovery_source_existing_database"}}</label>
|
||||||
|
</div>
|
||||||
|
<p class="help">{{ctx.Locale.Tr "install.recovery_source_existing_database_helper"}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="recovery-source-option{{if .InstallRecoveryOptionDatabaseBackupAvailable}} is-available{{end}}">
|
||||||
|
<div class="ui radio checkbox{{if not .InstallRecoveryOptionDatabaseBackupAvailable}} disabled{{end}}">
|
||||||
|
<input
|
||||||
|
id="recovery_mode_database_backup"
|
||||||
|
name="recovery_mode"
|
||||||
|
type="radio"
|
||||||
|
value="database_backup"
|
||||||
|
form="install-form"
|
||||||
|
{{if not .InstallRecoveryOptionDatabaseBackupAvailable}}disabled{{end}}
|
||||||
|
{{if eq .recovery_mode "database_backup"}}checked{{end}}
|
||||||
|
>
|
||||||
|
<label for="recovery_mode_database_backup">{{ctx.Locale.Tr "install.recovery_source_database_backup"}}</label>
|
||||||
|
</div>
|
||||||
|
{{if .InstallRecoveryOptionDatabaseBackupAvailable}}
|
||||||
|
<p class="help">{{ctx.Locale.Tr "install.recovery_source_database_backup_helper"}}</p>
|
||||||
|
{{else}}
|
||||||
|
<p class="help">{{ctx.Locale.Tr "install.recovery_source_database_backup_unavailable"}}</p>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="recovery-source-option{{if .InstallRecoveryOptionRepositoryFilesystemAvailable}} is-available{{end}}">
|
||||||
|
<div class="ui radio checkbox">
|
||||||
|
<input
|
||||||
|
id="recovery_mode_repository_filesystem"
|
||||||
|
name="recovery_mode"
|
||||||
|
type="radio"
|
||||||
|
value="repository_filesystem"
|
||||||
|
form="install-form"
|
||||||
|
{{if not .InstallRecoveryOptionRepositoryFilesystemAvailable}}disabled{{end}}
|
||||||
|
{{if eq .recovery_mode "repository_filesystem"}}checked{{end}}
|
||||||
|
>
|
||||||
|
<label for="recovery_mode_repository_filesystem">{{ctx.Locale.Tr "install.recovery_source_repository_filesystem"}}</label>
|
||||||
|
</div>
|
||||||
|
<p class="help">{{ctx.Locale.Tr "install.recovery_source_repository_filesystem_helper"}}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="reinstall-confirm">
|
<p
|
||||||
<div class="ui checkbox">
|
class="reinstall-message js-install-recovery-message"
|
||||||
<input id="reinstall_confirm_second" name="reinstall_confirm_second" type="checkbox" form="install-form">
|
data-existing-database-text="{{ctx.Locale.Tr "install.reinstall_confirm_message"}}"
|
||||||
<label for="reinstall_confirm_second">{{ctx.Locale.Tr "install.reinstall_confirm_check_2"}}</label>
|
data-database-backup-text="{{ctx.Locale.Tr "install.recovery_database_backup_message"}}"
|
||||||
|
data-repository-filesystem-text="{{ctx.Locale.Tr "install.recovery_repository_filesystem_message"}}"
|
||||||
|
>{{ctx.Locale.Tr "install.reinstall_confirm_message"}}</p>
|
||||||
|
<div class="js-install-recovery-confirm-panel" data-recovery-mode="existing_database">
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="reinstall_confirm_first" name="reinstall_confirm_first" type="checkbox" form="install-form" {{if .reinstall_confirm_first}}checked{{end}}>
|
||||||
|
<label for="reinstall_confirm_first">{{ctx.Locale.Tr "install.reinstall_confirm_check_1"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="reinstall_confirm_second" name="reinstall_confirm_second" type="checkbox" form="install-form" {{if .reinstall_confirm_second}}checked{{end}}>
|
||||||
|
<label for="reinstall_confirm_second">{{ctx.Locale.Tr "install.reinstall_confirm_check_2"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="reinstall_confirm_third" name="reinstall_confirm_third" type="checkbox" form="install-form" {{if .reinstall_confirm_third}}checked{{end}}>
|
||||||
|
<label for="reinstall_confirm_third">{{ctx.Locale.Tr "install.reinstall_confirm_check_3"}}</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="reinstall-confirm">
|
<div class="js-install-recovery-confirm-panel" data-recovery-mode="database_backup">
|
||||||
<div class="ui checkbox">
|
<div class="inline field">
|
||||||
<input id="reinstall_confirm_third" name="reinstall_confirm_third" type="checkbox" form="install-form">
|
<label for="backup_restore_id">{{ctx.Locale.Tr "install.recovery_database_backup_select"}}</label>
|
||||||
<label for="reinstall_confirm_third">{{ctx.Locale.Tr "install.reinstall_confirm_check_3"}}</label>
|
<select id="backup_restore_id" name="backup_restore_id" form="install-form">
|
||||||
|
{{range .InstallRecoveryBackups}}
|
||||||
|
<option value="{{.ID}}" {{if eq $.backup_restore_id .ID}}selected{{end}}>{{.Label}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
<span class="help">{{ctx.Locale.Tr "install.recovery_database_backup_select_helper"}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="database_backup_recovery_confirm_first" name="reinstall_confirm_first" type="checkbox" form="install-form" {{if .reinstall_confirm_first}}checked{{end}}>
|
||||||
|
<label for="database_backup_recovery_confirm_first">{{ctx.Locale.Tr "install.recovery_database_backup_confirm_check_1"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="database_backup_recovery_confirm_second" name="reinstall_confirm_second" type="checkbox" form="install-form" {{if .reinstall_confirm_second}}checked{{end}}>
|
||||||
|
<label for="database_backup_recovery_confirm_second">{{ctx.Locale.Tr "install.recovery_database_backup_confirm_check_2"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="database_backup_recovery_confirm_third" name="reinstall_confirm_third" type="checkbox" form="install-form" {{if .reinstall_confirm_third}}checked{{end}}>
|
||||||
|
<label for="database_backup_recovery_confirm_third">{{ctx.Locale.Tr "install.recovery_database_backup_confirm_check_3"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="js-install-recovery-confirm-panel" data-recovery-mode="repository_filesystem">
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="filesystem_recovery_confirm_first" name="reinstall_confirm_first" type="checkbox" form="install-form" {{if .reinstall_confirm_first}}checked{{end}}>
|
||||||
|
<label for="filesystem_recovery_confirm_first">{{ctx.Locale.Tr "install.recovery_repository_filesystem_confirm_check_1"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="filesystem_recovery_confirm_second" name="reinstall_confirm_second" type="checkbox" form="install-form" {{if .reinstall_confirm_second}}checked{{end}}>
|
||||||
|
<label for="filesystem_recovery_confirm_second">{{ctx.Locale.Tr "install.recovery_repository_filesystem_confirm_check_2"}}</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="reinstall-confirm">
|
||||||
|
<div class="ui checkbox">
|
||||||
|
<input id="filesystem_recovery_confirm_third" name="reinstall_confirm_third" type="checkbox" form="install-form" {{if .reinstall_confirm_third}}checked{{end}}>
|
||||||
|
<label for="filesystem_recovery_confirm_third">{{ctx.Locale.Tr "install.recovery_repository_filesystem_confirm_check_3"}}</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -762,19 +926,95 @@
|
|||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
dismissInstallSuccessFlash();
|
dismissInstallSuccessFlash();
|
||||||
|
|
||||||
{{if .Err_DbInstalledBefore}}
|
{{if or .Err_DbInstalledBefore .Err_RepositoryFilesystemRecovery .Err_DatabaseBackupRecovery}}
|
||||||
// start edit/add - by petru @ codex
|
// start edit/add - by petru @ codex
|
||||||
const reinstallModal = document.querySelector('.js-install-reinstall-modal');
|
const reinstallModal = document.querySelector('.js-install-reinstall-modal');
|
||||||
if (reinstallModal) {
|
if (reinstallModal) {
|
||||||
|
const recoveryModeInputs = reinstallModal.querySelectorAll('input[name="recovery_mode"]');
|
||||||
const reinstallConfirmInputs = reinstallModal.querySelectorAll('input[name^="reinstall_confirm_"]');
|
const reinstallConfirmInputs = reinstallModal.querySelectorAll('input[name^="reinstall_confirm_"]');
|
||||||
const reinstallConfirmButton = reinstallModal.querySelector('.js-install-confirm-button');
|
const reinstallConfirmButton = reinstallModal.querySelector('.js-install-confirm-button');
|
||||||
|
const reinstallAlert = reinstallModal.querySelector('.js-install-recovery-alert');
|
||||||
|
const reinstallMessage = reinstallModal.querySelector('.js-install-recovery-message');
|
||||||
|
const reinstallConfirmPanels = reinstallModal.querySelectorAll('.js-install-recovery-confirm-panel');
|
||||||
|
const getSelectedRecoveryMode = () => {
|
||||||
|
for (const input of recoveryModeInputs) {
|
||||||
|
if (input instanceof HTMLInputElement && input.checked) return input.value;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
const syncReinstallRecoveryPanel = () => {
|
||||||
|
const selectedRecoveryMode = getSelectedRecoveryMode();
|
||||||
|
for (const panel of reinstallConfirmPanels) {
|
||||||
|
if (!(panel instanceof HTMLElement)) continue;
|
||||||
|
const isActive = panel.dataset.recoveryMode === selectedRecoveryMode;
|
||||||
|
panel.classList.toggle('is-active', isActive);
|
||||||
|
for (const field of panel.querySelectorAll('select')) {
|
||||||
|
if (field instanceof HTMLSelectElement) {
|
||||||
|
field.disabled = !isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const input of panel.querySelectorAll('input[name^="reinstall_confirm_"]')) {
|
||||||
|
if (input instanceof HTMLInputElement) {
|
||||||
|
input.disabled = !isActive;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (reinstallAlert instanceof HTMLElement) {
|
||||||
|
const nextText = reinstallAlert.dataset[
|
||||||
|
selectedRecoveryMode === 'database_backup'
|
||||||
|
? 'databaseBackupText'
|
||||||
|
: selectedRecoveryMode === 'repository_filesystem'
|
||||||
|
? 'repositoryFilesystemText'
|
||||||
|
: 'existingDatabaseText'
|
||||||
|
];
|
||||||
|
if (nextText) reinstallAlert.textContent = nextText;
|
||||||
|
}
|
||||||
|
if (reinstallMessage instanceof HTMLElement) {
|
||||||
|
const nextText = reinstallMessage.dataset[
|
||||||
|
selectedRecoveryMode === 'database_backup'
|
||||||
|
? 'databaseBackupText'
|
||||||
|
: selectedRecoveryMode === 'repository_filesystem'
|
||||||
|
? 'repositoryFilesystemText'
|
||||||
|
: 'existingDatabaseText'
|
||||||
|
];
|
||||||
|
if (nextText) reinstallMessage.textContent = nextText;
|
||||||
|
}
|
||||||
|
};
|
||||||
const syncReinstallConfirmButton = () => {
|
const syncReinstallConfirmButton = () => {
|
||||||
if (!(reinstallConfirmButton instanceof HTMLButtonElement)) return;
|
if (!(reinstallConfirmButton instanceof HTMLButtonElement)) return;
|
||||||
reinstallConfirmButton.disabled = [...reinstallConfirmInputs].some((input) => !(input instanceof HTMLInputElement) || !input.checked);
|
const selectedRecoveryMode = getSelectedRecoveryMode();
|
||||||
|
if (!selectedRecoveryMode) {
|
||||||
|
reinstallConfirmButton.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activePanel = reinstallModal.querySelector(`.js-install-recovery-confirm-panel[data-recovery-mode="${selectedRecoveryMode}"]`);
|
||||||
|
if (!(activePanel instanceof HTMLElement)) {
|
||||||
|
reinstallConfirmButton.disabled = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const activeInputs = activePanel.querySelectorAll('input[name^="reinstall_confirm_"]');
|
||||||
|
const confirmationsAccepted = ![...activeInputs].some((input) => !(input instanceof HTMLInputElement) || !input.checked);
|
||||||
|
if (selectedRecoveryMode === 'database_backup') {
|
||||||
|
const backupSelect = activePanel.querySelector('#backup_restore_id');
|
||||||
|
reinstallConfirmButton.disabled = !(confirmationsAccepted && backupSelect instanceof HTMLSelectElement && backupSelect.value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
reinstallConfirmButton.disabled = !confirmationsAccepted;
|
||||||
};
|
};
|
||||||
|
for (const input of recoveryModeInputs) {
|
||||||
|
input.addEventListener('change', () => {
|
||||||
|
syncReinstallRecoveryPanel();
|
||||||
|
syncReinstallConfirmButton();
|
||||||
|
});
|
||||||
|
}
|
||||||
for (const input of reinstallConfirmInputs) {
|
for (const input of reinstallConfirmInputs) {
|
||||||
input.addEventListener('change', syncReinstallConfirmButton);
|
input.addEventListener('change', syncReinstallConfirmButton);
|
||||||
}
|
}
|
||||||
|
const backupRestoreSelect = reinstallModal.querySelector('#backup_restore_id');
|
||||||
|
if (backupRestoreSelect instanceof HTMLSelectElement) {
|
||||||
|
backupRestoreSelect.addEventListener('change', syncReinstallConfirmButton);
|
||||||
|
}
|
||||||
|
syncReinstallRecoveryPanel();
|
||||||
syncReinstallConfirmButton();
|
syncReinstallConfirmButton();
|
||||||
window.$(reinstallModal).modal({
|
window.$(reinstallModal).modal({
|
||||||
autofocus: false,
|
autofocus: false,
|
||||||
|
|||||||
@@ -75,6 +75,34 @@
|
|||||||
padding-top: 1.25rem;
|
padding-top: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.page-content.install .install-reinstall-confirm-modal .recovery-source-block {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content.install .install-reinstall-confirm-modal .recovery-source-option {
|
||||||
|
padding: 0.75rem 0;
|
||||||
|
border-top: 1px solid var(--color-secondary-alpha-40);
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content.install .install-reinstall-confirm-modal .recovery-source-option:first-of-type {
|
||||||
|
border-top: 0;
|
||||||
|
padding-top: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content.install .install-reinstall-confirm-modal .recovery-source-option .help {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.35rem;
|
||||||
|
margin-left: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content.install .install-reinstall-confirm-modal .js-install-recovery-confirm-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-content.install .install-reinstall-confirm-modal .js-install-recovery-confirm-panel.is-active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.ui.negative.message.install-reinstall-alert {
|
.ui.negative.message.install-reinstall-alert {
|
||||||
font-size: 19px;
|
font-size: 19px;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user