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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +118 -0
- data/Rakefile +33 -0
- data/lib/active_record/snapshot.rb +12 -0
- data/lib/active_record/snapshot/actions/create.rb +70 -0
- data/lib/active_record/snapshot/actions/import.rb +92 -0
- data/lib/active_record/snapshot/commands/all.rb +6 -0
- data/lib/active_record/snapshot/commands/bzip2.rb +15 -0
- data/lib/active_record/snapshot/commands/filter_tables.rb +41 -0
- data/lib/active_record/snapshot/commands/mysql.rb +46 -0
- data/lib/active_record/snapshot/commands/openssl.rb +23 -0
- data/lib/active_record/snapshot/commands/s3.rb +45 -0
- data/lib/active_record/snapshot/commands/select_snapshot.rb +14 -0
- data/lib/active_record/snapshot/configuration.rb +74 -0
- data/lib/active_record/snapshot/files/all.rb +3 -0
- data/lib/active_record/snapshot/files/list.rb +54 -0
- data/lib/active_record/snapshot/files/snapshot.rb +63 -0
- data/lib/active_record/snapshot/files/version.rb +43 -0
- data/lib/active_record/snapshot/railtie.rb +9 -0
- data/lib/active_record/snapshot/templates/snapshot.yml.tt +17 -0
- data/lib/active_record/snapshot/utils/logger.rb +32 -0
- data/lib/active_record/snapshot/utils/stepper.rb +13 -0
- data/lib/active_record/snapshot/version.rb +5 -0
- data/lib/active_record_snapshot.rb +1 -0
- data/lib/activerecord-snapshot.rb +1 -0
- data/lib/activerecord_snapshot.rb +1 -0
- data/lib/tasks/active_record/snapshot_tasks.rake +69 -0
- metadata +161 -0
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,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,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,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,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 @@
|
|
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: []
|