multisync 0.3.3 → 0.3.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d8a0350c5ae280342b7adaf8b0fc631fb8c3ee599b50c5102a0a65ac3a10d0c5
4
- data.tar.gz: 37cfb68379262654a58db051640cde44de7a2e7fc5da3a9a089cbcf28e5235ba
3
+ metadata.gz: 0f5ccf30a0608eca5f17002de411dc941cb90d192c5343863a8b7898d2ab3270
4
+ data.tar.gz: 94d44b2c52223526c379112fa756249ebc0da3b1a61f77413c70229b95fe4525
5
5
  SHA512:
6
- metadata.gz: 01b4d15706e79188cc5c99cce466f22dccffacffefc1289abacf82f2d0879cc631dd1a763faade9c0853bb92a5dc4b5bda6a5721c1ca6048d0d9a3e039f8d059
7
- data.tar.gz: 3d99412299c849635251f0feb2ffced54f39118d92a455379d3468d7751a32a6a8af968cd86712c9478a165688ec2e53a793fec0fcd6b28f845403a85a52e07a
6
+ metadata.gz: 841ef74fc08150c9c639551a6b7fe84a2f1c941f77cea8cb10dfe56d0447709f8fc5a992afe7b4d217e76d9ab78248ac81f5859b323b1de0c8d25b26cb6c43de
7
+ data.tar.gz: e77a0e739af5df1530487d035c51ad6c8cc853e6d783a17207d63fae20e2253fe6098b54f5f5cf77c025f8fb48868a337d05c5417f3352176367ac90d9ec6e9f
data/CHANGELOG.md ADDED
@@ -0,0 +1,101 @@
1
+ ## Release v0.3.7 (2021-10-28)
2
+
3
+ * Add grand total to summary
4
+
5
+
6
+ ## Release v0.3.6 (2019-08-20)
7
+
8
+ * Tweak output when specifing --print and/or --quiet
9
+
10
+
11
+ ## Release v0.3.5 (2019-08-13)
12
+
13
+ * Fix option quiet
14
+ * Catch SIGINT and print a summary after an interrupt
15
+
16
+
17
+ ## Release v0.3.4 (2019-07-23)
18
+
19
+ * Update help
20
+ * Update readme
21
+ * Update sample file
22
+
23
+
24
+ ## Release v0.3.3 (2019-07-23)
25
+
26
+ * Update gem description
27
+
28
+
29
+ ## Release v0.3.2 (2019-07-23)
30
+
31
+ * First public release
32
+
33
+
34
+ ## Release v0.3.1 (2018-06-01)
35
+
36
+ Changes in DSL
37
+ * removed "desc"
38
+ * "from" accepts a description option
39
+ * "to"" accepts a description option
40
+ * New: "template" and "include"
41
+
42
+ Changes in CLI
43
+ * New: timeout option
44
+ * New: quiet option
45
+ * polished output
46
+
47
+
48
+ ## Release v0.2.4 (2017-09-23)
49
+
50
+ * Fix: check command
51
+
52
+
53
+ ## Release v0.2.3 (2017-09-23)
54
+
55
+ * Fix: check remote path
56
+
57
+
58
+ ## Release v0.2.2 (2017-09-23)
59
+
60
+ * replaced shell_cmd gem with mixlib-shellout
61
+ * Fix: check path for paths containing spaces
62
+
63
+
64
+ ## Release v0.2.1 (2017-09-22)
65
+
66
+ * New: option "check_from" and "check_to" to let check host or path before sync
67
+ * New: "from" and "to" accept an optional check: true|false parameter
68
+ * Change summery output to a more compact tabular form
69
+ * Use rainbow for colorization
70
+ * Move "only_if" checks to runtime
71
+
72
+
73
+ ## Release v0.2.0 (2016-03-22)
74
+
75
+ * New: option only_if for preflight checks, prior to sync
76
+ * Command line option -p/--print changed to --show
77
+
78
+
79
+ ## Release v0.1.2 (2014-08-29)
80
+
81
+ * Mark default sets with an * when listing
82
+
83
+
84
+ ## Release v0.1.1 (2014-08-28)
85
+
86
+ * Add gem dependecy 'text-highlight'
87
+
88
+
89
+ ## Release v0.1.0 (2014-07-18)
90
+
91
+ * New: define one or more groups/syncs as default, to run when no sets have been given as args
92
+
93
+
94
+ ## Release v0.0.2 (2014-07-17)
95
+
96
+ * Fix: do no escape option strings
97
+
98
+
99
+ ## Release v0.0.1 (2014-07-11)
100
+
101
+ * First release
data/Gemfile CHANGED
@@ -1,5 +1,10 @@
1
- source 'https://rubygems.org'
2
- source 'https://gems.patrickmarchi.ch'
1
+ # frozen_string_literal: true
3
2
 
4
- # Specify your gem's dependencies in multisync.gemspec
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in gitoc.gemspec
5
6
  gemspec
7
+
8
+ gem "rake", "~> 13.0"
9
+
10
+ gem "rspec", "~> 3.0"
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2019 Patrick Marchi
3
+ Copyright (c) 2021 Patrick Marchi
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,30 +1,34 @@
1
- [![Gem Version](https://badge.fury.io/rb/amaze.svg)](https://badge.fury.io/rb/multisync)
1
+ [![Gem Version](https://badge.fury.io/rb/multisync.svg)](https://badge.fury.io/rb/multisync)
2
2
 
3
- # Multisync
3
+ # multisync
4
4
 
5
5
  Multisync offers a DSL to organize sets of rsync tasks. It takes advantage of templates, groups and inheritance to simplify things.
6
6
 
7
7
 
8
8
  ## Installation
9
9
 
10
- Add this line to your application's Gemfile:
10
+ Install multisync with:
11
11
 
12
- ```ruby
13
- gem 'multisync'
14
- ```
12
+ $ gem install multisync
15
13
 
16
- And then execute:
17
14
 
18
- $ bundle
15
+ ## Usage
19
16
 
20
- Or install it yourself as:
17
+ In order to run multisync you first need a catalog file (default: `~/.multisync.rb`). Copy the [sample file](sample/multisync.rb) to `~/.multisync.rb` and use it as a starting point to adjust it to your needs.
21
18
 
22
- $ gem install multisync
19
+ List your configuration (and check your catalog file for errors):
23
20
 
21
+ $ multisync -l
24
22
 
25
- ## Usage
26
23
 
27
- TODO: Write usage instructions here
24
+ Print out the rsync commands without executing them:
25
+
26
+ $ multisync -p
27
+
28
+
29
+ Run a group or task defined in your catalog file:
30
+
31
+ $ multisync nas
28
32
 
29
33
 
30
34
  ## Development
data/Rakefile CHANGED
@@ -1,2 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
- task :default => :spec
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console CHANGED
@@ -7,8 +7,8 @@ require "multisync"
7
7
  # with your gem easier. You can also use a different console, if you like.
8
8
 
9
9
  # (If you use this, don't forget to add pry to your Gemfile!)
10
- require "pry"
11
- Pry.start
10
+ # require "pry"
11
+ # Pry.start
12
12
 
13
- # require "irb"
14
- # IRB.start(__FILE__)
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -1,8 +1,5 @@
1
1
 
2
2
  class Multisync::Catalog
3
- autoload :List, 'multisync/catalog/list'
4
- autoload :Filter, 'multisync/catalog/filter'
5
-
6
3
  # top entity of definition
7
4
  attr_reader :definition
8
5
 
@@ -16,18 +13,10 @@ class Multisync::Catalog
16
13
  end
17
14
  end
18
15
 
19
- def list
20
- catalog_list = Multisync::Catalog::List.new
21
- definition.accept(catalog_list)
22
- catalog_list.result
16
+ def traverse visitor
17
+ definition.accept visitor
23
18
  end
24
19
 
25
- def filter sets
26
- catalog_filter = Multisync::Catalog::Filter.new sets
27
- definition.accept(catalog_filter)
28
- catalog_filter.result
29
- end
30
-
31
20
  def path
32
21
  return @path if File.exist? @path
33
22
  sample_path = File.expand_path('../../../sample/multisync.rb', __FILE__)
data/lib/multisync/cli.rb CHANGED
@@ -1,3 +1,4 @@
1
+
1
2
  require 'optparse'
2
3
  require 'rainbow/ext/string'
3
4
  require 'terminal-table'
@@ -13,14 +14,11 @@ class Multisync::Cli
13
14
 
14
15
  def parser
15
16
  OptionParser.new do |o|
16
- o.banner = "\nRun rsync jobs defined in the catalog file '#{options[:file]}'.\n\n"+
17
- "Usage: #{File.basename $0} [options] [SET] [...]\n\n"+
18
- " SET selects a section from the catalog (see option -l)\n"+
19
- " use / as a follow:\n"+
20
- " work/pictures to specify the sync defined in the group work and\n"+
21
- " home/pictures to specify the sync defined in the group home\n"+
22
- " pictures alone will select both syncs, the one in the group work\n"+
23
- " as well as the one in the group home"
17
+ o.banner = "\nRun rsync jobs defined in the catalog file '#{options[:file]}'.\n\n" +
18
+ "Usage: #{File.basename $0} [options] [SET] [...]\n\n" +
19
+ " SET selects a section from the catalog (see option -l)\n" +
20
+ " use / as a group/task separator.\n" +
21
+ " e.g. #{File.basename $0} nas/userdata"
24
22
  o.separator ''
25
23
  o.on('-l', '--list', "List the catalog") do
26
24
  options[:list] = true
@@ -46,63 +44,39 @@ class Multisync::Cli
46
44
 
47
45
  def start
48
46
  parser.parse!
49
- @sets = ARGV
47
+ options[:quiet] = false if options[:print]
50
48
 
51
- case
52
- when options[:list]
53
- list_definitions
54
- else
55
- run_tasks
56
- end
57
- puts
58
- end
59
-
60
- def list_definitions
61
- puts "Catalog: #{options[:file].color(:cyan)}"
62
- table = Terminal::Table.new(rows: catalog.list, style: table_style)
63
- puts
64
- puts table
65
- end
66
-
67
- def run_tasks
68
- tasks.each do |task|
69
- runtime.run task
70
- end
71
- return if options[:print]
72
- table = Terminal::Table.new(headings: summary_headings, rows: summary_data, style: table_style)
73
- puts
74
- puts
75
- puts table
76
- end
77
-
78
- def summary_headings
79
- %w( Source Destination Files + - → ∑ ↑ ).zip(%i( left left right right right right right right )).map{|v,a| {value: v, alignment: a} }
80
- end
81
-
82
- def summary_data
83
- tasks.map do |task|
84
- result = task.result
85
- desc = [task.source_description, "--> #{task.destination_description}"]
49
+ @sets = ARGV
50
+
51
+ if options[:list]
52
+ # List tasks
53
+ puts "Catalog: #{options[:file].color(:cyan)}"
54
+ puts
55
+ puts Multisync::List.new catalog
86
56
 
87
- case result[:action]
88
- when :run
89
- if result[:status].success?
90
- # successfull run
91
- stat = Multisync::RsyncStat.new(result[:stdout]).parse
92
- [*desc, *stat.to_a.map{|e| {value: e.color(:green), alignment: :right} } ]
93
- else
94
- # failed run
95
- [*desc, { value: result[:stderr].strip.color(:red), colspan: 6 } ]
57
+ else
58
+ # Run tasks
59
+ return if tasks.empty?
60
+ begin
61
+ tasks.each do |task|
62
+ runtime.run task
96
63
  end
97
- when :skip
98
- # skiped sync
99
- [*desc, { value: result[:skip_message].color(:yellow), colspan: 6 } ]
64
+ rescue Interrupt => e
65
+ $stderr.puts "\nAborted!".color(:red)
66
+ end
67
+ unless options[:print]
68
+ puts
69
+ puts
70
+ puts Multisync::Summary.new tasks
100
71
  end
101
72
  end
73
+
74
+ puts
102
75
  end
103
76
 
104
77
  def tasks
105
- @_tasks ||= catalog.filter sets
78
+ @_tasks ||= Multisync::Selector.new(catalog, sets).tasks
79
+ # @_tasks ||= catalog.filter sets
106
80
  end
107
81
 
108
82
  def catalog
@@ -120,11 +94,7 @@ class Multisync::Cli
120
94
  dryrun: false,
121
95
  quiet: false,
122
96
  file: Multisync::Catalog.default_catalog_path,
123
- timeout: 31536000,
97
+ timeout: 31536000, # 1 year
124
98
  }
125
99
  end
126
-
127
- def table_style
128
- { border_top: false, border_bottom: false, border_x: '–', border_y: '', border_i: '', padding_left: 0, padding_right: 3 }
129
- end
130
100
  end
@@ -0,0 +1,44 @@
1
+
2
+ require 'rainbow/ext/string'
3
+
4
+ class Multisync::List
5
+
6
+ # Given catalog
7
+ attr_reader :catalog
8
+
9
+ # Tasks
10
+ attr_reader :tasks
11
+
12
+ def initialize catalog
13
+ @catalog = catalog
14
+ @tasks = []
15
+ end
16
+
17
+ def to_s
18
+ catalog.traverse self
19
+ table.to_s
20
+ end
21
+
22
+ def table
23
+ Terminal::Table.new(rows: tasks, style: table_style)
24
+ 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)}"
33
+ end
34
+ end
35
+
36
+ def description subject
37
+ desc = [subject.source_description, subject.destination_description]
38
+ desc.any?(&:empty?) ? [] : [desc.first, ['--> ', desc.last].join]
39
+ end
40
+
41
+ def table_style
42
+ { border_top: false, border_bottom: false, border_x: '–', border_y: '', border_i: '', padding_left: 0, padding_right: 3 }
43
+ end
44
+ end
@@ -1,58 +1,100 @@
1
-
2
- require 'filesize'
1
+ require "filesize"
3
2
 
4
3
  class Multisync::RsyncStat
5
-
4
+
5
+ # Keep track of totals
6
+ def self.total
7
+ @total ||= Hash.new 0
8
+ end
9
+
10
+ # Sample output
11
+ # ...
12
+ # Number of files: 89,633 (reg: 59,322, dir: 30,311)
13
+ # Number of created files: 356 (reg: 281, dir: 75)
14
+ # Number of deleted files: 114 (reg: 101, dir: 13)
15
+ # Number of regular files transferred: 344
16
+ # Total file size: 546,410,522,192 bytes
17
+ # Total transferred file size: 7,991,491,676 bytes
18
+ # Literal data: 7,952,503,842 bytes
19
+ # Matched data: 38,987,834 bytes
20
+ # File list size: 3,063,808
21
+ # File list generation time: 2.414 seconds
22
+ # File list transfer time: 0.000 seconds
23
+ # Total bytes sent: 7,957,645,803
24
+ # Total bytes received: 101,299
25
+ #
26
+ # sent 7,957,645,803 bytes received 101,299 bytes 23,719,067.37 bytes/sec
27
+ # total size is 546,410,522,192 speedup is 68.66
6
28
  def initialize output
7
29
  @output = output
8
30
  end
9
31
 
10
- # Build an internal hash with normalized stats
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
32
+ # extracted returns a hash with labels as keys and extracted strings as values
20
33
  # {
21
- # "Number of files" => "35,648",
22
- # "Number of created files" => "0",
23
- # "Number of deleted files" => "0",
24
- # "Number of regular files transferred"=>"0",
34
+ # "Number of files" => "35,648",
35
+ # "Number of created files" => "2,120",
25
36
  # ...
26
37
  # }
27
- def scan
28
- @scan ||= @output.scan(/(#{definitions.map{|d| d[:match] }.join('|')}):\s+([,0-9]+)/).each_with_object({}) {|(k,v), o| o[k] = v }
29
- end
30
-
31
- def to_a
32
- [
33
- @stats[:files],
34
- @stats[:created],
35
- @stats[:deleted],
36
- @stats[:transferred],
37
- @stats[:file_size],
38
- @stats[:transferred_size],
39
- ]
40
- end
41
-
42
- def method_missing name
43
- key = name.to_sym
44
- return @stats[key] if @stats.keys.include? key
45
- super
46
- end
47
-
48
- def definitions
49
- [
50
- { key: :files, match: 'Number of files', coerce: ->(x) { x.gsub(',',"'") }, default: '0' },
51
- { key: :created, match: 'Number of created files', coerce: ->(x) { x.gsub(',',"'") }, default: '0' },
52
- { key: :deleted, match: 'Number of deleted files', coerce: ->(x) { x.gsub(',',"'") }, default: '0' },
53
- { key: :transferred, match: 'Number of regular files transferred', coerce: ->(x) { x.gsub(',',"'") }, default: '0' },
54
- { key: :file_size, match: 'Total file size', coerce: ->(x) { Filesize.new(x.gsub(',','').to_i).pretty }, default: '0 B' },
55
- { key: :transferred_size, match: 'Total transferred file size', coerce: ->(x) { Filesize.new(x.gsub(',','').to_i).pretty }, default: '0 B' },
56
- ]
38
+ def extracted
39
+ @extraced ||= @output.scan(/(#{labels.join('|')}):\s+([,0-9]+)/).to_h
40
+ end
41
+
42
+ # stats returns a hash with the follwing keys (and updates class total)
43
+ # {
44
+ # "Number of files" => 35648,
45
+ # "Number of created files" => 2120,
46
+ # "Number of deleted files" => 37,
47
+ # "Number of regular files transferred" => 394,
48
+ # "Total file size" => 204936349,
49
+ # "Total transferred file size" => 49239,
50
+ # }
51
+ def stats
52
+ @stats ||= labels.each_with_object({}) do |label, h|
53
+ value = extracted[label]&.delete(",").to_i
54
+
55
+ self.class.total[label] += value # update total
56
+ h[label] = value
57
+ end
58
+ end
59
+
60
+ def labels
61
+ self.class.format_map.keys
62
+ end
63
+
64
+ def formatted_values
65
+ self.class.format_values do |label|
66
+ stats[label]
67
+ end
68
+ end
69
+
70
+ def self.formatted_totals
71
+ format_values do |label|
72
+ total[label]
73
+ end
74
+ end
75
+
76
+ def self.format_values
77
+ format_map.map do |label, format|
78
+ format.call(yield label)
79
+ end
80
+ end
81
+
82
+ def self.format_map
83
+ {
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,
90
+ }
91
+ end
92
+
93
+ def self.to_numbers
94
+ ->(x) { x.to_s.gsub(/\B(?=(...)*\b)/, "'") }
95
+ end
96
+
97
+ def self.to_filesize
98
+ ->(x) { Filesize.new(x).pretty }
57
99
  end
58
100
  end
@@ -38,11 +38,15 @@ class Multisync::Runtime
38
38
  # special meaning for home, instead of passing it as literal char
39
39
  source, destination = [sync.source, sync.destination].map {|path| path.gsub(/\s+/, '\\ ') }
40
40
  cmd = "rsync #{rsync_options.join(' ')} #{source} #{destination}"
41
- rsync = Mixlib::ShellOut.new(cmd, live_stdout: $stdout, live_stderr: $stderr, timeout: timeout)
41
+ cmd_options = { timeout: timeout }
42
+ cmd_options.merge!({live_stdout: $stdout, live_stderr: $stderr}) unless quiet?
43
+ rsync = Mixlib::ShellOut.new(cmd, cmd_options)
42
44
  sync.result[:cmd] = rsync.command
43
-
44
- puts
45
- puts [sync.source_description, sync.destination_description].join(' --> ').color(:cyan)
45
+
46
+ unless quiet?
47
+ puts
48
+ puts [sync.source_description, sync.destination_description].join(' --> ').color(:cyan)
49
+ end
46
50
 
47
51
  # Perform all only_if checks, from top to bottom
48
52
  sync.checks.each do |check|
@@ -77,7 +81,7 @@ class Multisync::Runtime
77
81
  puts rsync.command
78
82
  else
79
83
  sync.result[:action] = :run
80
- puts rsync.command if dryrun?
84
+ puts rsync.command if dryrun? && !quiet?
81
85
  rsync.run_command
82
86
  sync.result[:status] = rsync.status
83
87
  sync.result[:stdout] = rsync.stdout
@@ -1,16 +1,26 @@
1
1
 
2
- class Multisync::Catalog::Filter
3
- # selected sets
2
+ class Multisync::Selector
3
+
4
+ # Given catalog
5
+ attr_reader :catalog
6
+
7
+ # Given set names
4
8
  attr_reader :sets
5
9
 
6
- # selected subjects
10
+ # Selected tasks
7
11
  attr_reader :result
8
12
 
9
- def initialize sets
10
- @sets = Array(sets)
13
+ def initialize catalog, sets
14
+ @catalog = catalog
15
+ @sets = sets
11
16
  @result = []
12
17
  end
13
-
18
+
19
+ def tasks
20
+ catalog.traverse self
21
+ result
22
+ end
23
+
14
24
  def visit subject, _level
15
25
  result << subject if selected?(subject)
16
26
  end
@@ -23,4 +33,4 @@ class Multisync::Catalog::Filter
23
33
  # subject matches any of the given sets
24
34
  sets.any? {|set| /\b#{set}\b/.match subject.fullname }
25
35
  end
26
- end
36
+ end
@@ -0,0 +1,53 @@
1
+ class Multisync::Summary
2
+
3
+ # All tasks to include in the summary
4
+ attr_reader :tasks
5
+
6
+ def initialize tasks
7
+ @tasks = tasks
8
+ end
9
+
10
+ def to_s
11
+ table.to_s
12
+ end
13
+
14
+ def table
15
+ Terminal::Table.new(headings: headings, rows: data, style: table_style)
16
+ end
17
+
18
+ def headings
19
+ %w( Source Destination Files + - → ∑ ↑ ).zip(%i( left left right right right right right right )).map{|v,a| {value: v, alignment: a} }
20
+ end
21
+
22
+ def data
23
+ tasks.map do |task|
24
+ result = task.result
25
+ desc = [task.source_description, "--> #{task.destination_description}"]
26
+
27
+ case result[:action]
28
+ when :run
29
+ if result[:status] && result[:status].success?
30
+ # successfull run
31
+ stats = Multisync::RsyncStat.new(result[:stdout])
32
+ [*desc, *stats.formatted_values.map{|e| {value: e.color(:green), alignment: :right} } ]
33
+ else
34
+ # failed or interrupted run
35
+ [*desc, { value: (result[:stderr] || 'n/a').strip.color(:red), colspan: 6 } ]
36
+ end
37
+
38
+ when :skip
39
+ # skiped sync
40
+ [*desc, { value: result[:skip_message].color(:yellow), colspan: 6 } ]
41
+
42
+ else
43
+ # not executed
44
+ [*desc, { value: 'not executed'.faint, colspan: 6 } ]
45
+ end
46
+ end.push ["Total".faint, "", *Multisync::RsyncStat.formatted_totals.map{|e| {value: e.faint, alignment: :right} } ]
47
+ end
48
+
49
+ def table_style
50
+ { border_top: false, border_bottom: false, border_x: '–', border_y: '', border_i: '', padding_left: 0, padding_right: 3 }
51
+ end
52
+
53
+ end
@@ -1,3 +1,3 @@
1
1
  module Multisync
2
- VERSION = "0.3.3"
2
+ VERSION = "0.3.7"
3
3
  end
data/lib/multisync.rb CHANGED
@@ -4,6 +4,9 @@ module Multisync
4
4
  autoload :Cli, 'multisync/cli'
5
5
  autoload :Definition, 'multisync/definition'
6
6
  autoload :Catalog, 'multisync/catalog'
7
+ autoload :Selector, 'multisync/selector'
7
8
  autoload :Runtime, 'multisync/runtime'
8
9
  autoload :RsyncStat, 'multisync/rsync_stat'
10
+ autoload :Summary, 'multisync/summary'
11
+ autoload :List, 'multisync/list'
9
12
  end
data/multisync.gemspec CHANGED
@@ -1,45 +1,41 @@
1
- lib = File.expand_path("../lib", __FILE__)
2
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
- require "multisync/version"
1
+
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "lib/multisync/version"
4
5
 
5
6
  Gem::Specification.new do |spec|
6
- spec.name = "multisync"
7
- spec.version = Multisync::VERSION
8
- spec.authors = ["Patrick Marchi"]
9
- spec.email = ["mail@patrickmarchi.ch"]
10
-
11
- spec.summary = %q{DSL for rsync.}
12
- spec.description = %q{Multisync offers a DSL to organize sets of rsync tasks.}
13
- spec.homepage = ""
14
- spec.license = "MIT"
15
-
16
- # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17
- # to allow pushing to a single host or delete this section to allow pushing to any host.
18
- if spec.respond_to?(:metadata)
19
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
-
21
- # spec.metadata["homepage_uri"] = spec.homepage
22
- # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
23
- # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
24
- else
25
- raise "RubyGems 2.0 or newer is required to protect against " \
26
- "public gem pushes."
27
- end
7
+ spec.name = "multisync"
8
+ spec.version = Multisync::VERSION
9
+ spec.authors = ["Patrick Marchi"]
10
+ spec.email = ["mail@patrickmarchi.ch"]
11
+
12
+ spec.summary = [spec.name, spec.version].join("-")
13
+ spec.description = "A DSL to organize sets of rsync tasks."
14
+ spec.homepage = "https://github.com/pmarchi/multisync"
15
+ spec.license = "MIT"
16
+ spec.required_ruby_version = ">= 2.4.0"
17
+
18
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
19
+
20
+ spec.metadata["homepage_uri"] = spec.homepage
21
+ spec.metadata["source_code_uri"] = spec.homepage
22
+ spec.metadata["changelog_uri"] = File.join(spec.homepage, "blob/main/CHANGELOG.md")
28
23
 
29
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
30
- f.match(%r{^(test|spec|features)/})
24
+ # Specify which files should be added to the gem when it is released.
25
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
26
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
27
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
31
28
  end
32
- spec.bindir = "exe"
33
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
29
+ spec.bindir = "exe"
30
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
34
31
  spec.require_paths = ["lib"]
35
32
 
33
+ # Uncomment to register a new dependency of your gem
36
34
  spec.add_dependency "mixlib-shellout"
37
35
  spec.add_dependency "filesize"
38
36
  spec.add_dependency "rainbow"
39
37
  spec.add_dependency "terminal-table"
40
38
 
41
- spec.add_development_dependency "bundler", "~> 1.16"
42
- spec.add_development_dependency "rake", "~> 10.0"
43
- spec.add_development_dependency "rspec"
44
- spec.add_development_dependency "pry"
39
+ # For more information and examples about making a new gem, checkout our
40
+ # guide at: https://bundler.io/guides/creating_gem.html
45
41
  end
data/sample/multisync.rb CHANGED
@@ -1,39 +1,115 @@
1
- options %w( --archive --delete --delete-excluded --exclude=.DS_Store )
2
-
3
- group :home do
4
- group :cloud do
5
- to "/Backup/Cloud"
6
-
7
- sync :dropbox do
8
- desc "Dropbox"
9
- from "~/Dropbox"
10
- options %q(--exclude='.dropbox.cache')
11
- end
12
-
13
- sync :copy do
14
- desc "Copy"
15
- from "~/Copy"
16
- options %w(--archive --delete --exclude='.copy.cache'), :override
17
- end
1
+ # Copy this file to your home: ~/.multisync.rb
2
+ # Choose one of the section A), B) or C) as a starting point
3
+ # to adjust the configuration to your needs.
4
+
5
+
6
+ ################################################################################
7
+
8
+ # A) Simple rsync task
9
+
10
+ sync :simple do
11
+ from "~/Documents"
12
+ to "/PathToExternalDisk"
13
+ options %w( --archive --exclude=.DS_Store ) # as array
14
+ end
15
+
16
+ # This task can be run with: "multisync simple"
17
+
18
+
19
+ ################################################################################
20
+
21
+ # B) Group of rsync tasks
22
+
23
+ group :userdata do
24
+
25
+ # Define the target path for the whole group and check the existance of the
26
+ # target path before running the rsync task.
27
+ # Also set an optional description for the target.
28
+ to "/PathToExternalDisk", description: "External HD", check: true
29
+
30
+ # Define rsync options for the whole group
31
+ options %w( --archive --exclude=.DS_Store )
32
+
33
+ sync :desktop do
34
+ # With optional description of the source
35
+ from "~/Desktop", description: "Desktop"
18
36
  end
19
37
 
20
- sync :pictures do
21
- desc "Pictures"
22
- from "~/Pictures/Private"
23
- to "/Backup/Home"
38
+ sync :documents do
39
+ from "~/Documents", description: "Documents"
40
+ end
41
+
42
+ sync :downloads do
43
+ from "~/Downloads", description: "Downloads"
44
+ # Add options specific to this task.
45
+ options %w( --exclude='*.download' )
24
46
  end
25
47
  end
26
48
 
27
- group :work do
28
- to "/Backup/Work"
49
+ # Run the whole group with: "multisync userdata"
50
+ # Run a single taks with: "multisync userdata/downloads"
51
+
52
+
53
+ ################################################################################
54
+
55
+ # C) Real world example using templates, defaults and options override
56
+
57
+ # rsync options for all tasks
58
+ options %w( --archive --delete --delete-excluded --delete-after --exclude=.DS_Store --exclude=.localized )
59
+
60
+
61
+ # Use templates to define a set of tasks that can be included later
62
+ template :data do
63
+
64
+ # Always check the existance of the source path
65
+ check_from true
66
+
67
+ # rsync tasks with uncomplete arguments:
68
+ # Define the target later where the template will be included.
69
+ # This can be used to sync multiple directories to different remote locations.
70
+ sync :documents do
71
+ from "~/Documents", description: "Documents"
72
+ end
29
73
 
30
74
  sync :pictures do
31
- desc "Pictures"
32
- from "~/Pictures/Work"
75
+ from "~/Pictures", description: "Pictures"
33
76
  end
34
77
 
35
- sync :doc do
36
- desc "Documentation"
37
- from "~/Work/doc"
78
+ sync :downloads do
79
+ from "~/Downloads", description: "Downloads"
80
+ # Don't merge options
81
+ options %w( --times --exclude='*.download' ), :override
38
82
  end
39
- end
83
+ end
84
+
85
+
86
+ group :hd do
87
+ # Uncomment the following line to run this group by default
88
+ # default
89
+
90
+ # Define the target to be used by all tasks.
91
+ # The existance of the target path should be checked.
92
+ to "/TargetPathToBackupDisk/MyComputer", description: "External Disk", check: true
93
+
94
+ # Include the template with the task definitions
95
+ include :data
96
+ end
97
+
98
+ group :nas do
99
+ to "user@nas.local:/data/backup/my_computer", description: "NAS", check: true
100
+
101
+ # Include the template with the task definitions
102
+ include :data
103
+ end
104
+
105
+ # Run both groups with: "multisync hd nas"
106
+ # Sync "desktop" to "hd" and "nas": "multisync desktop"
107
+ # Sync "desktop" to "hd" only: "multisync hd/desktop"
108
+
109
+
110
+ ################################################################################
111
+
112
+ # Additional notes
113
+ # - groups can be nested
114
+ # - use "default" to run one or more groups without specifing a name
115
+ # - "default" can also be set on the top level and defines all tasks as default.
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: multisync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.3
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Patrick Marchi
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-07-23 00:00:00.000000000 Z
11
+ date: 2021-10-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mixlib-shellout
@@ -66,63 +66,7 @@ dependencies:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
68
  version: '0'
69
- - !ruby/object:Gem::Dependency
70
- name: bundler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1.16'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1.16'
83
- - !ruby/object:Gem::Dependency
84
- name: rake
85
- requirement: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '10.0'
90
- type: :development
91
- prerelease: false
92
- version_requirements: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - "~>"
95
- - !ruby/object:Gem::Version
96
- version: '10.0'
97
- - !ruby/object:Gem::Dependency
98
- name: rspec
99
- requirement: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- type: :development
105
- prerelease: false
106
- version_requirements: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- - !ruby/object:Gem::Dependency
112
- name: pry
113
- requirement: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- type: :development
119
- prerelease: false
120
- version_requirements: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- description: Multisync offers a DSL to organize sets of rsync tasks.
69
+ description: A DSL to organize sets of rsync tasks.
126
70
  email:
127
71
  - mail@patrickmarchi.ch
128
72
  executables:
@@ -132,8 +76,8 @@ extra_rdoc_files: []
132
76
  files:
133
77
  - ".gitignore"
134
78
  - ".rspec"
79
+ - CHANGELOG.md
135
80
  - Gemfile
136
- - History.md
137
81
  - LICENSE.txt
138
82
  - README.md
139
83
  - Rakefile
@@ -142,25 +86,29 @@ files:
142
86
  - exe/multisync
143
87
  - lib/multisync.rb
144
88
  - lib/multisync/catalog.rb
145
- - lib/multisync/catalog/filter.rb
146
- - lib/multisync/catalog/list.rb
147
89
  - lib/multisync/cli.rb
148
90
  - lib/multisync/definition.rb
149
91
  - lib/multisync/definition/dsl.rb
150
92
  - lib/multisync/definition/entity.rb
151
93
  - lib/multisync/definition/null.rb
152
94
  - lib/multisync/definition/template.rb
95
+ - lib/multisync/list.rb
153
96
  - lib/multisync/rsync_stat.rb
154
97
  - lib/multisync/runtime.rb
98
+ - lib/multisync/selector.rb
99
+ - lib/multisync/summary.rb
155
100
  - lib/multisync/version.rb
156
101
  - multisync.gemspec
157
102
  - sample/multisync.rb
158
- homepage: ''
103
+ homepage: https://github.com/pmarchi/multisync
159
104
  licenses:
160
105
  - MIT
161
106
  metadata:
162
107
  allowed_push_host: https://rubygems.org
163
- post_install_message:
108
+ homepage_uri: https://github.com/pmarchi/multisync
109
+ source_code_uri: https://github.com/pmarchi/multisync
110
+ changelog_uri: https://github.com/pmarchi/multisync/blob/main/CHANGELOG.md
111
+ post_install_message:
164
112
  rdoc_options: []
165
113
  require_paths:
166
114
  - lib
@@ -168,15 +116,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
168
116
  requirements:
169
117
  - - ">="
170
118
  - !ruby/object:Gem::Version
171
- version: '0'
119
+ version: 2.4.0
172
120
  required_rubygems_version: !ruby/object:Gem::Requirement
173
121
  requirements:
174
122
  - - ">="
175
123
  - !ruby/object:Gem::Version
176
124
  version: '0'
177
125
  requirements: []
178
- rubygems_version: 3.0.1
179
- signing_key:
126
+ rubygems_version: 3.1.6
127
+ signing_key:
180
128
  specification_version: 4
181
- summary: DSL for rsync.
129
+ summary: multisync-0.3.7
182
130
  test_files: []
data/History.md DELETED
@@ -1,50 +0,0 @@
1
-
2
- # multisync
3
-
4
- ## v0.3.1 (2018-06-01)
5
- Changes in DSL
6
- - removed "desc"
7
- - "from" accepts a description option
8
- - "to"" accepts a description option
9
- - New: "template" and "include"
10
-
11
- Changes in CLI
12
- - New: timeout option
13
- - New: quiet option
14
- - polished output
15
-
16
- ## v0.2.4 (2017-09-23)
17
- - Fix: check command
18
-
19
- ## v0.2.3 (2017-09-23)
20
- - Fix: check remote path
21
-
22
- ## v0.2.2 (2017-09-23)
23
- - replaced shell_cmd gem with mixlib-shellout
24
- - Fix: check path for paths containing spaces
25
-
26
- ## v0.2.1 (2017-09-22)
27
- - New: option "check_from" and "check_to" to let check host or path before sync
28
- - New: "from" and "to" accept an optional check: true|false parameter
29
- - Change summery output to a more compact tabular form
30
- - Use rainbow for colorization
31
- - Move "only_if" checks to runtime
32
-
33
- ## v0.2.0 (2016-03-22)
34
- - New: option only_if for preflight checks, prior to sync
35
- - Command line option -p/--print changed to --show
36
-
37
- ## v0.1.2 (2014-08-29)
38
- - Mark default sets with an * when listing
39
-
40
- ## v0.1.1 (2014-08-28)
41
- - Add gem dependecy 'text-highlight'
42
-
43
- ## v0.1.0 (2014-07-18)
44
- - New: define one or more groups/syncs as default, to run when no sets have been given as args
45
-
46
- ## v0.0.2 (2014-07-17)
47
- - Fix: do no escape option strings
48
-
49
- ## v0.0.1 (2014-07-11)
50
- - First release
@@ -1,24 +0,0 @@
1
-
2
- class Multisync::Catalog::List
3
- # result
4
- attr_reader :result
5
-
6
- def initialize
7
- @result = []
8
- end
9
-
10
- def visit subject, level
11
- if level > 0
12
- tab = ''.ljust(2*(level-1), ' ')
13
- default = subject.default? ? ' *' : ''
14
- name = "#{tab}#{subject.name}#{default}"
15
- @result << [name, *description(subject)]
16
- # puts "#{name.ljust(32, ' ')}#{description(subject)}"
17
- end
18
- end
19
-
20
- def description subject
21
- desc = [subject.source_description, subject.destination_description]
22
- desc.any?(&:empty?) ? [] : [desc.first, ['--> ', desc.last].join]
23
- end
24
- end