activerecord-snapshot 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|