mirror of
https://github.com/discourse/discourse.git
synced 2024-11-28 11:38:36 +08:00
d5ef6188ed
It pauses Sidekiq, clears Redis (namespaced to the current site), clears Sidekiq jobs for the current site, restores the database and unpauses Sidekiq. Previously it stayed paused until the end of the restore. Redis is cleared because we don't want any old data lying around (e.g. old Sidekiq jobs). Most data in Redis is prefixed with the name of the multisite, but Sidekiq jobs in a multisite are all stored in the same keys. So, deleting those jobs requires a little bit more logic.
187 lines
5.2 KiB
Ruby
187 lines
5.2 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
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)
|
|
@user_id = user_id
|
|
@filename = filename
|
|
@factory = factory
|
|
@logger = factory.logger
|
|
@disable_emails = disable_emails
|
|
|
|
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)
|
|
@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
|
|
|
|
@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)
|
|
|
|
reload_site_settings
|
|
|
|
@system.unpause_sidekiq
|
|
@system.disable_readonly_mode
|
|
|
|
clear_category_cache
|
|
clear_emoji_cache
|
|
clear_theme_cache
|
|
clear_stats
|
|
reload_translations
|
|
|
|
@uploads_restorer.restore(@tmp_directory)
|
|
|
|
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 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
|
|
end
|
|
|
|
def reload_translations
|
|
log "Reloading translations..."
|
|
TranslationOverride.reload_all_overrides!
|
|
end
|
|
|
|
def notify_user
|
|
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
|
|
|
|
SystemMessage.create_from_system_user(
|
|
user, status,
|
|
logs: Discourse::Utils.pretty_logs(@logger.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
|
|
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
|