bitferry 0.0.3 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGES.md +12 -0
- data/README.md +2 -2
- data/lib/bitferry/cli.rb +38 -7
- data/lib/bitferry.rb +66 -27
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bb10ff66d68e1981c2011621d1d216cfe759e14378844f5101310b78e5938974
|
4
|
+
data.tar.gz: 0341d89784f9acaa607f9267ddcbd494901efa80ca9954d60d84484d177c63ff
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2ac8f19ed6ef8607082b8943d0cab8ae4fa973694bfac1ddc219fe34e2421c05116d9d4ea20213440b192a7a1eae50cc11a485f489976dd182ff5ea1dc5bf1d2
|
7
|
+
data.tar.gz: cf4dabac47d7b46ed13129b749d82c92cd216f91b3fb41c6a8d2db9114738ae875f38db99d0112d26a7176db639d5cba68ebdeaee8e9e6f61f7f21dc658de568
|
data/CHANGES.md
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
## 0.0.5
|
2
|
+
|
3
|
+
- Disable cache usage in Restic check profiles
|
4
|
+
- Show stale tasks in verbose mode only
|
5
|
+
- Pick the innermost Bitferry volume in case of volumes nesting
|
6
|
+
- Fix an options profile application bug
|
7
|
+
|
8
|
+
## 0.0.4
|
9
|
+
|
10
|
+
- Include/exclude path filters
|
11
|
+
- Include user's home in the Bitferry volume lookup list
|
12
|
+
|
1
13
|
## 0.0.3
|
2
14
|
|
3
15
|
- Windows bundle
|
data/README.md
CHANGED
@@ -6,7 +6,7 @@ The [Bitferry](https://github.com/okhlybov/bitferry) is aimed at establishing th
|
|
6
6
|
|
7
7
|
The intended usage ranges from maintaining simple directory copy to another location (disk, mount point) to complex many-to-many (online/offline) data replication/backup solution employing portable media as additional data storage and a means of data propagation between the offsites.
|
8
8
|
|
9
|
-
The core idea that drives Bitferry is the conversion of full (absolute)
|
9
|
+
The core idea that drives Bitferry is the conversion of full (absolute) endpoints' paths into the volume-relative ones, where the volume is a data file which is put along the endpoint's data and denotes the root of the directory hierarchy. This makes data position-independent which means that Bitferry is then able to restore the tasks' source-destination endpoint connections in spite of the volume location changes, which is a likely scenario in case of portable storage (different UNIX mount points, Windows drives etc.).
|
10
10
|
|
11
11
|
Bitferry is effectively a frontend to the [Rclone](https://rclone.org) and [Restic](https://restic.net) utilities.
|
12
12
|
|
@@ -211,7 +211,7 @@ Parameters:
|
|
211
211
|
Options:
|
212
212
|
-e Encrypt files in destination using default profile (alias for -E default)
|
213
213
|
-d Decrypt source files using default profile (alias for -D default)
|
214
|
-
-
|
214
|
+
-u Use extended encryption profile options (applies to -e, -d)
|
215
215
|
--process, -X OPTIONS Extra task processing profile/options
|
216
216
|
--encrypt, -E OPTIONS Encrypt files in destination using specified profile/options
|
217
217
|
--decrypt, -D OPTIONS Decrypt source files using specified profile/options
|
data/lib/bitferry/cli.rb
CHANGED
@@ -22,6 +22,31 @@ Encryption = %{
|
|
22
22
|
}
|
23
23
|
|
24
24
|
|
25
|
+
$process = nil
|
26
|
+
$encryption = nil
|
27
|
+
$include = []
|
28
|
+
$exclude = []
|
29
|
+
|
30
|
+
|
31
|
+
def ext_globs(exts) = exts.split(',').collect { |ext| "*.#{ext}" }
|
32
|
+
|
33
|
+
|
34
|
+
def setup_task(x, include: true)
|
35
|
+
x.option ['-i'], 'EXTS', 'Include file extensions (comma-separated list)', multivalued: true, attribute_name: :include_exts do |exts|
|
36
|
+
$include << ext_globs(exts)
|
37
|
+
end if include
|
38
|
+
x.option ['-x'], 'EXTS', 'Exclude file extensions (comma-separated list)', multivalued: true, attribute_name: :exclude_exts do |exts|
|
39
|
+
$exclude << ext_globs(exts)
|
40
|
+
end
|
41
|
+
x.option ['--include'], 'GLOBS', 'Include path specifications (comma-separated list)', multivalued: true, attribute_name: :include do |globs|
|
42
|
+
$include << globs.split(',')
|
43
|
+
end if include
|
44
|
+
x.option ['--exclude'], 'GLOBS', 'Exclude path specifications (comma-separated list)', multivalued: true, attribute_name: :exclude do |globs|
|
45
|
+
$exclude << globs.split(',')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
25
50
|
def setup_rclone_task(x)
|
26
51
|
x.parameter 'SOURCE', 'Source endpoint specifier'
|
27
52
|
x.parameter 'DESTINATION', 'Destination endpoint specifier'
|
@@ -33,7 +58,7 @@ def setup_rclone_task(x)
|
|
33
58
|
$encryption = Bitferry::Rclone::Decrypt
|
34
59
|
$profile = :default
|
35
60
|
end
|
36
|
-
x.option '-
|
61
|
+
x.option '-u', :flag, 'Apply extended (unicode) encryption profile options (alias for -E extended / -D extended)', attribute_name: :u do
|
37
62
|
$extended = true
|
38
63
|
end
|
39
64
|
x.option ['--process', '-X'], 'OPTIONS', 'Extra task processing profile/options' do |opts|
|
@@ -47,6 +72,7 @@ def setup_rclone_task(x)
|
|
47
72
|
$encryption = Bitferry::Rclone::Decrypt
|
48
73
|
$profile = opts
|
49
74
|
end
|
75
|
+
setup_task(x)
|
50
76
|
end
|
51
77
|
|
52
78
|
|
@@ -54,6 +80,7 @@ def create_rclone_task(task, *args, **opts)
|
|
54
80
|
task.new(*args,
|
55
81
|
process: $process,
|
56
82
|
encryption: $encryption&.new(obtain_password, process: $extended ? :extended : $profile),
|
83
|
+
include: $include.flatten.uniq, exclude: $exclude.flatten.uniq,
|
57
84
|
**opts
|
58
85
|
)
|
59
86
|
end
|
@@ -131,7 +158,7 @@ Clamp do
|
|
131
158
|
puts " #{task.tag} #{task.show_status}"
|
132
159
|
end
|
133
160
|
end
|
134
|
-
|
161
|
+
if !(xs = Bitferry::Task.stale).empty? && Bitferry.verbosity == :verbose
|
135
162
|
puts
|
136
163
|
puts '# Stale tasks'
|
137
164
|
puts
|
@@ -251,13 +278,14 @@ Clamp do
|
|
251
278
|
}
|
252
279
|
option '--force', :flag, 'Force overwriting existing repository' do $format = true end
|
253
280
|
option ['--attach', '-a'], :flag, 'Attach to existing repository' do $format = false end
|
254
|
-
option '-f', :flag, '
|
255
|
-
option '-c', :flag, '
|
281
|
+
option '-f', :flag, 'Apply default snapshot retention policy options (alias for -F default)', attribute_name: :f do $forget = :default end
|
282
|
+
option '-c', :flag, 'Apply default repository checking options (alias for -C default)', attribute_name: :c do $check = :default end
|
256
283
|
option ['--process', '-X'], 'OPTIONS', 'Extra task processing profile/options' do |opts| $process = opts end
|
257
|
-
option ['--forget', '-F'], 'OPTIONS', '
|
258
|
-
option ['--check', '-C'], 'OPTIONS', '
|
284
|
+
option ['--forget', '-F'], 'OPTIONS', 'Snapshot retention policy with profile/options' do |opts| $forget = opts end
|
285
|
+
option ['--check', '-C'], 'OPTIONS', 'Repository checking with profile/options' do |opts| $check = opts end
|
259
286
|
parameter 'SOURCE', 'Source endpoint specifier'
|
260
287
|
parameter 'REPOSITORY', 'Destination repository endpoint specifier'
|
288
|
+
setup_task(self, include: false)
|
261
289
|
def execute
|
262
290
|
bitferry {
|
263
291
|
Bitferry::Restic::Backup.new(
|
@@ -265,7 +293,8 @@ Clamp do
|
|
265
293
|
format: $format,
|
266
294
|
process: $process,
|
267
295
|
check: $check,
|
268
|
-
forget: $forget
|
296
|
+
forget: $forget,
|
297
|
+
exclude: $exclude.flatten.uniq
|
269
298
|
)
|
270
299
|
}
|
271
300
|
end
|
@@ -280,11 +309,13 @@ Clamp do
|
|
280
309
|
option ['--process', '-X'], 'OPTIONS', 'Extra task processing profile/options' do |opts| $process = opts end
|
281
310
|
parameter 'REPOSITORY', 'Source repository endpoint specifier'
|
282
311
|
parameter 'DESTINATION', 'Destination endpoint specifier'
|
312
|
+
setup_task(self)
|
283
313
|
def execute
|
284
314
|
bitferry {
|
285
315
|
Bitferry::Restic::Restore.new(
|
286
316
|
destination, repository, obtain_password,
|
287
317
|
process: $process,
|
318
|
+
include: $include.flatten.uniq, exclude: $exclude.flatten.uniq
|
288
319
|
)
|
289
320
|
}
|
290
321
|
end
|
data/lib/bitferry.rb
CHANGED
@@ -12,7 +12,7 @@ require 'shellwords'
|
|
12
12
|
module Bitferry
|
13
13
|
|
14
14
|
|
15
|
-
VERSION = '0.0.
|
15
|
+
VERSION = '0.0.5'
|
16
16
|
|
17
17
|
|
18
18
|
module Logging
|
@@ -39,7 +39,7 @@ module Bitferry
|
|
39
39
|
reset
|
40
40
|
log.info('restoring volumes')
|
41
41
|
result = true
|
42
|
-
roots = (environment_mounts + system_mounts).uniq
|
42
|
+
roots = (environment_mounts + system_mounts + [Dir.home]).uniq
|
43
43
|
log.info("distilled volume search path: #{roots.join(', ')}")
|
44
44
|
roots.each do |root|
|
45
45
|
if File.exist?(File.join(root, Volume::STORAGE))
|
@@ -341,12 +341,10 @@ module Bitferry
|
|
341
341
|
|
342
342
|
def self.endpoint(root)
|
343
343
|
path = Pathname.new(root).realdirpath
|
344
|
-
|
345
|
-
intact.sort { |v1, v2| v2.root.size <=> v1.root.size }.each do |volume|
|
344
|
+
intact.sort { |v1, v2| v2.root.to_s.size <=> v1.root.to_s.size }.each do |volume|
|
346
345
|
begin
|
347
|
-
|
348
|
-
stem
|
349
|
-
case stem.to_s
|
346
|
+
stem = path.relative_path_from(volume.root).to_s #.chomp('/')
|
347
|
+
case stem
|
350
348
|
when '.' then return volume.endpoint
|
351
349
|
when /^[^\.].*/ then return volume.endpoint(stem)
|
352
350
|
end
|
@@ -477,7 +475,7 @@ module Bitferry
|
|
477
475
|
when Array then option # Array is passed verbatim
|
478
476
|
when '-' then nil # Disable adding any options with -
|
479
477
|
when /^-/ then option.split(',') # Split comma-separated string into array --foo,bar --> [--foo, bar]
|
480
|
-
else route.fetch(option) # Obtain
|
478
|
+
else route.fetch(option.nil? ? nil : option.to_sym) # Obtain options from the profile database
|
481
479
|
end
|
482
480
|
end
|
483
481
|
|
@@ -498,6 +496,9 @@ module Bitferry
|
|
498
496
|
attr_reader :modified
|
499
497
|
|
500
498
|
|
499
|
+
attr_reader :include, :exclude
|
500
|
+
|
501
|
+
|
501
502
|
def process_options = @process_options.nil? ? [] : @process_options # As a mandatory option it should never be nil
|
502
503
|
|
503
504
|
|
@@ -530,10 +531,12 @@ module Bitferry
|
|
530
531
|
end
|
531
532
|
|
532
533
|
|
533
|
-
def initialize(tag: Bitferry.tag, modified: DateTime.now)
|
534
|
+
def initialize(tag: Bitferry.tag, modified: DateTime.now, include: [], exclude: [])
|
534
535
|
@tag = tag
|
535
536
|
@generation = 0
|
536
|
-
@
|
537
|
+
@include = include
|
538
|
+
@exclude = exclude
|
539
|
+
@modified = modified.is_a?(DateTime) ? modified : DateTime.parse(modified)
|
537
540
|
# FIXME handle process_options at this level
|
538
541
|
end
|
539
542
|
|
@@ -546,6 +549,8 @@ module Bitferry
|
|
546
549
|
|
547
550
|
|
548
551
|
def restore(hash)
|
552
|
+
@include = hash.fetch(:include, [])
|
553
|
+
@exclude = hash.fetch(:exclude, [])
|
549
554
|
@state = :intact
|
550
555
|
log.info("restored task #{tag}")
|
551
556
|
end
|
@@ -558,7 +563,9 @@ module Bitferry
|
|
558
563
|
def externalize
|
559
564
|
{
|
560
565
|
task: tag,
|
561
|
-
modified:
|
566
|
+
modified: modified,
|
567
|
+
include: include.empty? ? nil : include,
|
568
|
+
exclude: exclude.empty? ? nil : exclude
|
562
569
|
}.compact
|
563
570
|
end
|
564
571
|
|
@@ -584,6 +591,14 @@ module Bitferry
|
|
584
591
|
end
|
585
592
|
|
586
593
|
|
594
|
+
def show_filters
|
595
|
+
xs = []
|
596
|
+
xs << 'include: ' + include.join(',') unless include.empty?
|
597
|
+
xs << 'exclude: ' + exclude.join(',') unless exclude.empty?
|
598
|
+
xs.join(' ').to_s
|
599
|
+
end
|
600
|
+
|
601
|
+
|
587
602
|
def self.[](tag) = @@registry[tag]
|
588
603
|
|
589
604
|
|
@@ -609,8 +624,16 @@ module Bitferry
|
|
609
624
|
def self.reset = @@registry = {}
|
610
625
|
|
611
626
|
|
612
|
-
def self.register(task)
|
613
|
-
|
627
|
+
def self.register(task)
|
628
|
+
# Task with newer timestamp replaces already registered task, if any
|
629
|
+
if (xtag = @@registry[task.tag]).nil?
|
630
|
+
@@registry[task.tag] = task
|
631
|
+
elsif xtag.modified < task.modified
|
632
|
+
@@registry[task.tag] = task
|
633
|
+
else
|
634
|
+
xtag
|
635
|
+
end
|
636
|
+
end
|
614
637
|
|
615
638
|
def self.intact = live.filter { |task| task.intact? }
|
616
639
|
|
@@ -795,7 +818,7 @@ module Bitferry
|
|
795
818
|
end
|
796
819
|
|
797
820
|
|
798
|
-
def show_status = "#{show_operation} #{source.show_status} #{show_direction} #{destination.show_status}"
|
821
|
+
def show_status = "#{show_operation} #{source.show_status} #{show_direction} #{destination.show_status} #{show_filters}"
|
799
822
|
|
800
823
|
|
801
824
|
def show_operation = encryption.nil? ? '' : encryption.show_operation
|
@@ -833,8 +856,14 @@ module Bitferry
|
|
833
856
|
end
|
834
857
|
|
835
858
|
|
859
|
+
def include_filters = include.collect { |x| ['--filter', "+ #{x}"]}.flatten
|
860
|
+
|
861
|
+
|
862
|
+
def exclude_filters = ([Volume::STORAGE, Volume::STORAGE_] + exclude).collect { |x| ['--filter', "- #{x}"]}.flatten
|
863
|
+
|
864
|
+
|
836
865
|
def process_arguments
|
837
|
-
|
866
|
+
include_filters + exclude_filters + common_options + process_options + (
|
838
867
|
encryption.nil? ? [source.root.to_s, destination.root.to_s] : encryption.arguments(self)
|
839
868
|
)
|
840
869
|
end
|
@@ -846,7 +875,7 @@ module Bitferry
|
|
846
875
|
puts cms if Bitferry.verbosity == :verbose
|
847
876
|
log.info(cms)
|
848
877
|
status = Open3.pipeline(cmd).first
|
849
|
-
raise "rclone exit code #{status.exitstatus}" unless status.success?
|
878
|
+
raise RuntimeError, "rclone exit code #{status.exitstatus}" unless status.success?
|
850
879
|
status.success?
|
851
880
|
end
|
852
881
|
|
@@ -1011,6 +1040,9 @@ module Bitferry
|
|
1011
1040
|
def format = nil
|
1012
1041
|
|
1013
1042
|
|
1043
|
+
def include_filters = include.collect { |x| ['--include', x]}.flatten
|
1044
|
+
|
1045
|
+
|
1014
1046
|
def common_options
|
1015
1047
|
[
|
1016
1048
|
case Bitferry.verbosity
|
@@ -1037,7 +1069,8 @@ module Bitferry
|
|
1037
1069
|
begin
|
1038
1070
|
Dir.chdir(chdir) unless chdir.nil?
|
1039
1071
|
status = Open3.pipeline(cmd).first
|
1040
|
-
raise "restic exit code #{status.exitstatus}" unless status.success?
|
1072
|
+
raise RuntimeError, "restic exit code #{status.exitstatus}" unless status.success?
|
1073
|
+
status.success?
|
1041
1074
|
ensure
|
1042
1075
|
Dir.chdir(wd) unless chdir.nil?
|
1043
1076
|
end
|
@@ -1077,14 +1110,14 @@ module Bitferry
|
|
1077
1110
|
|
1078
1111
|
|
1079
1112
|
FORGET = {
|
1080
|
-
default: ['--prune', '--keep-within-hourly', '24h', '--keep-within-daily', '7d', '--keep-within-weekly', '30d', '--keep-within-monthly', '1y', '--keep-within-yearly', '100y']
|
1113
|
+
default: ['--prune', '--no-cache', '--keep-within-hourly', '24h', '--keep-within-daily', '7d', '--keep-within-weekly', '30d', '--keep-within-monthly', '1y', '--keep-within-yearly', '100y']
|
1081
1114
|
}
|
1082
1115
|
FORGET[nil] = nil # Skip processing retention policy by default
|
1083
1116
|
|
1084
1117
|
|
1085
1118
|
CHECK = {
|
1086
|
-
default: [],
|
1087
|
-
full: ['--read-data']
|
1119
|
+
default: ['--no-cache'],
|
1120
|
+
full: ['--no-cache', '--read-data']
|
1088
1121
|
}
|
1089
1122
|
CHECK[nil] = nil # Skip integrity checking by default
|
1090
1123
|
|
@@ -1102,7 +1135,10 @@ module Bitferry
|
|
1102
1135
|
end
|
1103
1136
|
|
1104
1137
|
|
1105
|
-
def
|
1138
|
+
def exclude_filters = ([Volume::STORAGE, Volume::STORAGE_] + exclude).collect { |x| ['--exclude', x]}.flatten
|
1139
|
+
|
1140
|
+
|
1141
|
+
def show_status = "#{show_operation} #{directory.show_status} #{show_direction} #{repository.show_status} #{show_filters}"
|
1106
1142
|
|
1107
1143
|
|
1108
1144
|
def show_operation = 'encrypt+backup'
|
@@ -1114,7 +1150,7 @@ module Bitferry
|
|
1114
1150
|
def process
|
1115
1151
|
begin
|
1116
1152
|
log.info("processing task #{tag}")
|
1117
|
-
execute('backup', '.', '--tag', "bitferry,#{tag}",
|
1153
|
+
execute('backup', '.', '--tag', "bitferry,#{tag}", *exclude_filters, *process_options, *common_options_simulate, chdir: directory.root)
|
1118
1154
|
unless check_options.nil?
|
1119
1155
|
log.info("checking repository in #{repository.root}")
|
1120
1156
|
execute('check', *check_options, *common_options)
|
@@ -1199,7 +1235,10 @@ module Bitferry
|
|
1199
1235
|
end
|
1200
1236
|
|
1201
1237
|
|
1202
|
-
def
|
1238
|
+
def exclude_filters = exclude.collect { |x| ['--exclude', x]}.flatten
|
1239
|
+
|
1240
|
+
|
1241
|
+
def show_status = "#{show_operation} #{repository.show_status} #{show_direction} #{directory.show_status} #{show_filters}"
|
1203
1242
|
|
1204
1243
|
|
1205
1244
|
def show_operation = 'decrypt+restore'
|
@@ -1230,7 +1269,7 @@ module Bitferry
|
|
1230
1269
|
log.info("processing task #{tag}")
|
1231
1270
|
begin
|
1232
1271
|
# FIXME restore specifically tagged latest snapshot
|
1233
|
-
execute('restore', 'latest', '--target',
|
1272
|
+
execute('restore', 'latest', '--target', directory.root.to_s, *include_filters, *exclude_filters, *process_options, *common_options, simulate: Bitferry.simulate?)
|
1234
1273
|
true
|
1235
1274
|
rescue
|
1236
1275
|
false
|
@@ -1326,7 +1365,7 @@ module Bitferry
|
|
1326
1365
|
|
1327
1366
|
def restore(hash)
|
1328
1367
|
@volume_tag = hash.fetch(:volume)
|
1329
|
-
@path = Pathname.new(hash.fetch(:path))
|
1368
|
+
@path = Pathname.new(hash.fetch(:path, ''))
|
1330
1369
|
end
|
1331
1370
|
|
1332
1371
|
|
@@ -1334,8 +1373,8 @@ module Bitferry
|
|
1334
1373
|
{
|
1335
1374
|
endpoint: :bitferry,
|
1336
1375
|
volume: volume_tag,
|
1337
|
-
path: path
|
1338
|
-
}
|
1376
|
+
path: path.to_s.empty? ? nil : path
|
1377
|
+
}.compact
|
1339
1378
|
end
|
1340
1379
|
|
1341
1380
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bitferry
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Oleg A. Khlybov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-03-
|
11
|
+
date: 2024-03-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|