imap-backup 10.0.1 → 11.0.0.rc1

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: 7675c421b9705eb8e4543fc9df035c6a9750b291688805e25cc7f6c2537936e8
4
- data.tar.gz: e3027362856d02201bf5d2633804b6744238684bb30979c07d8cc53ef4afaf36
3
+ metadata.gz: e9cff069fefde8147d1995b254f3f0613575ff4f52232c2fc64c91dc00c49bde
4
+ data.tar.gz: a1b8cb3d48f41b2773fd20b842b8d3e3b71e673b6797c83a345e341d087060d8
5
5
  SHA512:
6
- metadata.gz: 34771e1aa4dd8d8444e37b149d3a491b506363d6dfff00cd6ed6a8a7e501f6ac04e5d7ec24774fd36488bda96c6dfa6454b388d6afbe0f86e0b11317877100f0
7
- data.tar.gz: f721a34ed6bf6af08d7a87335241e684a3d878538534300821c9466bdff5215b539f1f9cffebe484567370481460d114922f62fc7d73daba56801594ac3171b0
6
+ metadata.gz: 4e7b83bac1004b853287689e98ed4d03d7c3c87bd05c4a22831ffe592782ff8916ab576778e41de572f872505e3beab5b923ff5d1d78ccf3e1a99d701ff95737
7
+ data.tar.gz: a0cc1cc27b225c315a68743dff999fc2d8b0d0c9fd5d68f03641b59d53ddc491da21f318b9c075927263367b9db0a50e80af727de0445c86a049bc55d2a32fdf
data/README.md CHANGED
@@ -100,6 +100,54 @@ For more information about a command, run
100
100
  imap-backup help COMMAND
101
101
  ```
102
102
 
103
+ # Performace
104
+
105
+ There are a couple of performance tweaks that you can use
106
+ to improve backup speed.
107
+
108
+ These are activated via two settings:
109
+
110
+ * Global setting "Delay download writes"
111
+ * Account setting "Multi-fetch size"
112
+
113
+ As with all performance tweaks, there are trade-offs.
114
+ If you are using a small virtual server or Raspberry Pi
115
+ to run your backups, you will probably want to leave
116
+ the deafult settings.
117
+ If, on the other hand, you are using a computer with a
118
+ fair bit of RAM, and you are dealing with a *lot* of email,
119
+ then changing these settings may be worthwhile.
120
+
121
+ ## Delay download writes
122
+
123
+ This setting affects all account backups.
124
+
125
+ When not set, each message is written to disk, one at a time.
126
+ Doing so means the message itself is appended to the MBox file,
127
+ but more importantly, the JSON metadata is rewritten to disk
128
+ from scratch.
129
+
130
+ When in use, all of a mailboxes unbackupped messages are
131
+ downloaded first, and then written to disk just once.
132
+
133
+ This speeds up backup as the metadata file is not rewritten
134
+ after each message is added, but it potentially uses much more memory.
135
+
136
+ ## Multi-fetch Size
137
+
138
+ By default, during backup, each message is downloaded one-by-one.
139
+
140
+ Using this setting, you can download chunks of emails at a time,
141
+ potentially speeding up the process.
142
+
143
+ If you're not using "Delayed downlaod writes",
144
+ using multi-fetch *will* mean that the backup process will use
145
+ more memory - equivalent to the size of the greater number
146
+ of messages downloaded at a time.
147
+
148
+ This behaviour may also exceed limits on your email provider,
149
+ so it's best to check before cranking it up!
150
+
103
151
  # Troubleshooting
104
152
 
105
153
  If you have problems:
data/docs/development.md CHANGED
@@ -43,6 +43,15 @@ or
43
43
  $ rspec --tag ~docker
44
44
  ```
45
45
 
46
+ # Performance Specs
47
+
48
+ ```sh
49
+ PERFORMANCE=1 rspec --order=defined
50
+ ```
51
+
52
+ Beware: the performance spec (just backup for now) takes a very
53
+ long time to run, approximately 24 hours!
54
+
46
55
  ### Debugging
47
56
 
48
57
  The feature specs are run 'out of process' via the Aruba gem.
@@ -38,12 +38,19 @@ module Imap::Backup
38
38
 
39
39
  Logger.logger.debug "[#{folder.name}] running backup"
40
40
  serializer.apply_uid_validity(folder.uid_validity)
41
- Downloader.new(
41
+ downloader = Downloader.new(
42
42
  folder,
43
43
  serializer,
44
44
  multi_fetch_size: account.multi_fetch_size,
45
45
  reset_seen_flags_after_fetch: account.reset_seen_flags_after_fetch
46
- ).run
46
+ )
47
+ if account.delay_download_writes
48
+ serializer.transaction do
49
+ downloader.run
50
+ end
51
+ else
52
+ downloader.run
53
+ end
47
54
  if account.mirror_mode
48
55
  Logger.logger.info "Mirror mode - Deleting messages only present locally"
49
56
  LocalOnlyMessageDeleter.new(folder, serializer).run
@@ -15,6 +15,7 @@ module Imap::Backup
15
15
  attr_reader :mirror_mode
16
16
  attr_reader :server
17
17
  attr_reader :connection_options
18
+ attr_accessor :delay_download_writes
18
19
  attr_reader :reset_seen_flags_after_fetch
19
20
  attr_reader :changes
20
21
 
@@ -27,7 +28,8 @@ module Imap::Backup
27
28
  @mirror_mode = options[:mirror_mode]
28
29
  @server = options[:server]
29
30
  @connection_options = options[:connection_options]
30
- @multi_fetch_size = options[:multi_fetch_size]
31
+ @delay_download_writes = true
32
+ @multi_fetch_size_orignal = options[:multi_fetch_size]
31
33
  @reset_seen_flags_after_fetch = options[:reset_seen_flags_after_fetch]
32
34
  @client = nil
33
35
  @changes = {}
@@ -42,6 +44,10 @@ module Imap::Backup
42
44
  client.namespace
43
45
  end
44
46
 
47
+ def capabilities
48
+ client.capability
49
+ end
50
+
45
51
  def restore
46
52
  restore = Account::Restore.new(account: self)
47
53
  restore.run
@@ -75,7 +81,8 @@ module Imap::Backup
75
81
  h[:mirror_mode] = true if @mirror_mode
76
82
  h[:server] = @server if @server
77
83
  h[:connection_options] = @connection_options if @connection_options
78
- h[:multi_fetch_size] = multi_fetch_size if @multi_fetch_size
84
+ h[:delay_download_writes] = delay_download_writes
85
+ h[:multi_fetch_size] = multi_fetch_size
79
86
  if @reset_seen_flags_after_fetch
80
87
  h[:reset_seen_flags_after_fetch] = @reset_seen_flags_after_fetch
81
88
  end
@@ -123,11 +130,13 @@ module Imap::Backup
123
130
  end
124
131
 
125
132
  def multi_fetch_size
126
- int = @multi_fetch_size.to_i
127
- if int.positive?
128
- int
129
- else
130
- DEFAULT_MULTI_FETCH_SIZE
133
+ @multi_fetch_size ||= begin
134
+ int = @multi_fetch_size_orignal.to_i
135
+ if int.positive?
136
+ int
137
+ else
138
+ DEFAULT_MULTI_FETCH_SIZE
139
+ end
131
140
  end
132
141
  end
133
142
 
@@ -41,7 +41,6 @@ module Imap::Backup
41
41
  results = requested_accounts(config).map do |account|
42
42
  serialized_folders = Account::SerializedFolders.new(account: account)
43
43
  folder_results = serialized_folders.map do |serializer, _folder|
44
- puts "serializer: #{serializer.inspect}"
45
44
  serializer.check_integrity!
46
45
  {name: serializer.folder, result: "OK"}
47
46
  rescue Serializer::FolderIntegrityError => e
@@ -23,6 +23,22 @@ module Imap::Backup
23
23
  end
24
24
  end
25
25
 
26
+ desc "capabilities EMAIL", "List server capabilities"
27
+ long_desc <<~DESC
28
+ Lists the IMAP capabilities supported by the IMAP server.
29
+ DESC
30
+ config_option
31
+ format_option
32
+ quiet_option
33
+ verbose_option
34
+ def capabilities(email)
35
+ Imap::Backup::Logger.setup_logging options
36
+ config = load_config(**options)
37
+ account = account(config, email)
38
+ capabilities = account.capabilities
39
+ Kernel.puts capabilities.join(", ")
40
+ end
41
+
26
42
  desc "namespaces EMAIL", "List account namespaces"
27
43
  long_desc <<~DESC
28
44
  Lists namespaces defined for an email account.
@@ -9,7 +9,7 @@ module Imap::Backup
9
9
  class Client::Default
10
10
  extend Forwardable
11
11
  def_delegators :imap, *%i(
12
- append authenticate create expunge namespace
12
+ append authenticate capability create expunge namespace
13
13
  responses uid_fetch uid_search uid_store
14
14
  )
15
15
 
@@ -14,6 +14,8 @@ module Imap::Backup
14
14
  VERSION = "2.0".freeze
15
15
 
16
16
  attr_reader :pathname
17
+ attr_reader :delay_download_writes
18
+ attr_reader :delay_download_writes_modified
17
19
 
18
20
  def self.default_pathname
19
21
  File.join(CONFIGURATION_DIRECTORY, "config.json")
@@ -25,6 +27,8 @@ module Imap::Backup
25
27
 
26
28
  def initialize(path: nil)
27
29
  @pathname = path || self.class.default_pathname
30
+ @delay_download_writes = false
31
+ @delay_download_writes_modified = false
28
32
  end
29
33
 
30
34
  def path
@@ -39,7 +43,8 @@ module Imap::Backup
39
43
  remove_deleted_accounts
40
44
  save_data = {
41
45
  version: VERSION,
42
- accounts: accounts.map(&:to_h)
46
+ accounts: accounts.map(&:to_h),
47
+ delay_download_writes: delay_download_writes
43
48
  }
44
49
  File.open(pathname, "w") { |f| f.write(JSON.pretty_generate(save_data)) }
45
50
  FileUtils.chmod(0o600, pathname) if !windows?
@@ -49,13 +54,26 @@ module Imap::Backup
49
54
  def accounts
50
55
  @accounts ||= begin
51
56
  ensure_loaded!
52
- data[:accounts].map { |data| Account.new(data) }
57
+ accounts = data[:accounts].map do |attr|
58
+ Account.new(attr)
59
+ end
60
+ inject_global_attributes(accounts)
53
61
  end
54
62
  end
55
63
 
64
+ def delay_download_writes=(value)
65
+ ensure_loaded!
66
+
67
+ @delay_download_writes = value
68
+ @delay_download_writes_modified = true
69
+ inject_global_attributes(accounts)
70
+ end
71
+
56
72
  def modified?
57
73
  ensure_loaded!
58
74
 
75
+ return true if delay_download_writes_modified
76
+
59
77
  accounts.any? { |a| a.modified? || a.marked_for_deletion? }
60
78
  end
61
79
 
@@ -65,6 +83,7 @@ module Imap::Backup
65
83
  return true if @data
66
84
 
67
85
  data
86
+ @delay_download_writes = data[:delay_download_writes]
68
87
  true
69
88
  end
70
89
 
@@ -83,6 +102,7 @@ module Imap::Backup
83
102
  end
84
103
 
85
104
  def remove_modified_flags
105
+ @delay_download_writes_modified = false
86
106
  accounts.each(&:clear_changes)
87
107
  end
88
108
 
@@ -90,6 +110,13 @@ module Imap::Backup
90
110
  accounts.reject!(&:marked_for_deletion?)
91
111
  end
92
112
 
113
+ def inject_global_attributes(accounts)
114
+ accounts.map do |a|
115
+ a.delay_download_writes = delay_download_writes
116
+ a
117
+ end
118
+ end
119
+
93
120
  def make_private(path)
94
121
  FileUtils.chmod(0o700, path) if FileMode.new(filename: path).mode != 0o700
95
122
  end
@@ -14,7 +14,7 @@ module Imap::Backup
14
14
  @mbox = mbox
15
15
  end
16
16
 
17
- def run(uid:, message:, flags:)
17
+ def single(uid:, message:, flags:)
18
18
  raise "Can't add messages without uid_validity" if !imap.uid_validity
19
19
 
20
20
  uid = uid.to_i
@@ -26,29 +26,43 @@ module Imap::Backup
26
26
  return
27
27
  end
28
28
 
29
- do_append uid, message, flags
29
+ rollback_on_error do
30
+ do_append uid, message, flags
31
+ rescue StandardError => e
32
+ raise <<-ERROR.gsub(/^\s*/m, "")
33
+ [#{folder}] failed to append message #{uid}:
34
+ #{message}. #{e}:
35
+ #{e.backtrace.join("\n")}"
36
+ ERROR
37
+ end
38
+ end
39
+
40
+ def multi(appends)
41
+ rollback_on_error do
42
+ appends.each do |a|
43
+ do_append a[:uid], a[:message], a[:flags]
44
+ end
45
+ end
30
46
  end
31
47
 
32
48
  private
33
49
 
34
50
  def do_append(uid, message, flags)
35
51
  mboxrd_message = Email::Mboxrd::Message.new(message)
36
- initial = mbox.length || 0
37
- mbox_appended = false
38
- begin
39
- serialized = mboxrd_message.to_serialized
40
- mbox.append serialized
41
- mbox_appended = true
42
- imap.append uid, serialized.length, flags
43
- rescue StandardError => e
44
- mbox.rewind(initial) if mbox_appended
52
+ serialized = mboxrd_message.to_serialized
53
+ mbox.append serialized
54
+ imap.append uid, serialized.length, flags: flags
55
+ end
45
56
 
46
- error = <<-ERROR.gsub(/^\s*/m, "")
47
- [#{folder}] failed to append message #{uid}:
48
- #{message}. #{e}:
49
- #{e.backtrace.join("\n")}"
50
- ERROR
51
- Logger.logger.warn error
57
+ def rollback_on_error(&block)
58
+ imap.transaction do
59
+ mbox.transaction do
60
+ block.call
61
+ rescue StandardError => e
62
+ Logger.logger.error e
63
+ imap.rollback
64
+ mbox.rollback
65
+ end
52
66
  end
53
67
  end
54
68
  end
@@ -15,6 +15,26 @@ module Imap::Backup
15
15
  @uid_validity = nil
16
16
  @messages = nil
17
17
  @version = nil
18
+ @savepoint = nil
19
+ end
20
+
21
+ def transaction(&block)
22
+ fail_in_transaction!(message: "Serializer::Imap: nested transactions are not supported")
23
+
24
+ ensure_loaded
25
+ @savepoint = {messages: messages.dup, uid_validity: uid_validity}
26
+
27
+ block.call
28
+
29
+ @savepoint = nil
30
+ save
31
+ end
32
+
33
+ def rollback
34
+ fail_outside_transaction!
35
+
36
+ @messages = @savepoint[:messages]
37
+ @uid_validity = @savepoint[:uid_validity]
18
38
  end
19
39
 
20
40
  def pathname
@@ -33,7 +53,7 @@ module Imap::Backup
33
53
  true
34
54
  end
35
55
 
36
- def append(uid, length, flags = [])
56
+ def append(uid, length, flags: [])
37
57
  offset =
38
58
  if messages.empty?
39
59
  0
@@ -109,14 +129,16 @@ module Imap::Backup
109
129
  end
110
130
 
111
131
  def save
132
+ return if @savepoint
133
+
112
134
  ensure_loaded
113
135
 
114
136
  raise "Cannot save metadata without a uid_validity" if !uid_validity
115
137
 
116
138
  data = {
117
- version: @version,
118
- uid_validity: @uid_validity,
119
- messages: @messages.map(&:to_h)
139
+ version: version,
140
+ uid_validity: uid_validity,
141
+ messages: messages.map(&:to_h)
120
142
  }
121
143
  content = data.to_json
122
144
  File.open(pathname, "w") { |f| f.write content }
@@ -162,5 +184,13 @@ module Imap::Backup
162
184
  def mbox
163
185
  @mbox ||= Serializer::Mbox.new(folder_path)
164
186
  end
187
+
188
+ def fail_in_transaction!(message: "Serializer::Imap: method not supported inside trasactions")
189
+ raise message if @savepoint
190
+ end
191
+
192
+ def fail_outside_transaction!
193
+ raise "This method can only be called inside a transaction" if !@savepoint
194
+ end
165
195
  end
166
196
  end
@@ -3,9 +3,25 @@ module Imap; end
3
3
  module Imap::Backup
4
4
  class Serializer::Mbox
5
5
  attr_reader :folder_path
6
+ attr_reader :savepoint
6
7
 
7
8
  def initialize(folder_path)
8
9
  @folder_path = folder_path
10
+ @savepoint = nil
11
+ end
12
+
13
+ def transaction(&block)
14
+ fail_in_transaction!(message: "Nested transactions are not supported")
15
+
16
+ @savepoint = {length: length}
17
+ block.call
18
+ @savepoint = nil
19
+ end
20
+
21
+ def rollback
22
+ fail_outside_transaction!
23
+
24
+ rewind(@savepoint[:length])
9
25
  end
10
26
 
11
27
  def valid?
@@ -31,6 +47,10 @@ module Imap::Backup
31
47
  File.unlink(pathname)
32
48
  end
33
49
 
50
+ def exist?
51
+ File.exist?(pathname)
52
+ end
53
+
34
54
  def length
35
55
  return nil if !exist?
36
56
 
@@ -51,18 +71,24 @@ module Imap::Backup
51
71
  end
52
72
  end
53
73
 
74
+ def touch
75
+ File.open(pathname, "a") {}
76
+ end
77
+
78
+ private
79
+
54
80
  def rewind(length)
55
81
  File.open(pathname, File::RDWR | File::CREAT, 0o644) do |f|
56
82
  f.truncate(length)
57
83
  end
58
84
  end
59
85
 
60
- def touch
61
- File.open(pathname, "a") {}
86
+ def fail_in_transaction!(message: "Method not supported inside trasactions")
87
+ raise message if savepoint
62
88
  end
63
89
 
64
- def exist?
65
- File.exist?(pathname)
90
+ def fail_outside_transaction!
91
+ raise "This method can only be called inside a transaction" if !savepoint
66
92
  end
67
93
  end
68
94
  end
@@ -28,16 +28,36 @@ module Imap::Backup
28
28
 
29
29
  attr_reader :folder
30
30
  attr_reader :path
31
+ attr_reader :dirty
31
32
 
32
33
  def initialize(path, folder)
33
34
  @path = path
34
35
  @folder = folder
35
36
  @validated = nil
37
+ @dirty = nil
38
+ end
39
+
40
+ def transaction(&block)
41
+ fail_in_transaction!(:transaction, message: "nested transactions are not supported")
42
+
43
+ validate!
44
+ @dirty = {append: []}
45
+
46
+ block.call
47
+
48
+ if dirty[:append].any?
49
+ appender = Serializer::Appender.new(folder: sanitized, imap: imap, mbox: mbox)
50
+ appender.multi(dirty[:append])
51
+ end
52
+
53
+ @dirty = nil
36
54
  end
37
55
 
38
56
  # Returns true if there are existing, valid files
39
57
  # false otherwise (in which case any existing files are deleted)
40
58
  def validate!
59
+ fail_in_transaction!(:validate!)
60
+
41
61
  return true if @validated
42
62
 
43
63
  optionally_migrate2to3
@@ -53,6 +73,8 @@ module Imap::Backup
53
73
  end
54
74
 
55
75
  def check_integrity!
76
+ fail_in_transaction!(:check_integrity!)
77
+
56
78
  if !imap.valid?
57
79
  message = ".imap file '#{imap.pathname}' is corrupt"
58
80
  raise FolderIntegrityError, message
@@ -104,6 +126,8 @@ module Imap::Backup
104
126
  end
105
127
 
106
128
  def delete
129
+ fail_in_transaction!(:delete)
130
+
107
131
  imap.delete
108
132
  @imap = nil
109
133
  mbox.delete
@@ -111,6 +135,7 @@ module Imap::Backup
111
135
  end
112
136
 
113
137
  def apply_uid_validity(value)
138
+ fail_in_transaction!(:apply_uid_validity)
114
139
  validate!
115
140
 
116
141
  case
@@ -126,19 +151,26 @@ module Imap::Backup
126
151
  end
127
152
 
128
153
  def force_uid_validity(value)
154
+ fail_in_transaction!(:force_uid_validity)
129
155
  validate!
130
156
 
131
157
  internal_force_uid_validity(value)
132
158
  end
133
159
 
134
160
  def append(uid, message, flags)
135
- validate!
161
+ if dirty
162
+ dirty[:append] << {uid: uid, message: message, flags: flags}
163
+ else
164
+ validate!
136
165
 
137
- appender = Serializer::Appender.new(folder: sanitized, imap: imap, mbox: mbox)
138
- appender.run(uid: uid, message: message, flags: flags)
166
+ appender = Serializer::Appender.new(folder: sanitized, imap: imap, mbox: mbox)
167
+ appender.single(uid: uid, message: message, flags: flags)
168
+ end
139
169
  end
140
170
 
141
171
  def update(uid, flags: nil)
172
+ fail_in_transaction!(:update)
173
+
142
174
  message = imap.get(uid)
143
175
  return if !message
144
176
 
@@ -147,17 +179,21 @@ module Imap::Backup
147
179
  end
148
180
 
149
181
  def each_message(required_uids = nil, &block)
182
+ fail_in_transaction!(:each_message)
183
+
184
+ return enum_for(:each_message, required_uids) if !block
185
+
150
186
  required_uids ||= uids
151
187
 
152
188
  validate!
153
189
 
154
- return enum_for(:each_message, required_uids) if !block
155
-
156
190
  enumerator = Serializer::MessageEnumerator.new(imap: imap)
157
191
  enumerator.run(uids: required_uids, &block)
158
192
  end
159
193
 
160
194
  def filter(&block)
195
+ fail_in_transaction!(:filter)
196
+
161
197
  temp_name = Serializer::UnusedNameFinder.new(serializer: self).run
162
198
  temp_folder_path = self.class.folder_path_for(path: path, folder: temp_name)
163
199
  new_mbox = Serializer::Mbox.new(temp_folder_path)
@@ -167,7 +203,7 @@ module Imap::Backup
167
203
  enumerator = Serializer::MessageEnumerator.new(imap: imap)
168
204
  enumerator.run(uids: uids) do |message|
169
205
  keep = block.call(message)
170
- appender.run(uid: message.uid, message: message.body, flags: message.flags) if keep
206
+ appender.single(uid: message.uid, message: message.body, flags: message.flags) if keep
171
207
  end
172
208
  imap.delete
173
209
  new_imap.rename imap.folder_path
@@ -251,5 +287,13 @@ module Imap::Backup
251
287
  rename new_name
252
288
  new_name
253
289
  end
290
+
291
+ def fail_in_transaction!(method, message: "not supported inside trasactions")
292
+ raise "Serializer##{method} #{message}" if dirty
293
+ end
294
+
295
+ def fail_outside_transaction!(method)
296
+ raise "Serializer##{method} can only be called inside a transaction" if !dirty
297
+ end
254
298
  end
255
299
  end
@@ -39,6 +39,7 @@ module Imap::Backup
39
39
  MENU
40
40
  account_items menu
41
41
  add_account_item menu
42
+ toggle_delay_download_writes menu
42
43
  if config.modified?
43
44
  menu.choice("save and exit") do
44
45
  config.save
@@ -70,6 +71,16 @@ module Imap::Backup
70
71
  end
71
72
  end
72
73
 
74
+ def toggle_delay_download_writes(menu)
75
+ new_value = config.delay_download_writes ? false : true
76
+ modified = config.delay_download_writes_modified ? " *" : ""
77
+ change = config.delay_download_writes ? "don't delay" : "delay"
78
+ menu_item = "#{change} download writes#{modified}"
79
+ menu.choice(menu_item) do
80
+ config.delay_download_writes = new_value
81
+ end
82
+ end
83
+
73
84
  def default_account_config(username)
74
85
  Imap::Backup::Account.new(
75
86
  username: username,
@@ -1,9 +1,9 @@
1
1
  module Imap; end
2
2
 
3
3
  module Imap::Backup
4
- MAJOR = 10
4
+ MAJOR = 11
5
5
  MINOR = 0
6
- REVISION = 1
7
- PRE = nil
6
+ REVISION = 0
7
+ PRE = "rc1".freeze
8
8
  VERSION = [MAJOR, MINOR, REVISION, PRE].compact.map(&:to_s).join(".")
9
9
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imap-backup
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.0.1
4
+ version: 11.0.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joe Yates
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-16 00:00:00.000000000 Z
11
+ date: 2023-07-18 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: highline
@@ -305,9 +305,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
305
305
  version: '2.6'
306
306
  required_rubygems_version: !ruby/object:Gem::Requirement
307
307
  requirements:
308
- - - ">="
308
+ - - ">"
309
309
  - !ruby/object:Gem::Version
310
- version: '0'
310
+ version: 1.3.1
311
311
  requirements: []
312
312
  rubygems_version: 3.3.7
313
313
  signing_key: