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.
@@ -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
@@ -0,0 +1,10 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ Gemfile.lock
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.6.5
6
+ before_install: gem install bundler -v 2.1.4
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in data_keeper.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 12.0"
7
+ gem "minitest", "~> 5.0"
@@ -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.
@@ -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
+
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -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__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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
@@ -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,3 @@
1
+ module DataKeeper
2
+ class Error < StandardError; end
3
+ 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,10 @@
1
+ module DataKeeper
2
+ class Railtie < Rails::Railtie
3
+ railtie_name :data_keeper
4
+
5
+ rake_tasks do
6
+ path = File.expand_path(__dir__)
7
+ Dir.glob("#{path}/tasks/*.rake").each { |f| load f }
8
+ end
9
+ end
10
+ 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
@@ -0,0 +1,3 @@
1
+ module DataKeeper
2
+ VERSION = "0.1.0"
3
+ 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: []