martilla 0.0.1 → 0.2.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,21 @@
1
+ require 'pony'
2
+
3
+ module Martilla
4
+ class Sendmail < EmailNotifier
5
+ def success(data)
6
+ Pony.mail(to: to_email,
7
+ from: from_email,
8
+ subject: success_subject,
9
+ html_body: success_html(data),
10
+ body: success_txt(data))
11
+ end
12
+
13
+ def error(msg, data)
14
+ Pony.mail(to: to_email,
15
+ from: from_email,
16
+ subject: failure_subject,
17
+ html_body: error_html(msg, data),
18
+ body: error_txt(msg, data))
19
+ end
20
+ end
21
+ end
@@ -1,5 +1,89 @@
1
- class Martilla::Notifiers::Ses < Martilla::Notifiers::Base
2
- def notify(opts)
1
+ require 'aws-sdk-ses'
3
2
 
3
+ module Martilla
4
+ class Ses < EmailNotifier
5
+ def success(data)
6
+ begin
7
+ ses_client.send_email(
8
+ destination: {
9
+ to_addresses: to_email.split(',')
10
+ },
11
+ message: {
12
+ body: {
13
+ html: {
14
+ charset: 'UTF-8',
15
+ data: success_html(data)
16
+ },
17
+ text: {
18
+ charset: 'UTF-8',
19
+ data: success_txt(data)
20
+ }
21
+ },
22
+ subject: {
23
+ charset: 'UTF-8',
24
+ data: success_subject
25
+ }
26
+ },
27
+ source: from_email
28
+ )
29
+ rescue Aws::SES::Errors::ServiceError => error
30
+ puts "Email not sent. Error message: #{error}"
31
+ end
32
+ end
33
+
34
+ def error(msg, data)
35
+ begin
36
+ ses_client.send_email(
37
+ destination: {
38
+ to_addresses: [
39
+ to_email.split(',')
40
+ ]
41
+ },
42
+ message: {
43
+ body: {
44
+ html: {
45
+ charset: 'UTF-8',
46
+ data: error_html(msg, data)
47
+ },
48
+ text: {
49
+ charset: 'UTF-8',
50
+ data: error_txt(msg, data)
51
+ }
52
+ },
53
+ subject: {
54
+ charset: 'UTF-8',
55
+ data: failure_subject
56
+ }
57
+ },
58
+ source: from_email
59
+ )
60
+ rescue Aws::SES::Errors::ServiceError => error
61
+ puts "Email not sent. Error message: #{error}"
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def ses_client
68
+ options = {}
69
+ options[:region] = aws_region unless aws_region.nil?
70
+ options[:access_key_id] = aws_access_key unless aws_access_key.nil?
71
+ options[:secret_access_key] = aws_secret_key unless aws_secret_key.nil?
72
+ Aws::SES::Client.new(options)
73
+ end
74
+
75
+ def aws_region
76
+ aws_region = @options['region']
77
+ raise config_error('region') if aws_region.nil?
78
+ aws_region
79
+ end
80
+
81
+ def aws_access_key
82
+ @options['access_key_id']
83
+ end
84
+
85
+ def aws_secret_key
86
+ @options['secret_access_key']
87
+ end
4
88
  end
5
89
  end
@@ -1,5 +1,4 @@
1
- class Martilla::Notifiers::Slack < Martilla::Notifiers::Base
2
- def notify(opts)
3
-
1
+ module Martilla
2
+ class Slack < Notifier
4
3
  end
5
4
  end
@@ -0,0 +1,70 @@
1
+ require 'pony'
2
+
3
+ module Martilla
4
+ class Smtp < EmailNotifier
5
+ def success(data)
6
+ Pony.mail(to: to_email,
7
+ from: from_email,
8
+ subject: success_subject,
9
+ via: :smtp,
10
+ html_body: success_html(data),
11
+ body: success_txt(data),
12
+ via_options: via_options)
13
+ end
14
+
15
+ def error(msg, data)
16
+ Pony.mail(to: to_email,
17
+ from: from_email,
18
+ subject: failure_subject,
19
+ via: :smtp,
20
+ html_body: error_html(msg, data),
21
+ body: error_txt(msg, data),
22
+ via_options: via_options)
23
+ end
24
+
25
+ private
26
+
27
+ def via_options
28
+ {
29
+ address: address,
30
+ port: port,
31
+ user_name: user_name,
32
+ password: password,
33
+ authentication: authentication, # :plain, :login, :cram_md5, no auth by default
34
+ domain: domain # the HELO domain provided by the client to the server
35
+ }
36
+ end
37
+
38
+ def address
39
+ email = @options['address']
40
+ raise config_error('address') if email.nil?
41
+ email
42
+ end
43
+
44
+ def domain
45
+ smtp_domain = @options['domain']
46
+ raise config_error('domain') if smtp_domain.nil?
47
+ smtp_domain
48
+ end
49
+
50
+ def user_name
51
+ smtp_user_name = @options['user_name']
52
+ raise config_error('user_name') if smtp_user_name.nil?
53
+ smtp_user_name
54
+ end
55
+
56
+ def password
57
+ smtp_password = @options['password']
58
+ raise config_error('password') if smtp_password.nil?
59
+ smtp_password
60
+ end
61
+
62
+ def port
63
+ @options['port'] || '25'
64
+ end
65
+
66
+ def authentication
67
+ @options['authentication'] || :plain
68
+ end
69
+ end
70
+ end
@@ -2,22 +2,62 @@ require 'forwardable'
2
2
 
3
3
  module Martilla
4
4
  class Storage
5
- extend Forwardable
5
+ attr_reader :options
6
6
 
7
7
  def initialize(config)
8
- # When a new core target is added to the project include it here
8
+ @options = config
9
+ raise Error.new(invalid_options_msg) if @options.nil?
10
+ end
11
+
12
+ def persist(temp_file:, gzip:)
13
+ raise NotImplementedError, 'You must implement the persist method'
14
+ end
15
+
16
+ def invalid_options_msg
17
+ 'Storage configuration is invalid. Details here: https://github.com/fdoxyz/martilla'
18
+ end
19
+
20
+ def suffix?
21
+ return true if @options['suffix'].nil?
22
+ @options['suffix']
23
+ end
24
+
25
+ def output_filename(gzip)
26
+ filename = @options['filename'] || 'backup.sql'
27
+ filename = append_datetime_suffix(filename) if suffix?
28
+ filename = "#{filename}.gz" if gzip
29
+ filename
30
+ end
31
+
32
+ def append_datetime_suffix(filename)
33
+ dirname = File.dirname(filename)
34
+ basename = File.basename(filename, '.*')
35
+
36
+ # 'dir_with_name' is the original filename WITHOUT the extension
37
+ dir_with_name = "#{dirname}/#{basename}"
38
+ dir_with_name = basename if dirname == '.'
39
+
40
+ extension = filename.gsub(dir_with_name, '')
41
+ timestamp = Time.now.strftime("%Y-%m-%dT%H%M%S")
42
+ "#{dirname}/#{basename}_#{timestamp}#{extension}"
43
+ end
44
+
45
+ def config_error(config_name)
46
+ Error.new("Storage adapter configuration requires #{config_name}. Details here: https://github.com/fdoxyz/martilla")
47
+ end
48
+
49
+ # When a new Storage is supported it needs to go here
50
+ def self.create(config = {})
9
51
  case config['type'].downcase
10
52
  when 'local'
11
- @storage = Storages::Local.new(config['options'])
53
+ Local.new(config['options'])
12
54
  when 's3'
13
- @storage = Storages::S3.new(config['options'])
55
+ S3.new(config['options'])
14
56
  when 'scp'
15
- @storage = Storages::Scp.new(config['options'])
57
+ Scp.new(config['options'])
16
58
  else
17
59
  raise Error.new("Invalid Storage type: #{config['type']}")
18
60
  end
19
61
  end
20
-
21
- def_delegators :@storage, :persist
22
62
  end
23
63
  end
@@ -1,5 +1,9 @@
1
- class Martilla::Storages::Local < Martilla::Storages::Base
2
- def persist(filepath, opts)
3
-
1
+ module Martilla
2
+ class Local < Storage
3
+ def persist(tmp_file:, gzip:)
4
+ `mv #{tmp_file} #{output_filename(gzip)}`
5
+ return nil if $?.success?
6
+ raise Error.new("Local storage failed with code #{$?.exitstatus}")
7
+ end
4
8
  end
5
9
  end
@@ -1,5 +1,42 @@
1
- class Martilla::Storages::S3 < Martilla::Storages::Base
2
- def persist(filepath, opts)
1
+ require 'aws-sdk-s3'
3
2
 
3
+ module Martilla
4
+ class S3 < Storage
5
+ def persist(tmp_file:, gzip:)
6
+ path = output_filename(gzip)
7
+ obj = s3_resource.bucket(bucket_name).object(path)
8
+
9
+ # Upload it
10
+ return nil if obj.upload_file(tmp_file)
11
+ raise Error.new('Error uploading backup to bucket')
12
+ end
13
+
14
+ private
15
+
16
+ def s3_resource
17
+ options = {}
18
+ options[:region] = aws_region unless aws_region.nil?
19
+ options[:access_key_id] = aws_access_key unless aws_access_key.nil?
20
+ options[:secret_access_key] = aws_secret_key unless aws_secret_key.nil?
21
+ Aws::S3::Resource.new(options)
22
+ end
23
+
24
+ def aws_region
25
+ @options['region']
26
+ end
27
+
28
+ def aws_access_key
29
+ @options['access_key_id']
30
+ end
31
+
32
+ def aws_secret_key
33
+ @options['secret_access_key']
34
+ end
35
+
36
+ def bucket_name
37
+ bucket = @options['bucket']
38
+ raise config_error('bucket') if bucket.nil?
39
+ bucket
40
+ end
4
41
  end
5
42
  end
@@ -1,5 +1,29 @@
1
- class Martilla::Storages::Scp < Martilla::Storages::Base
2
- def persist(filepath, opts)
1
+ module Martilla
2
+ class Scp < Storage
3
+ def persist(tmp_file:, gzip:)
4
+ `scp -i #{identity_file} #{user}@#{host}:#{output_filename(gzip)}`
5
+ return nil if $?.success?
6
+ raise Error.new("SCP storage failed with code #{$?.exitstatus}")
7
+ end
3
8
 
9
+ private
10
+
11
+ def host
12
+ scp_host = @options['host']
13
+ raise config_error('host') if scp_host.nil?
14
+ scp_host
15
+ end
16
+
17
+ def user
18
+ scp_user = @options['user']
19
+ raise config_error('user') if scp_user.nil?
20
+ scp_user
21
+ end
22
+
23
+ def identity_file
24
+ file = @options['identity_file']
25
+ raise config_error('identity_file') if file.nil?
26
+ file
27
+ end
4
28
  end
5
29
  end
@@ -0,0 +1,45 @@
1
+ module Martilla
2
+ class Backup
3
+ module Utilities
4
+ def duration_format(seconds)
5
+ case seconds
6
+ when 0..59
7
+ # 22s
8
+ "#{seconds}s"
9
+ when 60..3599
10
+ # 18m 19s
11
+ "#{s_to_m(seconds)}m #{seconds % 60}s"
12
+ else
13
+ # 7h 9m 51s
14
+ "#{s_to_h(seconds)}h #{s_to_m(seconds)}m #{seconds % 60}s"
15
+ end
16
+ end
17
+
18
+ def s_to_m(seconds)
19
+ (seconds / 60) % 60
20
+ end
21
+
22
+ def s_to_h(seconds)
23
+ (seconds / 3600) % 3600
24
+ end
25
+
26
+ def formatted_file_size
27
+ return if @file_size.nil?
28
+
29
+ if @file_size <= 799_999
30
+ compressed_file_size = @file_size / 2**10
31
+ formatted_size = '%.2f' % compressed_file_size
32
+ "#{formatted_size} KB"
33
+ elsif @file_size <= 799_999_999
34
+ compressed_file_size = @file_size / 2**20
35
+ formatted_size = '%.2f' % compressed_file_size
36
+ "#{formatted_size} MB"
37
+ else
38
+ compressed_file_size = @file_size / 2**30
39
+ formatted_size = '%.2f' % compressed_file_size
40
+ "#{formatted_size} GB"
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,3 +1,3 @@
1
1
  module Martilla
2
- VERSION = '0.0.1'
2
+ VERSION = '0.2.0'
3
3
  end
data/lib/martilla.rb CHANGED
@@ -1,31 +1,24 @@
1
1
  require 'martilla/version'
2
2
  require 'martilla/cli'
3
3
 
4
- module Martilla
5
- class Error < StandardError; end
4
+ require 'martilla/backup'
5
+
6
+ require 'martilla/database'
7
+ require 'martilla/databases/mysql'
8
+ require 'martilla/databases/postgres'
6
9
 
7
- def execute_backup(config)
8
- puts "EXECUTING BACKUP WITH CONFIG: #{config}"
9
- db = Database.new(config['db'])
10
- storage = Storage.new(config['storage'])
11
- notifiers = config['notifiers'].map { |c| Notifier.new(c) }
10
+ require 'martilla/notifier'
11
+ require 'martilla/notifiers/email_notifier'
12
+ require 'martilla/notifiers/smtp'
13
+ require 'martilla/notifiers/sendmail'
14
+ require 'martilla/notifiers/ses'
15
+ require 'martilla/notifiers/slack'
12
16
 
13
- begin
14
- # Perform DB dump & storage of backup
15
- temp_filepath = db.dump
16
- storage.persist(temp_filepath)
17
- rescue Exception => e
18
- puts "EXCEPTION RAISED: #{e.inspect}"
19
- notifiers.each do |notifier|
20
- notifier.error(e)
21
- end
22
- else
23
- puts "SUCCESS"
24
- notifiers.each do |notifier|
25
- notifier.success
26
- end
27
- end
17
+ require 'martilla/storage'
18
+ require 'martilla/storages/local'
19
+ require 'martilla/storages/s3'
20
+ require 'martilla/storages/scp'
28
21
 
29
- File.delete(temp_filepath) if File.exist?(temp_filepath)
30
- end
22
+ module Martilla
23
+ class Error < StandardError; end
31
24
  end
data/martilla.gemspec CHANGED
@@ -28,11 +28,16 @@ Gem::Specification.new do |spec|
28
28
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
29
  spec.require_paths = ['lib']
30
30
 
31
- spec.add_dependency 'pony', '~> 1.13'
32
- spec.add_dependency 'aws-ses', '~> 0.6.0'
33
31
  spec.add_dependency 'thor', '~> 0.20.3'
32
+ spec.add_dependency 'memoist', '~> 0.16.0'
33
+ spec.add_dependency 'pony', '~> 1.13'
34
+ spec.add_dependency 'aws-sdk-ses', '~> 1.26'
35
+ spec.add_dependency 'aws-sdk-s3', '~> 1.49'
34
36
 
35
37
  spec.add_development_dependency 'bundler', '~> 2.0'
36
38
  spec.add_development_dependency 'rake', '~> 13.0'
37
39
  spec.add_development_dependency 'rspec', '~> 3.9'
40
+ spec.add_development_dependency 'byebug', '~> 11.0'
41
+ spec.add_development_dependency 'pry-byebug', '~> 3.7'
42
+ spec.add_development_dependency 'simplecov', '~> 0.17.1'
38
43
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: martilla
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fernando Valverde
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-10-10 00:00:00.000000000 Z
11
+ date: 2019-10-19 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.20.3
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.20.3
27
+ - !ruby/object:Gem::Dependency
28
+ name: memoist
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.16.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.16.0
13
41
  - !ruby/object:Gem::Dependency
14
42
  name: pony
15
43
  requirement: !ruby/object:Gem::Requirement
@@ -25,33 +53,33 @@ dependencies:
25
53
  - !ruby/object:Gem::Version
26
54
  version: '1.13'
27
55
  - !ruby/object:Gem::Dependency
28
- name: aws-ses
56
+ name: aws-sdk-ses
29
57
  requirement: !ruby/object:Gem::Requirement
30
58
  requirements:
31
59
  - - "~>"
32
60
  - !ruby/object:Gem::Version
33
- version: 0.6.0
61
+ version: '1.26'
34
62
  type: :runtime
35
63
  prerelease: false
36
64
  version_requirements: !ruby/object:Gem::Requirement
37
65
  requirements:
38
66
  - - "~>"
39
67
  - !ruby/object:Gem::Version
40
- version: 0.6.0
68
+ version: '1.26'
41
69
  - !ruby/object:Gem::Dependency
42
- name: thor
70
+ name: aws-sdk-s3
43
71
  requirement: !ruby/object:Gem::Requirement
44
72
  requirements:
45
73
  - - "~>"
46
74
  - !ruby/object:Gem::Version
47
- version: 0.20.3
75
+ version: '1.49'
48
76
  type: :runtime
49
77
  prerelease: false
50
78
  version_requirements: !ruby/object:Gem::Requirement
51
79
  requirements:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
- version: 0.20.3
82
+ version: '1.49'
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: bundler
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -94,6 +122,48 @@ dependencies:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
124
  version: '3.9'
125
+ - !ruby/object:Gem::Dependency
126
+ name: byebug
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '11.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '11.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: pry-byebug
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '3.7'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '3.7'
153
+ - !ruby/object:Gem::Dependency
154
+ name: simplecov
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: 0.17.1
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: 0.17.1
97
167
  description: ''
98
168
  email:
99
169
  - fdov88@gmail.com
@@ -113,22 +183,24 @@ files:
113
183
  - bin/console
114
184
  - bin/martilla
115
185
  - bin/setup
186
+ - config.yml
116
187
  - lib/martilla.rb
188
+ - lib/martilla/backup.rb
117
189
  - lib/martilla/cli.rb
118
190
  - lib/martilla/database.rb
119
- - lib/martilla/databases/base.rb
120
191
  - lib/martilla/databases/mysql.rb
121
192
  - lib/martilla/databases/postgres.rb
122
193
  - lib/martilla/notifier.rb
123
- - lib/martilla/notifiers/base.rb
124
- - lib/martilla/notifiers/email.rb
194
+ - lib/martilla/notifiers/email_notifier.rb
195
+ - lib/martilla/notifiers/sendmail.rb
125
196
  - lib/martilla/notifiers/ses.rb
126
197
  - lib/martilla/notifiers/slack.rb
198
+ - lib/martilla/notifiers/smtp.rb
127
199
  - lib/martilla/storage.rb
128
- - lib/martilla/storages/base.rb
129
200
  - lib/martilla/storages/local.rb
130
201
  - lib/martilla/storages/s3.rb
131
202
  - lib/martilla/storages/scp.rb
203
+ - lib/martilla/utilities.rb
132
204
  - lib/martilla/version.rb
133
205
  - martilla.gemspec
134
206
  homepage: https://github.com/fdoxyz/martilla
@@ -1,11 +0,0 @@
1
- class Martilla::Databases::Base
2
- attr_reader :options
3
-
4
- def initialize(opts)
5
- @options = opts
6
- end
7
-
8
- def temp_filepath
9
- @options['tmp_file'] || '/tmp/db.sql'
10
- end
11
- end
@@ -1,7 +0,0 @@
1
- class Martilla::Notifiers::Base
2
- attr_reader :options
3
-
4
- def initialize(opts)
5
- @options = opts
6
- end
7
- end
@@ -1,5 +0,0 @@
1
- class Martilla::Notifiers::Email < Martilla::Notifiers::Base
2
- def notify(opts)
3
-
4
- end
5
- end
@@ -1,7 +0,0 @@
1
- class Martilla::Storages::Base
2
- attr_reader :options
3
-
4
- def initialize(opts)
5
- @options = opts
6
- end
7
- end