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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.rspec +4 -0
- data/.simplecov +9 -0
- data/.travis.yml +14 -0
- data/Gemfile +11 -0
- data/README.md +246 -0
- data/Rakefile +8 -0
- data/bin/synco +30 -0
- data/lib/synco.rb +51 -0
- data/lib/synco/command.rb +71 -0
- data/lib/synco/command/disk.rb +55 -0
- data/lib/synco/command/prune.rb +166 -0
- data/lib/synco/command/rotate.rb +86 -0
- data/lib/synco/command/spawn.rb +39 -0
- data/lib/synco/compact_formatter.rb +115 -0
- data/lib/synco/controller.rb +119 -0
- data/lib/synco/directory.rb +60 -0
- data/lib/synco/disk.rb +68 -0
- data/lib/synco/method.rb +56 -0
- data/lib/synco/methods/rsync.rb +162 -0
- data/lib/synco/methods/scp.rb +44 -0
- data/lib/synco/methods/zfs.rb +60 -0
- data/lib/synco/scope.rb +247 -0
- data/lib/synco/script.rb +128 -0
- data/lib/synco/server.rb +90 -0
- data/lib/synco/shell.rb +44 -0
- data/lib/synco/shells/ssh.rb +52 -0
- data/lib/synco/version.rb +23 -0
- data/media/LSync Logo.artx/Preview/preview.png +0 -0
- data/media/LSync Logo.artx/QuickLook/Preview.pdf +0 -0
- data/media/LSync Logo.artx/doc.thread +0 -0
- data/media/LSync Logo.png +0 -0
- data/spec/synco/backup_script.rb +63 -0
- data/spec/synco/directory_spec.rb +33 -0
- data/spec/synco/local_backup.rb +56 -0
- data/spec/synco/local_sync.rb +91 -0
- data/spec/synco/method_spec.rb +62 -0
- data/spec/synco/rsync_spec.rb +89 -0
- data/spec/synco/scp_spec.rb +58 -0
- data/spec/synco/script_spec.rb +51 -0
- data/spec/synco/shell_spec.rb +42 -0
- data/spec/synco/usb_spec.rb +76 -0
- data/spec/synco/zfs_spec.rb +50 -0
- data/synco.gemspec +35 -0
- metadata +254 -0
@@ -0,0 +1,119 @@
|
|
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
|
+
# Basic event handling and delegation.
|
23
|
+
class Controller
|
24
|
+
def self.build(*arguments, **options, &block)
|
25
|
+
controller = self.new(*arguments, **options)
|
26
|
+
|
27
|
+
if block_given?
|
28
|
+
yield(controller)
|
29
|
+
end
|
30
|
+
|
31
|
+
controller.freeze
|
32
|
+
|
33
|
+
return controller
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@events = Hash.new{|hash,key| hash[key] = Array.new}
|
38
|
+
@aborted = false
|
39
|
+
end
|
40
|
+
|
41
|
+
def freeze
|
42
|
+
@events.freeze
|
43
|
+
|
44
|
+
super
|
45
|
+
end
|
46
|
+
|
47
|
+
attr :events
|
48
|
+
|
49
|
+
# Register an event handler which may be triggered when an event is fired.
|
50
|
+
def on(event, &block)
|
51
|
+
@events[event] << block
|
52
|
+
end
|
53
|
+
|
54
|
+
# Fire an event which calls all registered event handlers in the order they were defined.
|
55
|
+
# The first argument is used to #instance_eval any handlers.
|
56
|
+
def fire(event, *args)
|
57
|
+
return false unless @events.key?(event)
|
58
|
+
|
59
|
+
handled = false
|
60
|
+
|
61
|
+
scope = args.shift
|
62
|
+
|
63
|
+
@events[event].each do |handler|
|
64
|
+
handled = true
|
65
|
+
|
66
|
+
if scope
|
67
|
+
scope.instance_exec(*args, &handler)
|
68
|
+
else
|
69
|
+
handler.call
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
return handled
|
74
|
+
end
|
75
|
+
|
76
|
+
# Try executing a given block of code and fire appropriate events.
|
77
|
+
#
|
78
|
+
# The sequence of events (registered via #on) are as follows:
|
79
|
+
# [+:prepare+] Fired before the block is executed. May call #abort! to cancel execution.
|
80
|
+
# [+:success+] Fired after the block of code has executed without raising an exception.
|
81
|
+
# [+:failure+] Fired if an exception is thrown during normal execution.
|
82
|
+
# [+:finish+] Fired at the end of execution regardless of failure.
|
83
|
+
#
|
84
|
+
# If #abort! has been called in the past, this function returns immediately.
|
85
|
+
def try(*arguments)
|
86
|
+
return if @aborted
|
87
|
+
|
88
|
+
begin
|
89
|
+
catch(abort_name) do
|
90
|
+
fire(:prepare, *arguments)
|
91
|
+
|
92
|
+
yield
|
93
|
+
|
94
|
+
fire(:success, *arguments)
|
95
|
+
end
|
96
|
+
rescue Exception => exception
|
97
|
+
# Propagage the exception unless it was handled in some specific way.
|
98
|
+
raise unless fire(:failure, *arguments, exception)
|
99
|
+
ensure
|
100
|
+
fire(:finish, *arguments)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Abort the current event handler. Aborting an event handler persistently implies that in
|
105
|
+
# the future it will still be aborted; thus calling #try will have no effect.
|
106
|
+
def abort!(persistent = false)
|
107
|
+
@aborted = true if persistent
|
108
|
+
|
109
|
+
throw abort_name
|
110
|
+
end
|
111
|
+
|
112
|
+
private
|
113
|
+
|
114
|
+
# The name used for throwing abortions.
|
115
|
+
def abort_name
|
116
|
+
("abort_" + self.class.name).downcase.to_sym
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,60 @@
|
|
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
|
+
|
23
|
+
module Synco
|
24
|
+
class AbsolutePathError < ArgumentError
|
25
|
+
end
|
26
|
+
|
27
|
+
# A specific directory which is relative to the root of a given server. Specific configuration details
|
28
|
+
# such as excludes and other options may be specified.
|
29
|
+
class Directory < Controller
|
30
|
+
def initialize(path, arguments: [])
|
31
|
+
if path.start_with?('/')
|
32
|
+
raise AbsolutePathError.new("#{path} must be relative!")
|
33
|
+
end
|
34
|
+
|
35
|
+
super()
|
36
|
+
|
37
|
+
@arguments = arguments
|
38
|
+
@path = self.class.normalize(path)
|
39
|
+
end
|
40
|
+
|
41
|
+
attr :path
|
42
|
+
attr :arguments
|
43
|
+
|
44
|
+
def depth
|
45
|
+
self.class.depth(@path)
|
46
|
+
end
|
47
|
+
|
48
|
+
def to_s
|
49
|
+
@path
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.normalize(path)
|
53
|
+
path.end_with?('/') ? path : path + '/'
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.depth(path)
|
57
|
+
path.count('/')
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
data/lib/synco/disk.rb
ADDED
@@ -0,0 +1,68 @@
|
|
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
|
+
# Depending on how you have things set up, you'll probably want to add
|
23
|
+
# %wheel ALL=(root) NOPASSWD: /bin/mount
|
24
|
+
# %wheel ALL=(root) NOPASSWD: /bin/umount
|
25
|
+
# to /etc/sudoers.d/synco
|
26
|
+
module LinuxDisk
|
27
|
+
def self.available?(disk_name)
|
28
|
+
File.exist?("/dev/disk/by-label/#{disk_name}")
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.mount(path, disk_name = nil)
|
32
|
+
if disk_name
|
33
|
+
system("sudo", "mount", "-L", disk_name, path)
|
34
|
+
else
|
35
|
+
system("sudo", "mount", path)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.unmount(path)
|
40
|
+
system("sudo", "umount", path)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module DarwinDisk
|
45
|
+
DISKUTIL = "diskutil"
|
46
|
+
|
47
|
+
def self.available?(disk_name)
|
48
|
+
system(DISKUTIL, "list", disk_name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.mount(path, disk_name = nil)
|
52
|
+
disk_name ||= File.basename(path)
|
53
|
+
|
54
|
+
system(DISKUTIL, "mount", "-mountPoint", path, disk_name)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.unmount(path)
|
58
|
+
system(DISKUTIL, "unmount", path)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
case RUBY_PLATFORM
|
63
|
+
when /darwin/
|
64
|
+
Disk = DarwinDisk
|
65
|
+
when /linux/
|
66
|
+
Disk = LinuxDisk
|
67
|
+
end
|
68
|
+
end
|
data/lib/synco/method.rb
ADDED
@@ -0,0 +1,56 @@
|
|
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 'fileutils'
|
22
|
+
require 'pathname'
|
23
|
+
require_relative 'controller'
|
24
|
+
|
25
|
+
module Synco
|
26
|
+
SNAPSHOT_NAME = 'latest.snapshot'
|
27
|
+
LATEST_NAME = 'latest'
|
28
|
+
BACKUP_NAME = '%Y.%m.%d-%H.%M.%S'
|
29
|
+
BACKUP_TIMEZONE = 'UTC'
|
30
|
+
|
31
|
+
# A backup method provides the interface to copy data from one system to another.
|
32
|
+
class Method < Controller
|
33
|
+
def initialize(*command, arguments: [], **options)
|
34
|
+
super()
|
35
|
+
|
36
|
+
@command = command.empty? ? default_command : command
|
37
|
+
@arguments = arguments
|
38
|
+
@options = options
|
39
|
+
end
|
40
|
+
|
41
|
+
attr :options
|
42
|
+
attr :arguments
|
43
|
+
|
44
|
+
def call(scope, arguments: [])
|
45
|
+
server = scope.current_server
|
46
|
+
directory = scope.directory
|
47
|
+
|
48
|
+
server.run(
|
49
|
+
*@command,
|
50
|
+
*arguments,
|
51
|
+
scope.master_server.connection_string(directory, on: server),
|
52
|
+
scope.target_server.connection_string(directory, on: server)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,162 @@
|
|
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 '../method'
|
22
|
+
require 'shellwords'
|
23
|
+
|
24
|
+
module Synco
|
25
|
+
module Methods
|
26
|
+
# RSync Exit Codes as of 2011:
|
27
|
+
# 0 Success
|
28
|
+
# 1 Syntax or usage error
|
29
|
+
# 2 Protocol incompatibility
|
30
|
+
# 3 Errors selecting input/output files, dirs
|
31
|
+
# 4 Requested action not supported: an attempt was made to manipulate 64-bit files on a platform
|
32
|
+
# that cannot support them; or an option was specified that is supported by the client and not by the server.
|
33
|
+
# 5 Error starting client-server protocol
|
34
|
+
# 6 Daemon unable to append to log-file
|
35
|
+
# 10 Error in socket I/O
|
36
|
+
# 11 Error in file I/O
|
37
|
+
# 12 Error in rsync protocol data stream
|
38
|
+
# 13 Errors with program diagnostics
|
39
|
+
# 14 Error in IPC code
|
40
|
+
# 20 Received SIGUSR1 or SIGINT
|
41
|
+
# 21 Some error returned by waitpid()
|
42
|
+
# 22 Error allocating core memory buffers
|
43
|
+
# 23 Partial transfer due to error
|
44
|
+
# 24 Partial transfer due to vanished source files
|
45
|
+
# 25 The --max-delete limit stopped deletions
|
46
|
+
# 30 Timeout in data send/receive
|
47
|
+
# 35 Timeout waiting for daemon connection
|
48
|
+
|
49
|
+
class RSync < Method
|
50
|
+
def default_command
|
51
|
+
['rsync']
|
52
|
+
end
|
53
|
+
|
54
|
+
def initialize(*command, arguments: [], archive: false, stats: nil, **options)
|
55
|
+
if archive
|
56
|
+
arguments << '--archive'
|
57
|
+
end
|
58
|
+
|
59
|
+
if stats
|
60
|
+
arguments << '--stats'
|
61
|
+
end
|
62
|
+
|
63
|
+
super
|
64
|
+
end
|
65
|
+
|
66
|
+
# This escapes the -e argument to rsync, as it's argv parser is a bit.. unique.
|
67
|
+
def escape(command)
|
68
|
+
case command
|
69
|
+
when Array
|
70
|
+
command.collect{|arg| escape(arg)}.join(' ')
|
71
|
+
when String
|
72
|
+
command =~ /\s|"|'/ ? command.dump : command
|
73
|
+
else
|
74
|
+
escape(command.to_s)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def connect_arguments(master_server, target_server)
|
79
|
+
return [] if master_server.same_host?(target_server)
|
80
|
+
|
81
|
+
# This gives the command required to connect to the remote server, e.g. `ssh example.com`
|
82
|
+
command = target_server.connection_command
|
83
|
+
|
84
|
+
# RSync -e option simply appends the hostname. There is no way to control this behaviour.
|
85
|
+
if command.last != target_server.host
|
86
|
+
raise ArgumentError.new("RSync shell requires hostname at end of command! #{command.inspect}")
|
87
|
+
else
|
88
|
+
command.pop
|
89
|
+
end
|
90
|
+
|
91
|
+
return ['-e', escape(command)]
|
92
|
+
end
|
93
|
+
|
94
|
+
def call(scope)
|
95
|
+
master_server = scope.master_server
|
96
|
+
target_server = scope.target_server
|
97
|
+
directory = scope.directory
|
98
|
+
|
99
|
+
master_server.run(
|
100
|
+
*@command,
|
101
|
+
*@arguments,
|
102
|
+
*directory.arguments,
|
103
|
+
*connect_arguments(master_server, target_server),
|
104
|
+
master_server.connection_string(directory, on: master_server),
|
105
|
+
target_server.connection_string(directory, on: master_server)
|
106
|
+
)
|
107
|
+
rescue CommandFailure => failure
|
108
|
+
raise unless failure.status.to_i == 24
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
class RSyncSnapshot < RSync
|
113
|
+
def initialize(*command, arguments: [], archive: true, stats: true, **options)
|
114
|
+
super
|
115
|
+
end
|
116
|
+
|
117
|
+
def snapshot_name
|
118
|
+
@options[:snapshot_name] || SNAPSHOT_NAME
|
119
|
+
end
|
120
|
+
|
121
|
+
def latest_name
|
122
|
+
@options[:latest_name] || LATEST_NAME
|
123
|
+
end
|
124
|
+
|
125
|
+
def compute_incremental_path(directory)
|
126
|
+
File.join(snapshot_name, directory.path)
|
127
|
+
end
|
128
|
+
|
129
|
+
def compute_link_arguments(directory, incremental_path)
|
130
|
+
depth = Directory.depth(incremental_path)
|
131
|
+
|
132
|
+
latest_path = File.join("../" * depth, latest_name, directory.path)
|
133
|
+
|
134
|
+
return ['--link-dest', latest_path]
|
135
|
+
end
|
136
|
+
|
137
|
+
def call(scope)
|
138
|
+
master_server = scope.master_server
|
139
|
+
target_server = scope.target_server
|
140
|
+
|
141
|
+
directory = scope.directory
|
142
|
+
incremental_path = compute_incremental_path(directory)
|
143
|
+
link_arguments = compute_link_arguments(directory, incremental_path)
|
144
|
+
|
145
|
+
# Create the destination backup directory
|
146
|
+
target_server.run('mkdir', '-p', target_server.full_path(incremental_path))
|
147
|
+
|
148
|
+
master_server.run(
|
149
|
+
*@command,
|
150
|
+
*@arguments,
|
151
|
+
*directory.arguments,
|
152
|
+
*connect_arguments(master_server, target_server),
|
153
|
+
*link_arguments,
|
154
|
+
master_server.connection_string(directory, on: master_server),
|
155
|
+
target_server.connection_string(incremental_path, on: master_server)
|
156
|
+
)
|
157
|
+
rescue CommandFailure => failure
|
158
|
+
raise unless failure.status.to_i == 24
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|