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 +4 -4
- data/CHANGELOG.md +101 -0
- data/Gemfile +8 -3
- data/LICENSE.txt +1 -1
- data/README.md +16 -12
- data/Rakefile +7 -1
- data/bin/console +4 -4
- data/lib/multisync/catalog.rb +2 -13
- data/lib/multisync/cli.rb +32 -62
- data/lib/multisync/list.rb +44 -0
- data/lib/multisync/rsync_stat.rb +89 -47
- data/lib/multisync/runtime.rb +9 -5
- data/lib/multisync/{catalog/filter.rb → selector.rb} +17 -7
- data/lib/multisync/summary.rb +53 -0
- data/lib/multisync/version.rb +1 -1
- data/lib/multisync.rb +3 -0
- data/multisync.gemspec +29 -33
- data/sample/multisync.rb +105 -29
- metadata +17 -69
- data/History.md +0 -50
- data/lib/multisync/catalog/list.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f5ccf30a0608eca5f17002de411dc941cb90d192c5343863a8b7898d2ab3270
|
4
|
+
data.tar.gz: 94d44b2c52223526c379112fa756249ebc0da3b1a61f77413c70229b95fe4525
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
2
|
-
source 'https://gems.patrickmarchi.ch'
|
1
|
+
# frozen_string_literal: true
|
3
2
|
|
4
|
-
|
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
data/README.md
CHANGED
@@ -1,30 +1,34 @@
|
|
1
|
-
[](https://badge.fury.io/rb/multisync)
|
2
2
|
|
3
|
-
#
|
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
|
-
|
10
|
+
Install multisync with:
|
11
11
|
|
12
|
-
|
13
|
-
gem 'multisync'
|
14
|
-
```
|
12
|
+
$ gem install multisync
|
15
13
|
|
16
|
-
And then execute:
|
17
14
|
|
18
|
-
|
15
|
+
## Usage
|
19
16
|
|
20
|
-
|
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
|
-
|
19
|
+
List your configuration (and check your catalog file for errors):
|
23
20
|
|
21
|
+
$ multisync -l
|
24
22
|
|
25
|
-
## Usage
|
26
23
|
|
27
|
-
|
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
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
|
-
|
14
|
-
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/lib/multisync/catalog.rb
CHANGED
@@ -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
|
20
|
-
|
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
|
20
|
-
"
|
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
|
-
|
47
|
+
options[:quiet] = false if options[:print]
|
50
48
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
-
|
98
|
-
|
99
|
-
|
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
|
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
|
data/lib/multisync/rsync_stat.rb
CHANGED
@@ -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
|
-
#
|
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" => "
|
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
|
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
|
-
|
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
|
data/lib/multisync/runtime.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
45
|
-
|
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::
|
3
|
-
|
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
|
-
#
|
10
|
+
# Selected tasks
|
7
11
|
attr_reader :result
|
8
12
|
|
9
|
-
def initialize sets
|
10
|
-
@
|
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
|
data/lib/multisync/version.rb
CHANGED
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
|
-
|
2
|
-
|
3
|
-
|
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
|
7
|
-
spec.version
|
8
|
-
spec.authors
|
9
|
-
spec.email
|
10
|
-
|
11
|
-
spec.summary
|
12
|
-
spec.description
|
13
|
-
spec.homepage
|
14
|
-
spec.license
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
30
|
-
|
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
|
33
|
-
spec.executables
|
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
|
-
|
42
|
-
|
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
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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 :
|
21
|
-
|
22
|
-
|
23
|
-
|
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 :
|
28
|
-
|
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
|
-
|
32
|
-
from "~/Pictures/Work"
|
75
|
+
from "~/Pictures", description: "Pictures"
|
33
76
|
end
|
34
77
|
|
35
|
-
sync :
|
36
|
-
|
37
|
-
|
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.
|
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:
|
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
|
-
|
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
|
-
|
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:
|
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.
|
179
|
-
signing_key:
|
126
|
+
rubygems_version: 3.1.6
|
127
|
+
signing_key:
|
180
128
|
specification_version: 4
|
181
|
-
summary:
|
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
|