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 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
  - - ">="