postgresql-backup 0.0.2 → 0.0.3

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: f34190c8ce07b53f5b82d3b310f5da641112088d7c951163dcb846be9724a00a
4
- data.tar.gz: d905f4743ceaa94f37ec7e3fbe17bc6e0928cd3ba15800c029705a32bfc8db4f
3
+ metadata.gz: 1e7a2f09d1cbc28e1fcb21aba0bd72b9770f17db9d70bf80b9c0d4205fd97ee6
4
+ data.tar.gz: ff5ef7a6a7f05e3e306034537c9d8cd7681b59968e96e0567a0d5ce76926819d
5
5
  SHA512:
6
- metadata.gz: 0e2227f822a916e585d7378155a351c09b2d1c4a6d533ac6e2eb09d1cd6113b92e1cae3482342958d59be4c703136c40d3cb7f9f29dce5e122ee1c7503e4c014
7
- data.tar.gz: 2932a78f48433e93da8e0e3c04bb79dc4f0710fc847e49eb619fc1c2f95c30bda3e5628c31a5985c409b84a53e9a8424cd42931de0bf495ae968e3dffe183524
6
+ metadata.gz: 8e3a92f37969d423dec46d9ba9a5afc8ff76fda398521f89cdcd64f576a6b1e6c8f5facd837053f76fabb1c06a9ac205550c0fb4de01c316023dbbe8094ef418
7
+ data.tar.gz: 620695bd227ca25beeb6cb746df745f9051d8a0297e65dc496f55dd35ffa53bfbda928b2bf94457ce367b1847376d3767ff23c8a4f9b725f9100b196525cfecd
data/README.md ADDED
@@ -0,0 +1,141 @@
1
+ # PostgreSQL backup
2
+
3
+ This gem automates PostgreSQL's backup and restore in your Rails project. It will inject two rake tasks that you can use to manage your data, either by using the local system or AWS S3 storage.
4
+
5
+ ## Getting started
6
+
7
+ Add the gem to your Rails project:
8
+
9
+ ```ruby
10
+ gem 'postgresql-backup'
11
+ ```
12
+
13
+ Go to the terminal and update your gems using bundler:
14
+
15
+ ```
16
+ bundle install
17
+ ```
18
+
19
+ Right now, your project already has two new rake tasks: `backup` and `restore`.
20
+
21
+ ## Configuration
22
+
23
+ If you intend to use the local file system to store the backup files, there is nothing more you need to do. Postgresql-backup has default configuration values and it uses the file system by default.
24
+
25
+ However, if you want to change the defaul values, like the name of the backup files or the folder where they are going to be stored, or if you prefer to use Amazon S3 service as a storage, you can do it.
26
+
27
+ Create a file inside the `config/initializers` folder. The name is not important, but it is a good practice to name it after with something related to what it does, like `database_backup.rb` or something like that.
28
+
29
+ Here is an example with all available options you can change:
30
+
31
+ ```ruby
32
+ PostgresqlBackup.configure do |config|
33
+ # This gem works with two possible repositories:
34
+ #
35
+ # * S3: use S3 instead of the file system by setting `S3` to the
36
+ # repository. Make sure you also set `aws_access_key_id` and
37
+ # `aws_secret_access_key` or an error will be raised when you try
38
+ # to execute the rake tasks. The `bucket` and `region` attributes
39
+ # can also be defined, but they have default values and can also
40
+ # be overriden (or set by the first time) when the rake is
41
+ # called.
42
+ #
43
+ # * File System: this is the default value. Files will be stored
44
+ # in the disk, into the folder defined in the `backup_folder`.
45
+ config.repository = 'S3'
46
+
47
+ # The folder where files will be stored in the file system.
48
+ # The default is `db/backups` and it will be ignored if you set
49
+ # `repository` to 'S3'.
50
+ config.backup_folder = ''
51
+
52
+ # Get your access key and secret key from AWS console:
53
+ # IAM -> Users -> Security Credentials tab -> access keys
54
+ config.aws_access_key_id = ''
55
+ config.aws_secret_access_key = ''
56
+
57
+ # The name of the bucket where the backup files will be stored
58
+ # (and from where they will be retrieved). The default value
59
+ # is `postgresql-backups`, but this will be ignored unless the
60
+ # repository is set to S3.
61
+ config.bucket = ''
62
+
63
+ # This is the region where your storage is. The default value
64
+ # is `us-east-1`. It will also be ignored unless the repository
65
+ # is set to S3.
66
+ config.region = ''
67
+
68
+ # Backup files are created using a pattern made by the current date
69
+ # and time. If you want to add a sufix to the files, change this
70
+ # attribute.
71
+ config.file_suffix = ''
72
+
73
+ # If you use S3 to upload the backup files, you need to provide a
74
+ # path where they are going to be stored. The remote path is the
75
+ # place to do that. The default value is `_backups/database/`
76
+ config.remote_path = ''
77
+ end
78
+ ```
79
+
80
+ ## Backing up your database
81
+
82
+ If you followed the steps above, you are ready to go. The simplest way to backup your data is by running the `dump` rake task:
83
+
84
+ ```
85
+ bundle exec rake postgresql_backup:dump
86
+ ```
87
+
88
+ However, you can set (or override) a few things when executing the rake:
89
+
90
+ - repository: `BKP_REPOSITORY='File System' bundle exec rake postgresql_backup:dump`
91
+ - bucket: `BKP_BUCKET='my-bucket' bundle exec rake postgresql_backup:dump`
92
+ - region: `BKP_REGION='us-east-1' bundle exec rake postgresql_backup:dump`
93
+ - remote_path: `BKP_REMOTE_PATH='_backups/database' bundle exec rake postgresql_backup:dump`
94
+
95
+ Be aware that, if the gem is configured to use the file system and you force the task to use S3, AWS related attributes must be set, like the access key and the secret key.
96
+
97
+ You can combine these variables above any way you want:
98
+
99
+ ```
100
+ BKP_REPOSITORY='S3' BKP_BUCKET='my-bucket' BKP_REGION='us-east-1' BKP_REMOTE_PATH='_backups/database' bundle exec rake postgresql_backup:dump
101
+ ```
102
+
103
+ Important note: config/database.yml is used for database configuration,
104
+ but you may be prompted for the database user's password.
105
+
106
+ ## Restoring data into your database
107
+
108
+ The basic way of restoring the database is by running the `restore` take task:
109
+
110
+ ```
111
+ bundle exec rake postgresql_backup:restore
112
+ ```
113
+
114
+ It will respect the configuration set during initialization or use default values when available. Just like in the `dump` task, you can override (or set) configuration values:
115
+
116
+ ```
117
+ REPOSITORY='S3' S3_BUCKET_NAME='my-bucket' bundle exec rake db:restore
118
+ ```
119
+
120
+ Again, you can use these environment variables:
121
+
122
+ - repository: `BKP_REPOSITORY='File System' bundle exec rake postgresql_backup:restore`
123
+ - bucket: `BKP_BUCKET='my-bucket' bundle exec rake postgresql_backup:restore`
124
+ - region: `BKP_REGION='us-east-1' bundle exec rake postgresql_backup:restore`
125
+ - remote_path: `BKP_REMOTE_PATH='_backups/database' bundle exec rake postgresql_backup:dump`
126
+
127
+ Or make any combination you want with them:
128
+
129
+ ```
130
+ BKP_REPOSITORY='S3' BKP_BUCKET='my-bucket' BKP_REGION='us-east-1' BKP_REMOTE_PATH='_backups/database' bundle exec rake postgresql_backup:restore
131
+ ```
132
+
133
+ This is useful when you are trying to restore a production database into your local machine. Even though you configured the gem to use a development bucket, it is necessary to read the backup file from a production bucket.
134
+
135
+ When you run the rake task to restore a database, it will list all available files for you to choose.
136
+
137
+ Important note: if you are trying to locally restore a backup that was created in a production environment, there is a trick you need to know. There is a table called `ar_internal_metadata` that stores the Rails environment the project is using. If you simply restore a production backup in a development database, Rails will think you are in production.
138
+
139
+ Everything will work just fine, but you may come across some strange warnings, like when you try to drop the database: it will say you are droping a production database to double check if this is your intended purpose.
140
+
141
+ To prevent this, every time the rake restores a backup file it tries to replace the environment being copied into the ar_internal_metadata table with the current Rails environment. Thus, `environment production` will become `environment development`.
data/lib/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require 'postgresql_backup'
2
+
3
+ path = File.expand_path(__dir__)
4
+ Dir.glob("#{path}/tasks/*.rake").each { |f| import f }
data/lib/railtie.rb ADDED
@@ -0,0 +1,11 @@
1
+ require 'postgresql_backup'
2
+ require 'rails'
3
+
4
+ class Railtie < Rails::Railtie
5
+ railtie_name 'postgresql-backup'
6
+
7
+ rake_tasks do
8
+ path = File.expand_path(__dir__)
9
+ Dir.glob("#{path}/tasks/*.rake").each { |f| load f }
10
+ end
11
+ end
@@ -0,0 +1,120 @@
1
+ require_relative '../tools/disclaimer'
2
+ require_relative '../tools/terminal'
3
+ require_relative '../tools/database'
4
+ require_relative '../tools/s3_storage'
5
+ require 'tty-prompt'
6
+ require 'tty-spinner'
7
+ require 'pastel'
8
+
9
+ namespace :postgresql_backup do
10
+ desc 'Dumps the database'
11
+ task dump: :environment do
12
+ title = pastel.yellow.bold('POSTGRESQL BACKUP')
13
+ text = 'You are about to backup your database. Relax on your seat, this process is usually fast and you don\'t need to do anything except wait for the process to end. Here is the current configuration for this backup:'
14
+ texts = [text, ' ', configuration_to_text].flatten
15
+ disclaimer.show(title: title, texts: texts)
16
+
17
+ file_path = Tools::Terminal.spinner('Backing up database') { db.dump }
18
+
19
+ if configuration.s3?
20
+ Tools::Terminal.spinner('Uploading file') { storage.upload(file_path) }
21
+ Tools::Terminal.spinner('Deleting local file') { File.delete(file_path) } if File.exist?(file_path)
22
+ end
23
+
24
+ puts ''
25
+ puts pastel.green('All done.')
26
+ end
27
+
28
+ desc 'Restores a database backup into the database'
29
+ task restore: :environment do
30
+ title = pastel.green('POSTGRESQL DATABASE RESTORE')
31
+ text = 'Let\'s get your data back. You will be prompted to choose the file to restore, but that\'s all, you can leave the rest to us. Here is the current configuration for this restore:'
32
+ texts = [text, ' ', configuration_to_text].flatten
33
+ disclaimer.show(title: title, texts: texts)
34
+ local_file_path = ''
35
+
36
+ files = Tools::Terminal.spinner('Loading backups list') { list_backup_files }
37
+
38
+ if files.present?
39
+ puts ''
40
+ file_name = prompt.select("Choose the file to restore", files)
41
+ puts ''
42
+
43
+ if configuration.s3?
44
+ local_file_path = Tools::Terminal.spinner('Downloading file') { storage.download(file_name) }
45
+ end
46
+
47
+ db.reset
48
+
49
+ Tools::Terminal.spinner('Restoring data') { db.restore(file_name) }
50
+
51
+ if configuration.s3?
52
+ Tools::Terminal.spinner('Deleting local file') { File.delete(local_file_path) }
53
+ end
54
+
55
+ puts ''
56
+ puts pastel.green('All done.')
57
+ else
58
+ spinner = TTY::Spinner.new("#{pastel.yellow("[:spinner] ")}Restoring data...")
59
+ error_message = "#{pastel.red.bold('failed')}. Backup files not found."
60
+ spinner.success(error_message)
61
+ end
62
+ end
63
+
64
+ private
65
+
66
+ def configuration
67
+ @configuration ||= begin
68
+ config = PostgresqlBackup.configuration.dup
69
+ config.repository = ENV['BKP_REPOSITORY'] if ENV['BKP_REPOSITORY'].present?
70
+ config.bucket = ENV['BKP_BUCKET'] if ENV['BKP_BUCKET'].present?
71
+ config.region = ENV['BKP_REGION'] if ENV['BKP_REGION'].present?
72
+ config.remote_path = ENV['BKP_REMOTE_PATH'] if ENV['BKP_REMOTE_PATH'].present?
73
+ config
74
+ end
75
+ end
76
+
77
+ def db
78
+ @db ||= Tools::Database.new(configuration)
79
+ end
80
+
81
+ def storage
82
+ @storage ||= Tools::S3Storage.new(configuration)
83
+ end
84
+
85
+ def disclaimer
86
+ @disclaimer ||= Tools::Disclaimer.new
87
+ end
88
+
89
+ def pastel
90
+ @pastel ||= Pastel.new
91
+ end
92
+
93
+ def prompt
94
+ @prompt ||= TTY::Prompt.new
95
+ end
96
+
97
+ def configuration_to_text
98
+ [
99
+ show_config_for('Repository', configuration.repository),
100
+ configuration.s3? ? nil : show_config_for('Folder', configuration.backup_folder),
101
+ show_config_for('File suffix', configuration.file_suffix),
102
+ configuration.s3? ? show_config_for('Bucket', configuration.bucket) : nil,
103
+ configuration.s3? ? show_config_for('Region', configuration.region) : nil,
104
+ configuration.s3? ? show_config_for('Remote path', configuration.remote_path) : nil
105
+ ].compact
106
+ end
107
+
108
+ def show_config_for(text, value)
109
+ return if value.empty?
110
+
111
+ "* #{pastel.yellow.bold(text)}: #{value}"
112
+ end
113
+
114
+ def list_backup_files
115
+ @list_backup_files ||= begin
116
+ source = configuration.s3? ? storage : db
117
+ source.list_files
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,97 @@
1
+ require 'byebug'
2
+
3
+ module Tools
4
+ class Database
5
+ def initialize(configuration)
6
+ @configuration = configuration
7
+ end
8
+
9
+ # Backup the database and save it on the backup folder set in the
10
+ # configuration.
11
+ # If you need to make the command more verbose, pass
12
+ # `debug: true` in the arguments of the function.
13
+ #
14
+ # Return the full path of the backup file created in the disk.
15
+ def dump(debug: false)
16
+ file_path = File.join(backup_folder, "#{file_name}#{file_suffix}.sql")
17
+
18
+ cmd = "PGPASSWORD='#{password}' pg_dump -F p -v -O -U '#{user}' -h '#{host}' -d '#{database}' -f '#{file_path}' -p '#{port}' "
19
+ debug ? system(cmd) : system(cmd, err: File::NULL)
20
+
21
+ file_path
22
+ end
23
+
24
+ # Drop the database and recreate it.
25
+ #
26
+ # This is done by invoking two Active Record's rake tasks:
27
+ #
28
+ # * rake db:drop
29
+ # * rake db:create
30
+ def reset
31
+ system('bundle exec rake db:drop db:create')
32
+ end
33
+
34
+ # Restore the database from a file in the file system.
35
+ #
36
+ # If you need to make the command more verbose, pass
37
+ # `debug: true` in the arguments of the function.
38
+ def restore(file_name, debug: false)
39
+ file_path = File.join(backup_folder, file_name)
40
+ output_redirection = debug ? '': ' > /dev/null'
41
+ cmd = "PGPASSWORD='#{password}' psql -U '#{user}' -h '#{host}' -d '#{database}' -f '#{file_path}' -p '#{port}' #{output_redirection}"
42
+ system(cmd)
43
+
44
+ file_path
45
+ end
46
+
47
+ # List all backup files from the local backup folder.
48
+ #
49
+ # Return a list of strings containing only the file names.
50
+ def list_files
51
+ Dir.glob("#{backup_folder}/*.sql")
52
+ .reject { |f| File.directory?(f) }
53
+ .map { |f| Pathname.new(f).basename }
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :configuration
59
+
60
+ def host
61
+ @host ||= ::ActiveRecord::Base.connection_config[:host]
62
+ end
63
+
64
+ def port
65
+ @port ||= ::ActiveRecord::Base.connection_config[:port]
66
+ end
67
+
68
+ def database
69
+ @database ||= ::ActiveRecord::Base.connection_config[:database]
70
+ end
71
+
72
+ def user
73
+ ::ActiveRecord::Base.connection_config[:username]
74
+ end
75
+
76
+ def password
77
+ @password ||= ::ActiveRecord::Base.connection_config[:password]
78
+ end
79
+
80
+ def file_name
81
+ @file_name ||= Time.current.strftime('%Y%m%d%H%M%S')
82
+ end
83
+
84
+ def file_suffix
85
+ return if configuration.file_suffix.empty?
86
+ @file_suffix ||= "_#{configuration.file_suffix}"
87
+ end
88
+
89
+ def backup_folder
90
+ @backup_folder ||= begin
91
+ File.join(Rails.root, configuration.backup_folder).tap do |folder|
92
+ FileUtils.mkdir_p(folder)
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,194 @@
1
+ require 'pastel'
2
+
3
+ module Tools
4
+ class Disclaimer
5
+ def initialize(columns: 80, horizontal_character: '=', vertical_character: '|', print_output: true)
6
+ @columns = columns
7
+ @horizontal_character = horizontal_character
8
+ @vertical_character = vertical_character
9
+ @print_output = print_output
10
+ end
11
+
12
+ # Prints a box containing a title and an
13
+ # explanation of the action that will be
14
+ # performed.
15
+ #
16
+ # Example:
17
+ #
18
+ # ========================================
19
+ # | |
20
+ # | POSTGRESQL BACKUP GEM |
21
+ # | |
22
+ # ========================================
23
+ # | |
24
+ # | Dolore ipsum sunt amet laborum |
25
+ # | voluptate elit tempor minim |
26
+ # | officia ex amet incididunt. |
27
+ # | |
28
+ # ========================================
29
+ def show(title: nil, texts: [])
30
+ output = ['']
31
+
32
+ if title
33
+ output << horizontal_character * columns
34
+ output << empty_line
35
+ output << centralized_text(title)
36
+ output << empty_line
37
+ end
38
+
39
+ output << horizontal_character * columns
40
+ output << empty_line
41
+
42
+ Array(texts).each do |text|
43
+ paragraphs = break_text_into_paragraphs(text)
44
+ paragraphs.each do |text|
45
+ output << left_aligned_text(text)
46
+ end
47
+ end
48
+ output << empty_line
49
+
50
+ output << horizontal_character * columns
51
+ output << ''
52
+
53
+ output.each { |line| puts line } if print_output
54
+
55
+ output
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :columns, :horizontal_character, :vertical_character, :print_output
61
+
62
+ # Create an empty line to give visual space for the text
63
+ # inside the disclaimer box.
64
+ #
65
+ # Example:
66
+ #
67
+ # | |
68
+ #
69
+ # Returns a string that represents an empty line,
70
+ # including the vertical characters to close the
71
+ # disclaimer box.
72
+ def empty_line
73
+ spaces = ' ' * (columns - 2 * length_of(vertical_character))
74
+
75
+ [
76
+ vertical_character,
77
+ spaces,
78
+ vertical_character
79
+ ].join
80
+ end
81
+
82
+ # Create a line inside the disclaimer box with a text aligned
83
+ # in the center of the box. There are cases when the centralized
84
+ # text needs an extra space to complete the box, if you want to
85
+ # learn more about how this is calculated, read the documentation
86
+ # of the method extra_space.
87
+ #
88
+ # Example:
89
+ #
90
+ # | POSTGRESQL BACKUP GEM |
91
+ #
92
+ # Return a string with a centralized text inside the disclaimer
93
+ # box.
94
+ def centralized_text(text)
95
+ gap = (columns - length_of(text) - 2 * length_of(vertical_character)) / 2
96
+ spaces = ' ' * gap
97
+
98
+ [
99
+ vertical_character,
100
+ spaces,
101
+ text + extra_space(text),
102
+ spaces,
103
+ vertical_character
104
+ ].join
105
+ end
106
+
107
+ # Create a line inside the disclaimer box with a text aligned
108
+ # in the left of the box.
109
+ #
110
+ # Example:
111
+ #
112
+ # | Dolore ipsum sunt amet laborum |
113
+ #
114
+ # Return a string with a left-aligned text inside the
115
+ # disclaimer box.
116
+ def left_aligned_text(text)
117
+ stripped_text = text.strip
118
+ spaces_around_text = 2
119
+ gap = (columns - 2 * spaces_around_text - 2 * length_of(vertical_character)) - length_of(stripped_text)
120
+ spaces = ' ' * gap
121
+
122
+ [
123
+ vertical_character,
124
+ ' ' * spaces_around_text,
125
+ stripped_text,
126
+ spaces,
127
+ ' ' * spaces_around_text,
128
+ vertical_character
129
+ ].join
130
+ end
131
+
132
+ # When the differente between the number of columns and the lenght
133
+ # of the text is odd, there will be a missing space to close the
134
+ # disclaimer box. That's because the division by 2 ceils the result.
135
+ #
136
+ # Example:
137
+ #
138
+ # Consider a box with 40 columns and the text `text`. To calculate
139
+ # the spaces around the text, first substract the vertical characters:
140
+ #
141
+ # 40 - 2 (we are supposing that the vertical character has length 1)
142
+ #
143
+ # Now, substract the length of the text and divide the result by 2 (
144
+ # each portion will be positioned at one side of the text):
145
+ #
146
+ # (38 - 5) / 2 = 16!
147
+ #
148
+ # If we add the numbers back, we have: 2 (vertical_character) + 2 * 16 (spaces) + 5 (text length) = 39
149
+ #
150
+ # The result is not 40, but it should be. In these cases, the need to
151
+ # add an extra space to compensate for this mathematical operation.
152
+ #
153
+ # Returns either one blank space or an empty space.
154
+ def extra_space(text)
155
+ text_length = length_of(text)
156
+ if (text_length.odd? && columns.even?) || (text_length.even? && columns.odd?)
157
+ ' '
158
+ else
159
+ ''
160
+ end
161
+ end
162
+
163
+ # Given a long text, this method breaks the text in small
164
+ # chunks that will fit inside the space delimited by the
165
+ # `columns` attribute.
166
+ #
167
+ # Returns an array of string.
168
+ def break_text_into_paragraphs(text)
169
+ paragraphs = []
170
+ position = 0
171
+ spaces_around_text = 2
172
+ paragraph_max_size = columns - 2 * spaces_around_text - 2 * length_of(vertical_character)
173
+ text_length = length_of(text)
174
+
175
+ loop do
176
+ break paragraphs if position == text_length
177
+
178
+ match = text.match(/.{1,#{paragraph_max_size}}(?=[ ]|\z)|.{,#{paragraph_max_size-1}}[ ]/, position)
179
+ return nil if match.nil?
180
+
181
+ paragraphs << match[0]
182
+ position += length_of(match[0])
183
+ end
184
+ end
185
+
186
+ def length_of(text)
187
+ pastel.undecorate(text).map { |part| part[:text] }.join.length
188
+ end
189
+
190
+ def pastel
191
+ @pastel ||= Pastel.new
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fog/aws'
4
+ require 'fileutils'
5
+ require 'pathname'
6
+
7
+ module Tools
8
+ class S3Storage
9
+ def initialize(configuration)
10
+ @configuration = configuration
11
+ @s3 = Fog::Storage.new(
12
+ provider: 'AWS',
13
+ region: configuration.region,
14
+ aws_access_key_id: configuration.aws_access_key_id,
15
+ aws_secret_access_key: configuration.aws_secret_access_key
16
+ )
17
+ end
18
+
19
+ # Send files to S3.
20
+ #
21
+ # To test this, go to the console and execute:
22
+ #
23
+ # ```
24
+ # file_path = "#{Rails.root}/Gemfile"
25
+ # S3Storage.new.upload(file_path)
26
+ # ```
27
+ #
28
+ # It will send the Gemfile to S3, inside the `remote_path` set in the
29
+ # configuration. The bucket name and the credentials are also read from
30
+ # the configuration.
31
+ def upload(file_path, tags = '')
32
+ file_name = Pathname.new(file_path).basename
33
+
34
+ remote_file.create(
35
+ key: File.join(remote_path, file_name),
36
+ body: File.open(file_path),
37
+ tags: tags
38
+ )
39
+ end
40
+
41
+ # List all the files in the bucket's remote path. The result
42
+ # is sorted in the reverse order, the most recent file will
43
+ # show up first.
44
+ #
45
+ # Return an array of strings, containing only the file name.
46
+ def list_files
47
+ files = remote_directory.files.map { |file| file.key }
48
+
49
+ # The first item in the array is only the path an can be discarded.
50
+ files = files.slice(1, files.length - 1) || []
51
+
52
+ files
53
+ .map { |file| Pathname.new(file).basename.to_s }
54
+ .sort
55
+ .reverse
56
+ end
57
+
58
+ # Create a local file with the contents of the remote file.
59
+ #
60
+ # The new file will be saved in the `backup_folder` that was set
61
+ # in the configuration (the default value is `db/backups`)
62
+ def download(file_name)
63
+ remote_file_path = File.join(remote_path, file_name)
64
+ local_file_path = File.join(backup_folder, file_name)
65
+
66
+ file_from_storage = remote_directory.files.get(remote_file_path)
67
+
68
+ prepare_local_folder(local_file_path)
69
+ create_local_file(local_file_path, file_from_storage)
70
+
71
+ local_file_path
72
+ end
73
+
74
+ private
75
+
76
+ attr_reader :configuration, :s3
77
+
78
+ # Force UTF-8 encoding and remove the production environment from
79
+ # the `ar_internal_metadata` table, unless the current Rails env
80
+ # is indeed `production`.
81
+ def file_body(file)
82
+ body = file.body.force_encoding("UTF-8")
83
+ return body if Rails.env.production?
84
+
85
+ body.gsub('environment production', "environment #{Rails.env}")
86
+ end
87
+
88
+ def bucket
89
+ @bucket ||= configuration.bucket
90
+ end
91
+
92
+ def region
93
+ @region ||= configuration.region
94
+ end
95
+
96
+ def remote_path
97
+ @remote_path ||= configuration.remote_path
98
+ end
99
+
100
+ def backup_folder
101
+ @backup_folder ||= configuration.backup_folder
102
+ end
103
+
104
+ def remote_directory
105
+ @remote_directory ||= s3.directories.get(bucket, prefix: remote_path)
106
+ end
107
+
108
+ def remote_file
109
+ @remote_file ||= s3.directories.new(key: bucket).files
110
+ end
111
+
112
+ # Make sure the path exists and that there are no files with
113
+ # the same name of the one that is being downloaded.
114
+ def prepare_local_folder(local_file_path)
115
+ FileUtils.mkdir_p(backup_folder)
116
+ File.delete(local_file_path) if File.exist?(local_file_path)
117
+ end
118
+
119
+ def create_local_file(local_file_path, file_from_storage)
120
+ File.open(local_file_path, 'w') do |local_file|
121
+ body = file_body(file_from_storage)
122
+ local_file.write(body)
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,17 @@
1
+ require 'pastel'
2
+ require 'tty-spinner'
3
+
4
+ module Tools
5
+ class Terminal
6
+ def self.spinner(text)
7
+ pastel = Pastel.new
8
+
9
+ spinner = TTY::Spinner.new("#{pastel.yellow("[:spinner] ")}#{text}...")
10
+ spinner.auto_spin
11
+ result = yield
12
+ spinner.success(pastel.green.bold("done."))
13
+
14
+ result
15
+ end
16
+ end
17
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: postgresql-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.2
4
+ version: 0.0.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artur Caliendo Prado
@@ -88,8 +88,16 @@ executables: []
88
88
  extensions: []
89
89
  extra_rdoc_files: []
90
90
  files:
91
+ - README.md
92
+ - lib/Rakefile
91
93
  - lib/configuration.rb
92
94
  - lib/postgresql_backup.rb
95
+ - lib/railtie.rb
96
+ - lib/tasks/backup.rake
97
+ - lib/tools/database.rb
98
+ - lib/tools/disclaimer.rb
99
+ - lib/tools/s3_storage.rb
100
+ - lib/tools/terminal.rb
93
101
  homepage: https://rubygems.org/gems/postgresql-backup
94
102
  licenses:
95
103
  - MIT