activerecord-snapshot 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: dcf198dbf40940ddc9a3cc50767c89f8f6ca88f3
4
+ data.tar.gz: 3e41aa133c618b1f687417596ef836baff755902
5
+ SHA512:
6
+ metadata.gz: 3e19714fd936fa8c671327b006ad08c8d8b2c2c0e20613ea05e36500e8627b19a3c1c24aeb66275e16ae6fae3c7461998f672a3434b70b86e499e3b06d1ee4de
7
+ data.tar.gz: 84cba03937050b8ed4ca0efb54420635c2b98ee400e1f7a7b11c962d5dc25411b919a43ee78a65a793fbe1b4cb9834352cfc19e19a83971e96d96892f8f1de84
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2017 Bernardo Farah
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,118 @@
1
+ # ActiveRecord::Snapshot
2
+
3
+ This gem provides rake tasks to create and import MySQL snapshots using S3. This
4
+ is pretty specialized for how CoverHound uses snapshots.
5
+
6
+ ## Dependencies
7
+
8
+ - S3
9
+ - MySQL
10
+ - bzip2
11
+ - openssl
12
+
13
+ ## Usage
14
+
15
+ ### Configuration
16
+
17
+ This file, looked for at `config/snapshot.yml`, allows for the following
18
+ configuration:
19
+
20
+ - `store.tmp` Working directory for snapshots
21
+ - `store.local` Local storage of snapshots
22
+ - `s3.*` S3 access keys, bucket, region and paths for storing regular and named
23
+ snapshots
24
+ - `ssl_key` Path to the key that will be used to encrypt the snapshot
25
+ - `tables` The tables that should be exported as part of the snapshot
26
+
27
+ ##### Sample
28
+
29
+ ```yml
30
+ # config/snapshot.yml
31
+ store:
32
+ tmp: <%= Rails.root.join("tmp/snapshots").to_s %>
33
+ local: <%= Rails.root.join("db/snapshots").to_s %>
34
+
35
+ s3:
36
+ access_key_id: 'foo'
37
+ secret_access_key: 'bar'
38
+ bucket: 'metal-bucket'
39
+ region: 'us-west-1'
40
+ paths:
41
+ snapshots: 'snapshots'
42
+ named_snapshots: 'named_snapshots'
43
+
44
+ ssl_key: "/dir/to/snapshots-secret.key"
45
+
46
+ tables:
47
+ - "example_table"
48
+ ```
49
+
50
+ ### Tasks
51
+
52
+ ##### `db:snapshot:create`
53
+
54
+ Creates a snapshot with the following naming convention:
55
+ `snapshot_YY-MM-DD_HH-MM.sql.bz2.enc`
56
+
57
+ This snapshot is then stored at `s3.paths.snapshots`. It is assigned a version
58
+ (incrementing off of a `snapshot_version` file, which is saved locally and on
59
+ S3) which is stored alongside its filename in the file `snapshot_list`.
60
+
61
+ This task only runs in production.
62
+
63
+ ##### `db:snapshot:create_named`
64
+
65
+ Creates a named snapshot: `[name].sql.bz2.enc` which is stored at
66
+ `s3.paths.named_snapshots`. These are not stored in `snapshot_list` or
67
+ `snapshot_version`.
68
+
69
+ ##### `db:snapshot:import`
70
+
71
+ When used without arguments, it imports the latest regular snapshot from S3,
72
+ then drops and replaces the local database.
73
+
74
+ Can be given arguments for the version:
75
+
76
+ `db:snapshot:import[12]` gets you the 12th regular snapshot
77
+ `db:snapshot:import['foo']` gets you your snapshot named `foo`
78
+
79
+ ##### `db:snapshot:import:only['foo bar']`
80
+
81
+ Imports _only_ the tables given as arguments (`foo` and `bar` in this example)
82
+ from the latest regular snapshot
83
+
84
+ ##### `db:snapshot:reload`
85
+
86
+ Reloads the current snapshot
87
+
88
+ ##### `db:snapshot:list`
89
+
90
+ Shows a list of snapshots
91
+
92
+ ##### `db:snapshot:list:load[n]`
93
+
94
+ Shows a list of the last `n` snapshots
95
+
96
+ ## Installation
97
+ Add this line to your application's Gemfile:
98
+
99
+ ```ruby
100
+ gem 'activerecord-snapshot'
101
+ ```
102
+
103
+ And then execute:
104
+ ```bash
105
+ $ bundle
106
+ ```
107
+
108
+ Or install it yourself as:
109
+ ```bash
110
+ $ gem install activerecord-snapshot
111
+ ```
112
+
113
+ ## Contributing
114
+
115
+ Be nice!
116
+
117
+ ## License
118
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,33 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'ActiveRecord::Snapshot'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.md')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+
18
+
19
+
20
+
21
+
22
+ require 'bundler/gem_tasks'
23
+
24
+ require 'rake/testtask'
25
+
26
+ Rake::TestTask.new(:test) do |t|
27
+ t.libs << 'test'
28
+ t.pattern = 'test/**/*_test.rb'
29
+ t.verbose = false
30
+ end
31
+
32
+
33
+ task default: :test
@@ -0,0 +1,12 @@
1
+ require_relative "snapshot/configuration"
2
+ require_relative "snapshot/utils/stepper"
3
+ require_relative "snapshot/commands/all"
4
+ require_relative "snapshot/files/all"
5
+ require_relative "snapshot/railtie"
6
+ require_relative "snapshot/actions/create"
7
+ require_relative "snapshot/actions/import"
8
+
9
+ module ActiveRecord
10
+ module Snapshot
11
+ end
12
+ end
@@ -0,0 +1,70 @@
1
+ require "active_record/snapshot/utils/logger"
2
+
3
+ module ActiveRecord
4
+ module Snapshot
5
+ class Create
6
+ def self.call(*args)
7
+ new(*args).call
8
+ end
9
+
10
+ def initialize(name: nil)
11
+ @named_snapshot = !name.nil?
12
+ @snapshot = Snapshot.new(name)
13
+ end
14
+
15
+ def call
16
+ Stepper.call(self, **steps)
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :snapshot, :named_snapshot
22
+
23
+ def config
24
+ ActiveRecord::Snapshot.config
25
+ end
26
+
27
+ def steps
28
+ {
29
+ dump: "Create dump of #{config.db.database} at #{snapshot.dump}",
30
+ compress: "Compress snapshot to #{snapshot.compressed}",
31
+ encrypt: "Encrypt snapshot to #{snapshot.encrypted}",
32
+ upload_snapshot: "Upload files to #{config.s3.bucket}"
33
+ }.tap do |s|
34
+ next if named_snapshot
35
+ s[:update_list] = "Update list from #{Version.current} to #{Version.next} with #{snapshot.encrypted}"
36
+ s[:upload_version_info] = "Upload version info to #{config.s3.bucket}"
37
+ end
38
+ end
39
+
40
+ def dump
41
+ config.adapter.dump(tables: config.tables, output: snapshot.dump)
42
+ end
43
+
44
+ def compress
45
+ Bzip2.compress(snapshot.dump)
46
+ end
47
+
48
+ def encrypt
49
+ OpenSSL.encrypt(
50
+ input: snapshot.compressed,
51
+ output: snapshot.encrypted
52
+ )
53
+ end
54
+
55
+ def update_list
56
+ Version.increment
57
+ List.add(version: Version.current, file: snapshot.encrypted)
58
+ end
59
+
60
+ def upload_snapshot
61
+ snapshot.upload
62
+ end
63
+
64
+ def upload_version_info
65
+ Version.upload
66
+ List.upload
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,92 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class Import
4
+ def self.call(*args)
5
+ new(*args).call
6
+ end
7
+
8
+ def initialize(version: nil, tables: [])
9
+ @version = version
10
+ if named_version?
11
+ name = version
12
+ else
13
+ @version, name = SelectSnapshot.call(version)
14
+ end
15
+ @snapshot = Snapshot.new(name)
16
+ @tables = tables
17
+ end
18
+
19
+ def call
20
+ Stepper.call(self, **steps)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :snapshot, :tables, :version
26
+
27
+ def config
28
+ ActiveRecord::Snapshot.config
29
+ end
30
+
31
+ def named_version?
32
+ !version.blank? && version.to_i.to_s != version.to_s
33
+ end
34
+
35
+ def version_downloaded?
36
+ version.to_i == Version.current && File.file?(snapshot.dump)
37
+ end
38
+
39
+ def steps
40
+ steps = {}
41
+ unless version_downloaded?
42
+ steps[:download] = "Download snapshot to #{snapshot.encrypted}"
43
+ steps[:decrypt] = "Decrypt snapshot to #{snapshot.compressed}"
44
+ steps[:decompress] = "Decompress snapshot to #{snapshot.dump}"
45
+ end
46
+
47
+ if tables.empty?
48
+ steps[:reset_database] = "Reset database"
49
+ else
50
+ steps[:filter_tables] = "Filter tables"
51
+ end
52
+
53
+ steps[:import] = "Importing the snapshot into #{config.db.database}"
54
+ steps[:save] = "Caching the new snapshot version" unless named_version? || tables.present?
55
+ steps
56
+ end
57
+
58
+ def download
59
+ snapshot.download
60
+ end
61
+
62
+ def decrypt
63
+ OpenSSL.decrypt(
64
+ input: snapshot.encrypted,
65
+ output: snapshot.compressed
66
+ ) && FileUtils.rm(snapshot.encrypted)
67
+ end
68
+
69
+ def decompress
70
+ Bzip2.decompress(snapshot.compressed)
71
+ end
72
+
73
+ def reset_database
74
+ Rake::Task["db:drop"].invoke
75
+ Rake::Task["db:create"].invoke
76
+ end
77
+
78
+ def filter_tables
79
+ FilterTables.call(tables: tables, sql_dump: snapshot.dump)
80
+ end
81
+
82
+ def import
83
+ config.adapter.import(input: snapshot.dump)
84
+ Rake::Task["db:schema:dump"].invoke
85
+ end
86
+
87
+ def save
88
+ Version.write(version)
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,6 @@
1
+ require_relative "bzip2"
2
+ require_relative "filter_tables"
3
+ require_relative "mysql"
4
+ require_relative "openssl"
5
+ require_relative "s3"
6
+ require_relative "select_snapshot"
@@ -0,0 +1,15 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class Bzip2
4
+ def self.compress(path)
5
+ return false unless File.file?(path)
6
+ system("nice bzip2 -z #{path}")
7
+ end
8
+
9
+ def self.decompress(path)
10
+ return false unless File.file?(path)
11
+ system("nice bunzip2 -f #{path}")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,41 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class FilterTables
4
+ def self.call(*args)
5
+ new(*args).call
6
+ end
7
+
8
+ def initialize(tables:, sql_dump:)
9
+ @tables = tables
10
+ @sql_dump = sql_dump
11
+ end
12
+
13
+ def call
14
+ tables.each(&method(:extract_table))
15
+ unify_tables
16
+ end
17
+
18
+ private
19
+
20
+ attr_reader :tables, :sql_dump
21
+
22
+ def table_file(table)
23
+ ActiveRecord::Snapshot.config.store.tmp.join("#{table}.sql").to_s
24
+ end
25
+
26
+ def extract_table(table)
27
+ system(<<~SH)
28
+ sed -ne \\
29
+ '/Table structure for table `#{table}`/,/Table structure for table/p' \\
30
+ #{sql_dump} \\
31
+ > #{table_file(table)}
32
+ SH
33
+ end
34
+
35
+ def unify_tables
36
+ all = tables.map(&method(:table_file))
37
+ system("cat #{all.join(" ")} > #{sql_dump}") && FileUtils.rm(all)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,46 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class MySQL
4
+ def self.dump(*args)
5
+ new.dump(*args)
6
+ end
7
+
8
+ def dump(tables:, output:)
9
+ dump_command("--no-data #{database} > #{output}") &&
10
+ dump_command("--quick #{database} #{tables.join(" ")} >> #{output}")
11
+ end
12
+
13
+ def self.import(*args)
14
+ new.import(*args)
15
+ end
16
+
17
+ def import(input:)
18
+ system(<<~SH)
19
+ nice mysql \\
20
+ --user #{username} \\
21
+ #{password ? '--password' : ''} #{password} \\
22
+ --host #{host} \\
23
+ #{database} < #{input}
24
+ SH
25
+ end
26
+
27
+ private
28
+
29
+ delegate :username, :password, :host, :database, to: :db_config
30
+
31
+ def db_config
32
+ ActiveRecord::Snapshot.config.db
33
+ end
34
+
35
+ def dump_command(args = "")
36
+ system(<<~SH)
37
+ nice mysqldump \\
38
+ --user #{username} \\
39
+ --password #{password} \\
40
+ --host #{host} \\
41
+ #{args}
42
+ SH
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class OpenSSL
4
+ def self.encrypt(input:, output:)
5
+ system(<<-SH)
6
+ nice openssl aes-256-cbc \\
7
+ -in #{input} \\
8
+ -out #{output} \\
9
+ -kfile #{ActiveRecord::Snapshot.config.ssl_key}
10
+ SH
11
+ end
12
+
13
+ def self.decrypt(input:, output:)
14
+ system(<<-SH)
15
+ nice openssl enc -d -aes-256-cbc \\
16
+ -in #{input} \\
17
+ -out #{output} \\
18
+ -kfile #{ActiveRecord::Snapshot.config.ssl_key}
19
+ SH
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,45 @@
1
+ require 'fog/aws'
2
+
3
+ module ActiveRecord
4
+ module Snapshot
5
+ class S3
6
+ def initialize(directory:)
7
+ @connection = create_connection
8
+ @directory = directory
9
+ end
10
+
11
+ def upload(path)
12
+ connection.put_object(config.bucket, aws_key(path), File.open(path))
13
+ end
14
+
15
+ def download_to(path)
16
+ File.open(path, "wb") { |f| f.write(read(path)) }
17
+ end
18
+
19
+ def read(path)
20
+ connection.get_object(config.bucket, aws_key(path)).body
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :connection, :directory
26
+
27
+ def config
28
+ ActiveRecord::Snapshot.config.s3
29
+ end
30
+
31
+ def aws_key(path)
32
+ File.join(directory, File.basename(path))
33
+ end
34
+
35
+ def create_connection
36
+ ::Fog::Storage.new(
37
+ provider: "AWS",
38
+ region: config.region,
39
+ aws_access_key_id: config.access_key_id,
40
+ aws_secret_access_key: config.secret_access_key
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,14 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class SelectSnapshot
4
+ def self.call(selected_version = nil)
5
+ if selected_version.blank?
6
+ List.download
7
+ List.last
8
+ else
9
+ List.get(version: selected_version)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,74 @@
1
+ require "yaml"
2
+ require "hashie"
3
+
4
+ # NOTE: Default lambdas below all have because of a bug with defaults + coercion
5
+ # Defaults aren't executed on class instantiation unless the lambda
6
+ # takes an argument. This is a problem, since the execution in #[]=
7
+ # happens AFTER the value is assigned - therefore AFTER the
8
+ # coercion happens
9
+
10
+ module ActiveRecord
11
+ module Snapshot
12
+ class Configuration < Hashie::Dash
13
+ class S3Paths < Hashie::Dash
14
+ include Hashie::Extensions::Dash::IndifferentAccess
15
+
16
+ property :snapshots, required: true
17
+ property :named_snapshots, required: true
18
+ end
19
+
20
+ class S3Config < Hashie::Dash
21
+ include Hashie::Extensions::Dash::Coercion
22
+ include Hashie::Extensions::Dash::IndifferentAccess
23
+
24
+ property :access_key_id, required: true
25
+ property :secret_access_key, required: true
26
+ property :bucket, required: true
27
+ property :region, default: "us-west-1"
28
+ property :paths, required: true, coerce: S3Paths
29
+ end
30
+
31
+ class DBConfig < Hashie::Dash
32
+ include Hashie::Extensions::Dash::IndifferentAccess
33
+
34
+ def initialize(database_hash)
35
+ super database_hash.slice("database", "username", "password", "host")
36
+ end
37
+
38
+ property :database, required: true
39
+ property :username, required: true
40
+ property :host, required: true
41
+ property :password
42
+ end
43
+
44
+ class StoreConfig < Hashie::Dash
45
+ include Hashie::Extensions::Dash::Coercion
46
+ include Hashie::Extensions::Dash::IndifferentAccess
47
+
48
+ property :tmp, default: ->(_) { ::Rails.root.join("tmp/snapshots") }, coerce: Pathname
49
+ property :local, default: ->(_) { ::Rails.root.join("db/snapshots") }, coerce: Pathname
50
+ end
51
+
52
+ include Hashie::Extensions::Dash::Coercion
53
+ include Hashie::Extensions::Dash::IndifferentAccess
54
+
55
+ property :db, default: ->(_) { ::Rails.application.config.database_configuration[Rails.env] }, coerce: DBConfig
56
+ property :s3, required: true, coerce: S3Config
57
+ property :ssl_key, required: true
58
+ property :tables, required: true
59
+ property :store, coerce: StoreConfig
60
+
61
+ def adapter
62
+ ActiveRecord::Snapshot::MySQL
63
+ end
64
+ end
65
+
66
+ def self.config_file
67
+ ::Rails.root.join("config", "snapshot.yml")
68
+ end
69
+
70
+ def self.config
71
+ @config ||= Configuration.new(YAML.safe_load(ERB.new(::File.read(config_file)).result))
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,3 @@
1
+ require_relative "list"
2
+ require_relative "snapshot"
3
+ require_relative "version"
@@ -0,0 +1,54 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class List
4
+ class << self
5
+ def download
6
+ s3.download_to(path)
7
+ end
8
+
9
+ def upload
10
+ s3.upload(path)
11
+ end
12
+
13
+ def add(version:, file:)
14
+ contents = File.read(path)
15
+ File.open(path, "w") do |f|
16
+ f.puts "#{version.to_i} #{File.basename(file)}"
17
+ f.write contents
18
+ end
19
+ end
20
+
21
+ def get(version:)
22
+ File.readlines(path).each do |line|
23
+ version_str, filename = line.split(" ")
24
+ return [version_str.to_i, filename] if version_str.to_i == version.to_i
25
+ end
26
+ []
27
+ end
28
+
29
+ def last
30
+ version_str, filename = File.open(path, &:readline).split(" ")
31
+ [version_str.to_i, filename]
32
+ end
33
+
34
+ def filename
35
+ "snapshot_list".freeze
36
+ end
37
+
38
+ def path
39
+ config.store.local.join(filename).to_s.freeze
40
+ end
41
+
42
+ private
43
+
44
+ def s3
45
+ S3.new(directory: config.s3.paths.snapshots)
46
+ end
47
+
48
+ def config
49
+ ActiveRecord::Snapshot.config
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,63 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class Snapshot
4
+ def initialize(filename = nil)
5
+ @filename = clean(filename) || dump_file
6
+ directory = named? ? paths.named_snapshots : paths.snapshots
7
+ @s3 = S3.new(directory: directory)
8
+ end
9
+
10
+ def named?
11
+ /\Asnapshot_\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}(\..+)?\z/ !~ File.basename(@filename)
12
+ end
13
+
14
+ def dump
15
+ @filename
16
+ end
17
+
18
+ def compressed
19
+ @filename + ".bz2"
20
+ end
21
+
22
+ def encrypted
23
+ compressed + ".enc"
24
+ end
25
+
26
+ def upload
27
+ s3.upload(encrypted)
28
+ end
29
+
30
+ def download
31
+ s3.download_to(encrypted)
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :s3
37
+
38
+ def paths
39
+ ActiveRecord::Snapshot.config.s3.paths
40
+ end
41
+
42
+ def clean(filename)
43
+ return unless filename
44
+ basename = File.basename(
45
+ filename.sub(/(\.sql)?(\.bz2)?(\.enc)?$/, ".sql")
46
+ )
47
+ local_path.join(basename).to_s
48
+ end
49
+
50
+ def dump_file
51
+ local_path.join("snapshot_#{timestamp}.sql").to_s
52
+ end
53
+
54
+ def local_path
55
+ ActiveRecord::Snapshot.config.store.local
56
+ end
57
+
58
+ def timestamp
59
+ Time.zone.now.strftime("%Y-%m-%d_%H-%M-%S")
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,43 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class Version
4
+ class << self
5
+ def current
6
+ return nil unless File.file?(path)
7
+ ::File.read(path).to_i
8
+ end
9
+
10
+ def next
11
+ current + 1
12
+ end
13
+
14
+ def increment
15
+ File.write(path, self.next)
16
+ end
17
+
18
+ def write(version)
19
+ return false unless version.to_i.to_s == version.to_s
20
+ File.write(path, version)
21
+ end
22
+
23
+ def upload
24
+ S3.new(directory: config.s3.paths.snapshots).upload(path)
25
+ end
26
+
27
+ def filename
28
+ "snapshot_version".freeze
29
+ end
30
+
31
+ def path
32
+ config.store.local.join(filename).to_s.freeze
33
+ end
34
+
35
+ private
36
+
37
+ def config
38
+ ActiveRecord::Snapshot.config
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,9 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class Railtie < ::Rails::Railtie
4
+ rake_tasks do
5
+ load "tasks/active_record/snapshot_tasks.rake"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ store:
2
+ tmp: <%= Rails.root.join("tmp/snapshots").to_s %>
3
+ local: <%= Rails.root.join("db/snapshots").to_s %>
4
+
5
+ s3:
6
+ access_key_id: 'key'
7
+ secret_access_key: 'key'
8
+ bucket: 'bucket'
9
+ region: 'region'
10
+ paths:
11
+ named_snapshots: 'custom_snapshots'
12
+ snapshots: 'snapshots'
13
+
14
+ ssl_key: "/dir/to/snapshots-secret.key"
15
+
16
+ tables:
17
+ - "example_table"
@@ -0,0 +1,32 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ class Logger
4
+ def self.call(*args, &block)
5
+ new(*args).call(&block)
6
+ end
7
+
8
+ def initialize(step)
9
+ @step = step
10
+ end
11
+
12
+ def call
13
+ start
14
+ yield.tap do |success|
15
+ success ? finish : failed
16
+ end
17
+ end
18
+
19
+ def start
20
+ puts "== Running: #{@step}"
21
+ end
22
+
23
+ def finish
24
+ puts "== Done"
25
+ end
26
+
27
+ def failed
28
+ $stderr.puts "== Failed: #{@step}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "logger"
2
+
3
+ module ActiveRecord
4
+ module Snapshot
5
+ class Stepper
6
+ def self.call(context, **steps)
7
+ steps.each do |step, message|
8
+ Logger.call(message, &context.method(step)) || abort
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveRecord
2
+ module Snapshot
3
+ VERSION = '0.2.1'
4
+ end
5
+ end
@@ -0,0 +1 @@
1
+ require "activerecord-snapshot"
@@ -0,0 +1 @@
1
+ require "active_record/snapshot.rb"
@@ -0,0 +1 @@
1
+ require "activerecord-snapshot"
@@ -0,0 +1,69 @@
1
+ namespace :db do
2
+ namespace :snapshot do
3
+ desc "Create a snapshot of the current database and store it in S3"
4
+ task create: :load do
5
+ abort "Meant for production only!" unless Rails.env.production?
6
+ ActiveRecord::Snapshot::Create.call
7
+ end
8
+
9
+ desc "Take a snapshot of the current database and store it in S3"
10
+ task create_named: :load do
11
+ abort "Do not run in production!" if Rails.env.production?
12
+ puts <<~TEXT
13
+ Please enter a unique name for this snapshot. You will need to remember this to access it later:
14
+ TEXT
15
+
16
+ snapshot_name = STDIN.gets.strip
17
+
18
+ abort "Please don't use spaces in your snapshot name." if snapshot_name =~ /\s/
19
+ abort "Please ensure your name is a string, integers are used for daily snapshots" if snapshot_name.to_i.to_s == snapshot_name
20
+
21
+ ActiveRecord::Snapshot::Create.call(name: snapshot_name)
22
+ end
23
+
24
+ desc "Import production database snapshot."
25
+ task :import, [:version] => [:load] do |_t, args|
26
+ abort "Do not run in prodution mode!" if Rails.env.production?
27
+ version = args.fetch(:version, "").strip
28
+ ActiveRecord::Snapshot::Import.call(version: version)
29
+ end
30
+
31
+ namespace :import do
32
+ desc "Import only specific tables from the most recent snapshot"
33
+ task :only, [:tables] => :load do |_t, args|
34
+ abort "Do not run in production mode!" if Rails.env.production?
35
+
36
+ if args[:tables].blank?
37
+ abort "Usage: bundle exec rake db:snapshot:import:only['table1 table2']"
38
+ end
39
+
40
+ tables = args[:tables].split(/[, ;'"]+/).reject(&:blank?)
41
+ ActiveRecord::Snapshot::Import.call(tables: tables)
42
+ end
43
+ end
44
+
45
+ desc "Reload current snapshot version"
46
+ task reload: :load do
47
+ version = ActiveRecord::Snapshot::Version.current
48
+ abort "No current version found" unless version
49
+ Rake::Task["db:snapshot:import"].invoke(version.to_s)
50
+ end
51
+
52
+ desc "Show available snapshot versions"
53
+ task :list, [:count] => [:load] do |_t, args|
54
+ version = ActiveRecord::Snapshot::Version.current
55
+ puts "Current snapshot version is #{version}" if version
56
+
57
+ path = ActiveRecord::Snapshot::List.path
58
+ File.file?(path) || ActiveRecord::Snapshot::List.download
59
+ lines = File.readlines(path)
60
+ count = args.fetch(:count, 11).to_i - 1
61
+
62
+ puts lines[0..count]
63
+ end
64
+
65
+ task load: :environment do
66
+ FileUtils.mkdir_p(ActiveRecord::Snapshot.config.store.values)
67
+ end
68
+ end
69
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-snapshot
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Bernardo Farah
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-06-20 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: railties
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 4.1.0
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '6.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 4.1.0
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '6.0'
33
+ - !ruby/object:Gem::Dependency
34
+ name: fog-aws
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '0.0'
47
+ - !ruby/object:Gem::Dependency
48
+ name: hashie
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 3.4.3
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 3.4.3
61
+ - !ruby/object:Gem::Dependency
62
+ name: mocha
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '='
66
+ - !ruby/object:Gem::Version
67
+ version: '1.1'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '='
73
+ - !ruby/object:Gem::Version
74
+ version: '1.1'
75
+ - !ruby/object:Gem::Dependency
76
+ name: pry
77
+ requirement: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - "~>"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.10.3
82
+ type: :development
83
+ prerelease: false
84
+ version_requirements: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - "~>"
87
+ - !ruby/object:Gem::Version
88
+ version: 0.10.3
89
+ - !ruby/object:Gem::Dependency
90
+ name: simplecov
91
+ requirement: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: '0'
96
+ type: :development
97
+ prerelease: false
98
+ version_requirements: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ description: Snapshots for ActiveRecord
104
+ email:
105
+ - ber@bernardo.me
106
+ executables: []
107
+ extensions: []
108
+ extra_rdoc_files: []
109
+ files:
110
+ - MIT-LICENSE
111
+ - README.md
112
+ - Rakefile
113
+ - lib/active_record/snapshot.rb
114
+ - lib/active_record/snapshot/actions/create.rb
115
+ - lib/active_record/snapshot/actions/import.rb
116
+ - lib/active_record/snapshot/commands/all.rb
117
+ - lib/active_record/snapshot/commands/bzip2.rb
118
+ - lib/active_record/snapshot/commands/filter_tables.rb
119
+ - lib/active_record/snapshot/commands/mysql.rb
120
+ - lib/active_record/snapshot/commands/openssl.rb
121
+ - lib/active_record/snapshot/commands/s3.rb
122
+ - lib/active_record/snapshot/commands/select_snapshot.rb
123
+ - lib/active_record/snapshot/configuration.rb
124
+ - lib/active_record/snapshot/files/all.rb
125
+ - lib/active_record/snapshot/files/list.rb
126
+ - lib/active_record/snapshot/files/snapshot.rb
127
+ - lib/active_record/snapshot/files/version.rb
128
+ - lib/active_record/snapshot/railtie.rb
129
+ - lib/active_record/snapshot/templates/snapshot.yml.tt
130
+ - lib/active_record/snapshot/utils/logger.rb
131
+ - lib/active_record/snapshot/utils/stepper.rb
132
+ - lib/active_record/snapshot/version.rb
133
+ - lib/active_record_snapshot.rb
134
+ - lib/activerecord-snapshot.rb
135
+ - lib/activerecord_snapshot.rb
136
+ - lib/tasks/active_record/snapshot_tasks.rake
137
+ homepage: https://github.com/coverhound/active-record-snapshot
138
+ licenses:
139
+ - MIT
140
+ metadata: {}
141
+ post_install_message:
142
+ rdoc_options: []
143
+ require_paths:
144
+ - lib
145
+ required_ruby_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ required_rubygems_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ requirements: []
156
+ rubyforge_project:
157
+ rubygems_version: 2.5.1
158
+ signing_key:
159
+ specification_version: 4
160
+ summary: Snapshots for ActiveRecord
161
+ test_files: []