sync.rb 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. require 'yaml'
  2. require 'pathname'
  3. require 'dotenv'
  4. # Edited by Andrew Cantino. Based on: https://gist.github.com/339471
  5. # Original info:
  6. #
  7. # Capistrano sync.rb task for syncing databases and directories between the
  8. # local development environment and different multi_stage environments. You
  9. # cannot sync directly between two multi_stage environments, always use your
  10. # local machine as loop way.
  11. #
  12. # This version pulls credentials for the remote database from
  13. # {shared_path}/config/database.yml on the remote server, thus eliminating
  14. # the requirement to have your production database credentials on your local
  15. # machine or in your repository.
  16. #
  17. # Author: Michael Kessler aka netzpirat
  18. # Gist: 111597
  19. #
  20. # Edits By: Evan Dorn, Logical Reality Design, March 2010
  21. # Gist: 339471
  22. #
  23. # Released under the MIT license.
  24. # Kindly sponsored by Screen Concept, www.screenconcept.ch
  25. namespace :sync do
  26. namespace :db do
  27. desc <<-DESC
  28. Syncs database from the selected environment to the local development environment.
  29. The database credentials will be read from your local config/database.yml file and a copy of the
  30. dump will be kept within the shared sync directory. The amount of backups that will be kept is
  31. declared in the sync_backups variable and defaults to 5.
  32. DESC
  33. task :down, :roles => :db, :only => {:primary => true} do
  34. run "mkdir -p #{shared_path}/sync"
  35. env = fetch :rails_env, 'production'
  36. filename = "database.#{env}.#{Time.now.strftime '%Y-%m-%d_%H:%M:%S'}.sql.bz2"
  37. on_rollback { delete "#{shared_path}/sync/#{filename}" }
  38. # Remote DB dump
  39. username, password, database, host = remote_database_config(env)
  40. hostcmd = host.nil? ? '' : "-h #{host}"
  41. puts "hostname: #{host}"
  42. puts "database: #{database}"
  43. opts = "-c --max_allowed_packet=128M --hex-blob --single-transaction --skip-extended-insert --quick"
  44. run "mysqldump #{opts} -u #{username} --password='#{password}' #{hostcmd} #{database} | bzip2 -9 > #{shared_path}/sync/#{filename}" do |channel, stream, data|
  45. puts data
  46. end
  47. purge_old_backups "database"
  48. # Download dump
  49. download "#{shared_path}/sync/#{filename}", filename
  50. # Local DB import
  51. username, password, database = database_config('development')
  52. system "bzip2 -d -c #{filename} | mysql --max_allowed_packet=128M -u #{username} --password='#{password}' #{database}"
  53. system "rake db:migrate"
  54. system "rake db:test:prepare"
  55. logger.important "sync database from '#{env}' to local has finished"
  56. end
  57. end
  58. namespace :fs do
  59. desc <<-DESC
  60. Sync declared remote directories to the local development environment. The synced directories must be declared
  61. as an array of Strings with the sync_directories variable. The path is relative to the rails root.
  62. DESC
  63. task :down, :roles => :web, :once => true do
  64. server, port = host_and_port
  65. Array(fetch(:sync_directories, [])).each do |syncdir|
  66. unless File.directory? "#{syncdir}"
  67. logger.info "create local '#{syncdir}' folder"
  68. Dir.mkdir "#{syncdir}"
  69. end
  70. logger.info "sync #{syncdir} from #{server}:#{port} to local"
  71. destination, base = Pathname.new(syncdir).split
  72. system "rsync --verbose --archive --compress --copy-links --delete --stats --rsh='ssh -p #{port}' #{user}@#{server}:#{current_path}/#{syncdir} #{destination.to_s}"
  73. end
  74. logger.important "sync filesystem from remote to local finished"
  75. end
  76. end
  77. # Used by database_config and remote_database_config to parse database configs that depend on .env files. Depends on the dotenv-rails gem.
  78. class EnvLoader
  79. def initialize(data)
  80. @env = Dotenv::Parser.call(data)
  81. end
  82. def with_loaded_env
  83. begin
  84. saved_env = ENV.to_hash.dup
  85. ENV.update(@env)
  86. yield
  87. ensure
  88. ENV.replace(saved_env)
  89. end
  90. end
  91. end
  92. #
  93. # Reads the database credentials from the local config/database.yml file
  94. # +db+ the name of the environment to get the credentials for
  95. # Returns username, password, database
  96. #
  97. def database_config(db)
  98. local_config = File.read('config/database.yml')
  99. local_env = File.read('.env')
  100. database = nil
  101. EnvLoader.new(local_env).with_loaded_env do
  102. database = YAML::load(ERB.new(local_config).result)
  103. end
  104. return database["#{db}"]['username'], database["#{db}"]['password'], database["#{db}"]['database'], database["#{db}"]['host']
  105. end
  106. #
  107. # Reads the database credentials from the remote config/database.yml file
  108. # +db+ the name of the environment to get the credentials for
  109. # Returns username, password, database
  110. #
  111. def remote_database_config(db)
  112. remote_config = capture("cat #{current_path}/config/database.yml")
  113. remote_env = capture("cat #{current_path}/.env")
  114. database = nil
  115. EnvLoader.new(remote_env).with_loaded_env do
  116. database = YAML::load(ERB.new(remote_config).result)
  117. end
  118. return database["#{db}"]['username'], database["#{db}"]['password'], database["#{db}"]['database'], database["#{db}"]['host']
  119. end
  120. #
  121. # Returns the actual host name to sync and port
  122. #
  123. def host_and_port
  124. return roles[:web].servers.first.host, ssh_options[:port] || roles[:web].servers.first.port || 22
  125. end
  126. #
  127. # Purge old backups within the shared sync directory
  128. #
  129. def purge_old_backups(base)
  130. count = fetch(:sync_backups, 5).to_i
  131. backup_files = capture("ls -xt #{shared_path}/sync/#{base}*").split.reverse
  132. if count >= backup_files.length
  133. logger.important "no old backups to clean up"
  134. else
  135. logger.info "keeping #{count} of #{backup_files.length} sync backups"
  136. delete_backups = (backup_files - backup_files.last(count)).join(" ")
  137. run "rm #{delete_backups}"
  138. end
  139. end
  140. end