mirror of
https://gh.wpcy.net/https://github.com/discourse/discourse.git
synced 2026-05-05 19:11:00 +08:00
Restoring a backup executes DB migrations after the DB is restored. Part of those migrations is a step for seeding content. That step might create uploads which depends on site settings. This change excludes the seeding from the DB migrations and runs it after site settings have been restored and reloaded.
226 lines
6.2 KiB
Ruby
226 lines
6.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "colored2"
|
|
|
|
module BackupRestore
|
|
RestoreDisabledError = Class.new(RuntimeError)
|
|
FilenameMissingError = Class.new(RuntimeError)
|
|
|
|
class Restorer
|
|
delegate :log, to: :@logger, private: true
|
|
|
|
attr_reader :success
|
|
|
|
def initialize(
|
|
user_id:,
|
|
filename:,
|
|
factory:,
|
|
disable_emails: true,
|
|
location: nil,
|
|
interactive: false
|
|
)
|
|
@user_id = user_id
|
|
@filename = filename
|
|
@factory = factory
|
|
@logger = factory.logger
|
|
@disable_emails = disable_emails
|
|
@interactive = interactive
|
|
|
|
ensure_restore_is_enabled
|
|
ensure_we_have_a_user
|
|
ensure_we_have_a_filename
|
|
|
|
@success = false
|
|
@current_db = RailsMultisite::ConnectionManagement.current_db
|
|
|
|
@system = factory.create_system_interface
|
|
@backup_file_handler = factory.create_backup_file_handler(@filename, @current_db, location)
|
|
@database_restorer = factory.create_database_restorer(@current_db)
|
|
@uploads_restorer = factory.create_uploads_restorer
|
|
end
|
|
|
|
def run
|
|
log "[STARTED]"
|
|
log "'#{@user_info[:username]}' has started the restore!"
|
|
|
|
# FIXME not atomic!
|
|
ensure_no_operation_is_running
|
|
@system.mark_restore_as_running
|
|
|
|
@system.listen_for_shutdown_signal
|
|
|
|
@filename, @tmp_directory, db_dump_path = @backup_file_handler.decompress
|
|
validate_backup_metadata
|
|
|
|
@system.enable_readonly_mode
|
|
@system.pause_sidekiq("restore")
|
|
@system.wait_for_sidekiq
|
|
@system.flush_redis
|
|
@system.clear_sidekiq_queues
|
|
|
|
@database_restorer.restore(db_dump_path, @interactive)
|
|
|
|
reload_site_settings
|
|
run_seed_fu
|
|
|
|
@system.disable_readonly_mode
|
|
|
|
clear_category_cache
|
|
clear_stats
|
|
reload_translations
|
|
|
|
restore_uploads
|
|
|
|
clear_emoji_cache
|
|
clear_theme_cache
|
|
|
|
after_restore_hook
|
|
rescue Compression::Strategy::ExtractFailed
|
|
log "ERROR: The uncompressed file is too big. Consider increasing the hidden " \
|
|
'"decompressed_backup_max_file_size_mb" setting.'
|
|
@database_restorer.rollback
|
|
rescue SystemExit
|
|
log "Restore process was cancelled!"
|
|
@database_restorer.rollback
|
|
rescue => ex
|
|
log "EXCEPTION: " + ex.message
|
|
log ex.backtrace.join("\n")
|
|
@database_restorer.rollback
|
|
else
|
|
@success = true
|
|
ensure
|
|
clean_up
|
|
notify_user
|
|
log "Finished!"
|
|
|
|
@success ? log("[SUCCESS]") : log("[FAILED]")
|
|
end
|
|
|
|
protected
|
|
|
|
def ensure_restore_is_enabled
|
|
return if Rails.env.development? || SiteSetting.allow_restore?
|
|
raise BackupRestore::RestoreDisabledError
|
|
end
|
|
|
|
def ensure_no_operation_is_running
|
|
raise BackupRestore::OperationRunningError if BackupRestore.is_operation_running?
|
|
end
|
|
|
|
def ensure_we_have_a_user
|
|
user = User.find_by(id: @user_id)
|
|
raise Discourse::InvalidParameters.new(:user_id) if user.blank?
|
|
|
|
# keep some user data around to check them against the newly restored database
|
|
@user_info = { id: user.id, username: user.username, email: user.email }
|
|
end
|
|
|
|
def ensure_we_have_a_filename
|
|
raise BackupRestore::FilenameMissingError if @filename.nil?
|
|
end
|
|
|
|
def validate_backup_metadata
|
|
@factory.create_meta_data_handler(@filename, @tmp_directory).validate
|
|
end
|
|
|
|
def reload_site_settings
|
|
log "Reloading site settings..."
|
|
SiteSetting.refresh!
|
|
|
|
DiscourseEvent.trigger(:site_settings_restored)
|
|
|
|
if @disable_emails && SiteSetting.disable_emails == "no"
|
|
log "Disabling outgoing emails for non-staff users..."
|
|
user = User.find_by_email(@user_info[:email]) || Discourse.system_user
|
|
SiteSetting.set_and_log(:disable_emails, "non-staff", user)
|
|
end
|
|
end
|
|
|
|
def run_seed_fu
|
|
log "Running seed fu..."
|
|
|
|
begin
|
|
Discourse::Application.load_tasks
|
|
Rake::Task["db:seed"].invoke
|
|
rescue => ex
|
|
log "Failed to run seed fu", ex
|
|
end
|
|
end
|
|
|
|
def clear_category_cache
|
|
log "Clearing category cache..."
|
|
Category.reset_topic_ids_cache
|
|
Category.clear_subcategory_ids
|
|
end
|
|
|
|
def clear_emoji_cache
|
|
log "Clearing emoji cache..."
|
|
Emoji.clear_cache
|
|
rescue => ex
|
|
log "Something went wrong while clearing emoji cache.", ex
|
|
end
|
|
|
|
def reload_translations
|
|
log "Reloading translations..."
|
|
TranslationOverride.reload_all_overrides!
|
|
end
|
|
|
|
def restore_uploads
|
|
if @interactive
|
|
puts ""
|
|
puts "Attention! Pausing restore before uploads.".red.bold
|
|
puts "You can work on the restored database in a separate Rails console."
|
|
puts ""
|
|
puts "Press any key to continue with the restore.".bold
|
|
puts ""
|
|
STDIN.getch
|
|
end
|
|
|
|
@uploads_restorer.restore(@tmp_directory)
|
|
end
|
|
|
|
def notify_user
|
|
return if @success && @user_id == Discourse::SYSTEM_USER_ID
|
|
|
|
if user = User.find_by_email(@user_info[:email])
|
|
log "Notifying '#{user.username}' of the end of the restore..."
|
|
status = @success ? :restore_succeeded : :restore_failed
|
|
|
|
logs = Discourse::Utils.logs_markdown(@logger.logs, user: user)
|
|
post = SystemMessage.create_from_system_user(user, status, logs: logs)
|
|
else
|
|
log "Could not send notification to '#{@user_info[:username]}' " \
|
|
"(#{@user_info[:email]}), because the user does not exist."
|
|
end
|
|
rescue => ex
|
|
log "Something went wrong while notifying user.", ex
|
|
end
|
|
|
|
def clean_up
|
|
log "Cleaning stuff up..."
|
|
@database_restorer.clean_up
|
|
@backup_file_handler.clean_up
|
|
@system.unpause_sidekiq
|
|
@system.disable_readonly_mode if Discourse.readonly_mode?
|
|
@system.mark_restore_as_not_running
|
|
end
|
|
|
|
def clear_theme_cache
|
|
log "Clear theme cache"
|
|
ThemeField.force_recompilation!
|
|
Theme.expire_site_cache!
|
|
Stylesheet::Manager.cache.clear
|
|
rescue => ex
|
|
log "Something went wrong while clearing theme cache.", ex
|
|
end
|
|
|
|
def clear_stats
|
|
Discourse.stats.remove("missing_s3_uploads")
|
|
end
|
|
|
|
def after_restore_hook
|
|
log "Executing the after_restore_hook..."
|
|
DiscourseEvent.trigger(:restore_complete)
|
|
end
|
|
end
|
|
end
|