mirror of
https://github.com/discourse/discourse.git
synced 2025-05-23 20:01:08 +08:00
FEATURE: Work with compressed version of pg_dump
during backup and restore.
This commit is contained in:
@ -1,3 +1,4 @@
|
|||||||
|
require "backup_restore/utils"
|
||||||
require "backup_restore/backuper"
|
require "backup_restore/backuper"
|
||||||
require "backup_restore/restorer"
|
require "backup_restore/restorer"
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
module BackupRestore
|
module BackupRestore
|
||||||
|
|
||||||
class Backuper
|
class Backuper
|
||||||
|
include BackupRestore::Utils
|
||||||
|
|
||||||
attr_reader :success
|
attr_reader :success
|
||||||
|
|
||||||
@ -42,8 +43,6 @@ module BackupRestore
|
|||||||
|
|
||||||
log "Finalizing backup..."
|
log "Finalizing backup..."
|
||||||
|
|
||||||
update_dump
|
|
||||||
|
|
||||||
create_archive
|
create_archive
|
||||||
|
|
||||||
after_create_hook
|
after_create_hook
|
||||||
@ -84,7 +83,7 @@ module BackupRestore
|
|||||||
@current_db = RailsMultisite::ConnectionManagement.current_db
|
@current_db = RailsMultisite::ConnectionManagement.current_db
|
||||||
@timestamp = Time.now.strftime("%Y-%m-%d-%H%M%S")
|
@timestamp = Time.now.strftime("%Y-%m-%d-%H%M%S")
|
||||||
@tmp_directory = File.join(Rails.root, "tmp", "backups", @current_db, @timestamp)
|
@tmp_directory = File.join(Rails.root, "tmp", "backups", @current_db, @timestamp)
|
||||||
@dump_filename = File.join(@tmp_directory, BackupRestore::DUMP_FILE)
|
@dump_filename = "#{File.join(@tmp_directory, BackupRestore::DUMP_FILE)}.gz"
|
||||||
@meta_filename = File.join(@tmp_directory, BackupRestore::METADATA_FILE)
|
@meta_filename = File.join(@tmp_directory, BackupRestore::METADATA_FILE)
|
||||||
@archive_directory = File.join(Rails.root, "public", "backups", @current_db)
|
@archive_directory = File.join(Rails.root, "public", "backups", @current_db)
|
||||||
@archive_basename = File.join(@archive_directory, "#{SiteSetting.title.parameterize}-#{@timestamp}")
|
@archive_basename = File.join(@archive_directory, "#{SiteSetting.title.parameterize}-#{@timestamp}")
|
||||||
@ -192,6 +191,7 @@ module BackupRestore
|
|||||||
"--no-owner", # do not output commands to set ownership of objects
|
"--no-owner", # do not output commands to set ownership of objects
|
||||||
"--no-privileges", # prevent dumping of access privileges
|
"--no-privileges", # prevent dumping of access privileges
|
||||||
"--verbose", # specifies verbose mode
|
"--verbose", # specifies verbose mode
|
||||||
|
"--compress=4", # Compression level of 4
|
||||||
host_argument, # the hostname to connect to (if any)
|
host_argument, # the hostname to connect to (if any)
|
||||||
port_argument, # the port to connect to (if any)
|
port_argument, # the port to connect to (if any)
|
||||||
username_argument, # the username to connect as (if any)
|
username_argument, # the username to connect as (if any)
|
||||||
@ -199,59 +199,32 @@ module BackupRestore
|
|||||||
].join(" ")
|
].join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
def update_dump
|
|
||||||
log "Updating dump for more awesomeness..."
|
|
||||||
|
|
||||||
`#{sed_command}`
|
|
||||||
end
|
|
||||||
|
|
||||||
def sed_command
|
|
||||||
# in order to limit the downtime when restoring as much as possible
|
|
||||||
# we force the restoration to happen in the "restore" schema
|
|
||||||
|
|
||||||
# during the restoration, this make sure we
|
|
||||||
# - drop the "restore" schema if it exists
|
|
||||||
# - create the "restore" schema
|
|
||||||
# - prepend the "restore" schema into the search_path
|
|
||||||
|
|
||||||
regexp = "SET search_path = public, pg_catalog;"
|
|
||||||
|
|
||||||
replacement = [ "DROP SCHEMA IF EXISTS restore CASCADE;",
|
|
||||||
"CREATE SCHEMA restore;",
|
|
||||||
"SET search_path = restore, public, pg_catalog;",
|
|
||||||
].join(" ")
|
|
||||||
|
|
||||||
# we only want to replace the VERY first occurence of the search_path command
|
|
||||||
expression = "1,/^#{regexp}$/s/#{regexp}/#{replacement}/"
|
|
||||||
|
|
||||||
# I tried to use the --in-place argument but it was SLOOOWWWWwwwwww
|
|
||||||
# so I output the result into another file and rename it back afterwards
|
|
||||||
[ "sed -e '#{expression}' < #{@dump_filename} > #{@dump_filename}.tmp",
|
|
||||||
"&&",
|
|
||||||
"mv #{@dump_filename}.tmp #{@dump_filename}",
|
|
||||||
].join(" ")
|
|
||||||
end
|
|
||||||
|
|
||||||
def create_archive
|
def create_archive
|
||||||
log "Creating archive: #{File.basename(@archive_basename)}.tar.gz"
|
log "Creating archive: #{File.basename(@archive_basename)}.tar.gz"
|
||||||
|
|
||||||
tar_filename = "#{@archive_basename}.tar"
|
tar_filename = "#{@archive_basename}.tar"
|
||||||
|
|
||||||
log "Making sure archive does not already exist..."
|
log "Making sure archive does not already exist..."
|
||||||
`rm -f #{tar_filename}`
|
execute_command("rm -f #{tar_filename}")
|
||||||
`rm -f #{tar_filename}.gz`
|
execute_command("rm -f #{tar_filename}.gz")
|
||||||
|
|
||||||
log "Creating empty archive..."
|
log "Creating empty archive..."
|
||||||
`tar --create --file #{tar_filename} --files-from /dev/null`
|
execute_command("tar --create --file #{tar_filename} --files-from /dev/null")
|
||||||
|
|
||||||
log "Archiving data dump..."
|
log "Archiving data dump..."
|
||||||
FileUtils.cd(File.dirname(@dump_filename)) do
|
FileUtils.cd(File.dirname("#{@dump_filename}")) do
|
||||||
`tar --append --dereference --file #{tar_filename} #{File.basename(@dump_filename)}`
|
execute_command(
|
||||||
|
"tar --append --dereference --file #{tar_filename} #{File.basename(@dump_filename)}",
|
||||||
|
"Failed to archive data dump."
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
log "Archiving metadata..."
|
log "Archiving metadata..."
|
||||||
FileUtils.cd(File.dirname(@meta_filename)) do
|
FileUtils.cd(File.dirname(@meta_filename)) do
|
||||||
`tar --append --dereference --file #{tar_filename} #{File.basename(@meta_filename)}`
|
execute_command(
|
||||||
|
"tar --append --dereference --file #{tar_filename} #{File.basename(@meta_filename)}",
|
||||||
|
"Failed to archive metadata."
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
if @with_uploads
|
if @with_uploads
|
||||||
@ -259,14 +232,17 @@ module BackupRestore
|
|||||||
|
|
||||||
log "Archiving uploads..."
|
log "Archiving uploads..."
|
||||||
FileUtils.cd(File.join(Rails.root, "public")) do
|
FileUtils.cd(File.join(Rails.root, "public")) do
|
||||||
`tar --append --dereference --file #{tar_filename} #{upload_directory}`
|
execute_command(
|
||||||
|
"tar --append --dereference --file #{tar_filename} #{upload_directory}",
|
||||||
|
"Failed to archive uploads."
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
remove_tmp_directory
|
remove_tmp_directory
|
||||||
|
|
||||||
log "Gzipping archive, this may take a while..."
|
log "Gzipping archive, this may take a while..."
|
||||||
`gzip -5 #{tar_filename}`
|
execute_command("gzip -5 #{tar_filename}", "Failed to gzip archive.")
|
||||||
end
|
end
|
||||||
|
|
||||||
def after_create_hook
|
def after_create_hook
|
||||||
|
@ -4,6 +4,7 @@ module BackupRestore
|
|||||||
class FilenameMissingError < RuntimeError; end
|
class FilenameMissingError < RuntimeError; end
|
||||||
|
|
||||||
class Restorer
|
class Restorer
|
||||||
|
include BackupRestore::Utils
|
||||||
|
|
||||||
attr_reader :success
|
attr_reader :success
|
||||||
|
|
||||||
@ -157,17 +158,26 @@ module BackupRestore
|
|||||||
def copy_archive_to_tmp_directory
|
def copy_archive_to_tmp_directory
|
||||||
log "Copying archive to tmp directory..."
|
log "Copying archive to tmp directory..."
|
||||||
source = File.join(Backup.base_directory, @filename)
|
source = File.join(Backup.base_directory, @filename)
|
||||||
`cp '#{source}' '#{@archive_filename}'`
|
execute_command("cp '#{source}' '#{@archive_filename}'", "Failed to copy archive to tmp directory.")
|
||||||
end
|
end
|
||||||
|
|
||||||
def unzip_archive
|
def unzip_archive
|
||||||
log "Unzipping archive, this may take a while..."
|
log "Unzipping archive, this may take a while..."
|
||||||
FileUtils.cd(@tmp_directory) { `gzip --decompress '#{@archive_filename}'` }
|
FileUtils.cd(@tmp_directory) do
|
||||||
|
execute_command("gzip --decompress '#{@archive_filename}'", "Failed to unzip archive.")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def extract_metadata
|
def extract_metadata
|
||||||
log "Extracting metadata file..."
|
log "Extracting metadata file..."
|
||||||
FileUtils.cd(@tmp_directory) { `tar --extract --file '#{@tar_filename}' #{BackupRestore::METADATA_FILE}` }
|
|
||||||
|
FileUtils.cd(@tmp_directory) do
|
||||||
|
execute_command(
|
||||||
|
"tar --extract --file '#{@tar_filename}' #{BackupRestore::METADATA_FILE}",
|
||||||
|
"Failed to extract metadata file."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
@metadata = Oj.load_file(@meta_filename)
|
@metadata = Oj.load_file(@meta_filename)
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -182,7 +192,13 @@ module BackupRestore
|
|||||||
|
|
||||||
def extract_dump
|
def extract_dump
|
||||||
log "Extracting dump file..."
|
log "Extracting dump file..."
|
||||||
FileUtils.cd(@tmp_directory) { `tar --extract --file '#{@tar_filename}' #{BackupRestore::DUMP_FILE}` }
|
|
||||||
|
FileUtils.cd(@tmp_directory) do
|
||||||
|
execute_command(
|
||||||
|
"tar --extract --file '#{@tar_filename}' #{BackupRestore::DUMP_FILE}.gz",
|
||||||
|
"Failed to extract dump file."
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def restore_dump
|
def restore_dump
|
||||||
@ -201,7 +217,7 @@ module BackupRestore
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
IO.popen("#{psql_command} 2>&1") do |pipe|
|
IO.popen("gzip -d < #{@dump_filename}.gz | #{sed_command} | #{psql_command} 2>&1") do |pipe|
|
||||||
begin
|
begin
|
||||||
while line = pipe.readline
|
while line = pipe.readline
|
||||||
logs << line
|
logs << line
|
||||||
@ -229,7 +245,6 @@ module BackupRestore
|
|||||||
[ password_argument, # pass the password to psql (if any)
|
[ password_argument, # pass the password to psql (if any)
|
||||||
"psql", # the psql command
|
"psql", # the psql command
|
||||||
"--dbname='#{db_conf.database}'", # connect to database *dbname*
|
"--dbname='#{db_conf.database}'", # connect to database *dbname*
|
||||||
"--file='#{@dump_filename}'", # read the dump
|
|
||||||
"--single-transaction", # all or nothing (also runs COPY commands faster)
|
"--single-transaction", # all or nothing (also runs COPY commands faster)
|
||||||
host_argument, # the hostname to connect to (if any)
|
host_argument, # the hostname to connect to (if any)
|
||||||
port_argument, # the port to connect to (if any)
|
port_argument, # the port to connect to (if any)
|
||||||
@ -237,6 +252,28 @@ module BackupRestore
|
|||||||
].join(" ")
|
].join(" ")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def sed_command
|
||||||
|
# in order to limit the downtime when restoring as much as possible
|
||||||
|
# we force the restoration to happen in the "restore" schema
|
||||||
|
|
||||||
|
# during the restoration, this make sure we
|
||||||
|
# - drop the "restore" schema if it exists
|
||||||
|
# - create the "restore" schema
|
||||||
|
# - prepend the "restore" schema into the search_path
|
||||||
|
|
||||||
|
regexp = "SET search_path = public, pg_catalog;"
|
||||||
|
|
||||||
|
replacement = [ "DROP SCHEMA IF EXISTS restore CASCADE;",
|
||||||
|
"CREATE SCHEMA restore;",
|
||||||
|
"SET search_path = restore, public, pg_catalog;",
|
||||||
|
].join(" ")
|
||||||
|
|
||||||
|
# we only want to replace the VERY first occurence of the search_path command
|
||||||
|
expression = "1,/^#{regexp}$/s/#{regexp}/#{replacement}/"
|
||||||
|
|
||||||
|
"sed -e '#{expression}'"
|
||||||
|
end
|
||||||
|
|
||||||
def switch_schema!
|
def switch_schema!
|
||||||
log "Switching schemas... try reloading the site in 5 minutes, if successful, then reboot and restore is complete."
|
log "Switching schemas... try reloading the site in 5 minutes, if successful, then reboot and restore is complete."
|
||||||
|
|
||||||
@ -279,7 +316,10 @@ module BackupRestore
|
|||||||
if `tar --list --file '#{@tar_filename}' | grep 'uploads/'`.present?
|
if `tar --list --file '#{@tar_filename}' | grep 'uploads/'`.present?
|
||||||
log "Extracting uploads..."
|
log "Extracting uploads..."
|
||||||
FileUtils.cd(File.join(Rails.root, "public")) do
|
FileUtils.cd(File.join(Rails.root, "public")) do
|
||||||
`tar --extract --keep-newer-files --file '#{@tar_filename}' uploads/`
|
execute_command(
|
||||||
|
"tar --extract --keep-newer-files --file '#{@tar_filename}' uploads/",
|
||||||
|
"Failed to extract uploads."
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
14
lib/backup_restore/utils.rb
Normal file
14
lib/backup_restore/utils.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module BackupRestore
|
||||||
|
module Utils
|
||||||
|
def execute_command(command, failure_message = "")
|
||||||
|
output = `#{command} 2>&1`
|
||||||
|
|
||||||
|
if !$?.success?
|
||||||
|
failure_message = "#{failure_message}\n" if !failure_message.blank?
|
||||||
|
raise "#{failure_message}#{output}"
|
||||||
|
end
|
||||||
|
|
||||||
|
output
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Reference in New Issue
Block a user