snapsync 0.3.7 → 0.4.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +15 -2
- data/lib/snapsync/auto_sync.rb +75 -39
- data/lib/snapsync/btrfs.rb +123 -35
- data/lib/snapsync/btrfs_subvolume.rb +103 -0
- data/lib/snapsync/cleanup.rb +1 -0
- data/lib/snapsync/cli.rb +73 -42
- data/lib/snapsync/partitions_monitor.rb +74 -7
- data/lib/snapsync/remote_pathname.rb +330 -0
- data/lib/snapsync/snapper_config.rb +6 -0
- data/lib/snapsync/snapshot.rb +32 -3
- data/lib/snapsync/{local_sync.rb → snapshot_transfer.rb} +27 -28
- data/lib/snapsync/ssh_popen.rb +107 -0
- data/lib/snapsync/sync.rb +8 -4
- data/lib/snapsync/sync_all.rb +1 -1
- data/lib/snapsync/{local_target.rb → sync_target.rb} +14 -4
- data/lib/snapsync/test.rb +2 -9
- data/lib/snapsync/util.rb +18 -0
- data/lib/snapsync/version.rb +1 -1
- data/lib/snapsync.rb +32 -2
- data/snapsync.gemspec +13 -6
- data/snapsync.service +4 -1
- data/snapsync.timer +10 -0
- metadata +117 -34
@@ -0,0 +1,330 @@
|
|
1
|
+
require 'weakref'
|
2
|
+
|
3
|
+
class Pathname
|
4
|
+
class NonZeroExitCode < RuntimeError
|
5
|
+
end
|
6
|
+
|
7
|
+
def path_part
|
8
|
+
to_s
|
9
|
+
end
|
10
|
+
|
11
|
+
def touch
|
12
|
+
FileUtils.touch(to_s)
|
13
|
+
end
|
14
|
+
|
15
|
+
def findmnt
|
16
|
+
# An optimization to quickly get the mountpoint.
|
17
|
+
# 3 levels are needed to get from `.snapshots/<id>/snapshot` to a cached entry.
|
18
|
+
# Will probably be fine.
|
19
|
+
cached = Snapsync._mountpointCache.fetch(self.to_s, nil)
|
20
|
+
cached = Snapsync._mountpointCache.fetch(self.parent.to_s, nil) unless cached
|
21
|
+
cached = Snapsync._mountpointCache.fetch(self.parent.parent.to_s, nil) unless cached
|
22
|
+
return cached.dup if cached
|
23
|
+
|
24
|
+
Snapsync.debug "Pathname ('#{self}').findmnt"
|
25
|
+
|
26
|
+
proc = IO.popen(Shellwords.join ['findmnt','-n','-o','TARGET','-T', self.to_s])
|
27
|
+
path = proc.read.strip
|
28
|
+
proc.close
|
29
|
+
if not $?.success?
|
30
|
+
raise NonZeroExitCode, "findmnt failed for #{self}"
|
31
|
+
end
|
32
|
+
|
33
|
+
raise "findmnt failed" unless path
|
34
|
+
p = Pathname.new path
|
35
|
+
# Update cache
|
36
|
+
p2 = self.dup
|
37
|
+
while p != p2
|
38
|
+
Snapsync._mountpointCache[p2.to_s] = p
|
39
|
+
p2 = p2.parent
|
40
|
+
end
|
41
|
+
p
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
module Snapsync
|
46
|
+
class << self
|
47
|
+
# @return [Hash]
|
48
|
+
attr_accessor :_mountpointCache
|
49
|
+
end
|
50
|
+
self._mountpointCache = {}
|
51
|
+
|
52
|
+
class AgnosticPath
|
53
|
+
def exist?
|
54
|
+
raise NotImplementedError
|
55
|
+
end
|
56
|
+
def file?
|
57
|
+
raise NotImplementedError
|
58
|
+
end
|
59
|
+
def directory?
|
60
|
+
raise NotImplementedError
|
61
|
+
end
|
62
|
+
def mountpoint?
|
63
|
+
raise NotImplementedError
|
64
|
+
end
|
65
|
+
def basename
|
66
|
+
raise NotImplementedError
|
67
|
+
end
|
68
|
+
def dirname
|
69
|
+
raise NotImplementedError
|
70
|
+
end
|
71
|
+
def parent
|
72
|
+
raise NotImplementedError
|
73
|
+
end
|
74
|
+
def each_child
|
75
|
+
raise NotImplementedError
|
76
|
+
end
|
77
|
+
def expand_path
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
def cleanpath
|
81
|
+
raise NotImplementedError
|
82
|
+
end
|
83
|
+
def mkdir
|
84
|
+
raise NotImplementedError
|
85
|
+
end
|
86
|
+
def mkpath
|
87
|
+
raise NotImplementedError
|
88
|
+
end
|
89
|
+
def rmtree
|
90
|
+
raise NotImplementedError
|
91
|
+
end
|
92
|
+
def unlink
|
93
|
+
raise NotImplementedError
|
94
|
+
end
|
95
|
+
# @return [AgnosticPath]
|
96
|
+
def +(path)
|
97
|
+
raise NotImplementedError
|
98
|
+
end
|
99
|
+
def read
|
100
|
+
raise NotImplementedError
|
101
|
+
end
|
102
|
+
def open(flags, &block)
|
103
|
+
raise NotImplementedError
|
104
|
+
end
|
105
|
+
def touch
|
106
|
+
raise NotImplementedError
|
107
|
+
end
|
108
|
+
|
109
|
+
def findmnt
|
110
|
+
raise NotImplementedError
|
111
|
+
end
|
112
|
+
|
113
|
+
# @return [String]
|
114
|
+
def path_part
|
115
|
+
raise NotImplementedError
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Ideally this would also inherit from AgnosticPath...
|
120
|
+
class LocalPathname < Pathname
|
121
|
+
end
|
122
|
+
|
123
|
+
class RemotePathname < AgnosticPath
|
124
|
+
|
125
|
+
# @return [URI::SshGit::Generic]
|
126
|
+
attr_reader :uri
|
127
|
+
|
128
|
+
# @return [Net::SSH::Connection::Session]
|
129
|
+
attr_reader :ssh
|
130
|
+
|
131
|
+
# @return [Net::SFTP::Session]
|
132
|
+
attr_reader :sftp
|
133
|
+
|
134
|
+
# @return [Net::SFTP::Operations::FileFactory]
|
135
|
+
attr_reader :sftp_f
|
136
|
+
|
137
|
+
# @param [String] dir
|
138
|
+
def initialize(dir)
|
139
|
+
if dir.instance_of? RemotePathname
|
140
|
+
@uri = dir.uri.dup
|
141
|
+
@ssh = dir.ssh
|
142
|
+
@sftp = dir.sftp
|
143
|
+
@sftp_f = dir.sftp_f
|
144
|
+
else
|
145
|
+
@uri = URI::SshGit.parse(dir)
|
146
|
+
|
147
|
+
raise RuntimeError.new('Host cannot be nil for remote pathname') if uri.host.nil?
|
148
|
+
|
149
|
+
Snapsync.debug "Opening new ssh session: "+uri.to_s
|
150
|
+
@ssh = Net::SSH.start(uri.host, uri.user, password: uri.password, non_interactive: true)
|
151
|
+
|
152
|
+
@sftp = @ssh.sftp
|
153
|
+
@sftp_f = @sftp.file
|
154
|
+
|
155
|
+
# # FIXME: these probably don't work
|
156
|
+
# @ssh_thr = Thread.new {
|
157
|
+
# ssh = WeakRef.new(@ssh)
|
158
|
+
# while ssh.weakref_alive?
|
159
|
+
# ssh.process 0.1
|
160
|
+
# end
|
161
|
+
# }
|
162
|
+
#
|
163
|
+
# @sftp_thr = Thread.new {
|
164
|
+
# sftp = WeakRef.new(@sftp)
|
165
|
+
# sftp.loop do
|
166
|
+
# sftp.weakref_alive?
|
167
|
+
# end
|
168
|
+
# }
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def initialize_dup(other)
|
173
|
+
super
|
174
|
+
@uri = @uri.dup
|
175
|
+
end
|
176
|
+
|
177
|
+
# Duplicates a new ssh session with same connection options
|
178
|
+
# @return [Net::SSH::Connection::Session]
|
179
|
+
# @yieldparam ssh [Net::SSH::Connection::Session]
|
180
|
+
def dup_ssh(&block)
|
181
|
+
Snapsync.debug "Opening new ssh session: "+uri.to_s
|
182
|
+
Net::SSH.start(uri.host, uri.user, password: uri.password, non_interactive: true, &block)
|
183
|
+
end
|
184
|
+
|
185
|
+
def exist?
|
186
|
+
begin
|
187
|
+
sftp_f.open(uri.path).close
|
188
|
+
return true
|
189
|
+
rescue Net::SFTP::StatusException
|
190
|
+
return directory?
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def file?
|
195
|
+
begin
|
196
|
+
sftp_f.open(uri.path).close
|
197
|
+
return true
|
198
|
+
rescue Net::SFTP::StatusException
|
199
|
+
return false
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def directory?
|
204
|
+
begin
|
205
|
+
sftp_f.directory? uri.path
|
206
|
+
rescue Net::SFTP::StatusException
|
207
|
+
return false
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def findmnt
|
212
|
+
cached = Snapsync._mountpointCache.fetch(self.to_s, nil)
|
213
|
+
cached = Snapsync._mountpointCache.fetch(self.parent.to_s, nil) unless cached
|
214
|
+
return cached if cached
|
215
|
+
|
216
|
+
Snapsync.debug "RemotePathname ('#{uri}').findmnt"
|
217
|
+
|
218
|
+
path = ssh.exec!(Shellwords.join ['findmnt','-n','-o','TARGET','-T',uri.path]).strip
|
219
|
+
p = self.dup
|
220
|
+
p.uri.path = path
|
221
|
+
|
222
|
+
# Update cache
|
223
|
+
p2 = self.dup
|
224
|
+
while p.uri.path != p2.uri.path
|
225
|
+
Snapsync._mountpointCache[p2.to_s] = p
|
226
|
+
p2 = p2.parent
|
227
|
+
end
|
228
|
+
|
229
|
+
p
|
230
|
+
end
|
231
|
+
|
232
|
+
def mountpoint?
|
233
|
+
Snapsync.debug "RemotePathname ('#{uri}').mountpoint?"
|
234
|
+
ssh.exec!(Shellwords.join ['mountpoint','-q',uri.path]).exitstatus == 0
|
235
|
+
end
|
236
|
+
|
237
|
+
def basename
|
238
|
+
Pathname.new(uri.path).basename
|
239
|
+
end
|
240
|
+
|
241
|
+
def dirname
|
242
|
+
o = self.dup
|
243
|
+
o.uri.path = Pathname.new(uri.path).dirname.to_s
|
244
|
+
o
|
245
|
+
end
|
246
|
+
|
247
|
+
def parent
|
248
|
+
o = self.dup
|
249
|
+
if o.uri.path == '/'
|
250
|
+
raise "Trying to get parent of root directory"
|
251
|
+
end
|
252
|
+
o.uri.path = Pathname.new(uri.path).parent.to_s
|
253
|
+
o
|
254
|
+
end
|
255
|
+
|
256
|
+
def each_child(with_directory=true, &block)
|
257
|
+
raise 'Only supports default value for with_directory' if not with_directory
|
258
|
+
|
259
|
+
sftp.dir.foreach(uri.path) do |entry|
|
260
|
+
next if entry.name == '.' or entry.name == '..'
|
261
|
+
|
262
|
+
o = self.dup
|
263
|
+
o.uri.path = (Pathname.new(o.uri.path) + entry.name).to_s
|
264
|
+
yield o
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def expand_path
|
269
|
+
o = self.dup
|
270
|
+
o.uri.path = ssh.exec!(Shellwords.join ['readlink', '-f', uri.path]).chomp
|
271
|
+
o
|
272
|
+
end
|
273
|
+
|
274
|
+
def cleanpath
|
275
|
+
o = self.dup
|
276
|
+
o.uri.path = Pathname.new(uri.path).cleanpath.to_s
|
277
|
+
o
|
278
|
+
end
|
279
|
+
|
280
|
+
def mkdir
|
281
|
+
sftp.mkdir!(uri.path)
|
282
|
+
end
|
283
|
+
|
284
|
+
def mkpath
|
285
|
+
sftp.mkdir!(uri.path)
|
286
|
+
end
|
287
|
+
|
288
|
+
def rmtree
|
289
|
+
raise 'Failed' unless ssh.exec!(Shellwords.join ['rm','-r', uri.path]).exitstatus == 0
|
290
|
+
end
|
291
|
+
|
292
|
+
def unlink
|
293
|
+
raise 'Failed' unless ssh.exec!(Shellwords.join ['rm', uri.path]).exitstatus == 0
|
294
|
+
end
|
295
|
+
|
296
|
+
def +(path)
|
297
|
+
o = self.dup
|
298
|
+
o.uri.path = (Pathname.new(uri.path) + path).to_s
|
299
|
+
o
|
300
|
+
end
|
301
|
+
|
302
|
+
def read
|
303
|
+
begin
|
304
|
+
sftp_f.open(uri.path).read
|
305
|
+
rescue Net::SFTP::StatusException => e
|
306
|
+
raise Errno::ENOENT, e.message, e.backtrace
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def open(flags, &block)
|
311
|
+
sftp_f.open uri.path, flags, &block
|
312
|
+
end
|
313
|
+
|
314
|
+
def touch
|
315
|
+
raise 'Failed' unless ssh.exec!(Shellwords.join ['touch', uri.path]).exitstatus == 0
|
316
|
+
end
|
317
|
+
|
318
|
+
def path_part
|
319
|
+
uri.path
|
320
|
+
end
|
321
|
+
|
322
|
+
def to_s
|
323
|
+
uri.to_s
|
324
|
+
end
|
325
|
+
|
326
|
+
def inspect
|
327
|
+
uri.to_s
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|
@@ -10,6 +10,7 @@ module Snapsync
|
|
10
10
|
attr_reader :subvolume
|
11
11
|
|
12
12
|
# The filesystem type
|
13
|
+
# @return [String]
|
13
14
|
attr_reader :fstype
|
14
15
|
|
15
16
|
def initialize(name)
|
@@ -75,6 +76,11 @@ module Snapsync
|
|
75
76
|
system("snapper", "-c", name, "delete", snapshot.num.to_s)
|
76
77
|
end
|
77
78
|
|
79
|
+
def cleanup
|
80
|
+
Snapsync.debug "SnapperConfig.cleanup"
|
81
|
+
system('snapper', '-c', name, 'cleanup', 'all')
|
82
|
+
end
|
83
|
+
|
78
84
|
# Create a SnapperConfig object from the data in a configuration file
|
79
85
|
#
|
80
86
|
# @param [#readlines] path the file
|
data/lib/snapsync/snapshot.rb
CHANGED
@@ -1,11 +1,24 @@
|
|
1
1
|
module Snapsync
|
2
2
|
# Representation of a single Snapper snapshot
|
3
3
|
class Snapshot
|
4
|
+
class SnapshotCompareError < RuntimeError
|
5
|
+
end
|
6
|
+
|
4
7
|
# The path to the snapshot directory
|
5
8
|
#
|
6
9
|
# @return [Pathname]
|
7
10
|
attr_reader :snapshot_dir
|
8
11
|
|
12
|
+
# @return [Btrfs]
|
13
|
+
attr_reader :btrfs
|
14
|
+
|
15
|
+
# @return [SubvolumeInfo]
|
16
|
+
def info
|
17
|
+
# Load btrfs subvolume info
|
18
|
+
@info = SubvolumeInfo.new(subvolume_dir) unless @info
|
19
|
+
@info
|
20
|
+
end
|
21
|
+
|
9
22
|
# The path to the snapshot's subvolume
|
10
23
|
#
|
11
24
|
# @return [Pathname]
|
@@ -59,8 +72,11 @@ module Snapsync
|
|
59
72
|
user_data['snapsync'] == target.uuid
|
60
73
|
end
|
61
74
|
|
75
|
+
|
76
|
+
# @param [AgnosticPath] snapshot_dir
|
62
77
|
def initialize(snapshot_dir)
|
63
78
|
@snapshot_dir = snapshot_dir
|
79
|
+
@btrfs = Btrfs.get(snapshot_dir)
|
64
80
|
|
65
81
|
if !snapshot_dir.directory?
|
66
82
|
raise InvalidSnapshot, "#{snapshot_dir} does not exist"
|
@@ -78,12 +94,21 @@ module Snapsync
|
|
78
94
|
# This is an estimate of the size required to send this snapshot using
|
79
95
|
# the given snapshot as parent
|
80
96
|
#
|
81
|
-
# @param [Snapshot] a reference snapshot, which would be used as parent
|
97
|
+
# @param [Snapshot] snapshot a reference snapshot, which would be used as parent
|
82
98
|
# in the 'send' operation
|
83
99
|
# @return [Integer] the size in bytes of the difference between the
|
84
100
|
# given snapshot and the current subvolume's state
|
85
101
|
def size_diff_from(snapshot)
|
86
|
-
|
102
|
+
if btrfs.mountpoint != snapshot.btrfs.mountpoint
|
103
|
+
recv_uuid = snapshot.info.received_uuid
|
104
|
+
local_snapshot = btrfs.subvolume_table.find do |s|
|
105
|
+
s.uuid == recv_uuid
|
106
|
+
end
|
107
|
+
raise "Cannot find snapshot with uuid #{recv_uuid} locally." if local_snapshot.nil?
|
108
|
+
snapshot_gen = local_snapshot.cgen
|
109
|
+
else
|
110
|
+
snapshot_gen = snapshot.info.gen_at_creation
|
111
|
+
end
|
87
112
|
size_diff_from_gen(snapshot_gen)
|
88
113
|
end
|
89
114
|
|
@@ -103,7 +128,7 @@ module Snapsync
|
|
103
128
|
# @return [Integer] size in bytes
|
104
129
|
# @see size_diff_from size
|
105
130
|
def size_diff_from_gen(gen)
|
106
|
-
|
131
|
+
btrfs.find_new(subvolume_dir, gen).inject(0) do |size, line|
|
107
132
|
if line =~ /len (\d+)/
|
108
133
|
size + Integer($1)
|
109
134
|
else size
|
@@ -111,6 +136,9 @@ module Snapsync
|
|
111
136
|
end
|
112
137
|
end
|
113
138
|
|
139
|
+
# @yieldparam path [AgnosticPath]
|
140
|
+
# @yieldparam snapshot [Snapshot, nil]
|
141
|
+
# @yieldparam err [InvalidSnapshot, nil]
|
114
142
|
def self.each_snapshot_raw(snapshot_dir)
|
115
143
|
return enum_for(__method__, snapshot_dir) if !block_given?
|
116
144
|
snapshot_dir.each_child do |path|
|
@@ -130,6 +158,7 @@ module Snapsync
|
|
130
158
|
# The directory is supposed to be maintained in a snapper-compatible
|
131
159
|
# foramt, meaning that the snapshot directory name must be the
|
132
160
|
# snapshot's number
|
161
|
+
# @yieldparam snapshot [Snapshot]
|
133
162
|
def self.each(snapshot_dir, with_partial: false)
|
134
163
|
return enum_for(__method__, snapshot_dir, with_partial: with_partial) if !block_given?
|
135
164
|
each_snapshot_raw(snapshot_dir) do |path, snapshot, error|
|
@@ -1,17 +1,26 @@
|
|
1
1
|
module Snapsync
|
2
|
-
#
|
3
|
-
class
|
2
|
+
# Snapshot transfer between two btrfs filesystems
|
3
|
+
class SnapshotTransfer
|
4
4
|
# The snapper configuration we should synchronize
|
5
5
|
#
|
6
6
|
# @return [SnapperConfig]
|
7
7
|
attr_reader :config
|
8
8
|
# The target directory into which to synchronize
|
9
9
|
#
|
10
|
-
# @return [
|
10
|
+
# @return [SyncTarget]
|
11
11
|
attr_reader :target
|
12
|
+
|
13
|
+
# @return [Btrfs] src filesystem
|
14
|
+
attr_reader :btrfs_src
|
15
|
+
|
16
|
+
# @return [Btrfs] dest filesystem
|
17
|
+
attr_reader :btrfs_dest
|
12
18
|
|
13
19
|
def initialize(config, target)
|
14
20
|
@config, @target = config, target
|
21
|
+
|
22
|
+
@btrfs_src = Btrfs.get(config.subvolume)
|
23
|
+
@btrfs_dest = Btrfs.get(@target.dir)
|
15
24
|
end
|
16
25
|
|
17
26
|
def create_synchronization_point
|
@@ -61,6 +70,7 @@ module Snapsync
|
|
61
70
|
counter
|
62
71
|
end
|
63
72
|
|
73
|
+
# @param [AgnosticPath] target_snapshot_dir
|
64
74
|
def synchronize_snapshot(target_snapshot_dir, src, parent: nil)
|
65
75
|
partial_marker_path = Snapshot.partial_marker_path(target_snapshot_dir)
|
66
76
|
|
@@ -79,23 +89,26 @@ module Snapsync
|
|
79
89
|
else
|
80
90
|
target_snapshot_dir.mkdir
|
81
91
|
end
|
82
|
-
|
92
|
+
partial_marker_path.touch
|
83
93
|
end
|
84
94
|
|
85
95
|
if copy_snapshot(target_snapshot_dir, src, parent: parent)
|
86
96
|
partial_marker_path.unlink
|
87
|
-
|
97
|
+
btrfs_dest.run("filesystem", "sync", target_snapshot_dir.path_part)
|
88
98
|
Snapsync.info "Successfully synchronized #{src.snapshot_dir}"
|
89
99
|
true
|
90
100
|
end
|
91
101
|
end
|
92
102
|
|
103
|
+
# @param [AgnosticPath] target_snapshot_dir
|
104
|
+
# @param [Snapshot] src
|
105
|
+
# @param [Snapshot, nil] parent
|
93
106
|
def copy_snapshot(target_snapshot_dir, src, parent: nil)
|
94
107
|
# This variable is used in the 'ensure' block. Make sure it is
|
95
108
|
# initialized properly
|
96
109
|
success = false
|
97
110
|
|
98
|
-
|
111
|
+
(target_snapshot_dir + "info.xml").open('w') do |io|
|
99
112
|
io.write (src.snapshot_dir + "info.xml").read
|
100
113
|
end
|
101
114
|
|
@@ -112,15 +125,15 @@ module Snapsync
|
|
112
125
|
start = Time.now
|
113
126
|
bytes_transferred = nil
|
114
127
|
bytes_transferred =
|
115
|
-
|
116
|
-
|
128
|
+
btrfs_src.popen('send', *parent_opt, src.subvolume_dir.to_s) do |send_io|
|
129
|
+
btrfs_dest.popen('receive', target_snapshot_dir.path_part, mode: 'w', out: '/dev/null') do |receive_io|
|
117
130
|
receive_io.sync = true
|
118
131
|
copy_stream(send_io, receive_io, estimated_size: estimated_size)
|
119
132
|
end
|
120
133
|
end
|
121
134
|
|
122
135
|
Snapsync.info "Flushing data to disk"
|
123
|
-
|
136
|
+
btrfs_dest.run("filesystem", "sync", target_snapshot_dir.path_part)
|
124
137
|
duration = Time.now - start
|
125
138
|
rate = bytes_transferred / duration
|
126
139
|
Snapsync.info "Transferred #{human_readable_size(bytes_transferred)} in #{human_readable_time(duration)} (#{human_readable_size(rate)}/s)"
|
@@ -128,10 +141,10 @@ module Snapsync
|
|
128
141
|
true
|
129
142
|
|
130
143
|
rescue Exception => e
|
131
|
-
Snapsync.warn "Failed to synchronize #{src.snapshot_dir}, deleting target directory"
|
132
144
|
subvolume_dir = target_snapshot_dir + "snapshot"
|
145
|
+
Snapsync.warn "Failed to synchronize #{src.snapshot_dir}, deleting target directory #{subvolume_dir}"
|
133
146
|
if subvolume_dir.directory?
|
134
|
-
|
147
|
+
btrfs_dest.run("subvolume", "delete", subvolume_dir.path_part)
|
135
148
|
end
|
136
149
|
if target_snapshot_dir.directory?
|
137
150
|
target_snapshot_dir.rmtree
|
@@ -143,6 +156,9 @@ module Snapsync
|
|
143
156
|
def sync
|
144
157
|
STDOUT.sync = true
|
145
158
|
|
159
|
+
# Do a snapper cleanup before syncing
|
160
|
+
config.cleanup
|
161
|
+
|
146
162
|
# First, create a snapshot and protect it against cleanup, to use as
|
147
163
|
# synchronization point
|
148
164
|
#
|
@@ -191,23 +207,6 @@ module Snapsync
|
|
191
207
|
|
192
208
|
last_common_snapshot
|
193
209
|
end
|
194
|
-
|
195
|
-
def human_readable_time(time)
|
196
|
-
hrs = time / 3600
|
197
|
-
min = (time / 60) % 60
|
198
|
-
sec = time % 60
|
199
|
-
"%02i:%02i:%02i" % [hrs, min, sec]
|
200
|
-
end
|
201
|
-
|
202
|
-
def human_readable_size(size, digits: 1)
|
203
|
-
order = ['B', 'kB', 'MB', 'GB']
|
204
|
-
magnitude =
|
205
|
-
if size > 0
|
206
|
-
Integer(Math.log2(size) / 10)
|
207
|
-
else 0
|
208
|
-
end
|
209
|
-
"%.#{digits}f#{order[magnitude]}" % [Float(size) / (1024 ** magnitude)]
|
210
|
-
end
|
211
210
|
end
|
212
211
|
end
|
213
212
|
|
@@ -0,0 +1,107 @@
|
|
1
|
+
module Snapsync
|
2
|
+
class SSHPopen
|
3
|
+
class NonZeroExitCode < RuntimeError
|
4
|
+
end
|
5
|
+
class ExitSignal < RuntimeError
|
6
|
+
end
|
7
|
+
|
8
|
+
# @return [IO]
|
9
|
+
attr_reader :read_buffer
|
10
|
+
|
11
|
+
# @return [IO]
|
12
|
+
attr_reader :write_buffer
|
13
|
+
|
14
|
+
# @param machine [RemotePathname]
|
15
|
+
# @param [Array] command
|
16
|
+
# @param options [Hash]
|
17
|
+
def initialize(machine, command, **options)
|
18
|
+
@read_buffer, read_buffer_in = IO.pipe
|
19
|
+
write_buffer_out, @write_buffer = IO.pipe
|
20
|
+
|
21
|
+
if options[:out]
|
22
|
+
read_buffer_in = File.open(options[:out], "w")
|
23
|
+
end
|
24
|
+
|
25
|
+
ready = Concurrent::AtomicBoolean.new(false)
|
26
|
+
@ssh_thr = Thread.new do
|
27
|
+
machine.dup_ssh do |ssh|
|
28
|
+
ready.make_true
|
29
|
+
if Snapsync.SSH_DEBUG
|
30
|
+
log = Logger.new(STDOUT)
|
31
|
+
log.level = Logger::DEBUG
|
32
|
+
ssh.logger = log
|
33
|
+
ssh.logger.sev_threshold = Logger::Severity::DEBUG
|
34
|
+
end
|
35
|
+
# @type [Net::SSH::Connection::Channel]
|
36
|
+
channel = ssh.open_channel do |channel|
|
37
|
+
Snapsync.debug "SSHPopen channel opened: #{channel}"
|
38
|
+
|
39
|
+
channel.on_data do |ch, data|
|
40
|
+
read_buffer_in.write(data)
|
41
|
+
end
|
42
|
+
channel.on_extended_data do |ch, data|
|
43
|
+
data = data.chomp
|
44
|
+
if data.length > 0
|
45
|
+
Snapsync.error data
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
channel.on_request("exit-status") do |ch2, data|
|
50
|
+
code = data.read_long
|
51
|
+
if code != 0
|
52
|
+
raise NonZeroExitCode, "Exited with code: #{code}"
|
53
|
+
else
|
54
|
+
Snapsync.debug "SSHPopen command finished."
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
channel.on_request("exit-signal") do |ch2, data|
|
59
|
+
exit_signal = data.read_long
|
60
|
+
raise ExitSignal, "Exited due to signal: #{exit_signal}"
|
61
|
+
end
|
62
|
+
|
63
|
+
channel.on_process do
|
64
|
+
begin
|
65
|
+
channel.send_data(write_buffer_out.read_nonblock(2 << 20))
|
66
|
+
rescue IO::EAGAINWaitReadable
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
channel.exec(Shellwords.join command)
|
71
|
+
end
|
72
|
+
|
73
|
+
ssh.loop(0.001) {
|
74
|
+
if write_buffer_out.closed?
|
75
|
+
channel.close
|
76
|
+
end
|
77
|
+
|
78
|
+
channel.active?
|
79
|
+
}
|
80
|
+
|
81
|
+
Snapsync.debug "SSHPopen session closed"
|
82
|
+
read_buffer_in.close
|
83
|
+
write_buffer_out.close
|
84
|
+
end
|
85
|
+
end
|
86
|
+
while ready.false?
|
87
|
+
sleep 0.001
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def sync=(sync)
|
92
|
+
# ignore
|
93
|
+
end
|
94
|
+
|
95
|
+
def read(nbytes = nil)
|
96
|
+
read_buffer.read nbytes
|
97
|
+
end
|
98
|
+
|
99
|
+
def write(data)
|
100
|
+
write_buffer.write data
|
101
|
+
end
|
102
|
+
|
103
|
+
def close
|
104
|
+
write_buffer.close
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|