synco 1.0.0

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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +22 -0
  3. data/.rspec +4 -0
  4. data/.simplecov +9 -0
  5. data/.travis.yml +14 -0
  6. data/Gemfile +11 -0
  7. data/README.md +246 -0
  8. data/Rakefile +8 -0
  9. data/bin/synco +30 -0
  10. data/lib/synco.rb +51 -0
  11. data/lib/synco/command.rb +71 -0
  12. data/lib/synco/command/disk.rb +55 -0
  13. data/lib/synco/command/prune.rb +166 -0
  14. data/lib/synco/command/rotate.rb +86 -0
  15. data/lib/synco/command/spawn.rb +39 -0
  16. data/lib/synco/compact_formatter.rb +115 -0
  17. data/lib/synco/controller.rb +119 -0
  18. data/lib/synco/directory.rb +60 -0
  19. data/lib/synco/disk.rb +68 -0
  20. data/lib/synco/method.rb +56 -0
  21. data/lib/synco/methods/rsync.rb +162 -0
  22. data/lib/synco/methods/scp.rb +44 -0
  23. data/lib/synco/methods/zfs.rb +60 -0
  24. data/lib/synco/scope.rb +247 -0
  25. data/lib/synco/script.rb +128 -0
  26. data/lib/synco/server.rb +90 -0
  27. data/lib/synco/shell.rb +44 -0
  28. data/lib/synco/shells/ssh.rb +52 -0
  29. data/lib/synco/version.rb +23 -0
  30. data/media/LSync Logo.artx/Preview/preview.png +0 -0
  31. data/media/LSync Logo.artx/QuickLook/Preview.pdf +0 -0
  32. data/media/LSync Logo.artx/doc.thread +0 -0
  33. data/media/LSync Logo.png +0 -0
  34. data/spec/synco/backup_script.rb +63 -0
  35. data/spec/synco/directory_spec.rb +33 -0
  36. data/spec/synco/local_backup.rb +56 -0
  37. data/spec/synco/local_sync.rb +91 -0
  38. data/spec/synco/method_spec.rb +62 -0
  39. data/spec/synco/rsync_spec.rb +89 -0
  40. data/spec/synco/scp_spec.rb +58 -0
  41. data/spec/synco/script_spec.rb +51 -0
  42. data/spec/synco/shell_spec.rb +42 -0
  43. data/spec/synco/usb_spec.rb +76 -0
  44. data/spec/synco/zfs_spec.rb +50 -0
  45. data/synco.gemspec +35 -0
  46. metadata +254 -0
@@ -0,0 +1,55 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ # This script takes a given path, and renames it with the given format.
22
+ # It then ensures that there is a symlink called "latest" that points
23
+ # to the renamed directory.
24
+
25
+ require 'samovar'
26
+
27
+ require_relative '../disk'
28
+
29
+ module Synco
30
+ module Command
31
+ class Mount < Samovar::Command
32
+ self.description = "Mount a disk with the given name."
33
+
34
+ one :path, "The disk mount point."
35
+ one :name, "The symbolic name of the disk to mount, e.g. disk label."
36
+
37
+ def invoke(parent)
38
+ # We may not have permission to make this directory, but we should still try:
39
+ FileUtils.mkpath(@path) rescue nil
40
+
41
+ Disk.mount(@path, @name)
42
+ end
43
+ end
44
+
45
+ class Unmount < Samovar::Command
46
+ self.description = "Unmount a disk with the given name."
47
+
48
+ one :path, "The disk mount point."
49
+
50
+ def invoke(parent)
51
+ Disk.unmount(@path)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,166 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ # This script takes a given path, and renames it with the given format.
22
+ # It then ensures that there is a symlink called "latest" that points
23
+ # to the renamed directory.
24
+
25
+ require 'samovar'
26
+
27
+ require 'periodical/filter'
28
+
29
+ # Required for strptime
30
+ require 'date'
31
+
32
+ module Synco
33
+ module Command
34
+ class Rotation
35
+ include Comparable
36
+
37
+ def initialize(path, time)
38
+ @path = path
39
+ @time = time
40
+ end
41
+
42
+ attr :time
43
+ attr :path
44
+
45
+ # Sort in reverse order by default
46
+ def <=> other
47
+ other.time <=> @time
48
+ end
49
+
50
+ def eql? other
51
+ case other
52
+ when Rotation
53
+ @path.eql?(other.path)
54
+ else
55
+ @path.eql?(other.to_s)
56
+ end
57
+ end
58
+
59
+ def hash
60
+ @path.hash
61
+ end
62
+
63
+ def to_s
64
+ @path
65
+ end
66
+ end
67
+
68
+ class Prune < Samovar::Command
69
+ self.description = "Prune old backups to reduce disk usage according to a given policy."
70
+
71
+ options do
72
+ option "--hourly <count>", "Set the number of hourly backups to keep.", default: 24
73
+ option "--daily <count>", "Set the number of daily backups to keep.", default: 7*4
74
+ option "--weekly <count>", "Set the number of weekly backups to keep.", default: 52
75
+ option "--monthly <count>", "Set the number of monthly backups to keep.", default: 12*3
76
+ option "--quarterly <count>", "Set the number of quaterly backups to keep.", default: 4*10
77
+ option "--yearly <count>", "Set the number of yearly backups to keep.", default: 20
78
+
79
+ option "--format <name>", "Set the name of the backup rotations, including strftime expansions.", default: BACKUP_NAME
80
+ option "--latest <name>", "The name of the latest backup symlink.", default: LATEST_NAME
81
+
82
+ option "--keep <new|old>", "Keep the younger or older backups within the same period division", default: 'old'
83
+
84
+ option "--dry", "Print out what would be done rather than doing it."
85
+ end
86
+
87
+ def policy
88
+ policy = Periodical::Filter::Policy.new
89
+
90
+ policy << Periodical::Filter::Hourly.new(@options[:hourly])
91
+ policy << Periodical::Filter::Daily.new(@options[:daily])
92
+ policy << Periodical::Filter::Weekly.new(@options[:weekly])
93
+ policy << Periodical::Filter::Monthly.new(@options[:monthly])
94
+ policy << Periodical::Filter::Quarterly.new(@options[:quarterly])
95
+ policy << Periodical::Filter::Yearly.new(@options[:yearly])
96
+
97
+ return policy
98
+ end
99
+
100
+ def current_backups
101
+ backups = []
102
+
103
+ Dir['*'].each do |path|
104
+ next if path == @options[:latest]
105
+ date_string = File.basename(path)
106
+
107
+ begin
108
+ backups << Rotation.new(path, DateTime.strptime(date_string, @options[:format]))
109
+ rescue ArgumentError
110
+ $stderr.puts "Skipping #{path}, error parsing #{date_string}: #{$!}"
111
+ end
112
+ end
113
+
114
+ return backups
115
+ end
116
+
117
+ def dry?
118
+ @options[:dry]
119
+ end
120
+
121
+ def print_rotation(keep, erase)
122
+ puts "*** Rotating backups (DRY!) ***"
123
+ puts "\tKeeping:"
124
+ keep.sort.each { |backup| puts "\t\t#{backup.path}" }
125
+ puts "\tErasing:"
126
+ erase.sort.each { |backup| puts "\t\t#{backup.path}" }
127
+ end
128
+
129
+ def perform_rotation(keep, erase)
130
+ puts "*** Rotating backups ***"
131
+ erase.sort.each do |backup|
132
+ puts "Erasing #{backup.path}..."
133
+ $stdout.flush
134
+
135
+ # Ensure that we can remove the backup
136
+ system("chmod", "-R", "ug+rwX", backup.path)
137
+ system("rm", "-rf", backup.path)
138
+ end
139
+ end
140
+
141
+ def invoke(parent)
142
+ backups = current_backups
143
+
144
+ retain, erase = policy.filter(backups, keep: @options[:keep].to_sym, &:time)
145
+
146
+ # We need to retain the latest backup regardless of policy
147
+ if latest = @options[:latest] and File.exist?(latest)
148
+ latest_path = File.readlink(options[:latest])
149
+ latest_rotation = erase.find{|rotation| rotation.path == latest_path}
150
+
151
+ if latest_rotation
152
+ puts "Retaining latest backup #{latest_rotation}"
153
+ erase.delete(latest_rotation)
154
+ retain << latest_rotation
155
+ end
156
+ end
157
+
158
+ if dry?
159
+ print_rotation(retain, erase)
160
+ else
161
+ perform_rotation(retain, erase)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,86 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ # This script takes a given path, and renames it with the given format.
22
+ # It then ensures that there is a symlink called "latest" that points
23
+ # to the renamed directory.
24
+
25
+ require 'samovar'
26
+
27
+ require_relative '../method'
28
+
29
+ module Synco
30
+ module Command
31
+ class Rotate < Samovar::Command
32
+ self.description = "Rotate a backup snapshot into a timestamped directory."
33
+
34
+ options do
35
+ option "--format <name>", "Set the name of the backup rotations, including strftime expansions.", default: BACKUP_NAME
36
+ option "--latest <name>", "The name of the latest backup symlink.", default: LATEST_NAME
37
+ option "--snapshot <name>", "The name of the in-progress backup snapshot.", default: SNAPSHOT_NAME
38
+
39
+ # option "--timezone <name>", "The default timezone for backup timestamps.", default: BACKUP_TIMEZONE
40
+ end
41
+
42
+ def backup_timestamp
43
+ timestamp = Time.now.utc
44
+
45
+ #if timezone = @options[:timezone]
46
+ # timestamp = timestamp.in_time_zone(timezone)
47
+ #end
48
+
49
+ return timestamp
50
+ end
51
+
52
+ def backup_name
53
+ backup_timestamp.strftime(@options[:format])
54
+ end
55
+
56
+ def invoke(parent)
57
+ snapshot_name = @options[:snapshot]
58
+ unless File.exist? snapshot_name
59
+ $stderr.puts "Snapshot directory #{snapshot_name} does not exist!"
60
+ exit(10)
61
+ end
62
+
63
+ rotated_name = backup_name
64
+ if File.exist? rotated_name
65
+ $stderr.puts "Destination rotation name #{rotated_name} already exists!"
66
+ exit(20)
67
+ end
68
+
69
+ puts "Rotating #{snapshot_name} to #{rotated_name} in #{Dir.pwd}"
70
+
71
+ # Move rotated dir
72
+ FileUtils.mv(snapshot_name, rotated_name)
73
+
74
+ # Recreate latest symlink
75
+ latest_link = @options[:latest]
76
+ if File.symlink?(latest_link)
77
+ puts "Removing old latest link..."
78
+ FileUtils.rm(latest_link)
79
+ end
80
+
81
+ puts "Creating latest symlink to #{rotated_name}"
82
+ FileUtils.ln_s(rotated_name, latest_link)
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,39 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ # This script takes a given path, and renames it with the given format.
22
+ # It then ensures that there is a symlink called "latest" that points
23
+ # to the renamed directory.
24
+
25
+ require 'samovar'
26
+
27
+ module Synco
28
+ module Command
29
+ class Spawn < Samovar::Command
30
+ self.description = "Run a command using the synco environment and root directory."
31
+
32
+ split :argv, "Command to spawn."
33
+
34
+ def invoke(parent)
35
+ Process.exec(*@argv)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,115 @@
1
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'rainbow'
22
+ require 'shellwords'
23
+
24
+ module Synco
25
+ class CompactFormatter
26
+ def initialize(verbose: true)
27
+ @start = Time.now
28
+ @verbose = verbose
29
+ end
30
+
31
+ def time_offset_prefix
32
+ offset = Time.now - @start
33
+ minutes = (offset/60).floor
34
+ seconds = (offset - (minutes*60))
35
+
36
+ if minutes > 0
37
+ "#{minutes}m#{seconds.floor}s"
38
+ else
39
+ "#{seconds.round(2)}s"
40
+ end.rjust(6)
41
+ end
42
+
43
+ def chdir_string(options)
44
+ if options[:chdir]
45
+ " in #{options[:chdir]}"
46
+ else
47
+ ""
48
+ end
49
+ end
50
+
51
+ def format_command(arguments, buffer)
52
+ arguments = arguments.dup
53
+
54
+ # environment = arguments.first.is_a?(Hash) ? arguments.shift : nil
55
+ options = arguments.last.is_a?(Hash) ? arguments.pop : nil
56
+
57
+ arguments = arguments.flatten.collect(&:to_s)
58
+
59
+ buffer << Rainbow(arguments.shelljoin).bright.blue
60
+
61
+ if options
62
+ buffer << chdir_string(options)
63
+ end
64
+
65
+ buffer << "\n"
66
+
67
+ # if environment
68
+ # environment.each do |key,value|
69
+ # buffer << "\texport #{key}=#{value.dump}\n"
70
+ # end
71
+ # end
72
+ end
73
+
74
+ def format_exception(exception, buffer)
75
+ buffer << Rainbow("#{exception.class}: #{exception}").bright.red << "\n"
76
+ exception.backtrace.each do |line|
77
+ buffer << "\t" << Rainbow(line).red << "\n"
78
+ end
79
+ end
80
+
81
+ def format_line(message, severity)
82
+ if severity == 'ERROR'
83
+ Rainbow(message).red
84
+ else
85
+ message
86
+ end
87
+ end
88
+
89
+ def call(severity, datetime, progname, message)
90
+ buffer = []
91
+ prefix = ""
92
+
93
+ if @verbose
94
+ prefix = time_offset_prefix
95
+ buffer << Rainbow(prefix).cyan + ": "
96
+ prefix = " " * (prefix.size) + "| "
97
+ end
98
+
99
+ if progname == 'shell' and message.kind_of? Array
100
+ format_command(message, buffer)
101
+ elsif message.kind_of? Exception
102
+ format_exception(message, buffer)
103
+ else
104
+ buffer << format_line(message, severity) << "\n"
105
+ end
106
+
107
+ result = buffer.join
108
+
109
+ # This fancy regex indents lines correctly depending on the prefix:
110
+ result.gsub!(/\n(?!$)/, "\n#{prefix}") unless prefix.empty?
111
+
112
+ return result
113
+ end
114
+ end
115
+ end