data_keeper 0.1.0
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/.gitignore +10 -0
- data/.travis.yml +6 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +111 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/data_keeper.gemspec +31 -0
- data/lib/data_keeper.rb +61 -0
- data/lib/data_keeper/database_config.rb +31 -0
- data/lib/data_keeper/database_helper.rb +24 -0
- data/lib/data_keeper/definition.rb +62 -0
- data/lib/data_keeper/dumper.rb +143 -0
- data/lib/data_keeper/error.rb +3 -0
- data/lib/data_keeper/loader.rb +161 -0
- data/lib/data_keeper/local_storage.rb +46 -0
- data/lib/data_keeper/railtie.rb +10 -0
- data/lib/data_keeper/tasks/data_keeper.rake +38 -0
- data/lib/data_keeper/version.rb +3 -0
- data/todo.md +14 -0
- metadata +121 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 9c4e8095729b6927bc11ae296fc30571577d1dfd5f5d910b1092f924981efa69
|
4
|
+
data.tar.gz: bb646294c8847fd637b765fd6a2f16af605dabe656b663d2cb0e6f54ff396f79
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cd3696d94b9ef70bf108e8c77ee880e1b97e68989955c6c7975b4f9cea9196b95417a17e6b5bf6e3a9baf2047cd0a1db524e2bc8dd660d2576f10385f38a038d
|
7
|
+
data.tar.gz: 1ecab9088e179625113bb3a490a83c60a16405924f527caa059ed1dc05c68527049f12cc985aa1280f6e094597903f5bf280420a21c98a1670eb527b4e766c52
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2020 Roger Campos
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,111 @@
|
|
1
|
+
# DataKeeper
|
2
|
+
|
3
|
+
In a rails app using postgresql, DataKeeper is a tool to create dumps of your database in production to be used later on for local development.
|
4
|
+
|
5
|
+
It automates the process of creating and storing them on the server, and applying them locally afterwards.
|
6
|
+
|
7
|
+
It supports full dumps, as well as partial dumps per specific tables or even specific rows (you provide a sql select).
|
8
|
+
On partial dumps, note you'll need to manage possible issues around foreign keys and maybe other constraints.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add this line to your application's Gemfile:
|
13
|
+
|
14
|
+
```ruby
|
15
|
+
gem 'data_keeper'
|
16
|
+
```
|
17
|
+
|
18
|
+
And then execute:
|
19
|
+
|
20
|
+
$ bundle install
|
21
|
+
|
22
|
+
Or install it yourself as:
|
23
|
+
|
24
|
+
$ gem install data_keeper
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
Configure the storage to use to save the generated dumps.
|
29
|
+
|
30
|
+
You can use a local storage, a simple option which stores the dumps in the same server running the code,
|
31
|
+
in a path of your choosing (consider that it must be writable by the user running this code in production).
|
32
|
+
You also configure how to reach that server from your local machine (currently only scp is supported), in
|
33
|
+
order to download these dumps later. Ex:
|
34
|
+
|
35
|
+
```ruby
|
36
|
+
DataKeeper.storage = DataKeeper::LocalStorage.new(
|
37
|
+
local_store_dir: "/users/fredy/backups/...",
|
38
|
+
remote_access: {
|
39
|
+
type: "scp",
|
40
|
+
host: "141.12.241.22",
|
41
|
+
port: "8622",
|
42
|
+
user: "fredy"
|
43
|
+
}
|
44
|
+
)
|
45
|
+
```
|
46
|
+
|
47
|
+
Other storages, like S3, could be implemented, but currently this gem only ships with local storage.
|
48
|
+
If you want to do your own, you can assign as an storage whatever object that responds to:
|
49
|
+
|
50
|
+
- `#save(file, filename, dump_name)`, where file is a File object and filename a string. This method should save the given
|
51
|
+
dump file.
|
52
|
+
|
53
|
+
- `#retrieve(dump_name) { |file| (...) }`, which should retrieve the latest stored dump with the given dump_name.
|
54
|
+
It should yield the given block passing the File object pointing to the retrieved dump file in the local filesystem,
|
55
|
+
which is expected to be cleaned up on block termination.
|
56
|
+
|
57
|
+
|
58
|
+
Then, declare some dumps to work with:
|
59
|
+
|
60
|
+
```ruby
|
61
|
+
# Dump the whole database
|
62
|
+
DataKeeper.define_dump(:whole_database, :full)
|
63
|
+
|
64
|
+
# Dump only selected tables, and a custom SQL
|
65
|
+
DataKeeper.define_dump(:config) do |d|
|
66
|
+
# Specific tables, all rows
|
67
|
+
d.table "products"
|
68
|
+
d.table "traits"
|
69
|
+
|
70
|
+
# Only some rows in the "vouchers" table. MAKE SURE your sql returns only columns from the target table!
|
71
|
+
d.sql(:vouchers, :used_vouchers) { Voucher.joins(cart: :order).where(orders: {status: "sent"}).to_sql }
|
72
|
+
|
73
|
+
# Possible additional code to run after applying the dump locally
|
74
|
+
d.on_after_load do
|
75
|
+
User.create! email: "test@gmail.com", password: "password"
|
76
|
+
end
|
77
|
+
end
|
78
|
+
```
|
79
|
+
|
80
|
+
Now, in production, you'll have run `DataKeeper.create_dump!("config")`, passing in the same of the dump
|
81
|
+
you defined before. Running this will create the dump file, from the server you run this code from,
|
82
|
+
and store it in the configured storage.
|
83
|
+
|
84
|
+
If you want to have always an up-to-date dump, you'll need to call this periodically, for example once per day.
|
85
|
+
|
86
|
+
Finally, to apply the dump locally, you can use the rake task:
|
87
|
+
|
88
|
+
`bin/rake data_keeper:pull[config]`
|
89
|
+
|
90
|
+
This will download the latest version available of the "config" dump, and apply it locally, destroying anything
|
91
|
+
in your current database. It will give you an error if you try to run this in a production environment.
|
92
|
+
|
93
|
+
Note when using raw sql, your statement is expected to return all columns for the configured table, in the default
|
94
|
+
order (`select *`). This uses pg's COPY from/to for the full table internally.
|
95
|
+
|
96
|
+
## Development
|
97
|
+
|
98
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
99
|
+
|
100
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
101
|
+
|
102
|
+
## Contributing
|
103
|
+
|
104
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/data_keeper.
|
105
|
+
|
106
|
+
|
107
|
+
## License
|
108
|
+
|
109
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
110
|
+
|
111
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "data_keeper"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/data_keeper.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require_relative 'lib/data_keeper/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "data_keeper"
|
5
|
+
spec.version = DataKeeper::VERSION
|
6
|
+
spec.authors = ["Roger Campos"]
|
7
|
+
spec.email = ["roger@rogercampos.com"]
|
8
|
+
|
9
|
+
spec.summary = %q{Easy management of database dumps for dev env}
|
10
|
+
spec.description = %q{Easy management of database dumps for dev env}
|
11
|
+
spec.homepage = "https://github.com/rogercampos/data_keeper"
|
12
|
+
spec.license = "MIT"
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
14
|
+
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
16
|
+
|
17
|
+
# Specify which files should be added to the gem when it is released.
|
18
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
19
|
+
spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
|
20
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
21
|
+
end
|
22
|
+
spec.bindir = "exe"
|
23
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
|
+
spec.require_paths = ["lib"]
|
25
|
+
|
26
|
+
spec.add_dependency "activerecord", ">= 5.2.0"
|
27
|
+
spec.add_dependency "terrapin", ">= 0.5.0"
|
28
|
+
spec.add_dependency "sshkit", ">= 1.20.0"
|
29
|
+
spec.add_dependency "rails", ">= 5.0.0"
|
30
|
+
|
31
|
+
end
|
data/lib/data_keeper.rb
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
require "data_keeper/version"
|
2
|
+
require "terrapin"
|
3
|
+
require "zlib"
|
4
|
+
require "rubygems/package"
|
5
|
+
|
6
|
+
require 'data_keeper/error'
|
7
|
+
require 'data_keeper/definition'
|
8
|
+
require 'data_keeper/dumper'
|
9
|
+
require 'data_keeper/loader'
|
10
|
+
require 'data_keeper/local_storage'
|
11
|
+
require 'data_keeper/database_helper'
|
12
|
+
require 'data_keeper/railtie' if defined?(Rails) && defined?(Rails::Railtie)
|
13
|
+
|
14
|
+
module DataKeeper
|
15
|
+
DumpDoesNotExist = Class.new(Error)
|
16
|
+
NoStorageDefined = Class.new(Error)
|
17
|
+
|
18
|
+
@dumps = {}
|
19
|
+
@storage = nil
|
20
|
+
|
21
|
+
def self.define_dump(name, type = :partial, &block)
|
22
|
+
@dumps[name.to_sym] = DefinitionBuilder.build(type, block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.create_dump!(name)
|
26
|
+
raise DumpDoesNotExist unless dump?(name)
|
27
|
+
raise NoStorageDefined if @storage.nil?
|
28
|
+
|
29
|
+
Dumper.new(name, @dumps[name.to_sym]).run! do |file, filename|
|
30
|
+
@storage.save(file, filename, name)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.fetch_and_load_dump!(name)
|
35
|
+
raise DumpDoesNotExist unless dump?(name)
|
36
|
+
raise NoStorageDefined if @storage.nil?
|
37
|
+
|
38
|
+
@storage.retrieve(name) do |file|
|
39
|
+
Loader.new(@dumps[name.to_sym], file).load!
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.load_dump!(name, path)
|
44
|
+
raise DumpDoesNotExist unless File.file?(path)
|
45
|
+
raise NoStorageDefined if @storage.nil?
|
46
|
+
|
47
|
+
Loader.new(@dumps[name.to_sym], File.open(path)).load!
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.dump?(name)
|
51
|
+
@dumps.key?(name.to_sym)
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.storage=(value)
|
55
|
+
@storage = value
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.clear_dumps!
|
59
|
+
@dumps = {}
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module DataKeeper
|
2
|
+
module DatabaseConfig
|
3
|
+
def database_connection_config
|
4
|
+
Rails.configuration.database_configuration[Rails.env]
|
5
|
+
end
|
6
|
+
|
7
|
+
def psql_env
|
8
|
+
env = { 'PGUSER' => database_connection_config['username'] }
|
9
|
+
env['PGPASSWORD'] = database_connection_config['password'] if database_connection_config['password']
|
10
|
+
env
|
11
|
+
end
|
12
|
+
|
13
|
+
def host
|
14
|
+
database_connection_config['host'] || '127.0.0.1'
|
15
|
+
end
|
16
|
+
|
17
|
+
def database
|
18
|
+
database_connection_config['database']
|
19
|
+
end
|
20
|
+
|
21
|
+
def port
|
22
|
+
database_connection_config['port']
|
23
|
+
end
|
24
|
+
|
25
|
+
def connection_args
|
26
|
+
connection_opts = '--host=:host'
|
27
|
+
connection_opts += ' --port=:port' if database_connection_config['port']
|
28
|
+
connection_opts
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module DataKeeper
|
2
|
+
class DatabaseHelper
|
3
|
+
include DatabaseConfig
|
4
|
+
|
5
|
+
def kill
|
6
|
+
cmd = Terrapin::CommandLine.new(
|
7
|
+
'psql',
|
8
|
+
"-c :command #{connection_args} --dbname #{database} &> /dev/null || true",
|
9
|
+
environment: psql_env
|
10
|
+
)
|
11
|
+
|
12
|
+
cmd.run(
|
13
|
+
database: database,
|
14
|
+
host: host,
|
15
|
+
port: port,
|
16
|
+
command: "SELECT pid, pg_terminate_backend(pid) as terminated FROM pg_stat_activity WHERE pid <> pg_backend_pid();"
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.kill
|
21
|
+
new.kill
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module DataKeeper
|
2
|
+
InvalidDumpType = Class.new(Error)
|
3
|
+
InvalidDumpDefinition = Class.new(Error)
|
4
|
+
|
5
|
+
class Definition
|
6
|
+
attr_reader :type, :on_after_load_block
|
7
|
+
|
8
|
+
def initialize(type, tables, sqls, on_after_load_block)
|
9
|
+
@type = type
|
10
|
+
@tables = tables
|
11
|
+
@sqls = sqls
|
12
|
+
@on_after_load_block = on_after_load_block
|
13
|
+
end
|
14
|
+
|
15
|
+
def full_tables
|
16
|
+
@tables
|
17
|
+
end
|
18
|
+
|
19
|
+
def sqls
|
20
|
+
@sqls
|
21
|
+
end
|
22
|
+
|
23
|
+
def full_tables_to_export
|
24
|
+
full_tables + ['schema_migrations']
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class DefinitionBuilder
|
29
|
+
attr_reader :tables, :raw_sqls, :on_after_load_block
|
30
|
+
|
31
|
+
def initialize(definition_block)
|
32
|
+
@tables = []
|
33
|
+
@raw_sqls = {}
|
34
|
+
instance_eval(&definition_block) if definition_block
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.build(type, block)
|
38
|
+
@type = type
|
39
|
+
raise InvalidDumpType, "Invalid type! use :partial or :full" unless [:partial, :full].include?(type)
|
40
|
+
|
41
|
+
builder = new(block)
|
42
|
+
|
43
|
+
Definition.new(type, builder.tables, builder.raw_sqls, builder.on_after_load_block)
|
44
|
+
end
|
45
|
+
|
46
|
+
def table(name)
|
47
|
+
raise InvalidDumpDefinition if @type == :full
|
48
|
+
raise(InvalidDumpDefinition, "table already defined") if @tables.include?(name)
|
49
|
+
@tables << name
|
50
|
+
end
|
51
|
+
|
52
|
+
def sql(table, name, &block)
|
53
|
+
raise InvalidDumpDefinition if @type == :full
|
54
|
+
raise(InvalidDumpDefinition, "sql already defined") if @raw_sqls.key?(name)
|
55
|
+
@raw_sqls[name] = [table, block]
|
56
|
+
end
|
57
|
+
|
58
|
+
def on_after_load(&block)
|
59
|
+
@on_after_load_block = block
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,143 @@
|
|
1
|
+
require 'data_keeper/database_config'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module DataKeeper
|
5
|
+
class Dumper
|
6
|
+
include DatabaseConfig
|
7
|
+
|
8
|
+
def initialize(name, definition)
|
9
|
+
@dump_name = name
|
10
|
+
@definition = definition
|
11
|
+
end
|
12
|
+
|
13
|
+
def run!(&block)
|
14
|
+
if @definition.type == :full
|
15
|
+
dump_full_database(&block)
|
16
|
+
else
|
17
|
+
dump_partial_database(&block)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def dump_full_database
|
24
|
+
Tempfile.create do |file|
|
25
|
+
cmd = Terrapin::CommandLine.new(
|
26
|
+
'pg_dump',
|
27
|
+
"#{connection_args} -x -Fc :database > :output_path",
|
28
|
+
environment: psql_env
|
29
|
+
)
|
30
|
+
|
31
|
+
cmd.run(
|
32
|
+
database: database,
|
33
|
+
host: host,
|
34
|
+
port: port,
|
35
|
+
output_path: file.path
|
36
|
+
)
|
37
|
+
|
38
|
+
yield file, "#{filename}.dump"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def dump_partial_database
|
43
|
+
Tempfile.create do |file|
|
44
|
+
file.binmode
|
45
|
+
|
46
|
+
Zlib::GzipWriter.wrap(file) do |gzip|
|
47
|
+
Gem::Package::TarWriter.new(gzip) do |tar|
|
48
|
+
dump_schema(tar)
|
49
|
+
dump_partial_tables(tar)
|
50
|
+
dump_sqls(tar)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
yield file, "#{filename}.tar.gz"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def dump_sqls(tar)
|
59
|
+
@definition.sqls.each do |name, (_table, sql)|
|
60
|
+
Tempfile.create do |table_file|
|
61
|
+
cmd = Terrapin::CommandLine.new(
|
62
|
+
'psql',
|
63
|
+
"#{connection_args} -d :database -c :command > #{table_file.path}",
|
64
|
+
environment: psql_env
|
65
|
+
)
|
66
|
+
|
67
|
+
cmd.run(
|
68
|
+
database: database,
|
69
|
+
host: host,
|
70
|
+
port: port,
|
71
|
+
command: "COPY (#{sql.call}) to STDOUT DELIMITER ',' CSV HEADER"
|
72
|
+
)
|
73
|
+
|
74
|
+
tar.add_file_simple("#{name}.csv", 0644, File.size(table_file.path)) do |io|
|
75
|
+
table_file.reopen(table_file)
|
76
|
+
|
77
|
+
while !table_file.eof?
|
78
|
+
io.write(table_file.read(2048))
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def dump_partial_tables(tar)
|
86
|
+
Tempfile.create do |tables_dump_file|
|
87
|
+
tables_dump_file.binmode
|
88
|
+
table_args = @definition.full_tables_to_export.map { |table| "-t #{table}" }.join(' ')
|
89
|
+
cmd = Terrapin::CommandLine.new(
|
90
|
+
'pg_dump',
|
91
|
+
"#{connection_args} -x -Fc :database #{table_args} > :output_path",
|
92
|
+
environment: psql_env
|
93
|
+
)
|
94
|
+
|
95
|
+
cmd.run(
|
96
|
+
database: database,
|
97
|
+
host: host,
|
98
|
+
port: port,
|
99
|
+
output_path: tables_dump_file.path
|
100
|
+
)
|
101
|
+
|
102
|
+
tar.add_file_simple("tables.dump", 0644, File.size(tables_dump_file.path)) do |io|
|
103
|
+
tables_dump_file.reopen(tables_dump_file)
|
104
|
+
|
105
|
+
while !tables_dump_file.eof?
|
106
|
+
io.write(tables_dump_file.read(2048))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def dump_schema(tar)
|
113
|
+
Tempfile.create do |schema_dump_file|
|
114
|
+
schema_dump_file.binmode
|
115
|
+
|
116
|
+
cmd = Terrapin::CommandLine.new(
|
117
|
+
'pg_dump',
|
118
|
+
"#{connection_args} -x --schema-only -Fc :database > :output_path",
|
119
|
+
environment: psql_env
|
120
|
+
)
|
121
|
+
|
122
|
+
cmd.run(
|
123
|
+
database: database,
|
124
|
+
host: host,
|
125
|
+
port: port,
|
126
|
+
output_path: schema_dump_file.path
|
127
|
+
)
|
128
|
+
|
129
|
+
tar.add_file_simple("schema.dump", 0644, File.size(schema_dump_file.path)) do |io|
|
130
|
+
schema_dump_file.reopen(schema_dump_file)
|
131
|
+
|
132
|
+
while !schema_dump_file.eof?
|
133
|
+
io.write(schema_dump_file.read(2048))
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def filename
|
140
|
+
"#{@dump_name}-#{Time.now.strftime("%Y%m%d-%H%M")}"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
require 'data_keeper/database_config'
|
2
|
+
|
3
|
+
module DataKeeper
|
4
|
+
class Loader
|
5
|
+
include DatabaseConfig
|
6
|
+
|
7
|
+
def initialize(dump, file)
|
8
|
+
@dump = dump
|
9
|
+
@file = file
|
10
|
+
end
|
11
|
+
|
12
|
+
def load!
|
13
|
+
if @dump.type == :full
|
14
|
+
load_full_database!
|
15
|
+
else
|
16
|
+
load_partial_database!
|
17
|
+
end
|
18
|
+
|
19
|
+
if @dump.on_after_load_block
|
20
|
+
@dump.on_after_load_block.call
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def load_full_database!
|
27
|
+
pg_restore = Terrapin::CommandLine.new(
|
28
|
+
'pg_restore',
|
29
|
+
"#{connection_args} -j 4 --no-owner --dbname #{database} #{@file.path} 2>/dev/null",
|
30
|
+
environment: psql_env
|
31
|
+
)
|
32
|
+
|
33
|
+
pg_restore.run(
|
34
|
+
database: database,
|
35
|
+
host: host,
|
36
|
+
port: port
|
37
|
+
)
|
38
|
+
|
39
|
+
cmd = Terrapin::CommandLine.new(
|
40
|
+
'psql',
|
41
|
+
"#{connection_args} -d :database -c :sql",
|
42
|
+
environment: psql_env
|
43
|
+
)
|
44
|
+
|
45
|
+
cmd.run(
|
46
|
+
database: database,
|
47
|
+
host: host,
|
48
|
+
port: port,
|
49
|
+
sql: "UPDATE ar_internal_metadata SET value = 'development'"
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
def load_partial_database!
|
54
|
+
inflate(@file.path) do |schema_path, tables_path, sql_files|
|
55
|
+
pg_restore = Terrapin::CommandLine.new(
|
56
|
+
'pg_restore',
|
57
|
+
"#{connection_args} -j 4 --no-owner --dbname #{database} #{schema_path} 2>/dev/null",
|
58
|
+
environment: psql_env
|
59
|
+
)
|
60
|
+
|
61
|
+
pg_restore.run(
|
62
|
+
database: database,
|
63
|
+
host: host,
|
64
|
+
port: port
|
65
|
+
)
|
66
|
+
|
67
|
+
pg_restore = Terrapin::CommandLine.new(
|
68
|
+
'pg_restore',
|
69
|
+
"#{connection_args} -c -j 4 --no-owner --dbname #{database} #{tables_path} 2>/dev/null",
|
70
|
+
environment: psql_env
|
71
|
+
)
|
72
|
+
|
73
|
+
pg_restore.run(
|
74
|
+
database: database,
|
75
|
+
host: host,
|
76
|
+
port: port
|
77
|
+
)
|
78
|
+
|
79
|
+
sql_files.each do |table, csv_path|
|
80
|
+
cmd = Terrapin::CommandLine.new(
|
81
|
+
'psql',
|
82
|
+
"#{connection_args} -d :database -c :command < :csv_path",
|
83
|
+
environment: psql_env
|
84
|
+
)
|
85
|
+
|
86
|
+
cmd.run(
|
87
|
+
database: database,
|
88
|
+
host: host,
|
89
|
+
port: port,
|
90
|
+
csv_path: csv_path,
|
91
|
+
command: "COPY #{table} FROM stdin DELIMITER ',' CSV HEADER"
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
Rake::Task['db:environment:set'].invoke
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
class InflatedFiles
|
100
|
+
attr_reader :errors
|
101
|
+
|
102
|
+
def initialize(dump, paths)
|
103
|
+
@dump = dump
|
104
|
+
@paths = paths
|
105
|
+
@errors = []
|
106
|
+
end
|
107
|
+
|
108
|
+
def valid?
|
109
|
+
@errors = []
|
110
|
+
|
111
|
+
validate("Schema file is missing") { !!schema_path } &&
|
112
|
+
validate("Tables file is missing") { !!tables_path } &&
|
113
|
+
validate("Not all sql custom dumps are present") do
|
114
|
+
sql_dumps.size == @dump.sqls.keys.size
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def schema_path
|
119
|
+
@schema_path ||= @paths.find { |x| File.basename(x) == "schema.dump" }
|
120
|
+
end
|
121
|
+
|
122
|
+
def tables_path
|
123
|
+
@tables_path ||= @paths.find { |x| File.basename(x) == "tables.dump" }
|
124
|
+
end
|
125
|
+
|
126
|
+
def sql_dumps
|
127
|
+
@sql_dumps ||= @dump.sqls.map do |name, (table, _proc)|
|
128
|
+
path = @paths.find { |x| File.basename(x) == "#{name}.csv" }
|
129
|
+
next unless path
|
130
|
+
|
131
|
+
[table, path]
|
132
|
+
end.compact
|
133
|
+
end
|
134
|
+
|
135
|
+
private
|
136
|
+
|
137
|
+
def validate(error_message)
|
138
|
+
result = yield
|
139
|
+
@errors << error_message unless result
|
140
|
+
result
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def inflate(path)
|
145
|
+
Dir.mktmpdir do |dir|
|
146
|
+
File.open(path, "rb") do |f|
|
147
|
+
Gem::Package.new("").extract_tar_gz(f, dir)
|
148
|
+
|
149
|
+
inflated_files = InflatedFiles.new(@dump, Dir.glob(File.join(dir, "*")))
|
150
|
+
raise inflated_files.errors.join(", ") unless inflated_files.valid?
|
151
|
+
|
152
|
+
yield(
|
153
|
+
inflated_files.schema_path,
|
154
|
+
inflated_files.tables_path,
|
155
|
+
inflated_files.sql_dumps
|
156
|
+
)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'sshkit'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
module DataKeeper
|
6
|
+
class LocalStorage
|
7
|
+
include SSHKit::DSL
|
8
|
+
|
9
|
+
def initialize(data)
|
10
|
+
@local_store_dir = data[:local_store_dir]
|
11
|
+
@remote_access = data[:remote_access]
|
12
|
+
end
|
13
|
+
|
14
|
+
def save(file, filename, dump_name)
|
15
|
+
path = dump_path(dump_name, filename)
|
16
|
+
FileUtils.mkdir_p(File.dirname(path))
|
17
|
+
|
18
|
+
FileUtils.cp(file.path, path)
|
19
|
+
end
|
20
|
+
|
21
|
+
def retrieve(dump_name)
|
22
|
+
tempfile = Tempfile.new
|
23
|
+
local_store_dir = @local_store_dir
|
24
|
+
|
25
|
+
on complete_host do
|
26
|
+
last_dump = capture :ls, "-1t #{File.join(local_store_dir, dump_name.to_s)} | head -n 1"
|
27
|
+
|
28
|
+
download! File.join(local_store_dir, dump_name.to_s, last_dump), tempfile.path
|
29
|
+
end
|
30
|
+
|
31
|
+
yield(tempfile)
|
32
|
+
ensure
|
33
|
+
tempfile.delete
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def complete_host
|
39
|
+
"#{@remote_access[:user]}@#{@remote_access[:host]}:#{@remote_access[:port]}"
|
40
|
+
end
|
41
|
+
|
42
|
+
def dump_path(dump_name, filename)
|
43
|
+
File.join(@local_store_dir, dump_name.to_s, filename)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
namespace :data_keeper do
|
2
|
+
task :kill do
|
3
|
+
raise "Cannot be run in production!" if Rails.env.production?
|
4
|
+
|
5
|
+
DataKeeper::DatabaseHelper.kill
|
6
|
+
end
|
7
|
+
|
8
|
+
desc "Fetches and loads the given dump in your local database. WARN: Will remove all your current data."
|
9
|
+
task :pull, [:name] => [:kill, "db:drop", "db:create"] do |_t, args|
|
10
|
+
raise "NOT IN PRODUCTION" if Rails.env.production?
|
11
|
+
|
12
|
+
name = args[:name]
|
13
|
+
|
14
|
+
if name.blank? || !DataKeeper.dump?(name)
|
15
|
+
raise "Please use this rake task giving a name of a configured dump. Ex: bin/rake data_keeper:pull[full]"
|
16
|
+
end
|
17
|
+
|
18
|
+
DataKeeper.fetch_and_load_dump!(name)
|
19
|
+
end
|
20
|
+
|
21
|
+
desc "Loads the given dump (found on the given local path) and applies it to your local database. WARN: Will remove all your current data."
|
22
|
+
task :load, [:name, :path] => [:kill, "db:drop", "db:create"] do |_t, args|
|
23
|
+
raise "NOT IN PRODUCTION" if Rails.env.production?
|
24
|
+
|
25
|
+
name = args[:name]
|
26
|
+
path = args[:path]
|
27
|
+
|
28
|
+
if name.blank? || !DataKeeper.dump?(name)
|
29
|
+
raise "Please use this rake task giving a name of a configured dump."
|
30
|
+
end
|
31
|
+
|
32
|
+
unless File.file?(path)
|
33
|
+
raise "The given file '#{path}' does not exist."
|
34
|
+
end
|
35
|
+
|
36
|
+
DataKeeper.load_dump!(name, path)
|
37
|
+
end
|
38
|
+
end
|
data/todo.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
- decouple from rails / ar
|
2
|
+
|
3
|
+
- implement partial loads. What about the structure? load from schema.rb first (db:schema:load or db:structure:apply),
|
4
|
+
then load db.
|
5
|
+
|
6
|
+
- add anonymizing feature of certain columns
|
7
|
+
|
8
|
+
- skip download if present in cache. Clear cache.
|
9
|
+
|
10
|
+
- add s3 storage
|
11
|
+
|
12
|
+
- add option to apply dump but creating tables with "create table unlogged", can be useful for performance in tests
|
13
|
+
or even locally
|
14
|
+
|
metadata
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: data_keeper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Roger Campos
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-12-09 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activerecord
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.2.0
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.2.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: terrapin
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 0.5.0
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 0.5.0
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sshkit
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: 1.20.0
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 1.20.0
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rails
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 5.0.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 5.0.0
|
69
|
+
description: Easy management of database dumps for dev env
|
70
|
+
email:
|
71
|
+
- roger@rogercampos.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".travis.yml"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- bin/console
|
83
|
+
- bin/setup
|
84
|
+
- data_keeper.gemspec
|
85
|
+
- lib/data_keeper.rb
|
86
|
+
- lib/data_keeper/database_config.rb
|
87
|
+
- lib/data_keeper/database_helper.rb
|
88
|
+
- lib/data_keeper/definition.rb
|
89
|
+
- lib/data_keeper/dumper.rb
|
90
|
+
- lib/data_keeper/error.rb
|
91
|
+
- lib/data_keeper/loader.rb
|
92
|
+
- lib/data_keeper/local_storage.rb
|
93
|
+
- lib/data_keeper/railtie.rb
|
94
|
+
- lib/data_keeper/tasks/data_keeper.rake
|
95
|
+
- lib/data_keeper/version.rb
|
96
|
+
- todo.md
|
97
|
+
homepage: https://github.com/rogercampos/data_keeper
|
98
|
+
licenses:
|
99
|
+
- MIT
|
100
|
+
metadata:
|
101
|
+
homepage_uri: https://github.com/rogercampos/data_keeper
|
102
|
+
post_install_message:
|
103
|
+
rdoc_options: []
|
104
|
+
require_paths:
|
105
|
+
- lib
|
106
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 2.3.0
|
111
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
|
+
requirements:
|
113
|
+
- - ">="
|
114
|
+
- !ruby/object:Gem::Version
|
115
|
+
version: '0'
|
116
|
+
requirements: []
|
117
|
+
rubygems_version: 3.0.3
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: Easy management of database dumps for dev env
|
121
|
+
test_files: []
|