multisync 0.3.6 → 0.4.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 +4 -4
- data/.standard.yml +3 -0
- data/{History.md → CHANGELOG.md} +73 -12
- data/LICENSE.txt +1 -1
- data/README.md +14 -11
- data/Rakefile +9 -1
- data/exe/multisync +2 -2
- data/lib/multisync/catalog.rb +10 -13
- data/lib/multisync/cli.rb +60 -88
- data/lib/multisync/colors.rb +23 -0
- data/lib/multisync/definition/dsl.rb +19 -21
- data/lib/multisync/definition/entity.rb +34 -47
- data/lib/multisync/definition/null.rb +11 -13
- data/lib/multisync/definition/template.rb +8 -9
- data/lib/multisync/definition.rb +5 -6
- data/lib/multisync/list.rb +29 -26
- data/lib/multisync/rsync_stat.rb +89 -48
- data/lib/multisync/runtime.rb +43 -48
- data/lib/multisync/selector.rb +44 -22
- data/lib/multisync/summary.rb +56 -21
- data/lib/multisync/version.rb +3 -1
- data/lib/multisync.rb +14 -9
- data/sample/multisync.rb +10 -18
- metadata +12 -59
- data/.gitignore +0 -13
- data/Gemfile +0 -5
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/multisync.gemspec +0 -45
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
class Multisync::Definition::Null < Multisync::Definition::Entity
|
|
3
|
-
|
|
4
2
|
def initialize
|
|
3
|
+
@level = -1
|
|
5
4
|
end
|
|
6
|
-
|
|
5
|
+
|
|
7
6
|
def register member
|
|
8
7
|
end
|
|
9
8
|
|
|
10
9
|
def fullname
|
|
11
|
-
|
|
10
|
+
""
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def rsync_options
|
|
@@ -19,34 +18,33 @@ class Multisync::Definition::Null < Multisync::Definition::Entity
|
|
|
19
18
|
def source
|
|
20
19
|
raise "no source (from) defined"
|
|
21
20
|
end
|
|
22
|
-
|
|
21
|
+
|
|
23
22
|
def source_description
|
|
24
|
-
|
|
23
|
+
""
|
|
25
24
|
end
|
|
26
|
-
|
|
25
|
+
|
|
27
26
|
# to (destination) is a required option and should be set at least at root level
|
|
28
27
|
def destination
|
|
29
28
|
raise "no destination (to) defined"
|
|
30
29
|
end
|
|
31
|
-
|
|
30
|
+
|
|
32
31
|
def destination_description
|
|
33
|
-
|
|
32
|
+
""
|
|
34
33
|
end
|
|
35
|
-
|
|
34
|
+
|
|
36
35
|
def default?
|
|
37
36
|
false
|
|
38
37
|
end
|
|
39
|
-
|
|
38
|
+
|
|
40
39
|
def checks
|
|
41
40
|
[]
|
|
42
41
|
end
|
|
43
42
|
|
|
44
43
|
def check_source?
|
|
45
44
|
false
|
|
46
|
-
end
|
|
45
|
+
end
|
|
47
46
|
|
|
48
47
|
def check_destination?
|
|
49
48
|
false
|
|
50
49
|
end
|
|
51
50
|
end
|
|
52
|
-
|
|
@@ -1,26 +1,25 @@
|
|
|
1
|
-
|
|
2
1
|
class Multisync::Definition::Template
|
|
3
2
|
include Multisync::Definition::Dsl
|
|
4
|
-
|
|
3
|
+
|
|
5
4
|
@registered = []
|
|
6
|
-
|
|
5
|
+
|
|
7
6
|
def self.register instance
|
|
8
7
|
@registered << instance
|
|
9
8
|
end
|
|
10
|
-
|
|
9
|
+
|
|
11
10
|
def self.lookup name
|
|
12
|
-
@registered.find {
|
|
11
|
+
@registered.find { _1.name == name }
|
|
13
12
|
end
|
|
14
|
-
|
|
13
|
+
|
|
15
14
|
# The name of the template
|
|
16
15
|
attr_reader :name
|
|
17
|
-
|
|
16
|
+
|
|
18
17
|
# The block the template holds
|
|
19
18
|
attr_reader :block
|
|
20
|
-
|
|
19
|
+
|
|
21
20
|
def initialize name, &block
|
|
22
21
|
@name = name
|
|
23
22
|
self.class.register self
|
|
24
23
|
@block = block
|
|
25
24
|
end
|
|
26
|
-
end
|
|
25
|
+
end
|
data/lib/multisync/definition.rb
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
|
|
2
1
|
class Multisync::Definition
|
|
3
|
-
autoload :Dsl,
|
|
4
|
-
autoload :Entity,
|
|
5
|
-
autoload :Null,
|
|
6
|
-
autoload :Template,
|
|
7
|
-
end
|
|
2
|
+
autoload :Dsl, "multisync/definition/dsl"
|
|
3
|
+
autoload :Entity, "multisync/definition/entity"
|
|
4
|
+
autoload :Null, "multisync/definition/null"
|
|
5
|
+
autoload :Template, "multisync/definition/template"
|
|
6
|
+
end
|
data/lib/multisync/list.rb
CHANGED
|
@@ -1,44 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
require 'rainbow/ext/string'
|
|
3
|
-
|
|
4
1
|
class Multisync::List
|
|
5
|
-
|
|
2
|
+
include Multisync::Colors
|
|
3
|
+
|
|
6
4
|
# Given catalog
|
|
7
5
|
attr_reader :catalog
|
|
8
|
-
|
|
6
|
+
|
|
9
7
|
# Tasks
|
|
10
8
|
attr_reader :tasks
|
|
11
9
|
|
|
12
|
-
def initialize
|
|
13
|
-
@
|
|
14
|
-
@tasks = []
|
|
10
|
+
def initialize tasks
|
|
11
|
+
@tasks = tasks
|
|
15
12
|
end
|
|
16
|
-
|
|
13
|
+
|
|
17
14
|
def to_s
|
|
18
|
-
|
|
19
|
-
table.to_s
|
|
15
|
+
"\n" + table.to_s
|
|
20
16
|
end
|
|
21
|
-
|
|
17
|
+
|
|
22
18
|
def table
|
|
23
|
-
Terminal::Table.new(rows:
|
|
19
|
+
Terminal::Table.new(rows: rows, style: table_style)
|
|
24
20
|
end
|
|
25
|
-
|
|
26
|
-
def
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
|
|
22
|
+
def rows
|
|
23
|
+
tasks.map do |task|
|
|
24
|
+
next unless task.level > 0
|
|
25
|
+
|
|
26
|
+
indent = "".ljust(2 * (task.level - 1), " ")
|
|
27
|
+
name = task.executeable? ? task.name : "#{task.name}#{as_note("/")}"
|
|
28
|
+
default = task.default? ? as_note(" *") : ""
|
|
29
|
+
[
|
|
30
|
+
[indent, name, default].join,
|
|
31
|
+
*descriptions(task)
|
|
32
|
+
]
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
|
-
|
|
36
|
-
def
|
|
37
|
-
|
|
38
|
-
|
|
35
|
+
|
|
36
|
+
def descriptions task
|
|
37
|
+
if [task.source_description, task.destination_description].any?(&:empty?)
|
|
38
|
+
["", "", ""]
|
|
39
|
+
else
|
|
40
|
+
[task.source_description, "-->", task.destination_description].map(&method(:as_note))
|
|
41
|
+
end
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def table_style
|
|
42
|
-
{
|
|
45
|
+
{border_x: as_note("─"), border_y: "", border_i: "", border_top: false, border_bottom: false, padding_left: 0, padding_right: 3}
|
|
43
46
|
end
|
|
44
47
|
end
|
data/lib/multisync/rsync_stat.rb
CHANGED
|
@@ -1,58 +1,99 @@
|
|
|
1
|
-
|
|
2
|
-
require 'filesize'
|
|
1
|
+
require "filesize"
|
|
3
2
|
|
|
4
3
|
class Multisync::RsyncStat
|
|
5
|
-
|
|
4
|
+
# Keep track of totals
|
|
5
|
+
def self.total
|
|
6
|
+
@total ||= Hash.new 0
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Sample output
|
|
10
|
+
# ...
|
|
11
|
+
# Number of files: 89,633 (reg: 59,322, dir: 30,311)
|
|
12
|
+
# Number of created files: 356 (reg: 281, dir: 75)
|
|
13
|
+
# Number of deleted files: 114 (reg: 101, dir: 13)
|
|
14
|
+
# Number of regular files transferred: 344
|
|
15
|
+
# Total file size: 546,410,522,192 bytes
|
|
16
|
+
# Total transferred file size: 7,991,491,676 bytes
|
|
17
|
+
# Literal data: 7,952,503,842 bytes
|
|
18
|
+
# Matched data: 38,987,834 bytes
|
|
19
|
+
# File list size: 3,063,808
|
|
20
|
+
# File list generation time: 2.414 seconds
|
|
21
|
+
# File list transfer time: 0.000 seconds
|
|
22
|
+
# Total bytes sent: 7,957,645,803
|
|
23
|
+
# Total bytes received: 101,299
|
|
24
|
+
#
|
|
25
|
+
# sent 7,957,645,803 bytes received 101,299 bytes 23,719,067.37 bytes/sec
|
|
26
|
+
# total size is 546,410,522,192 speedup is 68.66
|
|
6
27
|
def initialize output
|
|
7
28
|
@output = output
|
|
8
29
|
end
|
|
9
30
|
|
|
10
|
-
#
|
|
11
|
-
def parse
|
|
12
|
-
@stats = definitions.each_with_object({}) do |definition, stats|
|
|
13
|
-
value = scan[definition[:match]]
|
|
14
|
-
stats[definition[:key]] = value ? (definition[:coerce] ? definition[:coerce].call(value) : value) : definition[:default]
|
|
15
|
-
end
|
|
16
|
-
self
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
# Scan output and return a hash
|
|
31
|
+
# extracted returns a hash with labels as keys and extracted strings as values
|
|
20
32
|
# {
|
|
21
33
|
# "Number of files" => "35,648",
|
|
22
|
-
# "Number of created files" => "
|
|
23
|
-
# "Number of deleted files" => "0",
|
|
24
|
-
# "Number of regular files transferred"=>"0",
|
|
34
|
+
# "Number of created files" => "2,120",
|
|
25
35
|
# ...
|
|
26
36
|
# }
|
|
27
|
-
def
|
|
28
|
-
@
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
end
|
|
58
|
-
|
|
37
|
+
def extracted
|
|
38
|
+
@extraced ||= @output.scan(/(#{labels.join("|")}):\s+([,0-9]+)/).to_h
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# stats returns a hash with the follwing keys (and updates class total)
|
|
42
|
+
# {
|
|
43
|
+
# "Number of files" => 35648,
|
|
44
|
+
# "Number of created files" => 2120,
|
|
45
|
+
# "Number of deleted files" => 37,
|
|
46
|
+
# "Number of regular files transferred" => 394,
|
|
47
|
+
# "Total file size" => 204936349,
|
|
48
|
+
# "Total transferred file size" => 49239,
|
|
49
|
+
# }
|
|
50
|
+
def stats
|
|
51
|
+
@stats ||= labels.each_with_object({}) do |label, h|
|
|
52
|
+
value = extracted[label]&.delete(",").to_i
|
|
53
|
+
|
|
54
|
+
self.class.total[label] += value # update total
|
|
55
|
+
h[label] = value
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def labels
|
|
60
|
+
self.class.format_map.keys
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def formatted_values
|
|
64
|
+
self.class.format_values do |label|
|
|
65
|
+
stats[label]
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def self.formatted_totals
|
|
70
|
+
format_values do |label|
|
|
71
|
+
total[label]
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.format_values
|
|
76
|
+
format_map.map do |label, format|
|
|
77
|
+
format.call(yield label)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.format_map
|
|
82
|
+
{
|
|
83
|
+
"Number of files" => to_numbers,
|
|
84
|
+
"Number of created files" => to_numbers,
|
|
85
|
+
"Number of deleted files" => to_numbers,
|
|
86
|
+
"Number of regular files transferred" => to_numbers,
|
|
87
|
+
"Total file size" => to_filesize,
|
|
88
|
+
"Total transferred file size" => to_filesize
|
|
89
|
+
}
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.to_numbers
|
|
93
|
+
->(x) { x.to_s.gsub(/\B(?=(...)*\b)/, "'") }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.to_filesize
|
|
97
|
+
->(x) { Filesize.new(x).pretty }
|
|
98
|
+
end
|
|
99
|
+
end
|
data/lib/multisync/runtime.rb
CHANGED
|
@@ -1,82 +1,77 @@
|
|
|
1
|
-
|
|
2
|
-
require 'mixlib/shellout'
|
|
1
|
+
require "mixlib/shellout"
|
|
3
2
|
|
|
4
3
|
class Multisync::Runtime
|
|
4
|
+
include Multisync::Colors
|
|
5
5
|
|
|
6
6
|
# Runtime options
|
|
7
7
|
# dryrun: true|false
|
|
8
8
|
# show: true|false
|
|
9
9
|
attr_reader :options
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
def initialize options
|
|
12
12
|
@options = options
|
|
13
13
|
end
|
|
14
|
-
|
|
15
|
-
def dryrun?
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def quiet?
|
|
24
|
-
options[:quiet]
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def timeout
|
|
28
|
-
options[:timeout]
|
|
29
|
-
end
|
|
30
|
-
|
|
14
|
+
|
|
15
|
+
def dryrun? = options[:dryrun]
|
|
16
|
+
|
|
17
|
+
def show_only? = options[:print]
|
|
18
|
+
|
|
19
|
+
def quiet? = options[:quiet]
|
|
20
|
+
|
|
21
|
+
def timeout = options[:timeout]
|
|
22
|
+
|
|
31
23
|
def run sync
|
|
32
24
|
rsync_options = sync.rsync_options.dup
|
|
33
|
-
rsync_options.unshift
|
|
34
|
-
rsync_options.unshift
|
|
35
|
-
rsync_options.unshift
|
|
25
|
+
rsync_options.unshift "--stats"
|
|
26
|
+
rsync_options.unshift "--verbose" unless quiet?
|
|
27
|
+
rsync_options.unshift "--dry-run" if dryrun?
|
|
36
28
|
|
|
37
29
|
# escape path by hand, shellescape escapes also ~, but we want to keep its
|
|
38
30
|
# special meaning for home, instead of passing it as literal char
|
|
39
|
-
source, destination = [sync.source, sync.destination].map {
|
|
40
|
-
cmd = "rsync #{rsync_options.join(
|
|
41
|
-
cmd_options = {
|
|
42
|
-
|
|
31
|
+
source, destination = [sync.source, sync.destination].map { _1.gsub(/\s+/, "\\ ") }
|
|
32
|
+
cmd = "rsync #{rsync_options.join(" ")} #{source} #{destination}"
|
|
33
|
+
cmd_options = {timeout: timeout}
|
|
34
|
+
unless quiet?
|
|
35
|
+
cmd_options[:live_stdout] = $stdout
|
|
36
|
+
cmd_options[:live_stderr] = $stderr
|
|
37
|
+
end
|
|
43
38
|
rsync = Mixlib::ShellOut.new(cmd, cmd_options)
|
|
44
39
|
sync.result[:cmd] = rsync.command
|
|
45
40
|
|
|
46
41
|
unless quiet?
|
|
47
42
|
puts
|
|
48
|
-
puts [sync.source_description, sync.destination_description].join(
|
|
43
|
+
puts as_main([sync.source_description, sync.destination_description].join(" --> "))
|
|
49
44
|
end
|
|
50
|
-
|
|
45
|
+
|
|
51
46
|
# Perform all only_if checks, from top to bottom
|
|
52
47
|
sync.checks.each do |check|
|
|
53
48
|
next unless Mixlib::ShellOut.new(check[:cmd]).run_command.error?
|
|
54
49
|
|
|
55
|
-
puts check[:cmd]
|
|
56
|
-
puts "Skip:
|
|
50
|
+
puts as_skipped("#{check[:cmd]} (failed)")
|
|
51
|
+
puts as_note("Skip: #{rsync.command}")
|
|
57
52
|
sync.result[:action] = :skip
|
|
58
53
|
sync.result[:skip_message] = check[:message]
|
|
59
|
-
return
|
|
54
|
+
return false
|
|
60
55
|
end
|
|
61
|
-
|
|
56
|
+
|
|
62
57
|
# source check
|
|
63
|
-
if sync.check_source? && !
|
|
64
|
-
puts "Source #{sync.source} is not accessible"
|
|
65
|
-
puts "Skip:
|
|
58
|
+
if sync.check_source? && !check_path(sync.source, :source)
|
|
59
|
+
puts as_skipped("Source #{sync.source} is not accessible")
|
|
60
|
+
puts as_note("Skip: #{rsync.command}")
|
|
66
61
|
sync.result[:action] = :skip
|
|
67
62
|
sync.result[:skip_message] = "Source is not accessible"
|
|
68
63
|
return
|
|
69
64
|
end
|
|
70
|
-
|
|
65
|
+
|
|
71
66
|
# target check
|
|
72
|
-
if sync.check_destination? && !
|
|
73
|
-
puts "Destination #{sync.destination} is not accessible"
|
|
74
|
-
puts "Skip:
|
|
67
|
+
if sync.check_destination? && !check_path(sync.destination, :destination)
|
|
68
|
+
puts as_skipped("Destination #{sync.destination} is not accessible")
|
|
69
|
+
puts as_note("Skip: #{rsync.command}")
|
|
75
70
|
sync.result[:action] = :skip
|
|
76
71
|
sync.result[:skip_message] = "Destination is not accessible"
|
|
77
72
|
return
|
|
78
73
|
end
|
|
79
|
-
|
|
74
|
+
|
|
80
75
|
if show_only?
|
|
81
76
|
puts rsync.command
|
|
82
77
|
else
|
|
@@ -88,20 +83,20 @@ class Multisync::Runtime
|
|
|
88
83
|
sync.result[:stderr] = rsync.stderr
|
|
89
84
|
end
|
|
90
85
|
end
|
|
91
|
-
|
|
86
|
+
|
|
92
87
|
# checks a path
|
|
93
88
|
# if path includes a host, the reachability of the host will be checked
|
|
94
89
|
# the existence of the remote path will not be checked
|
|
95
90
|
# if path is a local source path, its existence will be checked
|
|
96
91
|
# if path is a local destination path, the existence of the parent will be checked
|
|
97
92
|
def check_path path, type = :source
|
|
98
|
-
if path.include?
|
|
99
|
-
host = path.split(
|
|
93
|
+
if path.include? ":"
|
|
94
|
+
host = path.split(":").first.split("@").last
|
|
100
95
|
Mixlib::ShellOut.new("ping -o -t 1 #{host}").run_command.status.success?
|
|
101
96
|
else
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
97
|
+
File.expand_path(path)
|
|
98
|
+
.then { (type == :destination) ? File.dirname(_1) : _1 }
|
|
99
|
+
.then { File.exist? _1 }
|
|
105
100
|
end
|
|
106
101
|
end
|
|
107
|
-
end
|
|
102
|
+
end
|
data/lib/multisync/selector.rb
CHANGED
|
@@ -1,36 +1,58 @@
|
|
|
1
|
-
|
|
2
1
|
class Multisync::Selector
|
|
3
|
-
|
|
4
2
|
# Given catalog
|
|
5
3
|
attr_reader :catalog
|
|
6
|
-
|
|
7
|
-
# Given
|
|
8
|
-
attr_reader :
|
|
9
|
-
|
|
4
|
+
|
|
5
|
+
# Given queries
|
|
6
|
+
attr_reader :queries
|
|
7
|
+
|
|
10
8
|
# Selected tasks
|
|
11
|
-
attr_reader :
|
|
9
|
+
attr_reader :results
|
|
12
10
|
|
|
13
|
-
def initialize catalog,
|
|
11
|
+
def initialize catalog, queries
|
|
14
12
|
@catalog = catalog
|
|
15
|
-
@
|
|
16
|
-
@
|
|
13
|
+
@queries = queries
|
|
14
|
+
@results = []
|
|
15
|
+
@all_subjects = []
|
|
16
|
+
@subjects_by_name = []
|
|
17
17
|
end
|
|
18
|
-
|
|
19
|
-
def tasks
|
|
18
|
+
|
|
19
|
+
def tasks parents: false
|
|
20
20
|
catalog.traverse self
|
|
21
|
-
|
|
21
|
+
parents ? selected_with_parents : selected
|
|
22
22
|
end
|
|
23
|
-
|
|
24
|
-
def visit subject
|
|
25
|
-
|
|
23
|
+
|
|
24
|
+
def visit subject
|
|
25
|
+
results << subject
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def selected_with_parents
|
|
29
|
+
@selected_with_parents ||= results.select { selected_or_parent_of_selected? _1 }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def selected_or_parent_of_selected? subject
|
|
33
|
+
!subject.fullname.empty? &&
|
|
34
|
+
selected_fullnames.any? { %r{^#{subject.fullname}(?:/|$)}.match _1 }
|
|
26
35
|
end
|
|
27
|
-
|
|
36
|
+
|
|
37
|
+
def selected_fullnames
|
|
38
|
+
@selected_fullnames ||= selected.map(&:fullname)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def selected
|
|
42
|
+
@selected ||= results.select { selected? _1 }
|
|
43
|
+
end
|
|
44
|
+
|
|
28
45
|
def selected? subject
|
|
46
|
+
# return only subjects with a fullname
|
|
47
|
+
return false if subject.fullname.empty?
|
|
48
|
+
|
|
49
|
+
# no queries defined, but subject is in the default set
|
|
50
|
+
return true if queries.empty? && subject.default?
|
|
51
|
+
|
|
29
52
|
# only return the leaves of the definition tree
|
|
30
|
-
return false unless subject.members.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
sets.any? {|set| /\b#{set}\b/.match subject.fullname }
|
|
53
|
+
# return false unless subject.members.any?
|
|
54
|
+
|
|
55
|
+
# subject matches any of the given queries
|
|
56
|
+
queries.any? { /\b#{_1}\b/.match subject.fullname }
|
|
35
57
|
end
|
|
36
58
|
end
|
data/lib/multisync/summary.rb
CHANGED
|
@@ -1,55 +1,90 @@
|
|
|
1
|
-
|
|
2
1
|
class Multisync::Summary
|
|
3
|
-
|
|
2
|
+
include Multisync::Colors
|
|
3
|
+
|
|
4
4
|
# All tasks to include in the summary
|
|
5
5
|
attr_reader :tasks
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
def initialize tasks
|
|
8
8
|
@tasks = tasks
|
|
9
9
|
end
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
def to_s
|
|
12
|
-
table.to_s
|
|
12
|
+
["", as_main("Summary"), table.to_s, failures].compact.join("\n")
|
|
13
13
|
end
|
|
14
|
-
|
|
14
|
+
|
|
15
15
|
def table
|
|
16
16
|
Terminal::Table.new(headings: headings, rows: data, style: table_style)
|
|
17
17
|
end
|
|
18
|
-
|
|
18
|
+
|
|
19
19
|
def headings
|
|
20
|
-
%w
|
|
20
|
+
%w[SOURCE DESTINATION FILES + - → ∑ ↑]
|
|
21
|
+
.map(&method(:as_note))
|
|
22
|
+
.zip(%i[left left right right right right right right])
|
|
23
|
+
.map { |v, a| {value: v, alignment: a} }
|
|
21
24
|
end
|
|
22
|
-
|
|
25
|
+
|
|
23
26
|
def data
|
|
24
|
-
# Exclude tasks with an empty result (> not run) first
|
|
25
27
|
tasks.map do |task|
|
|
26
28
|
result = task.result
|
|
27
|
-
desc = [task.source_description,
|
|
29
|
+
desc = [task.source_description, task.destination_description]
|
|
28
30
|
|
|
29
31
|
case result[:action]
|
|
30
32
|
when :run
|
|
31
|
-
if result[:status]
|
|
33
|
+
if result[:status]&.success?
|
|
32
34
|
# successfull run
|
|
33
|
-
|
|
34
|
-
[*desc, *
|
|
35
|
+
stats = Multisync::RsyncStat.new(result[:stdout])
|
|
36
|
+
[*desc, *stats.formatted_values.map { {value: as_success(_1), alignment: :right} }]
|
|
35
37
|
else
|
|
36
38
|
# failed or interrupted run
|
|
37
|
-
[*desc,
|
|
39
|
+
[*desc, as_message(as_fail("Failed, for more information see details below"))]
|
|
40
|
+
# [*desc, as_message(as_fail(result[:stderr] || "n/a").strip)]
|
|
38
41
|
end
|
|
39
42
|
|
|
40
43
|
when :skip
|
|
41
44
|
# skiped sync
|
|
42
|
-
[*desc,
|
|
45
|
+
[*desc, as_message(as_skipped(result[:skip_message]))]
|
|
43
46
|
|
|
44
47
|
else
|
|
45
48
|
# not executed
|
|
46
|
-
[*desc,
|
|
49
|
+
[*desc, as_message(as_note("not executed"))]
|
|
47
50
|
end
|
|
48
|
-
end
|
|
51
|
+
end.push(
|
|
52
|
+
[
|
|
53
|
+
as_note("Total"),
|
|
54
|
+
"",
|
|
55
|
+
*Multisync::RsyncStat
|
|
56
|
+
.formatted_totals
|
|
57
|
+
.map { {value: as_note(_1), alignment: :right} }
|
|
58
|
+
]
|
|
59
|
+
)
|
|
49
60
|
end
|
|
50
|
-
|
|
51
|
-
def
|
|
52
|
-
|
|
61
|
+
|
|
62
|
+
def failures
|
|
63
|
+
tasks
|
|
64
|
+
.select { _1.result[:action] == :run && !_1.result[:status]&.success? }
|
|
65
|
+
.flat_map do |task|
|
|
66
|
+
[
|
|
67
|
+
as_fail([task.source_description, task.destination_description].join(" --> ")),
|
|
68
|
+
message_or_nil(task.result[:stdout]),
|
|
69
|
+
message_or_nil(task.result[:stderr]),
|
|
70
|
+
""
|
|
71
|
+
].compact
|
|
72
|
+
end
|
|
73
|
+
# Add title if any failures
|
|
74
|
+
.tap { _1.unshift "\n#{as_main("Failures")}" if _1.any? }
|
|
75
|
+
# Return failures as string or nil
|
|
76
|
+
.then { _1.any? ? _1.join("\n") : nil }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def as_message message
|
|
80
|
+
{value: message, colspan: 6}
|
|
53
81
|
end
|
|
54
82
|
|
|
83
|
+
def message_or_nil message
|
|
84
|
+
(message.nil? || message.empty?) ? nil : message
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def table_style
|
|
88
|
+
{border_x: as_note("─"), border_y: "", border_i: "", border_top: false, border_bottom: false, padding_left: 0, padding_right: 3}
|
|
89
|
+
end
|
|
55
90
|
end
|
data/lib/multisync/version.rb
CHANGED