multisync 0.3.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 207be5b2d2b67ce8dbf2c5b5aeb9e2fc709ddfd1646d0c6b25409b814b3d3384
4
+ data.tar.gz: 14bfcf64063a60a0a42cc04b24bd8df0843a01ec7af63950b8c0df0d72693e9a
5
+ SHA512:
6
+ metadata.gz: 618fdf7e363491fcbb098e6d80689b22486ae22407faca9098def54bf6ae95395bbdbf93c6b4ba6c6e2e4b3124e10ba773c1eb534ede061cf524022a45e1f554
7
+ data.tar.gz: bc00ab0d977f2f28e83bcdee4683e3981bfcab6c3709dc34f761382d795cd96788b24cce1928647d971a89887239ae295022d80a600fb5afe1591ca65dd8c7ba
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+ source 'https://gems.patrickmarchi.ch'
3
+
4
+ # Specify your gem's dependencies in multisync.gemspec
5
+ gemspec
data/History.md ADDED
@@ -0,0 +1,50 @@
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
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Patrick Marchi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ [![Gem Version](https://badge.fury.io/rb/amaze.svg)](https://badge.fury.io/rb/multisync)
2
+
3
+ # Multisync
4
+
5
+ Multisync offers a DSL to organize sets of rsync tasks. It takes advantage of templates, groups and inheritance to simplify things.
6
+
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'multisync'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install multisync
23
+
24
+
25
+ ## Usage
26
+
27
+ TODO: Write usage instructions here
28
+
29
+
30
+ ## Development
31
+
32
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
33
+
34
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
35
+
36
+
37
+ ## Contributing
38
+
39
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/multisync.
40
+
41
+
42
+ ## License
43
+
44
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "multisync"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
12
+
13
+ # require "irb"
14
+ # IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/multisync ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'multisync'
4
+
5
+ Multisync::Cli.start
data/lib/multisync.rb ADDED
@@ -0,0 +1,9 @@
1
+ require "multisync/version"
2
+
3
+ module Multisync
4
+ autoload :Cli, 'multisync/cli'
5
+ autoload :Definition, 'multisync/definition'
6
+ autoload :Catalog, 'multisync/catalog'
7
+ autoload :Runtime, 'multisync/runtime'
8
+ autoload :RsyncStat, 'multisync/rsync_stat'
9
+ end
@@ -0,0 +1,40 @@
1
+
2
+ class Multisync::Catalog
3
+ autoload :List, 'multisync/catalog/list'
4
+ autoload :Filter, 'multisync/catalog/filter'
5
+
6
+ # top entity of definition
7
+ attr_reader :definition
8
+
9
+ def initialize path
10
+ @path = File.expand_path(path)
11
+ end
12
+
13
+ def definition
14
+ @_definition ||= Multisync::Definition::Entity.new(Multisync::Definition::Null.new, '__MAIN__').tap do |e|
15
+ e.instance_eval File.read(path)
16
+ end
17
+ end
18
+
19
+ def list
20
+ catalog_list = Multisync::Catalog::List.new
21
+ definition.accept(catalog_list)
22
+ catalog_list.result
23
+ end
24
+
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
+ def path
32
+ return @path if File.exist? @path
33
+ sample_path = File.expand_path('../../../sample/multisync.rb', __FILE__)
34
+ raise RuntimeError.new, "No catalog found at #{@path}. Copy sample from #{sample_path} to #{@path} and adjust to your needs."
35
+ end
36
+
37
+ def self.default_catalog_path
38
+ '~/.multisync.rb'
39
+ end
40
+ end
@@ -0,0 +1,26 @@
1
+
2
+ class Multisync::Catalog::Filter
3
+ # selected sets
4
+ attr_reader :sets
5
+
6
+ # selected subjects
7
+ attr_reader :result
8
+
9
+ def initialize sets
10
+ @sets = Array(sets)
11
+ @result = []
12
+ end
13
+
14
+ def visit subject, _level
15
+ result << subject if selected?(subject)
16
+ end
17
+
18
+ def selected? subject
19
+ # only return the leaves of the definition tree
20
+ return false unless subject.members.empty?
21
+ # no sets defined, but subject is in the default set
22
+ return true if sets.empty? && subject.default?
23
+ # subject matches any of the given sets
24
+ sets.any? {|set| /\b#{set}\b/.match subject.fullname }
25
+ end
26
+ end
@@ -0,0 +1,24 @@
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
@@ -0,0 +1,130 @@
1
+ require 'optparse'
2
+ require 'rainbow/ext/string'
3
+ require 'terminal-table'
4
+
5
+ class Multisync::Cli
6
+
7
+ def self.start
8
+ new.start
9
+ end
10
+
11
+ # Given sets to run or empty
12
+ attr_reader :sets
13
+
14
+ def parser
15
+ 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"
24
+ o.separator ''
25
+ o.on('-l', '--list', "List the catalog") do
26
+ options[:list] = true
27
+ end
28
+ o.on('-p', '--print', "Print the commands without executing them") do
29
+ options[:print] = true
30
+ end
31
+ o.on('-q', '--quiet', "Show only rsync summary") do
32
+ options[:quiet] = true
33
+ end
34
+ o.on('--catalog FILE', "Specify a catalog", "Default is #{options[:file]}") do |file|
35
+ options[:file] = file
36
+ end
37
+ o.on('--timeout SECS', Integer, "Timeout for rsync job", "Default is #{options[:timeout]}") do |timeout|
38
+ options[:timeout] = timeout
39
+ end
40
+ o.on('-n', '--dryrun', "Run rsync in dry-run mode") do
41
+ options[:dryrun] = true
42
+ end
43
+ o.separator ''
44
+ end
45
+ end
46
+
47
+ def start
48
+ parser.parse!
49
+ @sets = ARGV
50
+
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}"]
86
+
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 } ]
96
+ end
97
+ when :skip
98
+ # skiped sync
99
+ [*desc, { value: result[:skip_message].color(:yellow), colspan: 6 } ]
100
+ end
101
+ end
102
+ end
103
+
104
+ def tasks
105
+ @_tasks ||= catalog.filter sets
106
+ end
107
+
108
+ def catalog
109
+ @_catalog ||= Multisync::Catalog.new options[:file]
110
+ end
111
+
112
+ def runtime
113
+ @_runtime ||= Multisync::Runtime.new(options)
114
+ end
115
+
116
+ def options
117
+ @_options ||= {
118
+ list: false,
119
+ print: false,
120
+ dryrun: false,
121
+ quiet: false,
122
+ file: Multisync::Catalog.default_catalog_path,
123
+ timeout: 31536000,
124
+ }
125
+ 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
+ end
@@ -0,0 +1,7 @@
1
+
2
+ class Multisync::Definition
3
+ autoload :Dsl, 'multisync/definition/dsl'
4
+ autoload :Entity, 'multisync/definition/entity'
5
+ autoload :Null, 'multisync/definition/null'
6
+ autoload :Template, 'multisync/definition/template'
7
+ end
@@ -0,0 +1,61 @@
1
+
2
+ module Multisync::Definition::Dsl
3
+
4
+ # The DSL methods
5
+ def group name, &block
6
+ Multisync::Definition::Entity.new self, name, &block
7
+ end
8
+
9
+ def sync name, &block
10
+ Multisync::Definition::Entity.new self, name, &block
11
+ end
12
+
13
+ def template name, &block
14
+ Multisync::Definition::Template.new name, &block
15
+ end
16
+
17
+ def include name
18
+ template = Multisync::Definition::Template.lookup name
19
+ instance_eval &template.block
20
+ end
21
+
22
+ def from value, options={}
23
+ @from_value = value
24
+ # Check source's host or path before sync
25
+ @from_check = options[:check]
26
+ @from_description = options[:description]
27
+ end
28
+
29
+ def to value, options={}
30
+ @to_value = value
31
+ # Check destination's host or path before sync
32
+ @to_check = options[:check]
33
+ @to_description = options[:description]
34
+ end
35
+
36
+ def options rsync_options, mode=:append
37
+ @rsync_options_mode = mode
38
+ @rsync_options = Array(rsync_options)
39
+ end
40
+
41
+ def default
42
+ @default = true
43
+ end
44
+
45
+ # Defines a check, that should pass in order to invoke the sync
46
+ def only_if cmd, options={}
47
+ @check = { cmd: cmd, message: options.fetch(:message, cmd) }
48
+ end
49
+
50
+ # Check source's host or path before sync
51
+ # can also be set as option of "from"
52
+ def check_from flag=true
53
+ @from_check = flag
54
+ end
55
+
56
+ # Check destination's host or path before sync
57
+ # can also be set as option of "to"
58
+ def check_to flag=true
59
+ @to_check = flag
60
+ end
61
+ end
@@ -0,0 +1,116 @@
1
+
2
+ class Multisync::Definition::Entity
3
+
4
+ include Multisync::Definition::Dsl
5
+
6
+ # The parent of the group
7
+ attr_reader :parent
8
+
9
+ # The name of the group
10
+ attr_reader :name
11
+
12
+ # All members (groups or syncs) of this group
13
+ attr_reader :members
14
+
15
+ # Collected results after run as Hash
16
+ # {
17
+ # cmd: 'rsync --stats -v source destination',
18
+ # action: :run,
19
+ # status: #<Process::Status: pid 65416 exit 0>,
20
+ # stdout: '',
21
+ # stderr: '',
22
+ # skip_message: 'host not reachable',
23
+ # }
24
+
25
+ attr_reader :result
26
+
27
+ def initialize parent, name, &block
28
+ @members = []
29
+ @name = name.to_s
30
+ @parent = parent
31
+ parent.register self
32
+ instance_eval(&block) if block_given?
33
+ @result = {}
34
+ end
35
+
36
+ def to_h
37
+ {
38
+ fullname: fullname,
39
+ default: default?,
40
+ source: {
41
+ path: source,
42
+ description: source_description,
43
+ check: check_source?,
44
+ },
45
+ destination: {
46
+ path: destination,
47
+ description: destination_description,
48
+ check: check_destination?,
49
+ },
50
+ checks: checks,
51
+ rsync_options: rsync_options,
52
+ }
53
+ end
54
+
55
+ # Make the definition visitable
56
+ def accept visitor, level=0
57
+ visitor.visit self, level
58
+ members.map do |member|
59
+ member.accept visitor, level+1
60
+ end
61
+ end
62
+
63
+ def register member
64
+ members << member
65
+ end
66
+
67
+ # The name including all parents separated by "/"
68
+ def fullname
69
+ [parent.fullname, name].join '/'
70
+ end
71
+
72
+ # rsync source
73
+ def source
74
+ @from_value || parent.source
75
+ end
76
+
77
+ def source_description
78
+ @from_description || @from_value || parent.source_description
79
+ end
80
+
81
+ # rsync destination
82
+ def destination
83
+ @to_value || parent.destination
84
+ end
85
+
86
+ def destination_description
87
+ @to_description || @to_value || parent.destination_description
88
+ end
89
+
90
+ # rsync options
91
+ def rsync_options
92
+ opts = @rsync_options || []
93
+ return opts if @rsync_options_mode == :override
94
+ parent.rsync_options + opts
95
+ end
96
+
97
+ # Is this group/sync defined as default
98
+ def default?
99
+ @default || parent.default?
100
+ end
101
+
102
+ # All checks from parent to child
103
+ def checks
104
+ (parent.checks + [@check]).compact
105
+ end
106
+
107
+ # Should source's host or path be checked before sync?
108
+ def check_source?
109
+ @from_check.nil? ? parent.check_source? : @from_check
110
+ end
111
+
112
+ # Should destination's host or path be checked before sync?
113
+ def check_destination?
114
+ @to_check.nil? ? parent.check_destination? : @to_check
115
+ end
116
+ end
@@ -0,0 +1,52 @@
1
+
2
+ class Multisync::Definition::Null < Multisync::Definition::Entity
3
+
4
+ def initialize
5
+ end
6
+
7
+ def register member
8
+ end
9
+
10
+ def fullname
11
+ nil
12
+ end
13
+
14
+ def rsync_options
15
+ []
16
+ end
17
+
18
+ # from (source) is a required option and should be set at least at root level
19
+ def source
20
+ raise "no source (from) defined"
21
+ end
22
+
23
+ def source_description
24
+ ''
25
+ end
26
+
27
+ # to (destination) is a required option and should be set at least at root level
28
+ def destination
29
+ raise "no destination (to) defined"
30
+ end
31
+
32
+ def destination_description
33
+ ''
34
+ end
35
+
36
+ def default?
37
+ false
38
+ end
39
+
40
+ def checks
41
+ []
42
+ end
43
+
44
+ def check_source?
45
+ false
46
+ end
47
+
48
+ def check_destination?
49
+ false
50
+ end
51
+ end
52
+
@@ -0,0 +1,26 @@
1
+
2
+ class Multisync::Definition::Template
3
+ include Multisync::Definition::Dsl
4
+
5
+ @registered = []
6
+
7
+ def self.register instance
8
+ @registered << instance
9
+ end
10
+
11
+ def self.lookup name
12
+ @registered.find {|instance| instance.name == name }
13
+ end
14
+
15
+ # The name of the template
16
+ attr_reader :name
17
+
18
+ # The block the template holds
19
+ attr_reader :block
20
+
21
+ def initialize name, &block
22
+ @name = name
23
+ self.class.register self
24
+ @block = block
25
+ end
26
+ end
@@ -0,0 +1,58 @@
1
+
2
+ require 'filesize'
3
+
4
+ class Multisync::RsyncStat
5
+
6
+ def initialize output
7
+ @output = output
8
+ end
9
+
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
20
+ # {
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",
25
+ # ...
26
+ # }
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
+ ]
57
+ end
58
+ end
@@ -0,0 +1,103 @@
1
+
2
+ require 'mixlib/shellout'
3
+
4
+ class Multisync::Runtime
5
+
6
+ # Runtime options
7
+ # dryrun: true|false
8
+ # show: true|false
9
+ attr_reader :options
10
+
11
+ def initialize options
12
+ @options = options
13
+ end
14
+
15
+ def dryrun?
16
+ options[:dryrun]
17
+ end
18
+
19
+ def show_only?
20
+ options[:print]
21
+ end
22
+
23
+ def quiet?
24
+ options[:quiet]
25
+ end
26
+
27
+ def timeout
28
+ options[:timeout]
29
+ end
30
+
31
+ def run sync
32
+ rsync_options = sync.rsync_options.dup
33
+ rsync_options.unshift '--stats'
34
+ rsync_options.unshift '--verbose' unless quiet?
35
+ rsync_options.unshift '--dry-run' if dryrun?
36
+
37
+ # escape path by hand, shellescape escapes also ~, but we want to keep its
38
+ # special meaning for home, instead of passing it as literal char
39
+ source, destination = [sync.source, sync.destination].map {|path| path.gsub(/\s+/, '\\ ') }
40
+ cmd = "rsync #{rsync_options.join(' ')} #{source} #{destination}"
41
+ rsync = Mixlib::ShellOut.new(cmd, live_stdout: $stdout, live_stderr: $stderr, timeout: timeout)
42
+ sync.result[:cmd] = rsync.command
43
+
44
+ puts
45
+ puts [sync.source_description, sync.destination_description].join(' --> ').color(:cyan)
46
+
47
+ # Perform all only_if checks, from top to bottom
48
+ sync.checks.each do |check|
49
+ next unless Mixlib::ShellOut.new(check[:cmd]).run_command.error?
50
+
51
+ puts check[:cmd] + ' (failed)'
52
+ puts "Skip: ".color(:yellow) + rsync.command
53
+ sync.result[:action] = :skip
54
+ sync.result[:skip_message] = check[:message]
55
+ return
56
+ end
57
+
58
+ # source check
59
+ if sync.check_source? && ! check_path(sync.source, :source)
60
+ puts "Source #{sync.source} is not accessible"
61
+ puts "Skip: ".color(:yellow) + rsync.command
62
+ sync.result[:action] = :skip
63
+ sync.result[:skip_message] = "Source is not accessible"
64
+ return
65
+ end
66
+
67
+ # target check
68
+ if sync.check_destination? && ! check_path(sync.destination, :destination)
69
+ puts "Destination #{sync.destination} is not accessible"
70
+ puts "Skip: ".color(:yellow) + rsync.command
71
+ sync.result[:action] = :skip
72
+ sync.result[:skip_message] = "Destination is not accessible"
73
+ return
74
+ end
75
+
76
+ if show_only?
77
+ puts rsync.command
78
+ else
79
+ sync.result[:action] = :run
80
+ puts rsync.command if dryrun?
81
+ rsync.run_command
82
+ sync.result[:status] = rsync.status
83
+ sync.result[:stdout] = rsync.stdout
84
+ sync.result[:stderr] = rsync.stderr
85
+ end
86
+ end
87
+
88
+ # checks a path
89
+ # if path includes a host, the reachability of the host will be checked
90
+ # the existence of the remote path will not be checked
91
+ # if path is a local source path, its existence will be checked
92
+ # if path is a local destination path, the existence of the parent will be checked
93
+ def check_path path, type = :source
94
+ if path.include? ':'
95
+ host = path.split(':').first.split('@').last
96
+ Mixlib::ShellOut.new("ping -o -t 1 #{host}").run_command.status.success?
97
+ else
98
+ abs_path = File.expand_path path
99
+ abs_path = File.dirname abs_path if type == :destination
100
+ File.exist? abs_path
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,3 @@
1
+ module Multisync
2
+ VERSION = "0.3.2"
3
+ end
data/multisync.gemspec ADDED
@@ -0,0 +1,45 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "multisync/version"
4
+
5
+ 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{Manage rsync configurations in sets of rules.}
12
+ spec.description = %q{Manage rsync configurations organized in groups with inherited options.}
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
28
+
29
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
30
+ f.match(%r{^(test|spec|features)/})
31
+ end
32
+ spec.bindir = "exe"
33
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
34
+ spec.require_paths = ["lib"]
35
+
36
+ spec.add_dependency "mixlib-shellout"
37
+ spec.add_dependency "filesize"
38
+ spec.add_dependency "rainbow"
39
+ spec.add_dependency "terminal-table"
40
+
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"
45
+ end
@@ -0,0 +1,39 @@
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
18
+ end
19
+
20
+ sync :pictures do
21
+ desc "Pictures"
22
+ from "~/Pictures/Private"
23
+ to "/Backup/Home"
24
+ end
25
+ end
26
+
27
+ group :work do
28
+ to "/Backup/Work"
29
+
30
+ sync :pictures do
31
+ desc "Pictures"
32
+ from "~/Pictures/Work"
33
+ end
34
+
35
+ sync :doc do
36
+ desc "Documentation"
37
+ from "~/Work/doc"
38
+ end
39
+ end
metadata ADDED
@@ -0,0 +1,182 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: multisync
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.2
5
+ platform: ruby
6
+ authors:
7
+ - Patrick Marchi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-07-23 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: mixlib-shellout
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: filesize
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rainbow
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: terminal-table
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
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: Manage rsync configurations organized in groups with inherited options.
126
+ email:
127
+ - mail@patrickmarchi.ch
128
+ executables:
129
+ - multisync
130
+ extensions: []
131
+ extra_rdoc_files: []
132
+ files:
133
+ - ".gitignore"
134
+ - ".rspec"
135
+ - Gemfile
136
+ - History.md
137
+ - LICENSE.txt
138
+ - README.md
139
+ - Rakefile
140
+ - bin/console
141
+ - bin/setup
142
+ - exe/multisync
143
+ - lib/multisync.rb
144
+ - lib/multisync/catalog.rb
145
+ - lib/multisync/catalog/filter.rb
146
+ - lib/multisync/catalog/list.rb
147
+ - lib/multisync/cli.rb
148
+ - lib/multisync/definition.rb
149
+ - lib/multisync/definition/dsl.rb
150
+ - lib/multisync/definition/entity.rb
151
+ - lib/multisync/definition/null.rb
152
+ - lib/multisync/definition/template.rb
153
+ - lib/multisync/rsync_stat.rb
154
+ - lib/multisync/runtime.rb
155
+ - lib/multisync/version.rb
156
+ - multisync.gemspec
157
+ - sample/multisync.rb
158
+ homepage: ''
159
+ licenses:
160
+ - MIT
161
+ metadata:
162
+ allowed_push_host: https://rubygems.org
163
+ post_install_message:
164
+ rdoc_options: []
165
+ require_paths:
166
+ - lib
167
+ required_ruby_version: !ruby/object:Gem::Requirement
168
+ requirements:
169
+ - - ">="
170
+ - !ruby/object:Gem::Version
171
+ version: '0'
172
+ required_rubygems_version: !ruby/object:Gem::Requirement
173
+ requirements:
174
+ - - ">="
175
+ - !ruby/object:Gem::Version
176
+ version: '0'
177
+ requirements: []
178
+ rubygems_version: 3.0.1
179
+ signing_key:
180
+ specification_version: 4
181
+ summary: Manage rsync configurations in sets of rules.
182
+ test_files: []