activerecord-snapshot 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []