multisync 0.3.3 → 0.3.7

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