bitferry 0.0.3 → 0.0.5
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 +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
|