multisync 0.3.7 → 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.
@@ -1,18 +1,19 @@
1
-
2
1
  class Multisync::Definition::Entity
3
-
4
2
  include Multisync::Definition::Dsl
5
3
 
6
- # The parent of the group
4
+ # The parent of the entity
7
5
  attr_reader :parent
8
-
9
- # The name of the group
6
+
7
+ # The name of the entity
10
8
  attr_reader :name
11
-
12
- # All members (groups or syncs) of this group
9
+
10
+ # The level of the entity
11
+ attr_reader :level
12
+
13
+ # All members (groups or syncs) of this entity
13
14
  attr_reader :members
14
-
15
- # Collected results after run as Hash
15
+
16
+ # Collected result after run
16
17
  # {
17
18
  # cmd: 'rsync --stats -v source destination',
18
19
  # action: :run,
@@ -21,96 +22,82 @@ class Multisync::Definition::Entity
21
22
  # stderr: '',
22
23
  # skip_message: 'host not reachable',
23
24
  # }
24
-
25
+
25
26
  attr_reader :result
26
27
 
27
28
  def initialize parent, name, &block
28
29
  @members = []
29
- @name = name.to_s
30
+ @name = name
30
31
  @parent = parent
32
+ @level = parent.level + 1
31
33
  parent.register self
32
34
  instance_eval(&block) if block_given?
33
35
  @result = {}
34
36
  end
35
-
36
- def to_h
37
- {
38
- fullname: fullname,
39
- default: default?,
40
- source: {
41
- path: source,
42
- description: source_description,
43
- check: check_source?,
44
- },
45
- destination: {
46
- path: destination,
47
- description: destination_description,
48
- check: check_destination?,
49
- },
50
- checks: checks,
51
- rsync_options: rsync_options,
52
- }
53
- end
54
-
37
+
55
38
  # Make the definition visitable
56
- def accept visitor, level=0
57
- visitor.visit self, level
39
+ def accept visitor
40
+ visitor.visit self
58
41
  members.map do |member|
59
- member.accept visitor, level+1
42
+ member.accept visitor
60
43
  end
61
44
  end
62
-
45
+
63
46
  def register member
64
47
  members << member
65
48
  end
66
-
49
+
67
50
  # The name including all parents separated by "/"
68
51
  def fullname
69
- [parent.fullname, name].join '/'
52
+ [parent.fullname, name].reject(&:empty?).join("/")
70
53
  end
71
-
54
+
72
55
  # rsync source
73
56
  def source
74
57
  @from_value || parent.source
75
58
  end
76
-
59
+
77
60
  def source_description
78
61
  @from_description || @from_value || parent.source_description
79
62
  end
80
-
63
+
81
64
  # rsync destination
82
65
  def destination
83
66
  @to_value || parent.destination
84
67
  end
85
-
68
+
86
69
  def destination_description
87
70
  @to_description || @to_value || parent.destination_description
88
71
  end
89
-
72
+
90
73
  # rsync options
91
74
  def rsync_options
92
75
  opts = @rsync_options || []
93
76
  return opts if @rsync_options_mode == :override
94
77
  parent.rsync_options + opts
95
78
  end
96
-
79
+
97
80
  # Is this group/sync defined as default
98
81
  def default?
99
82
  @default || parent.default?
100
83
  end
101
-
84
+
85
+ def executeable?
86
+ members.none?
87
+ end
88
+
102
89
  # All checks from parent to child
103
90
  def checks
104
91
  (parent.checks + [@check]).compact
105
92
  end
106
-
93
+
107
94
  # Should source's host or path be checked before sync?
108
95
  def check_source?
109
96
  @from_check.nil? ? parent.check_source? : @from_check
110
97
  end
111
-
98
+
112
99
  # Should destination's host or path be checked before sync?
113
100
  def check_destination?
114
101
  @to_check.nil? ? parent.check_destination? : @to_check
115
102
  end
116
- end
103
+ end
@@ -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
- nil
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 {|instance| instance.name == name }
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
@@ -1,7 +1,6 @@
1
-
2
1
  class Multisync::Definition
3
- autoload :Dsl, 'multisync/definition/dsl'
4
- autoload :Entity, 'multisync/definition/entity'
5
- autoload :Null, 'multisync/definition/null'
6
- autoload :Template, 'multisync/definition/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
@@ -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 catalog
13
- @catalog = catalog
14
- @tasks = []
10
+ def initialize tasks
11
+ @tasks = tasks
15
12
  end
16
-
13
+
17
14
  def to_s
18
- catalog.traverse self
19
- table.to_s
15
+ "\n" + table.to_s
20
16
  end
21
-
17
+
22
18
  def table
23
- Terminal::Table.new(rows: tasks, style: table_style)
19
+ Terminal::Table.new(rows: rows, style: table_style)
24
20
  end
25
-
26
- def visit subject, level
27
- if level > 0
28
- tab = ''.ljust(2*(level-1), ' ')
29
- default = subject.default? ? ' *' : ''
30
- name = "#{tab}#{subject.name}#{default}"
31
- tasks << [name, *description(subject).map(&:faint)]
32
- # puts "#{name.ljust(32, ' ')}#{description(subject)}"
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 description subject
37
- desc = [subject.source_description, subject.destination_description]
38
- desc.any?(&:empty?) ? [] : [desc.first, ['--> ', desc.last].join]
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
- { border_top: false, border_bottom: false, border_x: '–', border_y: '', border_i: '', padding_left: 0, padding_right: 3 }
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
@@ -1,7 +1,6 @@
1
1
  require "filesize"
2
2
 
3
3
  class Multisync::RsyncStat
4
-
5
4
  # Keep track of totals
6
5
  def self.total
7
6
  @total ||= Hash.new 0
@@ -31,18 +30,18 @@ class Multisync::RsyncStat
31
30
 
32
31
  # extracted returns a hash with labels as keys and extracted strings as values
33
32
  # {
34
- # "Number of files" => "35,648",
35
- # "Number of created files" => "2,120",
33
+ # "Number of files" => "35,648",
34
+ # "Number of created files" => "2,120",
36
35
  # ...
37
36
  # }
38
37
  def extracted
39
- @extraced ||= @output.scan(/(#{labels.join('|')}):\s+([,0-9]+)/).to_h
38
+ @extraced ||= @output.scan(/(#{labels.join("|")}):\s+([,0-9]+)/).to_h
40
39
  end
41
40
 
42
41
  # stats returns a hash with the follwing keys (and updates class total)
43
42
  # {
44
- # "Number of files" => 35648,
45
- # "Number of created files" => 2120,
43
+ # "Number of files" => 35648,
44
+ # "Number of created files" => 2120,
46
45
  # "Number of deleted files" => 37,
47
46
  # "Number of regular files transferred" => 394,
48
47
  # "Total file size" => 204936349,
@@ -81,12 +80,12 @@ class Multisync::RsyncStat
81
80
 
82
81
  def self.format_map
83
82
  {
84
- 'Number of files' => to_numbers,
85
- 'Number of created files' => to_numbers,
86
- 'Number of deleted files' => to_numbers,
87
- 'Number of regular files transferred' => to_numbers,
88
- 'Total file size' => to_filesize,
89
- 'Total transferred file size' => to_filesize,
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
90
89
  }
91
90
  end
92
91
 
@@ -97,4 +96,4 @@ class Multisync::RsyncStat
97
96
  def self.to_filesize
98
97
  ->(x) { Filesize.new(x).pretty }
99
98
  end
100
- end
99
+ end
@@ -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
- options[:dryrun]
17
- end
18
-
19
- def show_only?
20
- options[:print]
21
- end
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 '--stats'
34
- rsync_options.unshift '--verbose' unless quiet?
35
- rsync_options.unshift '--dry-run' if dryrun?
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 {|path| path.gsub(/\s+/, '\\ ') }
40
- cmd = "rsync #{rsync_options.join(' ')} #{source} #{destination}"
41
- cmd_options = { timeout: timeout }
42
- cmd_options.merge!({live_stdout: $stdout, live_stderr: $stderr}) unless quiet?
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(' --> ').color(:cyan)
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] + ' (failed)'
56
- puts "Skip: ".color(:yellow) + rsync.command
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? && ! check_path(sync.source, :source)
64
- puts "Source #{sync.source} is not accessible"
65
- puts "Skip: ".color(:yellow) + rsync.command
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? && ! check_path(sync.destination, :destination)
73
- puts "Destination #{sync.destination} is not accessible"
74
- puts "Skip: ".color(:yellow) + rsync.command
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(':').first.split('@').last
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
- abs_path = File.expand_path path
103
- abs_path = File.dirname abs_path if type == :destination
104
- File.exist? abs_path
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
@@ -1,36 +1,58 @@
1
-
2
1
  class Multisync::Selector
3
-
4
2
  # Given catalog
5
3
  attr_reader :catalog
6
-
7
- # Given set names
8
- attr_reader :sets
9
-
4
+
5
+ # Given queries
6
+ attr_reader :queries
7
+
10
8
  # Selected tasks
11
- attr_reader :result
9
+ attr_reader :results
12
10
 
13
- def initialize catalog, sets
11
+ def initialize catalog, queries
14
12
  @catalog = catalog
15
- @sets = sets
16
- @result = []
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
- result
21
+ parents ? selected_with_parents : selected
22
22
  end
23
-
24
- def visit subject, _level
25
- result << subject if selected?(subject)
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.empty?
31
- # no sets defined, but subject is in the default set
32
- return true if sets.empty? && subject.default?
33
- # subject matches any of the given sets
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