pgchief 0.3.1 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 627e9af2dc140bd7210c9f4e999f5be13ba0bf514e6397619d42bef8a6d8b838
4
- data.tar.gz: 78a5456432854cebd9b6d6fb68f9f02256a9a37c626ac0d78ef35b3bf79f4f21
3
+ metadata.gz: adccab6ee77468761bf0d05d12954f7b0360547b95f5295797b911ed80f731e2
4
+ data.tar.gz: 2a4ae39f43e0da9913f252e70e0583d0e21dd45e48931e94a62c309298d287f8
5
5
  SHA512:
6
- metadata.gz: 26a19391751adbd9846cd70532becfa91c4f2e6a704405764c207e817f388b5ad900a5993eb2e20b574e09e78a089658b9f1f14591e9ea5f0349476fa7b49c13
7
- data.tar.gz: e96db718dd3e594b4f612d47b2d63bfe0792e94b4ba622d9093db6b2d8138c02cf8dc45885557a8048f767d280f597103a410468914496dd937e25b759eb2c18
6
+ metadata.gz: 7dea359e077b5c473c7df21e53067954dee0c426df76b8a375a026a3976593dbdd4024c6582650a2fbe1404a34affb1030fe0065842065ad594119cd3f757ace
7
+ data.tar.gz: 3b16494cea09736f6b2c16e55d2c5054898a6f023bf06b8862585313617104dc858e98750397baf6b606126aaf2f5bb109f9f02fbf11699b281bdeefd6c41c38
data/CHANGELOG.md CHANGED
@@ -13,6 +13,29 @@ and this project will try its best to adhere to [Semantic Versioning](https://se
13
13
 
14
14
  ### Fixes
15
15
 
16
+ ## [0.5.0]
17
+
18
+ ### Additions
19
+
20
+ * Restore database from local file(s)
21
+ * Restore database from s3
22
+
23
+ ## [0.4.0]
24
+
25
+ ### Changes
26
+
27
+ * Clean up the config object
28
+
29
+ ### Additions
30
+
31
+ * Back up option for databases: save to local filesystem or S3.
32
+
33
+ ### Fixes
34
+
35
+ * Capture error where the config file does not exist and provide some guidance.
36
+ * Make a `PG::ConnectionBad` error a little less scary(?)
37
+ * Do not inherit the base `Command` class in `ConfigCreate`. It doesn't need to connect to the DB.
38
+
16
39
  ## [0.3.1]
17
40
 
18
41
  ### Changes
@@ -75,7 +98,9 @@ and this project will try its best to adhere to [Semantic Versioning](https://se
75
98
  - Drop user ✅
76
99
  - List databases ✅
77
100
 
78
- [Unreleased]: https://github.com/jayroh/pgchief/compare/v0.3.0...HEAD
101
+ [Unreleased]: https://github.com/jayroh/pgchief/compare/v0.4.0...HEAD
102
+ [0.4.0]: https://github.com/jayroh/pgchief/releases/tag/v0.4.0
103
+ [0.3.1]: https://github.com/jayroh/pgchief/releases/tag/v0.3.1
79
104
  [0.3.0]: https://github.com/jayroh/pgchief/releases/tag/v0.3.0
80
105
  [0.2.0]: https://github.com/jayroh/pgchief/releases/tag/v0.2.0
81
106
  [0.1.0]: https://github.com/jayroh/pgchief/releases/tag/v0.1.0
data/README.md CHANGED
@@ -41,7 +41,7 @@ pgchief
41
41
 
42
42
  ## Config
43
43
 
44
- Format of `~/.pgchief.toml`
44
+ Format of `~/.config/pgchief/config.toml`
45
45
 
46
46
  ```toml
47
47
  # Connection string to superuser account at your PG instance
@@ -130,5 +130,9 @@ Give "rando-username" access to database(s):
130
130
  * [x] Give user permissions to use database
131
131
  * [x] Initialize toml file
132
132
  * [x] Display connection information
133
- * [ ] Back up database
134
- * [ ] Restore database
133
+ * [x] Back up database locally
134
+ * [x] Back up database to S3
135
+ * [x] Restore local database
136
+ * [x] Restore remote database @ S3
137
+ * [ ] Quickly back up via command line option
138
+ * [ ] Quickly restore via command line option
data/config/pgchief.toml CHANGED
@@ -8,3 +8,9 @@ backup_dir = "~/.config/pgchief/backups"
8
8
 
9
9
  # Location of saved database connection strings
10
10
  # credentials_file = "~/.config/pgchief/credentials"
11
+
12
+ # S3 config - if present, will back up to S3 instead of local filesystem
13
+ # s3_key = ""
14
+ # s3_secret = ""
15
+ # s3_region = "us-east-1"
16
+ # s3_path_prefix = "s3://bucket-name/database-backups/"
@@ -13,6 +13,8 @@ module Pgchief
13
13
  def initialize(*params)
14
14
  @params = params
15
15
  @conn = PG.connect(Pgchief::Config.pgurl)
16
+ rescue PG::ConnectionBad => e
17
+ puts "Cannot connect to database. #{e.message}"
16
18
  end
17
19
 
18
20
  def call
@@ -5,7 +5,11 @@ require "fileutils"
5
5
  module Pgchief
6
6
  module Command
7
7
  # Create a configuration file at $HOME
8
- class ConfigCreate < Base
8
+ class ConfigCreate
9
+ def self.call(dir: "#{Dir.home}/.config/pgchief")
10
+ new.call(dir: dir)
11
+ end
12
+
9
13
  def call(dir: "#{Dir.home}/.config/pgchief")
10
14
  return if File.exist?("#{dir}/config.toml")
11
15
 
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Pgchief
6
+ module Command
7
+ # Command object to drop a database
8
+ class DatabaseBackup < Base
9
+ extend Forwardable
10
+
11
+ def_delegators :@uploader, :configured?, :upload!, :s3_location
12
+
13
+ attr_reader :database
14
+
15
+ def call
16
+ @database = params.first
17
+ @uploader = Pgchief::Command::S3Upload.new(local_location)
18
+ raise Pgchief::Errors::DatabaseMissingError unless db_exists?
19
+
20
+ backup!
21
+ upload! if configured?
22
+
23
+ "Database '#{database}' backed up to #{location}"
24
+ rescue PG::Error => e
25
+ "Error: #{e.message}"
26
+ ensure
27
+ conn.close
28
+ end
29
+
30
+ private
31
+
32
+ def backup!
33
+ `pg_dump -Fc #{database} -f #{local_location}`
34
+ end
35
+
36
+ def db_exists?
37
+ query = "SELECT 1 FROM pg_database WHERE datname = '#{database}'"
38
+ conn.exec(query).any?
39
+ end
40
+
41
+ def location
42
+ configured? ? s3_location : local_location
43
+ end
44
+
45
+ def local_location
46
+ @local_location ||= begin
47
+ timestamp = Time.now.strftime("%Y%m%d%H%M%S")
48
+ "#{Pgchief::Config.backup_dir}#{database}-#{timestamp}.dump"
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Pgchief
6
+ module Command
7
+ # Command object to restore a database
8
+ class DatabaseRestore < Base
9
+ extend Forwardable
10
+
11
+ attr_reader :database, :filename
12
+
13
+ def_delegators :s3, :configured?, :client, :bucket, :path
14
+
15
+ def call
16
+ @database = params.first
17
+ @filename = params.last
18
+ raise Pgchief::Errors::DatabaseMissingError unless db_exists?
19
+
20
+ download! if configured?
21
+ restore!
22
+
23
+ "Database '#{database}' restored from #{filename}"
24
+ rescue PG::Error => e
25
+ "Error: #{e.message}"
26
+ ensure
27
+ conn.close
28
+ end
29
+
30
+ private
31
+
32
+ def download!
33
+ client.get_object(
34
+ bucket: bucket,
35
+ key: "#{path}#{filename}",
36
+ response_target: local_location
37
+ )
38
+ end
39
+
40
+ def restore!
41
+ `pg_restore --clean --no-owner --dbname=#{Pgchief::Config.pgurl}/#{database} #{local_location}`
42
+ end
43
+
44
+ def db_exists?
45
+ query = "SELECT 1 FROM pg_database WHERE datname = '#{database}'"
46
+ conn.exec(query).any?
47
+ end
48
+
49
+ def local_location
50
+ "#{Pgchief::Config.backup_dir}/#{filename}"
51
+ end
52
+
53
+ def s3
54
+ Pgchief::Config.s3
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgchief
4
+ module Command
5
+ # Class to upload a file to S3
6
+ class S3Upload
7
+ attr_reader :local_location, :file_name, :bucket, :path
8
+
9
+ def initialize(local_location)
10
+ @local_location = local_location
11
+ @file_name = File.basename(local_location)
12
+ end
13
+
14
+ def upload!
15
+ s3.client.put_object(
16
+ bucket: s3.bucket,
17
+ key: "#{s3.path}#{file_name}",
18
+ body: File.open(local_location, "rb"),
19
+ acl: "private",
20
+ content_type: "application/octet-stream"
21
+ )
22
+ end
23
+
24
+ def s3_location
25
+ "s3://#{s3.bucket}/#{s3.path}#{file_name}"
26
+ end
27
+
28
+ def configured?
29
+ s3.configured?
30
+ end
31
+
32
+ private
33
+
34
+ def s3
35
+ Pgchief::Config.s3
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Pgchief
6
+ class Config
7
+ # Class to store s3 configuration settings
8
+ class S3
9
+ extend Forwardable
10
+
11
+ def_delegators \
12
+ :config,
13
+ :s3_key,
14
+ :s3_secret,
15
+ :s3_region,
16
+ :s3_path_prefix
17
+
18
+ PREFIX_REGEX = %r{\As3://(?<bucket>(\w|-)*)/(?<path>(\w|/)*/)\z}
19
+
20
+ attr_reader :config
21
+
22
+ def initialize(config)
23
+ @config = config
24
+ end
25
+
26
+ def client
27
+ @client ||= Aws::S3::Client.new(
28
+ access_key_id: s3_key,
29
+ secret_access_key: s3_secret,
30
+ region: s3_region
31
+ )
32
+ end
33
+
34
+ def bucket
35
+ s3_match[:bucket]
36
+ end
37
+
38
+ def path
39
+ s3_match[:path]
40
+ end
41
+
42
+ def configured?
43
+ [
44
+ s3_key,
45
+ s3_secret,
46
+ s3_region,
47
+ s3_path_prefix
48
+ ].none?(&:nil?)
49
+ end
50
+
51
+ private
52
+
53
+ def s3_match
54
+ @s3_match ||= s3_path_prefix.match(PREFIX_REGEX)
55
+ end
56
+ end
57
+ end
58
+ end
@@ -7,30 +7,63 @@ module Pgchief
7
7
  class Config
8
8
  class << self
9
9
  attr_accessor \
10
- :backup_dir,
11
- :credentials_file
10
+ :s3_key,
11
+ :s3_secret,
12
+ :s3_region
12
13
 
13
14
  attr_writer :pgurl
14
15
 
15
- def load_config!(toml_file = "#{Dir.home}/.config/pgchief/config.toml")
16
- config = TomlRB.load_file(toml_file, symbolize_keys: true)
16
+ attr_reader \
17
+ :s3_path_prefix,
18
+ :backup_dir,
19
+ :credentials_file
20
+
21
+ def load_config!(toml_file = "#{Dir.home}/.config/pgchief/config.toml") # rubocop:disable Metrics/AbcSize
22
+ config = TomlRB.load_file(toml_file, symbolize_keys: true)
23
+ self.backup_dir = config[:backup_dir]
24
+ self.credentials_file = config[:credentials_file]
25
+ self.pgurl = config[:pgurl]
26
+ self.s3_key = config[:s3_key]
27
+ self.s3_secret = config[:s3_secret]
28
+ self.s3_region = config[:s3_region]
29
+ self.s3_path_prefix = config[:s3_path_prefix]
30
+ rescue Errno::ENOENT
31
+ puts config_missing_error(toml_file)
32
+ end
17
33
 
18
- @backup_dir = config[:backup_dir].gsub("~", Dir.home)
19
- @credentials_file = config[:credentials_file]&.gsub("~", Dir.home)
20
- @pgurl = config[:pgurl]
34
+ def s3
35
+ @s3 ||= Pgchief::Config::S3.new(self)
21
36
  end
22
37
 
23
38
  def pgurl
24
39
  ENV.fetch("DATABASE_URL", @pgurl)
25
40
  end
26
41
 
42
+ def backup_dir=(value)
43
+ @backup_dir = value ? "#{value.chomp("/")}/".gsub("~", Dir.home) : "/tmp/"
44
+ end
45
+
46
+ def s3_path_prefix=(value)
47
+ @s3_path_prefix = value ? "#{value.chomp("/")}/" : nil
48
+ end
49
+
50
+ def credentials_file=(value)
51
+ @credentials_file = value&.gsub("~", Dir.home)
52
+ end
53
+
27
54
  def set_up_file_structure!
28
55
  FileUtils.mkdir_p(backup_dir)
29
-
30
56
  return unless credentials_file && !File.exist?(credentials_file)
31
57
 
32
58
  FileUtils.touch(credentials_file)
33
59
  end
60
+
61
+ private
62
+
63
+ def config_missing_error(toml_file)
64
+ "You must create a config file at #{toml_file}.\n" \
65
+ "run `pgchief --init` to create it."
66
+ end
34
67
  end
35
68
  end
36
69
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+ require "pry"
5
+
6
+ module Pgchief
7
+ class Database
8
+ # Get a list of all backups for a given database
9
+ class Backups
10
+ extend Forwardable
11
+
12
+ def_delegators :s3, :bucket, :path, :client
13
+
14
+ def self.for(database, remote)
15
+ new(database, remote).for
16
+ end
17
+
18
+ attr_reader :database, :remote
19
+
20
+ def initialize(database, remote)
21
+ @database = database
22
+ @remote = remote
23
+ end
24
+
25
+ def for
26
+ remote ? remote_backups : local_backups
27
+ end
28
+
29
+ def remote_backups
30
+ @remote_backups ||= client.list_objects(
31
+ bucket: bucket,
32
+ prefix: "#{path}#{database}-"
33
+ ).contents
34
+ .map(&:key)
35
+ .sort
36
+ .last(3)
37
+ .reverse
38
+ .map { |f| File.basename(f) }
39
+ end
40
+
41
+ def local_backups
42
+ Dir["#{Pgchief::Config.backup_dir}#{database}-*.dump"]
43
+ .sort_by { |f| File.mtime(f) }
44
+ .reverse
45
+ .last(3)
46
+ .map { |f| File.basename(f) }
47
+ end
48
+
49
+ private
50
+
51
+ def s3
52
+ Pgchief::Config.s3
53
+ end
54
+ end
55
+ end
56
+ end
@@ -14,5 +14,9 @@ module Pgchief
14
14
  ensure
15
15
  conn.close
16
16
  end
17
+
18
+ def self.backups_for(database, remote: false)
19
+ Pgchief::Database::Backups.for(database, remote: remote)
20
+ end
17
21
  end
18
22
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgchief
4
+ module Prompt
5
+ # Class to prompt for which database to backup
6
+ class BackupDatabase < Base
7
+ def call
8
+ database = prompt.select("Which database needs backing up?", Pgchief::Database.all)
9
+ result = Pgchief::Command::DatabaseBackup.call(database)
10
+
11
+ prompt.say result
12
+ end
13
+ end
14
+ end
15
+ end
@@ -6,8 +6,14 @@ module Pgchief
6
6
  class DatabaseManagement < Base
7
7
  def call
8
8
  prompt = TTY::Prompt.new
9
- result = prompt.select("Database management", ["Create database", "Drop database", "Database List"])
10
- scope = result == "Database List" ? "command" : "prompt"
9
+ result = prompt.select("Database management", [
10
+ "Create database",
11
+ "Drop database",
12
+ "Database List",
13
+ "Backup database",
14
+ "Restore database"
15
+ ])
16
+ scope = result == "Database List" ? "command" : "prompt"
11
17
 
12
18
  klassify(scope, result).call
13
19
  end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pgchief
4
+ module Prompt
5
+ # Class to prompt for which database to restore
6
+ class RestoreDatabase < Base
7
+ def call
8
+ database = prompt.select("Which database needs restoring?", Pgchief::Database.all)
9
+ local_file = prompt.select("Which backup file do you want to restore?", Pgchief::Database.backups_for(database))
10
+ result = Pgchief::Command::DatabaseRestore.call(database, local_file)
11
+
12
+ prompt.say result
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pgchief
4
- VERSION = "0.3.1"
4
+ VERSION = "0.5.0"
5
5
  end
data/lib/pgchief.rb CHANGED
@@ -3,33 +3,41 @@
3
3
  require "pg"
4
4
  require "tty-prompt"
5
5
  require "tty-option"
6
+ require "aws-sdk-s3"
6
7
 
7
8
  require "pgchief/cli"
8
9
  require "pgchief/config"
10
+ require "pgchief/config/s3"
9
11
  require "pgchief/connection_string"
10
12
  require "pgchief/version"
11
13
  require "pgchief/database"
14
+ require "pgchief/database/backups"
12
15
  require "pgchief/user"
13
16
 
14
17
  require "pgchief/prompt/base"
15
- require "pgchief/prompt/start"
18
+ require "pgchief/prompt/backup_database"
16
19
  require "pgchief/prompt/create_database"
17
20
  require "pgchief/prompt/create_user"
18
21
  require "pgchief/prompt/database_management"
19
22
  require "pgchief/prompt/drop_database"
20
23
  require "pgchief/prompt/drop_user"
21
- require "pgchief/prompt/user_management"
22
24
  require "pgchief/prompt/grant_database_privileges"
25
+ require "pgchief/prompt/restore_database"
26
+ require "pgchief/prompt/start"
27
+ require "pgchief/prompt/user_management"
23
28
  require "pgchief/prompt/view_database_connection_string"
24
29
 
25
30
  require "pgchief/command"
26
31
  require "pgchief/command/base"
27
32
  require "pgchief/command/config_create"
33
+ require "pgchief/command/database_backup"
28
34
  require "pgchief/command/database_create"
29
35
  require "pgchief/command/database_drop"
30
36
  require "pgchief/command/database_list"
31
37
  require "pgchief/command/database_privileges_grant"
38
+ require "pgchief/command/database_restore"
32
39
  require "pgchief/command/retrieve_connection_string"
40
+ require "pgchief/command/s3_upload"
33
41
  require "pgchief/command/store_connection_string"
34
42
  require "pgchief/command/user_create"
35
43
  require "pgchief/command/user_drop"
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pgchief
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Oliveira
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-11-12 00:00:00.000000000 Z
11
+ date: 2024-11-28 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-s3
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: pg
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -89,19 +103,25 @@ files:
89
103
  - lib/pgchief/command.rb
90
104
  - lib/pgchief/command/base.rb
91
105
  - lib/pgchief/command/config_create.rb
106
+ - lib/pgchief/command/database_backup.rb
92
107
  - lib/pgchief/command/database_create.rb
93
108
  - lib/pgchief/command/database_drop.rb
94
109
  - lib/pgchief/command/database_list.rb
95
110
  - lib/pgchief/command/database_privileges_grant.rb
111
+ - lib/pgchief/command/database_restore.rb
96
112
  - lib/pgchief/command/retrieve_connection_string.rb
113
+ - lib/pgchief/command/s3_upload.rb
97
114
  - lib/pgchief/command/store_connection_string.rb
98
115
  - lib/pgchief/command/user_create.rb
99
116
  - lib/pgchief/command/user_drop.rb
100
117
  - lib/pgchief/command/user_list.rb
101
118
  - lib/pgchief/config.rb
119
+ - lib/pgchief/config/s3.rb
102
120
  - lib/pgchief/connection_string.rb
103
121
  - lib/pgchief/database.rb
122
+ - lib/pgchief/database/backups.rb
104
123
  - lib/pgchief/prompt.rb
124
+ - lib/pgchief/prompt/backup_database.rb
105
125
  - lib/pgchief/prompt/base.rb
106
126
  - lib/pgchief/prompt/create_database.rb
107
127
  - lib/pgchief/prompt/create_user.rb
@@ -109,6 +129,7 @@ files:
109
129
  - lib/pgchief/prompt/drop_database.rb
110
130
  - lib/pgchief/prompt/drop_user.rb
111
131
  - lib/pgchief/prompt/grant_database_privileges.rb
132
+ - lib/pgchief/prompt/restore_database.rb
112
133
  - lib/pgchief/prompt/start.rb
113
134
  - lib/pgchief/prompt/user_management.rb
114
135
  - lib/pgchief/prompt/view_database_connection_string.rb
@@ -139,7 +160,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
139
160
  - !ruby/object:Gem::Version
140
161
  version: '0'
141
162
  requirements: []
142
- rubygems_version: 3.2.33
163
+ rubygems_version: 3.5.21
143
164
  signing_key:
144
165
  specification_version: 4
145
166
  summary: A simple ruby script to manage postgresql databases and users