data_keeper 0.1.0 → 0.1.4

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9c4e8095729b6927bc11ae296fc30571577d1dfd5f5d910b1092f924981efa69
4
- data.tar.gz: bb646294c8847fd637b765fd6a2f16af605dabe656b663d2cb0e6f54ff396f79
3
+ metadata.gz: 0442b3f4ba1b60c62570ca379d4a5d6f2a2181ec9cb7561107d52a3492b62ec5
4
+ data.tar.gz: e7594ccf321cd010c5cf74926ff0dd881d4aee7e6c142b204a58c9f9df137ebd
5
5
  SHA512:
6
- metadata.gz: cd3696d94b9ef70bf108e8c77ee880e1b97e68989955c6c7975b4f9cea9196b95417a17e6b5bf6e3a9baf2047cd0a1db524e2bc8dd660d2576f10385f38a038d
7
- data.tar.gz: 1ecab9088e179625113bb3a490a83c60a16405924f527caa059ed1dc05c68527049f12cc985aa1280f6e094597903f5bf280420a21c98a1670eb527b4e766c52
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
- type: "scp",
40
- host: "141.12.241.22",
41
- port: "8622",
42
- user: "fredy"
39
+ host: "10.10.10.10",
40
+ port: "22",
41
+ user: "user"
43
42
  }
44
43
  )
45
44
  ```
46
45
 
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:
46
+ There's also support for storing the dumps in s3, using `DataKeeper::S3Storage` like in this example:
49
47
 
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.
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 file in the local filesystem,
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/[USERNAME]/data_keeper.
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.3.0")
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
- @dumps = {}
18
+ @dump_definition_builders = {}
19
19
  @storage = nil
20
20
 
21
21
  def self.define_dump(name, type = :partial, &block)
22
- @dumps[name.to_sym] = DefinitionBuilder.build(type, block)
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
- Dumper.new(name, @dumps[name.to_sym]).run! do |file, filename|
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(@dumps[name.to_sym], file).load!
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
- Loader.new(@dumps[name.to_sym], File.open(path)).load!
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
- @dumps.key?(name.to_sym)
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
- @dumps = {}
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
- attr_reader :tables, :raw_sqls, :on_after_load_block
29
+ def initialize(type, definition_block)
30
+ raise InvalidDumpType, "Invalid type! use :partial or :full" unless [:partial, :full].include?(type)
30
31
 
31
- def initialize(definition_block)
32
+ @type = type
32
33
  @tables = []
33
34
  @raw_sqls = {}
34
- instance_eval(&definition_block) if definition_block
35
+ @definition_block = definition_block
35
36
  end
36
37
 
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)
38
+ def evaluate!
39
+ instance_eval(&@definition_block) if @definition_block
42
40
 
43
- Definition.new(type, builder.tables, builder.raw_sqls, builder.on_after_load_block)
41
+ Definition.new(@type, @tables, @raw_sqls, @on_after_load_block)
44
42
  end
45
43
 
46
44
  def table(name)
@@ -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
 
@@ -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 #{database} #{schema_path} 2>/dev/null",
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} -c -j 4 --no-owner --dbname #{database} #{tables_path} 2>/dev/null",
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
@@ -1,3 +1,3 @@
1
1
  module DataKeeper
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.4"
3
3
  end
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.0
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: 2020-12-09 00:00:00.000000000 Z
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.3.0
111
+ version: 2.5.0
111
112
  required_rubygems_version: !ruby/object:Gem::Requirement
112
113
  requirements:
113
114
  - - ">="