expire 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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