├── .editorconfig ├── .gitignore ├── .rubocop.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── capistrano-db-tasks.gemspec ├── lib ├── capistrano-db-tasks.rb └── capistrano-db-tasks │ ├── asset.rb │ ├── compressors │ ├── base.rb │ ├── bzip2.rb │ ├── gzip.rb │ └── zstd.rb │ ├── database.rb │ ├── dbtasks.rb │ ├── util.rb │ └── version.rb └── test ├── capistrano_db_tasks_test.rb └── test_helper.rb /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | charset = utf-8 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | Gemfile.lock 3 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | Rails: 2 | Enabled: false 3 | 4 | AllCops: 5 | TargetRubyVersion: 2.3 6 | Exclude: 7 | - 'client/**/*' 8 | - 'db/**/*' 9 | - 'config/**/*' 10 | - 'script/**/*' 11 | - 'test/factories/**/*' 12 | - 'lib/configus.rb' 13 | - 'node_modules/**/*' 14 | - 'tmp/**/*' 15 | - '**/*.haml' 16 | 17 | Style/FrozenStringLiteralComment: 18 | Enabled: false 19 | 20 | Style/ClassAndModuleChildren: 21 | Enabled: false 22 | 23 | Style/Documentation: 24 | Enabled: false 25 | 26 | Metrics/ClassLength: 27 | Max: 150 28 | 29 | Metrics/CyclomaticComplexity: 30 | Max: 10 31 | 32 | Metrics/LineLength: 33 | Max: 160 34 | 35 | Metrics/AbcSize: 36 | Max: 40 37 | 38 | Metrics/MethodLength: 39 | Max: 30 40 | 41 | Metrics/ModuleLength: 42 | Max: 150 43 | 44 | Metrics/PerceivedComplexity: 45 | Max: 10 46 | 47 | Style/NumericLiterals: 48 | Enabled: false 49 | 50 | Style/RegexpLiteral: 51 | Enabled: false 52 | 53 | Style/AsciiComments: 54 | Enabled: false 55 | 56 | Style/StringLiterals: 57 | Enabled: false 58 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Capistrano-db-tasks Changelog 2 | 3 | Reverse Chronological Order: 4 | 5 | ## master 6 | 7 | https://github.com/sgruhier/capistrano-db-tasks/compare/v0.6...HEAD 8 | 9 | * Your contribution here! 10 | * Added support for [zstd compressor](http://facebook.github.io/zstd/) (@ocha) 11 | 12 | # 0.6 (Dec 14 2016) 13 | 14 | * Configurable dump folder #101 #75 #61 (@artempartos, @gmhawash) 15 | * Fixed previous release bugs (@sitnikovme, @slavajacobson) 16 | 17 | # 0.5 (Nov 29 2016) 18 | 19 | * Fixed iteration on remote/local assets dir #98 (@elthariel) 20 | * Fetch :user property on server #97 (@elthariel) 21 | * Add support of ENV['DATABASE_URL'] #54 #70 #99 (@numbata, @fabn, @donbobka, @ktaragorn, @markgandolfo, @leifcr, @elthariel) 22 | * Specify database for pg\_terminate_backend #93 (@stevenchanin) 23 | * Show local execution failure log #89 (@dtaniwaki) 24 | * Add postigs to allowed PG adapters #91 (@matfiz) 25 | * Added database name to --ignore-table statements for MySQL #76 (@km-digitalpatrioten) 26 | * Add :db\_ignore\_tables option #65 (@rdeshpande) 27 | * Update README.markdown #67 (@alexbrinkman) 28 | * Using gzip instead of bzip2 (configurable) #48 #59 (@numbata) 29 | 30 | # 0.4 (Feb 26 2015) 31 | 32 | https://github.com/sgruhier/capistrano-db-tasks/compare/v0.3...v0.4 33 | 34 | * Set correct username for pg connection #55 (@numbata) 35 | * Protect remote server from pushing #51 (@IntractableQuery) 36 | * Use stage name as rails\_env #49 (@bronislav) 37 | * Remove local db dump after db:push if db_local_clean is set #47 (@pugetive) 38 | * Fixed app:pull and app:push tasks #42 (@iamdeuterium) 39 | * Added space between -p and the password #41 (@iamdeuterium) 40 | * Add option to skip data synchronization prompt question #37 (@rafaelsales) 41 | * Add option to remove remote dump file after downloading to local #36 (@rafaelsales) 42 | * Use port option from database.yml #35 (@numbata) 43 | * Use heroku dump/restore arguments for postgresql #26 (@mdpatrick) 44 | 45 | # 0.3 (Feb 9 2014) 46 | 47 | https://github.com/sgruhier/capistrano-db-tasks/compare/v0.2.1...v0.3 48 | 49 | * Capistrano 3 support PR #23 (@sauliusgrigaitis) 50 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gemspec -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009 Sébastien Gruhier - Xilinus/Maptimize 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 11 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 12 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 13 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 14 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 15 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 16 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CapistranoDbTasks | [![Gem Downloads](http://img.shields.io/gem/dt/capistrano-db-tasks.svg)](https://rubygems.org/gems/capistrano-db-tasks) [![Code Climate](https://codeclimate.com/github/sgruhier/capistrano-db-tasks/badges/gpa.svg)](https://codeclimate.com/github/sgruhier/capistrano-db-tasks) [![Gem Version](https://badge.fury.io/rb/capistrano-db-tasks.svg)](http://badge.fury.io/rb/capistrano-db-tasks) 2 | 3 | Add database AND assets tasks to capistrano to a Rails project. It only works with capistrano 3. Older versions until 0.3 works with capistrano 2. 4 | 5 | Currently 6 | 7 | * It only supports mysql and postgresql (both side remote and local) 8 | * Synchronize assets remote to local and local to remote 9 | 10 | Commands mysql, mysqldump (or pg\_dump, psql), bzip2 and unbzip2 (or gzip) must be in your PATH 11 | 12 | Feel free to fork and to add more database support or new tasks. 13 | 14 | ## Install 15 | 16 | Add it as a gem: 17 | 18 | ```ruby 19 | gem "capistrano-db-tasks", require: false 20 | ``` 21 | 22 | Add to config/deploy.rb: 23 | 24 | ```ruby 25 | require 'capistrano-db-tasks' 26 | 27 | # if you haven't already specified 28 | set :rails_env, "production" 29 | 30 | # if you want to remove the local dump file after loading 31 | set :db_local_clean, true 32 | 33 | # if you want to remove the dump file from the server after downloading 34 | set :db_remote_clean, true 35 | 36 | # if you want to exclude table from dump 37 | set :db_ignore_tables, [] 38 | 39 | # if you want to exclude table data (but not table schema) from dump 40 | set :db_ignore_data_tables, [] 41 | 42 | # configure location where the dump file should be created 43 | set :db_dump_dir, "./db" 44 | 45 | # If you want to import assets, you can change default asset dir (default = system) 46 | # This directory must be in your shared directory on the server 47 | set :assets_dir, %w(public/assets public/att) 48 | set :local_assets_dir, %w(public/assets public/att) 49 | 50 | # if you want to work on a specific local environment (default = ENV['RAILS_ENV'] || 'development') 51 | set :locals_rails_env, "production" 52 | 53 | # if you are highly paranoid and want to prevent any push operation to the server 54 | set :disallow_pushing, true 55 | 56 | # if you prefer bzip2/unbzip2 instead of gzip 57 | set :compressor, :bzip2 58 | ``` 59 | 60 | Add to .gitignore 61 | 62 | ```yml 63 | /db/*.sql 64 | ``` 65 | 66 | [How to install bzip2 on Windows](http://stackoverflow.com/a/25625988/3324219) 67 | 68 | ## Available tasks 69 | 70 | ``` 71 | app:local:sync || app:pull # Synchronize your local assets AND database using remote assets and database 72 | app:remote:sync || app:push # Synchronize your remote assets AND database using local assets and database 73 | 74 | assets:local:sync || assets:pull # Synchronize your local assets using remote assets 75 | assets:remote:sync || assets:push # Synchronize your remote assets using local assets 76 | 77 | db:local:sync || db:pull # Synchronize your local database using remote database data 78 | db:remote:sync || db:push # Synchronize your remote database using local database data 79 | ``` 80 | 81 | ## Example 82 | 83 | #### Replace your local database with the production database 84 | 85 | This use case allows you to have the same data on your machine as your production. 86 | You then can reproduce or test things before to apply to your production. 87 | 88 | ```bash 89 | cap db:pull 90 | cap production db:pull # if you are using capistrano-ext to have multistages 91 | ``` 92 | 93 | #### Replace your local database using a dump file stored on your machine 94 | 95 | In case you have a dump file on your machine (you used `db:pull` and kept some files) 96 | and you want to replay one of them, you can use the `db:local:load`: 97 | 98 | ``` 99 | cap development db:local:load DUMP_FILE=db/myapp_production_2018-01-10-150434.sql 100 | ``` 101 | (You have to create a `config/deploy/development.rb` file containing 102 | `set :stage, :development` at least in order to get this working) 103 | 104 | ## Contributors 105 | 106 | * tilsammans (http://github.com/tilsammansee) 107 | * bigfive (http://github.com/bigfive) 108 | * jakemauer (http://github.com/jakemauer) 109 | * tjoneseng (http://github.com/tjoneseng) 110 | * numbata (http://github.com/numbata) 111 | * rafaelsales (http://github.com/rafaelsales) 112 | * rdeshpande (http://github.com/rdeshpande) 113 | 114 | ## TODO 115 | 116 | * May be change project's name as it's not only database tasks now :) 117 | * Add tests 118 | 119 | Copyright (c) 2009 [Sébastien Gruhier - XILINUS], released under the MIT license 120 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'rake' 2 | require 'rake/testtask' 3 | require 'rdoc/task' 4 | require 'bundler' 5 | require 'bundler/gem_tasks' 6 | 7 | desc 'Default: run unit tests.' 8 | task :default => :test 9 | 10 | desc 'Test the capistrano_db_tasks plugin.' 11 | Rake::TestTask.new(:test) do |t| 12 | t.libs << 'lib' 13 | t.libs << 'test' 14 | t.pattern = 'test/**/*_test.rb' 15 | t.verbose = true 16 | end 17 | 18 | desc 'Generate documentation for the capistrano_db_tasks plugin.' 19 | Rake::RDocTask.new(:rdoc) do |rdoc| 20 | rdoc.rdoc_dir = 'rdoc' 21 | rdoc.title = 'CapistranoDbTasks' 22 | rdoc.options << '--line-numbers' << '--inline-source' 23 | rdoc.rdoc_files.include('README') 24 | rdoc.rdoc_files.include('lib/**/*.rb') 25 | end 26 | -------------------------------------------------------------------------------- /capistrano-db-tasks.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | lib = File.expand_path("../lib", __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require "capistrano-db-tasks/version" 5 | 6 | Gem::Specification.new do |gem| 7 | gem.name = "capistrano-db-tasks" 8 | gem.version = CapistranoDbTasks::VERSION 9 | gem.authors = ["Sebastien Gruhier"] 10 | gem.email = ["sebastien.gruhier@xilinus.com"] 11 | gem.homepage = "https://github.com/sgruhier/capistrano-db-tasks" 12 | gem.summary = "A collection of capistrano tasks for syncing assets and databases" 13 | gem.description = "A collection of capistrano tasks for syncing assets and databases" 14 | 15 | gem.rubyforge_project = "capistrano-db-tasks" 16 | 17 | gem.licenses = ["MIT"] 18 | 19 | gem.files = `git ls-files`.split("\n") 20 | gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 21 | gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) } 22 | gem.require_paths = ["lib"] 23 | 24 | gem.add_runtime_dependency "capistrano", ">= 3.0.0" 25 | end 26 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks.rb: -------------------------------------------------------------------------------- 1 | require "capistrano" 2 | require File.expand_path("#{File.dirname(__FILE__)}/capistrano-db-tasks/dbtasks") 3 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/asset.rb: -------------------------------------------------------------------------------- 1 | module Asset 2 | extend self 3 | 4 | def remote_to_local(cap) 5 | servers = Capistrano::Configuration.env.send(:servers) 6 | server = servers.detect { |s| s.roles.include?(:app) } 7 | port = server.netssh_options[:port] || 22 8 | user = server.netssh_options[:user] || server.properties.fetch(:user) 9 | dirs = [cap.fetch(:assets_dir)].flatten 10 | local_dirs = [cap.fetch(:local_assets_dir)].flatten 11 | 12 | dirs.each_index do |idx| 13 | system("rsync -a --del -L -K -vv --progress --rsh='ssh -p #{port}' #{user}@#{server}:#{cap.current_path}/#{dirs[idx]} #{local_dirs[idx]}") 14 | end 15 | end 16 | 17 | def local_to_remote(cap) 18 | servers = Capistrano::Configuration.env.send(:servers) 19 | server = servers.detect { |s| s.roles.include?(:app) } 20 | port = server.netssh_options[:port] || 22 21 | user = server.netssh_options[:user] || server.properties.fetch(:user) 22 | dirs = [cap.fetch(:assets_dir)].flatten 23 | local_dirs = [cap.fetch(:local_assets_dir)].flatten 24 | 25 | dirs.each_index do |idx| 26 | system("rsync -a --del -L -K -vv --progress --rsh='ssh -p #{port}' ./#{dirs[idx]} #{user}@#{server}:#{cap.current_path}/#{local_dirs[idx]}") 27 | end 28 | end 29 | 30 | def to_string(cap) 31 | [cap.fetch(:assets_dir)].flatten.join(" ") 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/compressors/base.rb: -------------------------------------------------------------------------------- 1 | module Compressors 2 | class Base; end 3 | end 4 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/compressors/bzip2.rb: -------------------------------------------------------------------------------- 1 | module Compressors 2 | class Bzip2 < Base 3 | class << self 4 | def file_extension 5 | "bz2" 6 | end 7 | 8 | def compress(from, to = nil) 9 | to = case to 10 | when "-" 11 | "-c --stdout" 12 | when nil 13 | "" 14 | else 15 | "-c --stdout > #{to}" 16 | end 17 | 18 | "bzip2 #{from} #{to}" 19 | end 20 | 21 | def decompress(from, to = nil) 22 | from = "-f #{from}" unless from == "-" 23 | 24 | to = case to 25 | when "-" 26 | "-c --stdout" 27 | when nil 28 | "" 29 | else 30 | "-c --stdout > #{to}" 31 | end 32 | 33 | "bunzip2 -f #{from} #{to}" 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/compressors/gzip.rb: -------------------------------------------------------------------------------- 1 | module Compressors 2 | class Gzip < Base 3 | class << self 4 | def file_extension 5 | "gz" 6 | end 7 | 8 | def compress(from, to = nil) 9 | from = from == :stdin ? "-" : from 10 | to = case to 11 | when '-' 12 | "-c --stdout" 13 | when nil 14 | "" 15 | else 16 | "-c --stdout > #{to}" 17 | end 18 | 19 | "gzip #{from} #{to}" 20 | end 21 | 22 | def decompress(from, to = nil) 23 | from = from == :stdin ? "-" : from 24 | to = case to 25 | when :stdout 26 | "-c --stdout" 27 | when nil 28 | "" 29 | else 30 | "-c --stdout > #{to}" 31 | end 32 | 33 | "gzip -d #{from} #{to}" 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/compressors/zstd.rb: -------------------------------------------------------------------------------- 1 | module Compressors 2 | class Zstd < Base 3 | COMPRESSOR_BIN = :zstd 4 | 5 | class << self 6 | def file_extension 7 | "zst" 8 | end 9 | 10 | def compress(from, to = nil) 11 | level = 3 12 | from = from == :stdin ? "-" : from 13 | to = case to 14 | when '-' 15 | "-c --stdout" 16 | when nil 17 | "" 18 | else 19 | "-c --stdout > #{to}" 20 | end 21 | 22 | "#{COMPRESSOR_BIN} -#{level} #{from} #{to}" 23 | end 24 | 25 | def decompress(from, to = nil) 26 | from = from == :stdin ? "-" : from 27 | to = case to 28 | when :stdout 29 | "-c --stdout" 30 | when nil 31 | "" 32 | else 33 | "-c --stdout > #{to}" 34 | end 35 | 36 | "#{COMPRESSOR_BIN} -d --rm #{from} #{to}" 37 | end 38 | end 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/database.rb: -------------------------------------------------------------------------------- 1 | module Database 2 | class Base 3 | DBCONFIG_BEGIN_FLAG = "__CAPISTRANODB_CONFIG_BEGIN_FLAG__".freeze 4 | DBCONFIG_END_FLAG = "__CAPISTRANODB_CONFIG_END_FLAG__".freeze 5 | 6 | attr_accessor :config, :output_file 7 | 8 | def initialize(cap_instance) 9 | @cap = cap_instance 10 | end 11 | 12 | def mysql? 13 | @config['adapter'] =~ /^mysql/ 14 | end 15 | 16 | def postgresql? 17 | %w(postgresql pg postgis chronomodel).include? @config['adapter'] 18 | end 19 | 20 | def credentials 21 | credential_params = "" 22 | username = @config['username'] || @config['user'] 23 | 24 | if mysql? 25 | credential_params << " -u #{username} " if username 26 | credential_params << " -p'#{@config['password']}' " if @config['password'] 27 | credential_params << " -h #{@config['host']} " if @config['host'] 28 | credential_params << " -S #{@config['socket']} " if @config['socket'] 29 | credential_params << " -P #{@config['port']} " if @config['port'] 30 | elsif postgresql? 31 | credential_params << " -U #{username} " if username 32 | credential_params << " -h #{@config['host']} " if @config['host'] 33 | credential_params << " -p #{@config['port']} " if @config['port'] 34 | end 35 | 36 | credential_params 37 | end 38 | 39 | def database 40 | @config['database'] 41 | end 42 | 43 | def current_time 44 | Time.now.strftime("%Y-%m-%d-%H%M%S") 45 | end 46 | 47 | def output_file 48 | @output_file ||= "#{database}_#{current_time}.sql.#{compressor.file_extension}" 49 | end 50 | 51 | def compressor 52 | @compressor ||= begin 53 | compressor_klass = @cap.fetch(:compressor).to_s.split('_').collect(&:capitalize).join 54 | klass = Object.module_eval("::Compressors::#{compressor_klass}", __FILE__, __LINE__) 55 | klass 56 | end 57 | end 58 | 59 | private 60 | 61 | def pgpass 62 | @config['password'] ? "PGPASSWORD='#{@config['password']}'" : "" 63 | end 64 | 65 | def dump_cmd 66 | if mysql? 67 | "mysqldump #{credentials} #{database} #{dump_cmd_opts}" 68 | elsif postgresql? 69 | "#{pgpass} pg_dump #{credentials} #{database} #{dump_cmd_opts}" 70 | end 71 | end 72 | 73 | def import_cmd(file) 74 | if mysql? 75 | "mysql #{credentials} -D #{database} < #{file}" 76 | elsif postgresql? 77 | terminate_connection_sql = "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = '#{database}' AND pid <> pg_backend_pid();" 78 | "#{pgpass} psql -c \"#{terminate_connection_sql};\" #{credentials} #{database}; #{pgpass} dropdb #{credentials} #{database}; #{pgpass} createdb #{credentials} #{database}; #{pgpass} psql #{credentials} -d #{database} < #{file}" 79 | end 80 | end 81 | 82 | def dump_cmd_opts 83 | if mysql? 84 | "--lock-tables=false #{dump_cmd_ignore_tables_opts} #{dump_cmd_ignore_data_tables_opts}" 85 | elsif postgresql? 86 | "--no-acl --no-owner #{dump_cmd_ignore_tables_opts} #{dump_cmd_ignore_data_tables_opts}" 87 | end 88 | end 89 | 90 | def dump_cmd_ignore_tables_opts 91 | ignore_tables = @cap.fetch(:db_ignore_tables, []) 92 | if mysql? 93 | ignore_tables.map { |t| "--ignore-table=#{database}.#{t}" }.join(" ") 94 | elsif postgresql? 95 | ignore_tables.map { |t| "--exclude-table=#{t}" }.join(" ") 96 | end 97 | end 98 | 99 | def dump_cmd_ignore_data_tables_opts 100 | ignore_tables = @cap.fetch(:db_ignore_data_tables, []) 101 | ignore_tables.map { |t| "--exclude-table-data=#{t}" }.join(" ") if postgresql? 102 | end 103 | end 104 | 105 | class Remote < Base 106 | def initialize(cap_instance) 107 | super(cap_instance) 108 | puts "Loading remote database config" 109 | @cap.within @cap.current_path do 110 | @cap.with rails_env: @cap.fetch(:rails_env) do 111 | run_string = "runner \"puts '#{DBCONFIG_BEGIN_FLAG}' + ActiveRecord::Base.connection.instance_variable_get(:@config).to_yaml + '#{DBCONFIG_END_FLAG}'\"" 112 | dirty_config_content = 113 | if @cap.capture(:ruby, "bin/rails -v", '2>/dev/null').size > 0 114 | @cap.capture(:ruby, "bin/rails #{run_string}", '2>/dev/null') 115 | else 116 | @cap.capture(:rails, run_string, '2>/dev/null') 117 | end 118 | # Remove all warnings, errors and artefacts produced by bunlder, rails and other useful tools 119 | config_content = dirty_config_content.match(/#{DBCONFIG_BEGIN_FLAG}(.*?)#{DBCONFIG_END_FLAG}/m)[1] 120 | @config = YAML.load(config_content).each_with_object({}) { |(k, v), h| h[k.to_s] = v } 121 | end 122 | end 123 | end 124 | 125 | def dump 126 | @cap.execute "cd #{@cap.current_path} && #{dump_cmd} | #{compressor.compress('-', db_dump_file_path)}" 127 | self 128 | end 129 | 130 | def download(local_file = "#{output_file}") 131 | @cap.within @cap.current_path do 132 | @cap.download! db_dump_file_path, local_file 133 | end 134 | end 135 | 136 | def clean_dump_if_needed 137 | if @cap.fetch(:db_remote_clean) 138 | @cap.execute "rm -f #{db_dump_file_path}" 139 | else 140 | puts "leaving #{db_dump_file_path} on the server (add \"set :db_remote_clean, true\" to deploy.rb to remove)" 141 | end 142 | end 143 | 144 | # cleanup = true removes the mysqldump file after loading, false leaves it in db/ 145 | def load(file, cleanup) 146 | unzip_file = File.join(File.dirname(file), File.basename(file, ".#{compressor.file_extension}")) 147 | # @cap.run "cd #{@cap.current_path} && bunzip2 -f #{file} && RAILS_ENV=#{@cap.rails_env} bundle exec rake db:drop db:create && #{import_cmd(unzip_file)}" 148 | @cap.execute "cd #{@cap.current_path} && #{compressor.decompress(file)} && RAILS_ENV=#{@cap.fetch(:rails_env)} && #{import_cmd(unzip_file)}" 149 | @cap.execute("cd #{@cap.current_path} && rm #{unzip_file}") if cleanup 150 | end 151 | 152 | private 153 | 154 | def db_dump_file_path 155 | "#{db_dump_dir}/#{output_file}" 156 | end 157 | 158 | def db_dump_dir 159 | @cap.fetch(:db_dump_dir) || "#{@cap.current_path}/db" 160 | end 161 | end 162 | 163 | class Local < Base 164 | def initialize(cap_instance) 165 | super(cap_instance) 166 | puts "Loading local database config" 167 | dir_with_escaped_spaces = Dir.pwd.gsub ' ', '\ ' 168 | command = "#{dir_with_escaped_spaces}/bin/rails runner \"puts '#{DBCONFIG_BEGIN_FLAG}' + Rails.application.config.database_configuration[Rails.env].to_yaml + '#{DBCONFIG_END_FLAG}'\"" 169 | stdout, status = Open3.capture2(command) 170 | raise "Error running command (status=#{status}): #{command}" if status != 0 171 | 172 | config_content = stdout.match(/#{DBCONFIG_BEGIN_FLAG}(.*?)#{DBCONFIG_END_FLAG}/m)[1] 173 | @config = YAML.load(config_content).each_with_object({}) { |(k, v), h| h[k.to_s] = v } 174 | end 175 | 176 | # cleanup = true removes the mysqldump file after loading, false leaves it in db/ 177 | def load(file, cleanup) 178 | unzip_file = File.join(File.dirname(file), File.basename(file, ".#{compressor.file_extension}")) 179 | puts "executing local: #{compressor.decompress(file)} && #{import_cmd(unzip_file)}" 180 | execute("#{compressor.decompress(file)} && #{import_cmd(unzip_file)}") 181 | if cleanup 182 | puts "removing #{unzip_file}" 183 | File.unlink(unzip_file) 184 | else 185 | puts "leaving #{unzip_file} (specify :db_local_clean in deploy.rb to remove)" 186 | end 187 | puts "Completed database import" 188 | end 189 | 190 | def dump 191 | execute "#{dump_cmd} | #{compressor.compress('-', output_file)}" 192 | self 193 | end 194 | 195 | def upload 196 | remote_file = "#{@cap.current_path}/#{output_file}" 197 | @cap.within @cap.current_path do 198 | @cap.upload! output_file, remote_file 199 | end 200 | end 201 | 202 | private 203 | 204 | def execute(cmd) 205 | result = system cmd 206 | @cap.error "Failed to execute the local command: #{cmd}" unless result 207 | result 208 | end 209 | end 210 | 211 | class << self 212 | def check(local_db, remote_db = nil) 213 | return if mysql_db_valid?(local_db, remote_db) 214 | return if postgresql_db_valid?(local_db, remote_db) 215 | 216 | raise 'Only mysql or postgresql on remote and local server is supported' 217 | end 218 | 219 | def mysql_db_valid?(local_db, remote_db) 220 | local_db.mysql? && (remote_db.nil? || remote_db && remote_db.mysql?) 221 | end 222 | 223 | def postgresql_db_valid?(local_db, remote_db) 224 | local_db.postgresql? && 225 | (remote_db.nil? || (remote_db && remote_db.postgresql?)) 226 | end 227 | 228 | def remote_to_local(instance) 229 | local_db = Database::Local.new(instance) 230 | remote_db = Database::Remote.new(instance) 231 | 232 | check(local_db, remote_db) 233 | 234 | begin 235 | remote_db.dump.download 236 | rescue Exception => e 237 | puts "E[#{e.class}]: #{e.message}" 238 | ensure 239 | remote_db.clean_dump_if_needed 240 | end 241 | local_db.load(remote_db.output_file, instance.fetch(:db_local_clean)) 242 | end 243 | 244 | def local_to_remote(instance) 245 | local_db = Database::Local.new(instance) 246 | remote_db = Database::Remote.new(instance) 247 | 248 | check(local_db, remote_db) 249 | 250 | local_db.dump.upload 251 | remote_db.load(local_db.output_file, instance.fetch(:db_local_clean)) 252 | File.unlink(local_db.output_file) if instance.fetch(:db_local_clean) 253 | end 254 | 255 | def local_to_local(instance, dump_file) 256 | local_db = Database::Local.new(instance) 257 | 258 | check(local_db) 259 | 260 | local_db.load(dump_file, instance.fetch(:db_local_clean)) 261 | end 262 | end 263 | end 264 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/dbtasks.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path("#{File.dirname(__FILE__)}/util") 2 | require File.expand_path("#{File.dirname(__FILE__)}/database") 3 | require File.expand_path("#{File.dirname(__FILE__)}/asset") 4 | require File.expand_path("#{File.dirname(__FILE__)}/compressors/base") 5 | require File.expand_path("#{File.dirname(__FILE__)}/compressors/bzip2") 6 | require File.expand_path("#{File.dirname(__FILE__)}/compressors/gzip") 7 | require File.expand_path("#{File.dirname(__FILE__)}/compressors/zstd") 8 | 9 | set :local_rails_env, ENV['RAILS_ENV'] || 'development' unless fetch(:local_rails_env) 10 | set :rails_env, fetch(:stage) || 'production' unless fetch(:rails_env) 11 | set :db_local_clean, false unless fetch(:db_local_clean) 12 | set :assets_dir, 'system' unless fetch(:assets_dir) 13 | set :local_assets_dir, 'public' unless fetch(:local_assets_dir) 14 | set :skip_data_sync_confirm, ENV['SKIP_DATA_SYNC_CONFIRM'].to_s.casecmp('true').zero? 15 | set :disallow_pushing, false unless fetch(:disallow_pushing) 16 | set :compressor, :gzip unless fetch(:compressor) 17 | 18 | namespace :capistrano_db_tasks do 19 | task :check_can_push do 20 | raise "pushing is disabled, set disallow_pushing to false to carry out this operation" if fetch(:disallow_pushing) 21 | end 22 | end 23 | 24 | namespace :db do 25 | namespace :remote do 26 | desc 'Synchronize your remote database using local database data' 27 | task :sync => 'capistrano_db_tasks:check_can_push' do 28 | on roles(:db) do 29 | if fetch(:skip_data_sync_confirm) || Util.prompt('Are you sure you want to REPLACE THE REMOTE DATABASE with local database') 30 | Database.local_to_remote(self) 31 | end 32 | end 33 | end 34 | end 35 | 36 | namespace :local do 37 | desc 'Synchronize your local database using remote database data' 38 | task :sync do 39 | on roles(:db) do 40 | puts "Local database: #{Database::Local.new(self).database}" 41 | if fetch(:skip_data_sync_confirm) || Util.prompt('Are you sure you want to erase your local database with server database') 42 | Database.remote_to_local(self) 43 | end 44 | end 45 | end 46 | 47 | desc 'Replace your local database using a dump file from the DUMP_FILE ' \ 48 | 'environment variable' 49 | task :load do 50 | run_locally do 51 | if ENV['DUMP_FILE'].nil? 52 | raise 'You must give a dump file using the DUMP_FILE environment ' \ 53 | 'variable' 54 | end 55 | 56 | unless File.exist?(ENV['DUMP_FILE']) 57 | raise "File #{ENV['DUMP_FILE']} doesn't exists" 58 | end 59 | 60 | if fetch(:skip_data_sync_confirm) || 61 | Util.prompt('Are you sure you want to erase your local database ' \ 62 | "with the dump file #{ENV['DUMP_FILE']}") 63 | Database.local_to_local(self, ENV['DUMP_FILE']) 64 | end 65 | end 66 | end 67 | end 68 | 69 | desc 'Synchronize your local database using remote database data' 70 | task :pull => "db:local:sync" 71 | 72 | desc 'Synchronize your remote database using local database data' 73 | task :push => "db:remote:sync" 74 | end 75 | 76 | namespace :assets do 77 | namespace :remote do 78 | desc 'Synchronize your remote assets using local assets' 79 | task :sync => 'capistrano_db_tasks:check_can_push' do 80 | on roles(:app) do 81 | puts "Assets directories: #{fetch(:assets_dir)}" 82 | if fetch(:skip_data_sync_confirm) || Util.prompt("Are you sure you want to erase your server assets with local assets") 83 | Asset.local_to_remote(self) 84 | end 85 | end 86 | end 87 | end 88 | 89 | namespace :local do 90 | desc 'Synchronize your local assets using remote assets' 91 | task :sync do 92 | on roles(:app) do 93 | puts "Assets directories: #{fetch(:local_assets_dir)}" 94 | if fetch(:skip_data_sync_confirm) || Util.prompt("Are you sure you want to erase your local assets with server assets") 95 | Asset.remote_to_local(self) 96 | end 97 | end 98 | end 99 | end 100 | 101 | desc 'Synchronize your local assets using remote assets' 102 | task :pull => "assets:local:sync" 103 | 104 | desc 'Synchronize your remote assets using local assets' 105 | task :push => "assets:remote:sync" 106 | end 107 | 108 | namespace :app do 109 | namespace :remote do 110 | desc 'Synchronize your remote assets AND database using local assets and database' 111 | task :sync => 'capistrano_db_tasks:check_can_push' do 112 | if fetch(:skip_data_sync_confirm) || Util.prompt("Are you sure you want to REPLACE THE REMOTE DATABASE AND your remote assets with local database and assets(#{fetch(:assets_dir)})") 113 | on roles(:db) do 114 | Database.local_to_remote(self) 115 | end 116 | 117 | on roles(:app) do 118 | Asset.local_to_remote(self) 119 | end 120 | end 121 | end 122 | end 123 | 124 | namespace :local do 125 | desc 'Synchronize your local assets AND database using remote assets and database' 126 | task :sync do 127 | puts "Local database : #{Database::Local.new(self).database}" 128 | puts "Assets directories : #{fetch(:local_assets_dir)}" 129 | if fetch(:skip_data_sync_confirm) || Util.prompt("Are you sure you want to erase your local database AND your local assets with server database and assets(#{fetch(:assets_dir)})") 130 | on roles(:db) do 131 | Database.remote_to_local(self) 132 | end 133 | 134 | on roles(:app) do 135 | Asset.remote_to_local(self) 136 | end 137 | end 138 | end 139 | end 140 | 141 | desc 'Synchronize your local assets AND database using remote assets and database' 142 | task :pull => "app:local:sync" 143 | 144 | desc 'Synchronize your remote assets AND database using local assets and database' 145 | task :push => "app:remote:sync" 146 | end 147 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/util.rb: -------------------------------------------------------------------------------- 1 | module Util 2 | def self.prompt(msg, prompt = "(y)es, (n)o ") 3 | ask(:answer, "#{msg} #{prompt} ? ") 4 | (fetch(:answer) =~ /^y$|^yes$/i).to_i.zero? 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/capistrano-db-tasks/version.rb: -------------------------------------------------------------------------------- 1 | module CapistranoDbTasks 2 | VERSION = "0.6".freeze 3 | end 4 | -------------------------------------------------------------------------------- /test/capistrano_db_tasks_test.rb: -------------------------------------------------------------------------------- 1 | require 'test_helper' 2 | 3 | class CapistranoDbTasksTest < ActiveSupport::TestCase 4 | # Replace this with your real tests. 5 | test "the truth" do 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'active_support' 3 | require 'active_support/test_case' --------------------------------------------------------------------------------