snapsync 0.3.8 → 0.4.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 +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 +31 -2
- data/snapsync.gemspec +13 -6
- data/snapsync.service +4 -1
- data/snapsync.timer +10 -0
- metadata +117 -33
@@ -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
|