expire 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.reek.yml +23 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +45 -0
  6. data/.simplecov +3 -0
  7. data/.travis.yml +7 -0
  8. data/Gemfile +6 -0
  9. data/Gemfile.lock +152 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +551 -0
  12. data/Rakefile +11 -0
  13. data/bin/console +14 -0
  14. data/bin/setup +8 -0
  15. data/example_rules/bad_rules.yml +2 -0
  16. data/example_rules/good_rules.yml +1 -0
  17. data/exe/expire +7 -0
  18. data/expire.gemspec +54 -0
  19. data/lib/expire.rb +51 -0
  20. data/lib/expire/all_backups_expired_error.rb +7 -0
  21. data/lib/expire/backup.rb +74 -0
  22. data/lib/expire/backup_from_path_service.rb +56 -0
  23. data/lib/expire/backup_list.rb +69 -0
  24. data/lib/expire/cli.rb +221 -0
  25. data/lib/expire/command.rb +122 -0
  26. data/lib/expire/commands/newest.rb +21 -0
  27. data/lib/expire/commands/oldest.rb +21 -0
  28. data/lib/expire/commands/purge.rb +23 -0
  29. data/lib/expire/commands/remove.rb +26 -0
  30. data/lib/expire/commands/rule_classes.rb +18 -0
  31. data/lib/expire/commands/rule_names.rb +20 -0
  32. data/lib/expire/commands/rule_option_names.rb +20 -0
  33. data/lib/expire/from_now_keep_adjective_for_rule_base.rb +38 -0
  34. data/lib/expire/from_now_keep_daily_for_rule.rb +7 -0
  35. data/lib/expire/from_now_keep_hourly_for_rule.rb +7 -0
  36. data/lib/expire/from_now_keep_monthly_for_rule.rb +7 -0
  37. data/lib/expire/from_now_keep_most_recent_for_rule.rb +41 -0
  38. data/lib/expire/from_now_keep_weekly_for_rule.rb +8 -0
  39. data/lib/expire/from_now_keep_yearly_for_rule.rb +8 -0
  40. data/lib/expire/from_range_value.rb +29 -0
  41. data/lib/expire/generate_backup_list_service.rb +45 -0
  42. data/lib/expire/invalid_path_error.rb +7 -0
  43. data/lib/expire/keep_adjective_for_rule_base.rb +34 -0
  44. data/lib/expire/keep_adjective_rule_base.rb +97 -0
  45. data/lib/expire/keep_daily_for_rule.rb +7 -0
  46. data/lib/expire/keep_daily_rule.rb +7 -0
  47. data/lib/expire/keep_hourly_for_rule.rb +7 -0
  48. data/lib/expire/keep_hourly_rule.rb +7 -0
  49. data/lib/expire/keep_monthly_for_rule.rb +7 -0
  50. data/lib/expire/keep_monthly_rule.rb +7 -0
  51. data/lib/expire/keep_most_recent_for_rule.rb +31 -0
  52. data/lib/expire/keep_most_recent_rule.rb +38 -0
  53. data/lib/expire/keep_weekly_for_rule.rb +8 -0
  54. data/lib/expire/keep_weekly_rule.rb +7 -0
  55. data/lib/expire/keep_yearly_for_rule.rb +7 -0
  56. data/lib/expire/keep_yearly_rule.rb +7 -0
  57. data/lib/expire/no_backups_error.rb +7 -0
  58. data/lib/expire/no_rules_error.rb +7 -0
  59. data/lib/expire/numerus_unit.rb +10 -0
  60. data/lib/expire/path_already_exists_error.rb +7 -0
  61. data/lib/expire/playground.rb +62 -0
  62. data/lib/expire/purge_service.rb +91 -0
  63. data/lib/expire/refine_all_and_none.rb +29 -0
  64. data/lib/expire/report_base.rb +23 -0
  65. data/lib/expire/report_enhanced.rb +14 -0
  66. data/lib/expire/report_expired.rb +10 -0
  67. data/lib/expire/report_kept.rb +10 -0
  68. data/lib/expire/report_null.rb +21 -0
  69. data/lib/expire/report_simple.rb +14 -0
  70. data/lib/expire/rule_base.rb +56 -0
  71. data/lib/expire/rule_list.rb +52 -0
  72. data/lib/expire/rules.rb +66 -0
  73. data/lib/expire/templates/newest/.gitkeep +1 -0
  74. data/lib/expire/templates/oldest/.gitkeep +1 -0
  75. data/lib/expire/templates/purge/.gitkeep +1 -0
  76. data/lib/expire/templates/remove/.gitkeep +1 -0
  77. data/lib/expire/templates/rule_classes/.gitkeep +1 -0
  78. data/lib/expire/templates/rule_names/.gitkeep +1 -0
  79. data/lib/expire/templates/rule_option_names/.gitkeep +1 -0
  80. data/lib/expire/unknown_rule_error.rb +10 -0
  81. data/lib/expire/version.rb +9 -0
  82. metadata +321 -0
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'cucumber/rake/task'
3
+ require 'rspec/core/rake_task'
4
+
5
+ Cucumber::Rake::Task.new(:cucumber) do |task|
6
+ task.cucumber_opts = '--format pretty'
7
+ end
8
+
9
+ RSpec::Core::RakeTask.new(:spec)
10
+
11
+ task :default => [:spec, :cucumber]
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "expire"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,2 @@
1
+ ---
2
+ no_such_parameter: no such value
@@ -0,0 +1 @@
1
+ monthly: 5
data/exe/expire ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # -*- mode: ruby
5
+
6
+ require 'expire/cli'
7
+ Expire::CLI.start
data/expire.gemspec ADDED
@@ -0,0 +1,54 @@
1
+
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "expire/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "expire"
8
+ spec.version = Expire::VERSION
9
+ spec.authors = ["Thomas Regnet"]
10
+ # spec.email = ["TODO: Write your email address"]
11
+
12
+ spec.summary = %q{Calculate expired backups.}
13
+ spec.homepage = 'https://github.com/thomasregnet/expire'
14
+ spec.license = "MIT"
15
+
16
+ # Prevent pushing this gem to RubyGems.org.
17
+ # To allow pushes either set the 'allowed_push_host'
18
+ # to allow pushing to a single host or delete this section
19
+ # to allow pushing to any host.
20
+ if spec.respond_to?(:metadata)
21
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
22
+
23
+ spec.metadata['homepage_uri'] = spec.homepage
24
+ spec.metadata['source_code_uri'] = spec.homepage
25
+ else
26
+ raise "RubyGems 2.0 or newer is required to protect against " \
27
+ "public gem pushes."
28
+ end
29
+
30
+ # Specify which files should be added to the gem when it is released.
31
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
32
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ["lib"]
38
+
39
+ spec.add_dependency 'activesupport', '~> 6.1'
40
+ spec.add_dependency 'pastel', '~> 0.8'
41
+ spec.add_dependency 'thor', '~> 1.1'
42
+ spec.add_dependency 'zeitwerk', "~> 2.4"
43
+
44
+ spec.add_development_dependency 'aruba', '~> 1.0'
45
+ spec.add_development_dependency 'bundler', '~> 2.1'
46
+ spec.add_development_dependency 'byebug', '~> 11.1'
47
+ spec.add_development_dependency 'cucumber', '~> 5.3'
48
+ spec.add_development_dependency 'rake', '~> 13.0'
49
+ spec.add_development_dependency 'reek', '~> 6.0'
50
+ spec.add_development_dependency 'rspec', '~> 3.0'
51
+ spec.add_development_dependency 'rubocop', '~> 1.9'
52
+ spec.add_development_dependency 'rubocop-rspec', '~> 2.2'
53
+ spec.add_development_dependency 'simplecov', '~> 0.21'
54
+ end
data/lib/expire.rb ADDED
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support'
4
+ require 'active_support/core_ext'
5
+ require 'active_support/core_ext/date_and_time/calculations'
6
+ require 'date'
7
+ require 'yaml'
8
+ require 'zeitwerk'
9
+
10
+ loader = Zeitwerk::Loader.for_gem
11
+ loader.inflector.inflect('cli' => 'CLI')
12
+ loader.setup
13
+
14
+ # Expire backup directories
15
+ module Expire
16
+ # Exception derived from StandardError
17
+ class Error < StandardError; end
18
+ # Your code goes here...
19
+
20
+ def self.create_playground(base)
21
+ Playground.create(base)
22
+ end
23
+
24
+ def self.newest(path)
25
+ GenerateBackupListService.call(path).newest
26
+ end
27
+
28
+ def self.oldest(path)
29
+ GenerateBackupListService.call(path).oldest
30
+ end
31
+
32
+ def self.purge(path, options)
33
+ PurgeService.call(path, options)
34
+ end
35
+
36
+ def self.remove(path)
37
+ FileUtils.rm_r(path)
38
+ end
39
+
40
+ def self.rule_classes
41
+ Expire::RuleList.class_names
42
+ end
43
+
44
+ def self.rule_names
45
+ Expire::RuleList.names
46
+ end
47
+
48
+ def self.rule_option_names
49
+ Expire::RuleList.option_names
50
+ end
51
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Thrown if all backups are expired
5
+ class AllBackupsExpiredError < StandardError
6
+ end
7
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Representation of a single backup
5
+ class Backup < Delegator
6
+ include Comparable
7
+
8
+ def initialize(datetime:, pathname:)
9
+ @datetime = datetime
10
+ @pathname = pathname
11
+
12
+ # @reasons_to_keep is a Set so a reason can added multiple times
13
+ # but appears only once
14
+ @reasons_to_keep = Set.new
15
+ end
16
+
17
+ attr_reader :datetime, :pathname, :reasons_to_keep
18
+ alias __getobj__ datetime
19
+
20
+ def same_hour?(other)
21
+ return false unless same_day?(other)
22
+ return true if hour == other.hour
23
+
24
+ false
25
+ end
26
+
27
+ def same_day?(other)
28
+ return false unless same_week?(other)
29
+ return true if day == other.day
30
+
31
+ false
32
+ end
33
+
34
+ def same_week?(other)
35
+ return false unless same_year?(other)
36
+ return true if cweek == other.cweek
37
+
38
+ false
39
+ end
40
+
41
+ def same_month?(other)
42
+ return false unless same_year?(other)
43
+ return true if month == other.month
44
+
45
+ false
46
+ end
47
+
48
+ def same_year?(other)
49
+ year == other.year
50
+ end
51
+
52
+ # The <=> method seems not to be delegated so we need to implement it
53
+ # Note that this Class includes the Comparable module
54
+ def <=>(other)
55
+ datetime <=> other.datetime
56
+ end
57
+
58
+ def add_reason_to_keep(reason)
59
+ reasons_to_keep << reason
60
+ end
61
+
62
+ # def datetime
63
+ # backup.datetime
64
+ # end
65
+
66
+ def expired?
67
+ reasons_to_keep.empty?
68
+ end
69
+
70
+ def keep?
71
+ reasons_to_keep.any?
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Expire
4
+ # Take a path and return an instance of Expire::Backup
5
+ class BackupFromPathService
6
+ def self.call(path:, by: :path)
7
+ new(path: path, by: by).call
8
+ end
9
+
10
+ def initialize(path:, by: :path)
11
+ @by = by
12
+ @pathname = Pathname.new(path)
13
+
14
+ raise ArgumentError, "by: must be :ctime, :mtime or :path, not #{by}" unless %i[ctime mtime path].include?(by)
15
+ end
16
+
17
+ attr_reader :by, :pathname
18
+
19
+ def call
20
+ Backup.new(datetime: datetime, pathname: pathname)
21
+ end
22
+
23
+ private
24
+
25
+ def datetime
26
+ digits = extract_digits
27
+
28
+ year = digits[0..3].to_i
29
+ month = digits[4..5].to_i
30
+ day = digits[6..7].to_i
31
+ hour = digits[8..9].to_i
32
+ minute = digits[10..11].to_i
33
+
34
+ datetime_for(year, month, day, hour, minute)
35
+ end
36
+
37
+ def datetime_for(year, month, day, hour, minute)
38
+ DateTime.new(year, month, day, hour, minute)
39
+ rescue Date::Error
40
+ raise InvalidPathError, "can't construct date and time from #{pathname}"
41
+ end
42
+
43
+ def extract_digits
44
+ basename = pathname.basename.to_s
45
+
46
+ digits = basename.gsub(/[^0-9]/, '')
47
+
48
+ digits_length = digits.length
49
+
50
+ return digits if digits_length == 12
51
+ return digits if digits_length == 14
52
+
53
+ raise InvalidPathError, "can't extract date and time from #{basename}"
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ module Expire
6
+ # All Backups go here
7
+ class BackupList
8
+ include Enumerable
9
+ extend Forwardable
10
+
11
+ def initialize(backups = [])
12
+ # @backups = backups.sort.reverse
13
+ @backups = backups
14
+ end
15
+
16
+ attr_reader :backups
17
+
18
+ def_delegators :backups, :each, :empty?, :last, :length, :<<
19
+
20
+ def one_per(noun)
21
+ backups_per_noun = self.class.new
22
+ return backups_per_noun unless any?
23
+
24
+ reversed = sort.reverse
25
+
26
+ backups_per_noun << reversed.first
27
+
28
+ message = "same_#{noun}?"
29
+
30
+ reversed.each do |backup|
31
+ backups_per_noun << backup unless backup.send(message, backups_per_noun.last)
32
+ end
33
+
34
+ backups_per_noun
35
+ end
36
+
37
+ def most_recent(amount = 1)
38
+ self.class.new(sort.reverse.first(amount))
39
+ end
40
+
41
+ def newest
42
+ backups.max
43
+ end
44
+
45
+ def oldest
46
+ backups.min
47
+ end
48
+
49
+ def not_older_than(reference_datetime)
50
+ sort.select { |backup| backup.datetime >= reference_datetime }
51
+ end
52
+
53
+ def expired
54
+ self.class.new(backups.select(&:expired?))
55
+ end
56
+
57
+ def expired_count
58
+ expired.length
59
+ end
60
+
61
+ def keep
62
+ self.class.new(backups.select(&:keep?))
63
+ end
64
+
65
+ def keep_count
66
+ keep.length
67
+ end
68
+ end
69
+ end
data/lib/expire/cli.rb ADDED
@@ -0,0 +1,221 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'expire'
4
+ require 'thor'
5
+
6
+ module Expire
7
+ # Command line interface
8
+ # rubocop:disable Metrics/ClassLength
9
+ class CLI < Thor
10
+ desc 'rule_option_names', 'List rule option names ordered by their rank'
11
+ method_option(
12
+ :help,
13
+ aliases: '-h',
14
+ type: :boolean,
15
+ desc: 'Display usage information'
16
+ )
17
+ def rule_option_names(*)
18
+ if options[:help]
19
+ invoke :help, ['rule_option_names']
20
+ else
21
+ require_relative 'commands/rule_option_names'
22
+ Expire::Commands::RuleOptionNames.new(options).execute
23
+ end
24
+ end
25
+
26
+ desc 'rule_names', 'List rule names ordered by their rank'
27
+ method_option(
28
+ :help,
29
+ aliases: '-h',
30
+ type: :boolean,
31
+ desc: 'Display usage information'
32
+ )
33
+ def rule_names(*)
34
+ if options[:help]
35
+ invoke :help, ['rule_names']
36
+ else
37
+ require_relative 'commands/rule_names'
38
+ Expire::Commands::RuleNames.new(options).execute
39
+ end
40
+ end
41
+
42
+ desc 'rule_classes', 'List rule classes ordered by their rank'
43
+ method_option(
44
+ :help,
45
+ aliases: '-h',
46
+ type: :boolean,
47
+ desc: 'Display usage information'
48
+ )
49
+ def rule_classes(*)
50
+ if options[:help]
51
+ invoke :help, ['rule_classes']
52
+ else
53
+ require_relative 'commands/rule_classes'
54
+ Expire::Commands::RuleClasses.new(options).execute
55
+ end
56
+ end
57
+
58
+ desc 'remove PATH', 'Remove PATH from the filesystem'
59
+ method_option :help, aliases: '-h', type: :boolean,
60
+ desc: 'Display usage information'
61
+ def remove(path)
62
+ if options[:help]
63
+ invoke :help, ['remove']
64
+ else
65
+ require_relative 'commands/remove'
66
+ Expire::Commands::Remove.new(path: path).execute
67
+ end
68
+ end
69
+
70
+ desc 'oldest PATH', 'Show the oldest backup'
71
+ method_option :help, aliases: '-h', type: :boolean,
72
+ desc: 'Display usage information'
73
+ def oldest(path)
74
+ if options[:help]
75
+ invoke :help, ['oldest']
76
+ else
77
+ require_relative 'commands/oldest'
78
+ Expire::Commands::Oldest.new(path, options).execute
79
+ end
80
+ end
81
+
82
+ desc 'newest PATH', 'Show the newest backup'
83
+ method_option :help, aliases: '-h', type: :boolean,
84
+ desc: 'Display usage information'
85
+ def newest(path)
86
+ if options[:help]
87
+ invoke :help, ['newest']
88
+ else
89
+ require_relative 'commands/newest'
90
+ Expire::Commands::Newest.new(path, options).execute
91
+ end
92
+ end
93
+ # Play with test-data
94
+ class Playground < Thor
95
+ desc 'create PATH', 'play with test-data'
96
+ def create(path)
97
+ Expire.create_playground(path)
98
+ end
99
+ end
100
+
101
+ desc 'purge PATH', 'Remove expired backups from PATH'
102
+ method_option :help, aliases: '-h', type: :boolean,
103
+ desc: 'Display usage information'
104
+ method_option :format, aliases: '-f', type: :string,
105
+ enum: %w[expired kept none simple enhanced],
106
+ default: 'none',
107
+ desc: 'output format'
108
+ method_option :purge_command, aliases: '--cmd', type: :string,
109
+ desc: 'run command to purge the backup'
110
+ method_option :rules_file, aliases: '-r', type: :string,
111
+ desc: 'read expire-rules from file'
112
+ method_option :simulate, aliases: '-s', type: :boolean,
113
+ desc: 'Simulate purge, do not delete anything'
114
+ method_option(
115
+ :keep_most_recent,
116
+ type: :string,
117
+ desc: 'keep the <integer> most recent backups'
118
+ )
119
+ method_option(
120
+ :keep_most_recent_for,
121
+ type: :string,
122
+ desc: 'keep the most recent backups for <integer> <unit>'
123
+ )
124
+ method_option(
125
+ :from_now_keep_most_recent_for,
126
+ type: :string,
127
+ desc: 'keep the most recent backups for <integer> <unit> calculated from now'
128
+ )
129
+ method_option(
130
+ :keep_hourly,
131
+ type: :string,
132
+ desc: 'keep the <integer> most recent backups from different hours'
133
+ )
134
+ method_option(
135
+ :keep_daily,
136
+ type: :string,
137
+ desc: 'keep the <integer> most recent backups from different days'
138
+ )
139
+ method_option(
140
+ :keep_weekly,
141
+ type: :string,
142
+ desc: 'keep the <integer> most recent backups from different weeks'
143
+ )
144
+ method_option(
145
+ :keep_monthly,
146
+ type: :string,
147
+ desc: 'keep the <integer> most recent backups from different months'
148
+ )
149
+ method_option(
150
+ :keep_yearly,
151
+ type: :string,
152
+ desc: 'keep the <integer> most recent backups from different years'
153
+ )
154
+ method_option(
155
+ :keep_hourly_for,
156
+ type: :string,
157
+ desc: 'keep one backup per hour for <integer> <unit>'
158
+ )
159
+ method_option(
160
+ :keep_daily_for,
161
+ type: :string,
162
+ desc: 'keep one backup per day for <integer> <unit>'
163
+ )
164
+ method_option(
165
+ :keep_weekly_for,
166
+ type: :string,
167
+ desc: 'keep one backup per week for <integer> <unit>'
168
+ )
169
+ method_option(
170
+ :keep_monthly_for,
171
+ type: :string,
172
+ desc: 'keep one backup per month for <integer> <unit>'
173
+ )
174
+ method_option(
175
+ :keep_yearly_for,
176
+ type: :string,
177
+ desc: 'keep one backup per year for <integer> <unit>'
178
+ )
179
+ method_option(
180
+ :from_now_keep_hourly_for,
181
+ type: :string,
182
+ desc: 'keep one backup per hour for <integer> <unit> calculated from now'
183
+ )
184
+ method_option(
185
+ :from_now_keep_daily_for,
186
+ type: :string,
187
+ desc: 'keep one backup per hour for <integer> <unit> calculated from now'
188
+ )
189
+ method_option(
190
+ :from_now_keep_weekly_for,
191
+ type: :string,
192
+ desc: 'keep one backup per hour for <integer> <unit> calculated from now'
193
+ )
194
+ method_option(
195
+ :from_now_keep_monthly_for,
196
+ type: :string,
197
+ desc: 'keep one backup per hour for <integer> <unit> calculated from now'
198
+ )
199
+ method_option(
200
+ :from_now_keep_yearly_for,
201
+ type: :string,
202
+ desc: 'keep one backup per hour for <integer> <unit> calculated from now'
203
+ )
204
+ def purge(path)
205
+ if options[:help]
206
+ invoke :help, ['purge']
207
+ else
208
+ require_relative 'commands/purge'
209
+ Expire::Commands::Purge.new(path, options).execute
210
+ end
211
+ end
212
+
213
+ desc 'playground', 'play with test-data'
214
+ subcommand 'playground', Playground
215
+
216
+ def self.exit_on_failure?
217
+ true
218
+ end
219
+ end
220
+ # rubocop:enable Metrics/ClassLength
221
+ end