require 'yaml' require 'pathname' require 'dotenv' # Edited by Andrew Cantino. Based on: https://gist.github.com/339471 # Original info: # # Capistrano sync.rb task for syncing databases and directories between the # local development environment and different multi_stage environments. You # cannot sync directly between two multi_stage environments, always use your # local machine as loop way. # # This version pulls credentials for the remote database from # {shared_path}/config/database.yml on the remote server, thus eliminating # the requirement to have your production database credentials on your local # machine or in your repository. # # Author: Michael Kessler aka netzpirat # Gist: 111597 # # Edits By: Evan Dorn, Logical Reality Design, March 2010 # Gist: 339471 # # Released under the MIT license. # Kindly sponsored by Screen Concept, www.screenconcept.ch namespace :sync do namespace :db do desc <<-DESC Syncs database from the selected environment to the local development environment. The database credentials will be read from your local config/database.yml file and a copy of the dump will be kept within the shared sync directory. The amount of backups that will be kept is declared in the sync_backups variable and defaults to 5. DESC task :down, :roles => :db, :only => {:primary => true} do run "mkdir -p #{shared_path}/sync" env = fetch :rails_env, 'production' filename = "database.#{env}.#{Time.now.strftime '%Y-%m-%d_%H:%M:%S'}.sql.bz2" on_rollback { delete "#{shared_path}/sync/#{filename}" } # Remote DB dump username, password, database, host = remote_database_config(env) hostcmd = host.nil? ? '' : "-h #{host}" puts "hostname: #{host}" puts "database: #{database}" opts = "-c --max_allowed_packet=128M --hex-blob --single-transaction --skip-extended-insert --quick" run "mysqldump #{opts} -u #{username} --password='#{password}' #{hostcmd} #{database} | bzip2 -9 > #{shared_path}/sync/#{filename}" do |channel, stream, data| puts data end purge_old_backups "database" # Download dump download "#{shared_path}/sync/#{filename}", filename # Local DB import username, password, database = database_config('development') system "bzip2 -d -c #{filename} | mysql --max_allowed_packet=128M -u #{username} --password='#{password}' #{database}" system "rake db:migrate" system "rake db:test:prepare" logger.important "sync database from '#{env}' to local has finished" end end namespace :fs do desc <<-DESC Sync declared remote directories to the local development environment. The synced directories must be declared as an array of Strings with the sync_directories variable. The path is relative to the rails root. DESC task :down, :roles => :web, :once => true do server, port = host_and_port Array(fetch(:sync_directories, [])).each do |syncdir| unless File.directory? "#{syncdir}" logger.info "create local '#{syncdir}' folder" Dir.mkdir "#{syncdir}" end logger.info "sync #{syncdir} from #{server}:#{port} to local" destination, base = Pathname.new(syncdir).split system "rsync --verbose --archive --compress --copy-links --delete --stats --rsh='ssh -p #{port}' #{user}@#{server}:#{current_path}/#{syncdir} #{destination.to_s}" end logger.important "sync filesystem from remote to local finished" end end # Used by database_config and remote_database_config to parse database configs that depend on .env files. Depends on the dotenv-rails gem. class EnvLoader < Dotenv::Environment def initialize(data) @data = data load end def with_loaded_env begin saved_env = ENV.to_hash.dup ENV.update(self) yield ensure ENV.replace(saved_env) end end def read @data.split("\n") end end # # Reads the database credentials from the local config/database.yml file # +db+ the name of the environment to get the credentials for # Returns username, password, database # def database_config(db) local_config = File.read('config/database.yml') local_env = File.read('.env') database = nil EnvLoader.new(local_env).with_loaded_env do database = YAML::load(ERB.new(local_config).result) end return database["#{db}"]['username'], database["#{db}"]['password'], database["#{db}"]['database'], database["#{db}"]['host'] end # # Reads the database credentials from the remote config/database.yml file # +db+ the name of the environment to get the credentials for # Returns username, password, database # def remote_database_config(db) remote_config = capture("cat #{current_path}/config/database.yml") remote_env = capture("cat #{current_path}/.env") database = nil EnvLoader.new(remote_env).with_loaded_env do database = YAML::load(ERB.new(remote_config).result) end return database["#{db}"]['username'], database["#{db}"]['password'], database["#{db}"]['database'], database["#{db}"]['host'] end # # Returns the actual host name to sync and port # def host_and_port return roles[:web].servers.first.host, ssh_options[:port] || roles[:web].servers.first.port || 22 end # # Purge old backups within the shared sync directory # def purge_old_backups(base) count = fetch(:sync_backups, 5).to_i backup_files = capture("ls -xt #{shared_path}/sync/#{base}*").split.reverse if count >= backup_files.length logger.important "no old backups to clean up" else logger.info "keeping #{count} of #{backup_files.length} sync backups" delete_backups = (backup_files - backup_files.last(count)).join(" ") run "rm #{delete_backups}" end end end