cantino/huginn

View on GitHub
lib/capistrano/sync.rb

Summary

Maintainability
A
50 mins
Test Coverage
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
    def initialize(data)
      @env = Dotenv::Parser.call(data)
    end

    def with_loaded_env
      begin
        saved_env = ENV.to_hash.dup
        ENV.update(@env)
        yield
      ensure
        ENV.replace(saved_env)
      end
    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