data_keeper 0.1.0 → 0.1.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +28 -11
- data/data_keeper.gemspec +1 -2
- data/lib/data_keeper.rb +11 -7
- data/lib/data_keeper/definition.rb +7 -9
- data/lib/data_keeper/dumper.rb +2 -2
- data/lib/data_keeper/loader.rb +17 -3
- data/lib/data_keeper/s3_storage.rb +103 -0
- data/lib/data_keeper/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0442b3f4ba1b60c62570ca379d4a5d6f2a2181ec9cb7561107d52a3492b62ec5
|
4
|
+
data.tar.gz: e7594ccf321cd010c5cf74926ff0dd881d4aee7e6c142b204a58c9f9df137ebd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 12a6f3303abd4528976c51e49361f183b306fc352df0d9bc8cb3c2e44845f61920f8b9bf4e70c438cd42a8a33703dd98693a71c10f0a79e90d5015c8ac2977bb
|
7
|
+
data.tar.gz: a5b9ecb78feecd034287fd86e34bdbd151d028a61d0be6ea58494b35102c3080d2c7c97eb3c5816d470a66a2f52a44c773a64a96ee2e3d3a01562ad5b8fc101c
|
data/README.md
CHANGED
@@ -36,23 +36,40 @@ order to download these dumps later. Ex:
|
|
36
36
|
DataKeeper.storage = DataKeeper::LocalStorage.new(
|
37
37
|
local_store_dir: "/users/fredy/backups/...",
|
38
38
|
remote_access: {
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
user: "fredy"
|
39
|
+
host: "10.10.10.10",
|
40
|
+
port: "22",
|
41
|
+
user: "user"
|
43
42
|
}
|
44
43
|
)
|
45
44
|
```
|
46
45
|
|
47
|
-
|
48
|
-
If you want to do your own, you can assign as an storage whatever object that responds to:
|
46
|
+
There's also support for storing the dumps in s3, using `DataKeeper::S3Storage` like in this example:
|
49
47
|
|
50
|
-
|
51
|
-
|
48
|
+
```ruby
|
49
|
+
# Explicit require is necessary
|
50
|
+
require 'data_keeper/s3_storage'
|
51
|
+
|
52
|
+
DataKeeper.storage = DataKeeper::S3Storage.new(
|
53
|
+
bucket: 'bucket-name',
|
54
|
+
store_dir: 'dumps/',
|
55
|
+
acl: "private",
|
56
|
+
remote_access: {
|
57
|
+
access_key_id: Rails.application.credentials.access_key_id,
|
58
|
+
secret_access_key: Rails.application.credentials.secret_access_key,
|
59
|
+
region: 'eu-central-1'
|
60
|
+
}
|
61
|
+
)
|
62
|
+
```
|
63
|
+
|
64
|
+
|
65
|
+
Other storages can be implemented. An storage can be any object that responds to those two methods:
|
66
|
+
|
67
|
+
- `#save(file, filename, dump_name)`, where file is a File object and filename and dump_name are strings.
|
68
|
+
This method should save the given dump file in the store.
|
52
69
|
|
53
70
|
- `#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
|
55
|
-
which is expected to be cleaned up on block termination.
|
71
|
+
It should yield the given block passing the `File` or `Tempfile` object pointing to the retrieved dump
|
72
|
+
file in the local filesystem, which is expected to be cleaned up on block termination.
|
56
73
|
|
57
74
|
|
58
75
|
Then, declare some dumps to work with:
|
@@ -101,7 +118,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
101
118
|
|
102
119
|
## Contributing
|
103
120
|
|
104
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
121
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rogercampos/data_keeper.
|
105
122
|
|
106
123
|
|
107
124
|
## License
|
data/data_keeper.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.description = %q{Easy management of database dumps for dev env}
|
11
11
|
spec.homepage = "https://github.com/rogercampos/data_keeper"
|
12
12
|
spec.license = "MIT"
|
13
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
13
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.5.0")
|
14
14
|
|
15
15
|
spec.metadata["homepage_uri"] = spec.homepage
|
16
16
|
|
@@ -27,5 +27,4 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_dependency "terrapin", ">= 0.5.0"
|
28
28
|
spec.add_dependency "sshkit", ">= 1.20.0"
|
29
29
|
spec.add_dependency "rails", ">= 5.0.0"
|
30
|
-
|
31
30
|
end
|
data/lib/data_keeper.rb
CHANGED
@@ -15,18 +15,20 @@ module DataKeeper
|
|
15
15
|
DumpDoesNotExist = Class.new(Error)
|
16
16
|
NoStorageDefined = Class.new(Error)
|
17
17
|
|
18
|
-
@
|
18
|
+
@dump_definition_builders = {}
|
19
19
|
@storage = nil
|
20
20
|
|
21
21
|
def self.define_dump(name, type = :partial, &block)
|
22
|
-
@
|
22
|
+
@dump_definition_builders[name.to_sym] = DefinitionBuilder.new(type, block)
|
23
23
|
end
|
24
24
|
|
25
25
|
def self.create_dump!(name)
|
26
26
|
raise DumpDoesNotExist unless dump?(name)
|
27
27
|
raise NoStorageDefined if @storage.nil?
|
28
28
|
|
29
|
-
|
29
|
+
definition = @dump_definition_builders[name.to_sym].evaluate!
|
30
|
+
|
31
|
+
Dumper.new(name, definition).run! do |file, filename|
|
30
32
|
@storage.save(file, filename, name)
|
31
33
|
end
|
32
34
|
end
|
@@ -34,9 +36,10 @@ module DataKeeper
|
|
34
36
|
def self.fetch_and_load_dump!(name)
|
35
37
|
raise DumpDoesNotExist unless dump?(name)
|
36
38
|
raise NoStorageDefined if @storage.nil?
|
39
|
+
definition = @dump_definition_builders[name.to_sym].evaluate!
|
37
40
|
|
38
41
|
@storage.retrieve(name) do |file|
|
39
|
-
Loader.new(
|
42
|
+
Loader.new(definition, file).load!
|
40
43
|
end
|
41
44
|
end
|
42
45
|
|
@@ -44,11 +47,12 @@ module DataKeeper
|
|
44
47
|
raise DumpDoesNotExist unless File.file?(path)
|
45
48
|
raise NoStorageDefined if @storage.nil?
|
46
49
|
|
47
|
-
|
50
|
+
definition = @dump_definition_builders[name.to_sym].evaluate!
|
51
|
+
Loader.new(definition, File.open(path)).load!
|
48
52
|
end
|
49
53
|
|
50
54
|
def self.dump?(name)
|
51
|
-
@
|
55
|
+
@dump_definition_builders.key?(name.to_sym)
|
52
56
|
end
|
53
57
|
|
54
58
|
def self.storage=(value)
|
@@ -56,6 +60,6 @@ module DataKeeper
|
|
56
60
|
end
|
57
61
|
|
58
62
|
def self.clear_dumps!
|
59
|
-
@
|
63
|
+
@dump_definition_builders = {}
|
60
64
|
end
|
61
65
|
end
|
@@ -26,21 +26,19 @@ module DataKeeper
|
|
26
26
|
end
|
27
27
|
|
28
28
|
class DefinitionBuilder
|
29
|
-
|
29
|
+
def initialize(type, definition_block)
|
30
|
+
raise InvalidDumpType, "Invalid type! use :partial or :full" unless [:partial, :full].include?(type)
|
30
31
|
|
31
|
-
|
32
|
+
@type = type
|
32
33
|
@tables = []
|
33
34
|
@raw_sqls = {}
|
34
|
-
|
35
|
+
@definition_block = definition_block
|
35
36
|
end
|
36
37
|
|
37
|
-
def
|
38
|
-
|
39
|
-
raise InvalidDumpType, "Invalid type! use :partial or :full" unless [:partial, :full].include?(type)
|
40
|
-
|
41
|
-
builder = new(block)
|
38
|
+
def evaluate!
|
39
|
+
instance_eval(&@definition_block) if @definition_block
|
42
40
|
|
43
|
-
Definition.new(type,
|
41
|
+
Definition.new(@type, @tables, @raw_sqls, @on_after_load_block)
|
44
42
|
end
|
45
43
|
|
46
44
|
def table(name)
|
data/lib/data_keeper/dumper.rb
CHANGED
@@ -35,7 +35,7 @@ module DataKeeper
|
|
35
35
|
output_path: file.path
|
36
36
|
)
|
37
37
|
|
38
|
-
yield file, "#{filename}.dump"
|
38
|
+
yield File.open(file.path), "#{filename}.dump"
|
39
39
|
end
|
40
40
|
end
|
41
41
|
|
@@ -51,7 +51,7 @@ module DataKeeper
|
|
51
51
|
end
|
52
52
|
end
|
53
53
|
|
54
|
-
yield file, "#{filename}.tar.gz"
|
54
|
+
yield File.open(file.path), "#{filename}.tar.gz"
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
data/lib/data_keeper/loader.rb
CHANGED
@@ -24,6 +24,13 @@ module DataKeeper
|
|
24
24
|
private
|
25
25
|
|
26
26
|
def load_full_database!
|
27
|
+
cmd = Terrapin::CommandLine.new(
|
28
|
+
'psql',
|
29
|
+
"#{connection_args} -d :database -c :command",
|
30
|
+
environment: psql_env
|
31
|
+
)
|
32
|
+
cmd.run(database: database, host: host, port: port, command: "drop schema if exists public")
|
33
|
+
|
27
34
|
pg_restore = Terrapin::CommandLine.new(
|
28
35
|
'pg_restore',
|
29
36
|
"#{connection_args} -j 4 --no-owner --dbname #{database} #{@file.path} 2>/dev/null",
|
@@ -52,9 +59,16 @@ module DataKeeper
|
|
52
59
|
|
53
60
|
def load_partial_database!
|
54
61
|
inflate(@file.path) do |schema_path, tables_path, sql_files|
|
62
|
+
cmd = Terrapin::CommandLine.new(
|
63
|
+
'psql',
|
64
|
+
"#{connection_args} -d :database -c :command",
|
65
|
+
environment: psql_env
|
66
|
+
)
|
67
|
+
cmd.run(database: database, host: host, port: port, command: "drop schema if exists public")
|
68
|
+
|
55
69
|
pg_restore = Terrapin::CommandLine.new(
|
56
70
|
'pg_restore',
|
57
|
-
"#{connection_args} -j 4 --no-owner --dbname
|
71
|
+
"#{connection_args} -j 4 --no-owner --dbname :database #{schema_path} 2>/dev/null",
|
58
72
|
environment: psql_env
|
59
73
|
)
|
60
74
|
|
@@ -66,7 +80,7 @@ module DataKeeper
|
|
66
80
|
|
67
81
|
pg_restore = Terrapin::CommandLine.new(
|
68
82
|
'pg_restore',
|
69
|
-
"#{connection_args} -
|
83
|
+
"#{connection_args} --data-only -j 4 --no-owner --disable-triggers --dbname :database #{tables_path} 2>/dev/null",
|
70
84
|
environment: psql_env
|
71
85
|
)
|
72
86
|
|
@@ -88,7 +102,7 @@ module DataKeeper
|
|
88
102
|
host: host,
|
89
103
|
port: port,
|
90
104
|
csv_path: csv_path,
|
91
|
-
command: "COPY #{table} FROM stdin DELIMITER ',' CSV HEADER"
|
105
|
+
command: "ALTER TABLE #{table} DISABLE TRIGGER all; COPY #{table} FROM stdin DELIMITER ',' CSV HEADER"
|
92
106
|
)
|
93
107
|
end
|
94
108
|
|
@@ -0,0 +1,103 @@
|
|
1
|
+
begin
|
2
|
+
require 'aws-sdk-s3'
|
3
|
+
rescue LoadError
|
4
|
+
raise "You must include the 'aws-sdk-s3' gem in your Gemfile in order to use this s3 storage."
|
5
|
+
end
|
6
|
+
|
7
|
+
module DataKeeper
|
8
|
+
class S3Storage
|
9
|
+
class Client
|
10
|
+
NoSuchKey = Class.new(StandardError)
|
11
|
+
|
12
|
+
def initialize(client_options:, bucket: nil)
|
13
|
+
@client_options = client_options
|
14
|
+
@client = Aws::S3::Client.new(client_options)
|
15
|
+
@bucket = bucket
|
16
|
+
end
|
17
|
+
|
18
|
+
def delete_files(file_paths)
|
19
|
+
@client.delete_objects(
|
20
|
+
bucket: @bucket,
|
21
|
+
delete: {
|
22
|
+
objects: file_paths.map { |key| { key: key } }
|
23
|
+
}
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def list_contents(prefix = '')
|
28
|
+
@client.list_objects(bucket: @bucket, prefix: prefix).contents
|
29
|
+
rescue Aws::S3::Errors::NoSuchKey
|
30
|
+
raise NoSuchKey, prefix
|
31
|
+
end
|
32
|
+
|
33
|
+
# Streams all contents from `path` into the provided io object, calling #write to it.
|
34
|
+
# io can be a File, or any other IO-like object.
|
35
|
+
def stream_to_io(path, io, opts = {})
|
36
|
+
@client.get_object(opts.merge(
|
37
|
+
bucket: @bucket,
|
38
|
+
key: path
|
39
|
+
), target: io)
|
40
|
+
rescue Aws::S3::Errors::NoSuchKey
|
41
|
+
raise NoSuchKey, path
|
42
|
+
end
|
43
|
+
|
44
|
+
# Uploads the given file into the target_path in the s3 bucket.
|
45
|
+
# `file` must be a file stored locally. Can be either a raw string (path),
|
46
|
+
# or a File/Tempfile object (close is up to you).
|
47
|
+
def put_file(target_path, file, options = {})
|
48
|
+
file.rewind if file.respond_to?(:rewind)
|
49
|
+
|
50
|
+
s3 = Aws::S3::Resource.new(@client_options)
|
51
|
+
obj = s3.bucket(@bucket).object(target_path)
|
52
|
+
obj.upload_file(file, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize(bucket:, store_dir:, remote_access:, acl: "public-read", keep_amount: 3)
|
57
|
+
@bucket = bucket
|
58
|
+
@store_dir = store_dir
|
59
|
+
@remote_access = remote_access
|
60
|
+
@acl = acl
|
61
|
+
@keep_amount = keep_amount
|
62
|
+
end
|
63
|
+
|
64
|
+
def save(file, filename, dump_name)
|
65
|
+
path = dump_path(dump_name, filename)
|
66
|
+
|
67
|
+
s3_client.put_file(path, file, acl: @acl)
|
68
|
+
|
69
|
+
prefix = "#{@store_dir}#{dump_name.to_s}"
|
70
|
+
|
71
|
+
keys_to_delete = s3_client.list_contents(prefix).sort_by(&:last_modified).reverse[@keep_amount..-1]
|
72
|
+
|
73
|
+
return unless keys_to_delete
|
74
|
+
|
75
|
+
s3_client.delete_files(keys_to_delete.map(&:key))
|
76
|
+
|
77
|
+
true
|
78
|
+
end
|
79
|
+
|
80
|
+
def retrieve(dump_name)
|
81
|
+
prefix = "#{@store_dir}#{dump_name.to_s}"
|
82
|
+
last_dump = s3_client.list_contents(prefix).sort_by(&:last_modified).reverse.first
|
83
|
+
|
84
|
+
Tempfile.create do |tmp_file|
|
85
|
+
tmp_file.binmode
|
86
|
+
s3_client.stream_to_io(last_dump.key, tmp_file)
|
87
|
+
tmp_file.flush
|
88
|
+
|
89
|
+
yield(tmp_file)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
private
|
94
|
+
|
95
|
+
def s3_client
|
96
|
+
@s3_client ||= Client.new(bucket: @bucket, client_options: @remote_access)
|
97
|
+
end
|
98
|
+
|
99
|
+
def dump_path(dump_name, filename)
|
100
|
+
File.join(@store_dir, dump_name.to_s, "#{SecureRandom.alphanumeric(40)}-#{filename}")
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
data/lib/data_keeper/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: data_keeper
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roger Campos
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-08-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -91,6 +91,7 @@ files:
|
|
91
91
|
- lib/data_keeper/loader.rb
|
92
92
|
- lib/data_keeper/local_storage.rb
|
93
93
|
- lib/data_keeper/railtie.rb
|
94
|
+
- lib/data_keeper/s3_storage.rb
|
94
95
|
- lib/data_keeper/tasks/data_keeper.rake
|
95
96
|
- lib/data_keeper/version.rb
|
96
97
|
- todo.md
|
@@ -107,7 +108,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
108
|
requirements:
|
108
109
|
- - ">="
|
109
110
|
- !ruby/object:Gem::Version
|
110
|
-
version: 2.
|
111
|
+
version: 2.5.0
|
111
112
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
113
|
requirements:
|
113
114
|
- - ">="
|