multisync 0.3.2

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