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,90 @@
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_relative 'controller'
22
+ require_relative 'directory'
23
+ require_relative 'shells/ssh'
24
+
25
+ require 'shellwords'
26
+
27
+ module Synco
28
+ class Server < Controller
29
+ def initialize(name, root: '/', shell: nil, **options)
30
+ super()
31
+
32
+ @name = name
33
+
34
+ case @name
35
+ when Symbol
36
+ @host = "localhost"
37
+ else
38
+ @host = name.to_s
39
+ end
40
+
41
+ @root = root
42
+ @shell = shell || Shells::SSH.new
43
+
44
+ @options = options
45
+ end
46
+
47
+ # The name of the server in the configuration (might be the same as the host).
48
+ attr :name
49
+
50
+ # The host name (e.g. DNS entry) for the given server.
51
+ attr_accessor :host
52
+
53
+ # The root path on the server in which all other directories will be relative to.
54
+ attr_accessor :root
55
+
56
+ # The shell to use to connect to the server.
57
+ attr_accessor :shell
58
+
59
+ attr_accessor :mountpoint
60
+
61
+ # Give the full path for a particular subdirectory.
62
+ def full_path(directory = "")
63
+ path = File.expand_path(directory.to_s, @root)
64
+
65
+ Directory.normalize(path)
66
+ end
67
+
68
+ # Give a general connection string (e.g +"host:/directory"+ or +"/directory"+ if local).
69
+ def connection_string(directory, on: nil)
70
+ if self.host == on.host
71
+ return full_path(directory).to_s
72
+ else
73
+ return @host + ":" + Shellwords.escape(full_path(directory))
74
+ end
75
+ end
76
+
77
+ def connection_command
78
+ @shell.connection_command(self)
79
+ end
80
+
81
+ def same_host?(other)
82
+ @host == other.host
83
+ end
84
+
85
+ # String representation of the server for logging.
86
+ def to_s
87
+ "#{@host}:#{full_path}"
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,44 @@
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 'pathname'
22
+
23
+ module Synco
24
+ # A shell provides access to a server, typically to run commands.
25
+ class Shell
26
+ def initialize(*command, arguments: [], **options)
27
+ @command = command.empty? ? default_command : command
28
+
29
+ @arguments = arguments
30
+ @options = options
31
+ end
32
+
33
+ attr :arguments
34
+
35
+ # The command required to connect to the remote machine.
36
+ def connection_command(server, *arguments)
37
+ [*@command, *@arguments, *arguments, server.host]
38
+ end
39
+
40
+ def to_s
41
+ "<#{self.class} #{@command} #{@options.inspect}>"
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,52 @@
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_relative '../shell'
22
+
23
+ module Synco
24
+ module Shells
25
+ # SSH shell provides access to a remote server using SSH.
26
+ class SSH < Shell
27
+ def default_command
28
+ ['ssh']
29
+ end
30
+
31
+ def initialize(*command, arguments: [], port: nil, key: nil, user: nil, batch_mode: nil, **options)
32
+ if port
33
+ arguments << '-p' << port
34
+ end
35
+
36
+ if key
37
+ arguments << '-i' << value
38
+ end
39
+
40
+ if user
41
+ arguments << '-l' << value
42
+ end
43
+
44
+ unless batch_mode.nil?
45
+ arguments << '-o' << "BatchMode=#{batch_mode ? 'yes' : 'no'}"
46
+ end
47
+
48
+ super
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,23 @@
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
+ module Synco
22
+ VERSION = "1.0.0"
23
+ end
Binary file
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env rspec
2
+
3
+ # Copyright, 2015, by Samuel G. D. Williams. <http://www.codeotaku.com>
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.
22
+
23
+ require 'fingerprint'
24
+ require 'process/group'
25
+ require 'fileutils'
26
+ require 'digest'
27
+
28
+ require 'synco/scope'
29
+ require 'synco/script'
30
+
31
+ SYNCO_PATH = File.expand_path("../../bin/synco", __dir__)
32
+
33
+ RSpec.shared_context "backup script" do
34
+ def create_files(master_path, target_path)
35
+ FileUtils.rm_rf master_path
36
+ FileUtils.rm_rf target_path
37
+
38
+ FileUtils.mkdir_p master_path
39
+ FileUtils.mkdir_p target_path
40
+
41
+ (1...10).each do |i|
42
+ path = File.join(master_path, i.to_s)
43
+
44
+ FileUtils.mkdir(path)
45
+
46
+ text = Digest::MD5.hexdigest(i.to_s)
47
+
48
+ File.open(File.join(path, i.to_s), "w") { |f| f.write(text) }
49
+ end
50
+ end
51
+
52
+ let(:tmp_path) {File.join(__dir__, 'tmp')}
53
+ let(:master_path) {File.join(__dir__, 'tmp/master')}
54
+ let(:target_path) {File.join(__dir__, 'tmp/target')}
55
+
56
+ before(:each) do
57
+ create_files(master_path, target_path)
58
+ end
59
+
60
+ after(:each) do
61
+ # FileUtils.rm_rf tmp_path
62
+ end
63
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env rspec
2
+
3
+ # Copyright, 2016, by Samuel G. D. Williams. <http://www.codeotaku.com>
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.
22
+
23
+ require 'synco/directory'
24
+
25
+ describe Synco::Directory.new(".", arguments: ['--foo']) do
26
+ it "should have arguments" do
27
+ expect(subject.arguments).to include('--foo')
28
+ end
29
+
30
+ it "must be relative path" do
31
+ expect{Synco::Directory.new("/var")}.to raise_error(ArgumentError)
32
+ end
33
+ end
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'synco'
4
+ require 'synco/shells/ssh'
5
+ require 'synco/methods/rsync'
6
+
7
+ # Check we've got two paths for sync.
8
+ unless ARGV.size == 2
9
+ $stderr.puts "Usage: #{$0} [master] [copy]"
10
+ exit(255)
11
+ end
12
+
13
+ # $growl = Growl.new "localhost", "Synco", ["Backup Status"]
14
+ # $growl.register
15
+
16
+ $stdout.sync = true
17
+ $stderr.sync = true
18
+
19
+ Synco::run_script do |script|
20
+ self.method = Synco::Methods::RSyncSnapshot.new(:push, "--archive", "--compress", "--stats")
21
+
22
+ self.master = :src
23
+
24
+ server(:src) do |server|
25
+ server.root = ARGV[0]
26
+ end
27
+
28
+ server(:dst) do |server|
29
+ server.root = ARGV[1]
30
+
31
+ # Runs after all directories have been successfully backed up.
32
+ server.on(:success) do
33
+ target.run "synco", "rotate"
34
+ target.run "synco", "prune"
35
+ end
36
+ end
37
+
38
+ # This event is fired before the backup starts
39
+ #script.on(:prepare) do
40
+ # $growl.notify "Backup Status", "Starting Backup", "Starting at #{Time.now.to_s}"
41
+ #end
42
+ #
43
+ ## This event occurs if the backup is successful
44
+ #script.on(:success) do
45
+ # $growl.notify "Backup Status", "Backup Successful", script.log.string
46
+ #end
47
+ #
48
+ ## This event occurs if any part of the backup fails and is not handled elsewhere.
49
+ #script.on(:failure) do |failure|
50
+ # $growl.notify "Backup Status", "Backup Failure", failure.to_s
51
+ #
52
+ # raise failure
53
+ #end
54
+
55
+ backup('./')
56
+ end
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # You need to have the FSSM gem installed for this to work.
4
+ require 'fssm'
5
+
6
+ require 'synco'
7
+ require 'synco/shells/ssh'
8
+ require 'synco/methods/rsync'
9
+ require 'synco/event_timer'
10
+
11
+ MONITOR = ARGV.delete "-m"
12
+
13
+ # Check we've got two paths for sync.
14
+ unless ARGV.size == 2
15
+ $stderr.puts "Usage: #{$0} [master] [copy]"
16
+ exit(255)
17
+ end
18
+
19
+ script = Synco::Script.new do |script|
20
+ script.method = Synco::Methods::RSync.new(:push, :arguments => ["--archive", "--delete"])
21
+
22
+ script.master = :src
23
+
24
+ server(:src) do |server|
25
+ server.root = ARGV[0]
26
+ end
27
+
28
+ server(:dst) do |server|
29
+ server.root = ARGV[1]
30
+
31
+ #server.on(:prepare) do
32
+ # logger.warn "Aborting backup!"
33
+ # server.abort!
34
+ #end
35
+ end
36
+
37
+ #server(:remote) do |server|
38
+ # server.host = "ayako.oriontransfer.org"
39
+ # server.shell = Synco::Shells::SSH.new(:user => "nobody")
40
+ # server.root = "/tmp/sync-test"
41
+ #end
42
+
43
+ #server(:remote2) do |server|
44
+ # server.host = "remote2.example.com"
45
+ # server.root = "/tmp/sync-test"
46
+ #
47
+ # server.on(:prepare) do |controller|
48
+ # controller.run! "mysqldump", "..."
49
+ # end
50
+ #
51
+ # server.on(:success) do |controller|
52
+ # controller.run! "uname", "-a"
53
+ # end
54
+ #end
55
+
56
+ backup('./')
57
+ end
58
+
59
+ # Initial sync:
60
+
61
+ context = Synco::Context.new(script)
62
+ context.run!
63
+
64
+ if MONITOR
65
+ # Monitor directories for changes:
66
+ monitor = FSSM::Monitor.new(:directories => true)
67
+
68
+ # The event timer aggregates events into a single callback which will be called at most
69
+ # once every k seconds (where k = 10 in this case).
70
+ $event_timer = Synco::EventTimer.new(10) do
71
+ $script.run!
72
+ end
73
+
74
+ $script.directories.each do |directory|
75
+ full_path = $script[:src].full_path(directory)
76
+ puts "Monitoring path: #{full_path}"
77
+
78
+ monitor.path(full_path, "**/*") do
79
+ update { $event_timer.trigger! }
80
+ delete { $event_timer.trigger! }
81
+ create { $event_timer.trigger! }
82
+ end
83
+ end
84
+
85
+ begin
86
+ monitor.run
87
+ ensure
88
+ # We should wait for the backup to complete nicely.
89
+ $event_timer.join
90
+ end
91
+ end