postgresql-backup-sql 0.0.8

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a09bead7816d581d87be31f02135d158f659b2ef1206e4d624f955e1527381a3
4
+ data.tar.gz: bcb7ca786ba4dbb03d4d917d806cf785d342b7fbd56dc2600de2197d7c90a1eb
5
+ SHA512:
6
+ metadata.gz: '09da4ddbdf7c247d5a2f1c035cc0bcb14c0a1ffaad2a9231cadaac88aa652b3d8db199ecd4987c9bdce4ab65f28317866ed64156c2c299ddf2b2ff73039736f3'
7
+ data.tar.gz: fd2fcdd6b1ad6380ae9480d4e8de51a884ac6d5bf3b1a8163d5998c7c1bd64fb0285ec2a75e10c5d815db972a3004df51dd6e2d58febe3db3ae5ef09a96f409d
data/CHANGELOG.md ADDED
@@ -0,0 +1,16 @@
1
+ ### v0.0.8 - 2022-11-29
2
+ - Changed the way to get the connection settings with the database.
3
+ - FROM ActiveRecord::Base.connection_db_config.configuration_hash
4
+ - TO ActiveRecord::Base.connection_config
5
+ ### v0.0.7 - 2022-04-19
6
+
7
+ - Add support for ruby 3 and fog-aws 3.13.0
8
+
9
+ ### v0.0.6 - 2021-04-25
10
+
11
+ - Add github as the homepage of the gem
12
+ - Add changelog to the project
13
+
14
+ ### v0.0.3 - 2021-04-06
15
+
16
+ - Export all lib files
data/README.md ADDED
@@ -0,0 +1,175 @@
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
+ The current version supports ruby 3. If you need backward compatibiliy.
6
+
7
+ ## How it looks like?
8
+
9
+ Dump:
10
+ ![](https://res.cloudinary.com/ongmungazi/image/upload/v1650388791/ruby-gem/dump.gif)
11
+
12
+ Restore:
13
+ ![](https://res.cloudinary.com/ongmungazi/image/upload/v1650388791/ruby-gem/restore.gif)
14
+
15
+ ## Getting started
16
+
17
+ Add the gem to your Rails project:
18
+
19
+ ```ruby
20
+ gem 'postgresql-backup-sql'
21
+ ```
22
+
23
+ Go to the terminal and update your gems using bundler:
24
+
25
+ ```
26
+ bundle install
27
+ ```
28
+
29
+ In the Rakefile of your project, add `require 'postgresql_backup'` anywhere **before** this line:
30
+
31
+ ```
32
+ Rails.application.load_tasks
33
+ ```
34
+
35
+ Right now, your project already has two new rake tasks: `postgresql_backup:dump` and `postgresql_backup:restore`.
36
+
37
+ ## Configuration
38
+
39
+ If you intend to use the local file system to store the backup files, there is nothing more you need to do. Postgresql-backup-sql has default configuration values and it uses the file system by default.
40
+
41
+ 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.
42
+
43
+ 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.
44
+
45
+ Here is an example with all available options you can change:
46
+
47
+ ```ruby
48
+ require 'postgresql_backup'
49
+
50
+ PostgresqlBackup.configure do |config|
51
+ # This gem works with two possible repositories:
52
+ #
53
+ # * S3: use S3 instead of the file system by setting `S3` to the
54
+ # repository. Make sure you also set `aws_access_key_id` and
55
+ # `aws_secret_access_key` or an error will be raised when you try
56
+ # to execute the rake tasks. The `bucket` and `region` attributes
57
+ # can also be defined, but they have default values and can also
58
+ # be overriden (or set by the first time) when the rake is
59
+ # called.
60
+ #
61
+ # * File System: this is the default value. Files will be stored
62
+ # in the disk, into the folder defined in the `backup_folder`.
63
+ config.repository = 'S3'
64
+
65
+ # The folder where files will be stored in the file system.
66
+ # The default is `db/backups` and it will be ignored if you set
67
+ # `repository` to 'S3'.
68
+ config.backup_folder = ''
69
+
70
+ # Get your access key and secret key from AWS console:
71
+ # IAM -> Users -> Security Credentials tab -> access keys
72
+ config.aws_access_key_id = ''
73
+ config.aws_secret_access_key = ''
74
+
75
+ # The name of the bucket where the backup files will be stored
76
+ # (and from where they will be retrieved). The default value
77
+ # is `postgresql-backups`, but this will be ignored unless the
78
+ # repository is set to S3.
79
+ config.bucket = ''
80
+
81
+ # This is the region where your storage is. The default value
82
+ # is `us-east-1`. It will also be ignored unless the repository
83
+ # is set to S3.
84
+ config.region = ''
85
+
86
+ # Backup files are created using a pattern made by the current date
87
+ # and time. If you want to add a sufix to the files, change this
88
+ # attribute.
89
+ config.file_suffix = ''
90
+
91
+ # If you use S3 to upload the backup files, you need to provide a
92
+ # path where they are going to be stored. The remote path is the
93
+ # place to do that. The default value is `_backups/database/`
94
+ config.remote_path = ''
95
+ end
96
+ ```
97
+
98
+ ## Backing up your database
99
+
100
+ 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:
101
+
102
+ ```
103
+ bundle exec rake postgresql_backup:dump
104
+ ```
105
+
106
+ However, you can set (or override) a few things when executing the rake:
107
+
108
+ - repository: `BKP_REPOSITORY='File System' bundle exec rake postgresql_backup:dump`
109
+ - bucket: `BKP_BUCKET='my-bucket' bundle exec rake postgresql_backup:dump`
110
+ - region: `BKP_REGION='us-east-1' bundle exec rake postgresql_backup:dump`
111
+ - remote_path: `BKP_REMOTE_PATH='_backups/database' bundle exec rake postgresql_backup:dump`
112
+
113
+ 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.
114
+
115
+ You can combine these variables above any way you want:
116
+
117
+ ```
118
+ BKP_REPOSITORY='S3' BKP_BUCKET='my-bucket' BKP_REGION='us-east-1' BKP_REMOTE_PATH='_backups/database' bundle exec rake postgresql_backup:dump
119
+ ```
120
+
121
+ Important note: config/database.yml is used for database configuration,
122
+ but you may be prompted for the database user's password.
123
+
124
+ ## Restoring data into your database
125
+
126
+ The basic way of restoring the database is by running the `restore` take task:
127
+
128
+ ```
129
+ bundle exec rake postgresql_backup:restore
130
+ ```
131
+
132
+ 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:
133
+
134
+ ```
135
+ REPOSITORY='S3' S3_BUCKET_NAME='my-bucket' bundle exec rake db:restore
136
+ ```
137
+
138
+ Again, you can use these environment variables:
139
+
140
+ - repository: `BKP_REPOSITORY='File System' bundle exec rake postgresql_backup:restore`
141
+ - bucket: `BKP_BUCKET='my-bucket' bundle exec rake postgresql_backup:restore`
142
+ - region: `BKP_REGION='us-east-1' bundle exec rake postgresql_backup:restore`
143
+ - remote_path: `BKP_REMOTE_PATH='_backups/database' bundle exec rake postgresql_backup:dump`
144
+
145
+ Or make any combination you want with them:
146
+
147
+ ```
148
+ BKP_REPOSITORY='S3' BKP_BUCKET='my-bucket' BKP_REGION='us-east-1' BKP_REMOTE_PATH='_backups/database' bundle exec rake postgresql_backup:restore
149
+ ```
150
+
151
+ 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.
152
+
153
+ When you run the rake task to restore a database, it will list all available files for you to choose.
154
+
155
+ 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.
156
+
157
+ 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.
158
+
159
+ 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`.
160
+
161
+ ## I want to contribute
162
+
163
+ Feel free to open a pull request with the changes you want to make. Remember to update `CHANGELOG.md` with the change you are proposing, because once the PR is merged, it is important to show which changes are being made to the gem.
164
+
165
+ The first thing to do is to update the dependencies. If you do not have bundle installed, run `gem install bundler`. Then:
166
+
167
+ ```
168
+ bundle install
169
+ ```
170
+
171
+ To run the tests, we use rspec:
172
+
173
+ ```
174
+ rspec
175
+ ```
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 }
@@ -0,0 +1,28 @@
1
+ class Configuration
2
+ attr_accessor :repository, :backup_folder, :bucket, :region, :file_suffix,
3
+ :remote_path, :aws_access_key_id, :aws_secret_access_key
4
+
5
+ def initialize(
6
+ repository: 'file system',
7
+ backup_folder: 'db/backups',
8
+ file_suffix: '',
9
+ aws_access_key_id: '',
10
+ aws_secret_access_key: '',
11
+ bucket: '',
12
+ region: '',
13
+ remote_path: '_backups/database/'
14
+ )
15
+ @repository = repository
16
+ @backup_folder = backup_folder
17
+ @file_suffix = file_suffix
18
+ @aws_access_key_id = aws_access_key_id
19
+ @aws_secret_access_key = aws_secret_access_key
20
+ @bucket = bucket
21
+ @region = region
22
+ @remote_path = remote_path
23
+ end
24
+
25
+ def s3?
26
+ repository.downcase == 's3'
27
+ end
28
+ end
@@ -0,0 +1,15 @@
1
+ require_relative 'configuration'
2
+
3
+ class PostgresqlBackup
4
+ require_relative 'railtie' if defined?(Rails)
5
+
6
+ class << self
7
+ def configuration
8
+ @configuration ||= Configuration.new
9
+ end
10
+
11
+ def configure
12
+ yield(configuration)
13
+ end
14
+ end
15
+ end
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-sql'
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,96 @@
1
+
2
+ module Tools
3
+ class Database
4
+ def initialize(configuration)
5
+ @configuration = configuration
6
+ end
7
+
8
+ # Backup the database and save it on the backup folder set in the
9
+ # configuration.
10
+ # If you need to make the command more verbose, pass
11
+ # `debug: true` in the arguments of the function.
12
+ #
13
+ # Return the full path of the backup file created in the disk.
14
+ def dump(debug: false)
15
+ file_path = File.join(backup_folder, "#{file_name}#{file_suffix}.sql")
16
+
17
+ cmd = "PGPASSWORD='#{password}' pg_dump -F p -v -O -U '#{user}' -h '#{host}' -d '#{database}' -f '#{file_path}' -p '#{port}' "
18
+ debug ? system(cmd) : system(cmd, err: File::NULL)
19
+
20
+ file_path
21
+ end
22
+
23
+ # Drop the database and recreate it.
24
+ #
25
+ # This is done by invoking two Active Record's rake tasks:
26
+ #
27
+ # * rake db:drop
28
+ # * rake db:create
29
+ def reset
30
+ system('bundle exec rake db:drop db:create')
31
+ end
32
+
33
+ # Restore the database from a file in the file system.
34
+ #
35
+ # If you need to make the command more verbose, pass
36
+ # `debug: true` in the arguments of the function.
37
+ def restore(file_name, debug: false)
38
+ file_path = File.join(backup_folder, file_name)
39
+ output_redirection = debug ? '': ' > /dev/null'
40
+ cmd = "PGPASSWORD='#{password}' psql -U '#{user}' -h '#{host}' -d '#{database}' -f '#{file_path}' -p '#{port}' #{output_redirection}"
41
+ system(cmd)
42
+
43
+ file_path
44
+ end
45
+
46
+ # List all backup files from the local backup folder.
47
+ #
48
+ # Return a list of strings containing only the file names.
49
+ def list_files
50
+ Dir.glob("#{backup_folder}/*.sql")
51
+ .reject { |f| File.directory?(f) }
52
+ .map { |f| Pathname.new(f).basename }
53
+ end
54
+
55
+ private
56
+
57
+ attr_reader :configuration
58
+
59
+ def host
60
+ @host ||= ::ActiveRecord::Base.connection_config[:host]
61
+ end
62
+
63
+ def port
64
+ @port ||= ::ActiveRecord::Base.connection_config[:port]
65
+ end
66
+
67
+ def database
68
+ @database ||= ::ActiveRecord::Base.connection_config[:database]
69
+ end
70
+
71
+ def user
72
+ ::ActiveRecord::Base.connection_config[:username]
73
+ end
74
+
75
+ def password
76
+ @password ||= ::ActiveRecord::Base.connection_config[:password]
77
+ end
78
+
79
+ def file_name
80
+ @file_name ||= Time.current.strftime('%Y%m%d%H%M%S')
81
+ end
82
+
83
+ def file_suffix
84
+ return if configuration.file_suffix.empty?
85
+ @file_suffix ||= "_#{configuration.file_suffix}"
86
+ end
87
+
88
+ def backup_folder
89
+ @backup_folder ||= begin
90
+ File.join(Rails.root, configuration.backup_folder).tap do |folder|
91
+ FileUtils.mkdir_p(folder)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ 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 ADDED
@@ -0,0 +1,153 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: postgresql-backup-sql
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.8
5
+ platform: ruby
6
+ authors:
7
+ - Adário Muatelembe
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-11-29 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bump
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.8.0
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.8.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.10.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.10.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: simplecov
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.21.2
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.21.2
55
+ - !ruby/object:Gem::Dependency
56
+ name: fog-aws
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.13.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.13.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: pastel
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.8.0
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.8.0
83
+ - !ruby/object:Gem::Dependency
84
+ name: tty-prompt
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.23.0
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.23.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: tty-spinner
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.9.3
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.9.3
111
+ description: This gem automates PostgreSQL's backup and restore in your Rails project.
112
+ It will inject two rake tasks that you can use to manage your data, either by using
113
+ the local system or AWS S3 storage. forked from arturcp/postgresql-backup
114
+ email: amuatelembe@gmail.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - CHANGELOG.md
120
+ - README.md
121
+ - lib/Rakefile
122
+ - lib/configuration.rb
123
+ - lib/postgresql_backup.rb
124
+ - lib/railtie.rb
125
+ - lib/tasks/backup.rake
126
+ - lib/tools/database.rb
127
+ - lib/tools/disclaimer.rb
128
+ - lib/tools/s3_storage.rb
129
+ - lib/tools/terminal.rb
130
+ homepage: https://github.com/muatsoftgit/postgresql-backup
131
+ licenses:
132
+ - MIT
133
+ metadata: {}
134
+ post_install_message:
135
+ rdoc_options: []
136
+ require_paths:
137
+ - lib
138
+ required_ruby_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ requirements: []
149
+ rubygems_version: 3.1.2
150
+ signing_key:
151
+ specification_version: 4
152
+ summary: Automate PostgreSQL's backup and restore
153
+ test_files: []