expire 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.reek.yml +23 -0
- data/.rspec +3 -0
- data/.rubocop.yml +45 -0
- data/.simplecov +3 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +152 -0
- data/LICENSE.txt +21 -0
- data/README.md +551 -0
- data/Rakefile +11 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/example_rules/bad_rules.yml +2 -0
- data/example_rules/good_rules.yml +1 -0
- data/exe/expire +7 -0
- data/expire.gemspec +54 -0
- data/lib/expire.rb +51 -0
- data/lib/expire/all_backups_expired_error.rb +7 -0
- data/lib/expire/backup.rb +74 -0
- data/lib/expire/backup_from_path_service.rb +56 -0
- data/lib/expire/backup_list.rb +69 -0
- data/lib/expire/cli.rb +221 -0
- data/lib/expire/command.rb +122 -0
- data/lib/expire/commands/newest.rb +21 -0
- data/lib/expire/commands/oldest.rb +21 -0
- data/lib/expire/commands/purge.rb +23 -0
- data/lib/expire/commands/remove.rb +26 -0
- data/lib/expire/commands/rule_classes.rb +18 -0
- data/lib/expire/commands/rule_names.rb +20 -0
- data/lib/expire/commands/rule_option_names.rb +20 -0
- data/lib/expire/from_now_keep_adjective_for_rule_base.rb +38 -0
- data/lib/expire/from_now_keep_daily_for_rule.rb +7 -0
- data/lib/expire/from_now_keep_hourly_for_rule.rb +7 -0
- data/lib/expire/from_now_keep_monthly_for_rule.rb +7 -0
- data/lib/expire/from_now_keep_most_recent_for_rule.rb +41 -0
- data/lib/expire/from_now_keep_weekly_for_rule.rb +8 -0
- data/lib/expire/from_now_keep_yearly_for_rule.rb +8 -0
- data/lib/expire/from_range_value.rb +29 -0
- data/lib/expire/generate_backup_list_service.rb +45 -0
- data/lib/expire/invalid_path_error.rb +7 -0
- data/lib/expire/keep_adjective_for_rule_base.rb +34 -0
- data/lib/expire/keep_adjective_rule_base.rb +97 -0
- data/lib/expire/keep_daily_for_rule.rb +7 -0
- data/lib/expire/keep_daily_rule.rb +7 -0
- data/lib/expire/keep_hourly_for_rule.rb +7 -0
- data/lib/expire/keep_hourly_rule.rb +7 -0
- data/lib/expire/keep_monthly_for_rule.rb +7 -0
- data/lib/expire/keep_monthly_rule.rb +7 -0
- data/lib/expire/keep_most_recent_for_rule.rb +31 -0
- data/lib/expire/keep_most_recent_rule.rb +38 -0
- data/lib/expire/keep_weekly_for_rule.rb +8 -0
- data/lib/expire/keep_weekly_rule.rb +7 -0
- data/lib/expire/keep_yearly_for_rule.rb +7 -0
- data/lib/expire/keep_yearly_rule.rb +7 -0
- data/lib/expire/no_backups_error.rb +7 -0
- data/lib/expire/no_rules_error.rb +7 -0
- data/lib/expire/numerus_unit.rb +10 -0
- data/lib/expire/path_already_exists_error.rb +7 -0
- data/lib/expire/playground.rb +62 -0
- data/lib/expire/purge_service.rb +91 -0
- data/lib/expire/refine_all_and_none.rb +29 -0
- data/lib/expire/report_base.rb +23 -0
- data/lib/expire/report_enhanced.rb +14 -0
- data/lib/expire/report_expired.rb +10 -0
- data/lib/expire/report_kept.rb +10 -0
- data/lib/expire/report_null.rb +21 -0
- data/lib/expire/report_simple.rb +14 -0
- data/lib/expire/rule_base.rb +56 -0
- data/lib/expire/rule_list.rb +52 -0
- data/lib/expire/rules.rb +66 -0
- data/lib/expire/templates/newest/.gitkeep +1 -0
- data/lib/expire/templates/oldest/.gitkeep +1 -0
- data/lib/expire/templates/purge/.gitkeep +1 -0
- data/lib/expire/templates/remove/.gitkeep +1 -0
- data/lib/expire/templates/rule_classes/.gitkeep +1 -0
- data/lib/expire/templates/rule_names/.gitkeep +1 -0
- data/lib/expire/templates/rule_option_names/.gitkeep +1 -0
- data/lib/expire/unknown_rule_error.rb +10 -0
- data/lib/expire/version.rb +9 -0
- metadata +321 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
module Expire
|
6
|
+
# Helpers for TTY
|
7
|
+
class Command
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
def_delegators :command, :run
|
11
|
+
|
12
|
+
# Execute this command
|
13
|
+
#
|
14
|
+
# @api public
|
15
|
+
def execute(*)
|
16
|
+
raise(
|
17
|
+
NotImplementedError,
|
18
|
+
"#{self.class}##{__method__} must be implemented"
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# The external commands runner
|
23
|
+
#
|
24
|
+
# @see http://www.rubydoc.info/gems/tty-command
|
25
|
+
#
|
26
|
+
# @api public
|
27
|
+
def command(**options)
|
28
|
+
require 'tty-command'
|
29
|
+
TTY::Command.new(options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# The cursor movement
|
33
|
+
#
|
34
|
+
# @see http://www.rubydoc.info/gems/tty-cursor
|
35
|
+
#
|
36
|
+
# @api public
|
37
|
+
def cursor
|
38
|
+
require 'tty-cursor'
|
39
|
+
TTY::Cursor
|
40
|
+
end
|
41
|
+
|
42
|
+
# Open a file or text in the user's preferred editor
|
43
|
+
#
|
44
|
+
# @see http://www.rubydoc.info/gems/tty-editor
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def editor
|
48
|
+
require 'tty-editor'
|
49
|
+
TTY::Editor
|
50
|
+
end
|
51
|
+
|
52
|
+
# File manipulation utility methods
|
53
|
+
#
|
54
|
+
# @see http://www.rubydoc.info/gems/tty-file
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def generator
|
58
|
+
require 'tty-file'
|
59
|
+
TTY::File
|
60
|
+
end
|
61
|
+
|
62
|
+
# Terminal output paging
|
63
|
+
#
|
64
|
+
# @see http://www.rubydoc.info/gems/tty-pager
|
65
|
+
#
|
66
|
+
# @api public
|
67
|
+
def pager(**options)
|
68
|
+
require 'tty-pager'
|
69
|
+
TTY::Pager.new(options)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Terminal platform and OS properties
|
73
|
+
#
|
74
|
+
# @see http://www.rubydoc.info/gems/tty-pager
|
75
|
+
#
|
76
|
+
# @api public
|
77
|
+
def platform
|
78
|
+
require 'tty-platform'
|
79
|
+
TTY::Platform.new
|
80
|
+
end
|
81
|
+
|
82
|
+
# The interactive prompt
|
83
|
+
#
|
84
|
+
# @see http://www.rubydoc.info/gems/tty-prompt
|
85
|
+
#
|
86
|
+
# @api public
|
87
|
+
def prompt(**options)
|
88
|
+
require 'tty-prompt'
|
89
|
+
TTY::Prompt.new(options)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Get terminal screen properties
|
93
|
+
#
|
94
|
+
# @see http://www.rubydoc.info/gems/tty-screen
|
95
|
+
#
|
96
|
+
# @api public
|
97
|
+
def screen
|
98
|
+
require 'tty-screen'
|
99
|
+
TTY::Screen
|
100
|
+
end
|
101
|
+
|
102
|
+
# The unix which utility
|
103
|
+
#
|
104
|
+
# @see http://www.rubydoc.info/gems/tty-which
|
105
|
+
#
|
106
|
+
# @api public
|
107
|
+
def which(*args)
|
108
|
+
require 'tty-which'
|
109
|
+
TTY::Which.which(*args)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Check if executable exists
|
113
|
+
#
|
114
|
+
# @see http://www.rubydoc.info/gems/tty-which
|
115
|
+
#
|
116
|
+
# @api public
|
117
|
+
def exec_exist?(*args)
|
118
|
+
require 'tty-which'
|
119
|
+
TTY::Which.exist?(*args)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
|
5
|
+
module Expire
|
6
|
+
module Commands
|
7
|
+
# Print the newest backup
|
8
|
+
class Newest < Expire::Command
|
9
|
+
def initialize(path, options)
|
10
|
+
@path = path
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :path
|
15
|
+
|
16
|
+
def execute(input: $stdin, output: $stdout)
|
17
|
+
output.puts Expire.newest(path).pathname
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
|
5
|
+
module Expire
|
6
|
+
module Commands
|
7
|
+
# Print the oldest backup
|
8
|
+
class Oldest < Expire::Command
|
9
|
+
def initialize(path, options)
|
10
|
+
@path = path
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :path
|
15
|
+
|
16
|
+
def execute(input: $stdin, output: $stdout)
|
17
|
+
output.puts Expire.oldest(path).pathname
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
|
5
|
+
module Expire
|
6
|
+
module Commands
|
7
|
+
# Purge expired backups
|
8
|
+
class Purge < Expire::Command
|
9
|
+
def initialize(path, options)
|
10
|
+
@path = path
|
11
|
+
@options = options
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :path, :options
|
15
|
+
|
16
|
+
def execute(input: $stdin, output: $stdout)
|
17
|
+
Expire.purge(path, options)
|
18
|
+
rescue StandardError => _e
|
19
|
+
exit 1
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
|
5
|
+
module Expire
|
6
|
+
module Commands
|
7
|
+
# Remove files and directories
|
8
|
+
class Remove < Expire::Command
|
9
|
+
def initialize(path:)
|
10
|
+
@path = path
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_reader :path
|
14
|
+
|
15
|
+
def execute(output: $stdout)
|
16
|
+
begin
|
17
|
+
Expire.remove(path)
|
18
|
+
rescue Errno::ENOENT => e
|
19
|
+
output.puts "can't remove #{path}: #{e}"
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
output.puts "removed #{path}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
|
5
|
+
module Expire
|
6
|
+
module Commands
|
7
|
+
# Print all rule-classes
|
8
|
+
class RuleClasses < Expire::Command
|
9
|
+
def initialize(_); end
|
10
|
+
|
11
|
+
def execute(output: $stdout)
|
12
|
+
Expire.rule_classes.each do |rule_class|
|
13
|
+
output.puts rule_class
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
|
5
|
+
module Expire
|
6
|
+
module Commands
|
7
|
+
# Print the names of all rules
|
8
|
+
class RuleNames < Expire::Command
|
9
|
+
def initialize(options)
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(input: $stdin, output: $stdout)
|
14
|
+
Expire.rule_names.each do |rule_name|
|
15
|
+
output.puts rule_name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../command'
|
4
|
+
|
5
|
+
module Expire
|
6
|
+
module Commands
|
7
|
+
# Print the option names of all rules
|
8
|
+
class RuleOptionNames < Expire::Command
|
9
|
+
def initialize(options)
|
10
|
+
@options = options
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(input: $stdin, output: $stdout)
|
14
|
+
Expire.rule_option_names.each do |option_name|
|
15
|
+
output.puts option_name
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Expire
|
4
|
+
# Base class for from-now rules
|
5
|
+
class FromNowKeepAdjectiveForRuleBase < KeepAdjectiveRuleBase
|
6
|
+
extend FromRangeValue
|
7
|
+
include NumerusUnit
|
8
|
+
|
9
|
+
PRIMARY_RANK = 40
|
10
|
+
|
11
|
+
def self.primary_rank
|
12
|
+
PRIMARY_RANK
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(unit:, **args)
|
16
|
+
super(**args)
|
17
|
+
|
18
|
+
@unit = unit
|
19
|
+
end
|
20
|
+
|
21
|
+
attr_reader :unit
|
22
|
+
|
23
|
+
def apply(backups, reference_datetime)
|
24
|
+
minimal_datetime = reference_datetime - amount.send(unit)
|
25
|
+
kept = backups.one_per(spacing).not_older_than(minimal_datetime)
|
26
|
+
|
27
|
+
kept.each { |backup| backup.add_reason_to_keep(reason_to_keep) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def primary_rank
|
31
|
+
self.class.primary_rank
|
32
|
+
end
|
33
|
+
|
34
|
+
def reason_to_keep
|
35
|
+
"from now keep all backups for #{amount} #{numerus_unit}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Expire
|
4
|
+
# Keep the most recent backups from now for a certain
|
5
|
+
# period of time.
|
6
|
+
class FromNowKeepMostRecentForRule < RuleBase
|
7
|
+
extend FromRangeValue
|
8
|
+
include NumerusUnit
|
9
|
+
|
10
|
+
RULE_RANK = 12
|
11
|
+
|
12
|
+
def self.rank
|
13
|
+
RULE_RANK
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(unit:, **args)
|
17
|
+
super(**args)
|
18
|
+
|
19
|
+
@unit = unit
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :unit
|
23
|
+
|
24
|
+
def apply(backups, datetime_now)
|
25
|
+
reference_datetime = datetime_now - amount.send(unit)
|
26
|
+
kept = backups.not_older_than(reference_datetime)
|
27
|
+
|
28
|
+
kept.each do |backup|
|
29
|
+
backup.add_reason_to_keep(reason_to_keep)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def rank
|
34
|
+
self.class.rank
|
35
|
+
end
|
36
|
+
|
37
|
+
def reason_to_keep
|
38
|
+
"from now keep most recent backups for #{amount} #{numerus_unit}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Expire
|
4
|
+
# Provide a pseudo constructor for rules that require a time range
|
5
|
+
module FromRangeValue
|
6
|
+
using RefineAllAndNone
|
7
|
+
|
8
|
+
FROM_VALUE_REGEX = /
|
9
|
+
\A
|
10
|
+
(([0-9](_[0-9]+){0,})+)
|
11
|
+
(\s+|\.)
|
12
|
+
(hour|day|week|month|year)s?
|
13
|
+
\z
|
14
|
+
/x.freeze
|
15
|
+
|
16
|
+
def from_value(string, **args)
|
17
|
+
# return new(args.merge({ amount: 0, unit: nil })) if string.none?
|
18
|
+
return new(**args.merge(amount: 0, unit: nil)) if string.none?
|
19
|
+
|
20
|
+
stripped_down = string.strip.downcase
|
21
|
+
match = stripped_down.match FROM_VALUE_REGEX
|
22
|
+
raise ArgumentError, "#{string} is not a valid range value" unless match
|
23
|
+
|
24
|
+
amount = Integer(match[1])
|
25
|
+
unit = match[5]
|
26
|
+
new(**args.merge(amount: amount, unit: unit))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|