synco 1.0.0

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