bitferry 0.0.1

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.
Files changed (6) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +233 -0
  3. data/bin/bitferry +3 -0
  4. data/lib/bitferry/cli.rb +344 -0
  5. data/lib/bitferry.rb +1370 -0
  6. metadata +104 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 07536a8e52281f9dbe3a085aa9bedff6164f116babcb163149e8a13e2214f771
4
+ data.tar.gz: a81dbb898d67e9c9e2083871c69bfd6396e56caa12904b085cf8e7c5e85248d5
5
+ SHA512:
6
+ metadata.gz: c8025822e4520ec87254036acd3b7c6f3933949b3a0737cc9d12d91d5ae4d5fa5d98487a9f2d60a18c15cfb9d4009212b35ead99ce415ececdc42b48c03bcd7f
7
+ data.tar.gz: 1af8e4d44dd64818f78eadb27b62233e33bd4f7f1cc493dfafbcb7ca4defd77e5ea868ad5060ee28e6eefc3f4c122842e7774b4627506305ee553e12157ea3c0
data/README.md ADDED
@@ -0,0 +1,233 @@
1
+ # Bitferry - file synchronization/backup automation tool
2
+
3
+ <div align="right"><i>Ein Backup ist kein Backup</i></div><br><br>
4
+
5
+ The [Bitferry](https://github.com/okhlybov/bitferry) is aimed at establishing the automated file synchronization/replication/backup routes between multiple endpoints where the latter can be the local directories, online cloud remotes or portable offline storages.
6
+
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
+
9
+ Bitferry is a frontend to the [Rclone](https://rclone.org) and [Restic](https://restic.net) utilities.
10
+
11
+
12
+ ## Features
13
+
14
+ * Multiplatform (Windows / UNIX / macOSX) operation
15
+
16
+ * Automated task-based data processing
17
+
18
+ * One way / two way data synchronization
19
+
20
+ * Recursive directory copy / update / synchronize
21
+
22
+ * Incremental directory backup with snapshotting
23
+
24
+ * File/repository password-based end-to-end encryption
25
+
26
+ * Online cloud storage relay
27
+
28
+ * Offline portable storage (USB flash, HDDs, SSDs etc.) relay
29
+
30
+
31
+ ## Use cases
32
+
33
+ * Maintain an update-only files copy in a separate location on the same site
34
+
35
+ * Maintain offline secure two way file synchronization between two offsites
36
+
37
+ * Maintain an incremental files backup on a portable medium with multiple offsite copies of the repository
38
+
39
+
40
+ ## Implementation
41
+
42
+ The Bitferry itself is written in [Ruby](https://www.ruby-lang.org) programming language. Being a Ruby code, the Bitferry requires the platform-specific Ruby runtime, version 3.0 or higher.
43
+
44
+ The source code is hosted on [GitHub](https://github.com/okhlybov/bitferry) and the binary releases in form of a GEM package are distributed through the [RubyGems](https://rubygems.org/gems/bitferry) repository channel.
45
+
46
+ In addition, the platform-specific [Rclone](https://github.com/rclone/rclone/releases) and [Restic](https://github.com/restic/restic/releases) executables are required to be accessible through the `PATH` directory list or through the respective `RCLONE` and `RESTIC` environment variables.
47
+
48
+
49
+ ## Kickstart
50
+
51
+ Install Bitferry
52
+
53
+ ```shell
54
+ gem install bitferry
55
+ ```
56
+
57
+ Prepare source Bitferry volume for a mounted local filesystem
58
+
59
+ ```shell
60
+ bitferry create volume /data
61
+ ```
62
+
63
+ Prepare destination Bitferry volume for a mounted portable storage
64
+
65
+ ```shell
66
+ bitferry create volume /mnt/usb-drive
67
+ ```
68
+
69
+ Ensure the volumes are intact
70
+
71
+ ```shell
72
+ bitferry show
73
+ ```
74
+
75
+ ```
76
+ # Intact volumes
77
+
78
+ d2f10024 /data
79
+ e42f2d8c /mnt/usb-drive
80
+ ```
81
+
82
+ Create a (Rclone) sync task with data encryption
83
+
84
+ ```shell
85
+ bitferry create task sync -e /data /mnt/usb-drive/backup
86
+ ```
87
+
88
+ Review the changes
89
+
90
+ ```shell
91
+ bitferry
92
+ ```
93
+
94
+ ```
95
+ # Intact volumes
96
+
97
+ d2f10024 /data
98
+ e42f2d8c /mnt/usb-drive
99
+
100
+
101
+ # Intact tasks
102
+
103
+ 89e1c119 encrypt+synchronize :d2f10024: --> :e42f2d8c:backup
104
+ ```
105
+
106
+ Perform a dry run of the specific task
107
+
108
+ ```shell
109
+ bitferry process -vn 89e
110
+ ```
111
+
112
+ <details>
113
+ <summary>...</summary>
114
+
115
+ ```
116
+ rclone sync --filter -\ .bitferry --filter -\ .bitferry\~ --verbose --progress --dry-run --metadata --crypt-filename-encoding base32 --crypt-filename-encryption standard --crypt-remote /mnt/usb-drive/backup /data :crypt:
117
+ 2024/03/05 11:46:45 NOTICE: README.md: Skipped copy as --dry-run is set (size 3.073Ki)
118
+ 2024/03/05 11:46:45 NOTICE: LICENSE: Skipped copy as --dry-run is set (size 1.467Ki)
119
+ 2024/03/05 11:46:45 NOTICE: bitferry.gemspec: Skipped copy as --dry-run is set (size 996)
120
+ Transferred: 5.513 KiB / 5.513 KiB, 100%, 0 B/s, ETA -
121
+ Transferred: 3 / 3, 100%
122
+ Elapsed time: 0.0s
123
+ 2024/03/05 11:46:45 NOTICE:
124
+ Transferred: 5.513 KiB / 5.513 KiB, 100%, 0 B/s, ETA -
125
+ Transferred: 3 / 3, 100%
126
+ Elapsed time: 0.0s
127
+ ```
128
+
129
+ </details>
130
+
131
+ Process all intact tasks in sequence
132
+
133
+ ```shell
134
+ bitferry -v x
135
+ ```
136
+
137
+ <details>
138
+ <summary>...</summary>
139
+
140
+ ```
141
+ rclone sync --filter -\ .bitferry --filter -\ .bitferry\~ --verbose --progress --metadata --crypt-filename-encoding base32 --crypt-filename-encryption standard --crypt-remote /mnt/usb-drive/backup /data :crypt:
142
+ 2024/03/05 11:44:31 INFO : LICENSE: Copied (new)
143
+ 2024/03/05 11:44:31 INFO : README.md: Copied (new)
144
+ 2024/03/05 11:44:31 INFO : bitferry.gemspec: Copied (new)
145
+ Transferred: 5.653 KiB / 5.653 KiB, 100%, 0 B/s, ETA -
146
+ Transferred: 3 / 3, 100%
147
+ Elapsed time: 0.0s
148
+ 2024/03/05 11:44:31 INFO :
149
+ Transferred: 5.653 KiB / 5.653 KiB, 100%, 0 B/s, ETA -
150
+ Transferred: 3 / 3, 100%
151
+ Elapsed time: 0.0s
152
+ ```
153
+
154
+ </details>
155
+
156
+ Observe the result
157
+
158
+ ```shell
159
+ ls -l /mnt/usb-drive/backup
160
+ ```
161
+
162
+ <details>
163
+ <summary>...</summary>
164
+
165
+ ```
166
+ -rw-r--r-- 1 user user 1044 feb 27 17:09 0u1vi7ka5p88u62kof9k6mf2z00354g6fa0c9a0g6di2f0ocds80
167
+ -rw-r--r-- 1 user user 1550 jan 29 11:57 21dgu5vs2c4rjfkieeemjvaf78
168
+ -rw-r--r-- 1 user user 3195 mar 5 11:43 m9rhq3q2m5h2q5l1ke00u0gdjc
169
+ ```
170
+
171
+ </details>
172
+
173
+ Examine the detailed usage instructions
174
+
175
+ ```shell
176
+ bitferry c t s -h
177
+ ```
178
+
179
+ <details>
180
+ <summary>...</summary>
181
+
182
+ ```
183
+ Usage:
184
+ bitferry c t s [OPTIONS] SOURCE DESTINATION
185
+
186
+ Create source --> destination one way file synchronization task.
187
+
188
+ The task operates recursively on two specified endpoints.
189
+ This task copies newer source files while skipping unchanged files in destination.
190
+ Also, it deletes destination files which are non-existent in source.
191
+
192
+ The endpoint may be one of:
193
+ * directory -- absolute or relative local directory (/data, ../source, c:\data)
194
+ * local:directory, :directory -- absolute local directory (:/data, local:c:\data)
195
+ * :tag:directory -- path relative to the intact volume matched by (partial) tag (:fa2c:source/data)
196
+
197
+ The former case resolves specified directory againt an intact volume to make it volume-relative.
198
+ It is an error if there is no intact volume that encompasses specified directory.
199
+ The local: directory is left as is (not resolved against volumes).
200
+ The :tag: directory is bound to the specified volume.
201
+
202
+
203
+
204
+ The encryption mode is controlled by --encrypt or --decrypt options.
205
+ The mandatory password will be read from the standard input channel (pipe or keyboard).
206
+
207
+ This task employs the Rclone worker.
208
+
209
+ Parameters:
210
+ SOURCE Source endpoint specifier
211
+ DESTINATION Destination endpoint specifier
212
+
213
+ Options:
214
+ -e Encrypt files in destination using default profile (alias for -E default)
215
+ -d Decrypt source files using default profile (alias for -D default)
216
+ -x Use extended encryption profile options (applies to -e, -d)
217
+ --process, -X OPTIONS Extra task processing profile/options
218
+ --encrypt, -E OPTIONS Encrypt files in destination using specified profile/options
219
+ --decrypt, -D OPTIONS Decrypt source files using specified profile/options
220
+ --version Print version
221
+ --verbose, -v Extensive logging
222
+ --quiet, -q Disable logging
223
+ --dry-run, -n Simulation mode (make no on-disk changes)
224
+ -h, --help print help
225
+ ```
226
+
227
+ </details>
228
+
229
+ ## The rest is about to come
230
+
231
+ *Cheers!*
232
+
233
+ Oleg A. Khlybov <fougas@mail.ru>
data/bin/bitferry ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env ruby
2
+ require 'bitferry/cli'
3
+ #
@@ -0,0 +1,344 @@
1
+ require 'clamp'
2
+ require 'bitferry'
3
+ require 'io/console'
4
+
5
+
6
+ Endpoint = %{
7
+ The endpoint may be one of:
8
+ * directory -- absolute or relative local directory (/data, ../source, c:\\data)
9
+ * local:directory, :directory -- absolute local directory (:/data, local:c:\\data)
10
+ * :tag:directory -- path relative to the intact volume matched by (partial) tag (:fa2c:source/data)
11
+
12
+ The former case resolves specified directory againt an intact volume to make it volume-relative.
13
+ It is an error if there is no intact volume that encompasses specified directory.
14
+ The local: directory is left as is (not resolved against volumes).
15
+ The :tag: directory is bound to the specified volume.
16
+ }
17
+
18
+
19
+ Encryption = %{
20
+ The encryption mode is controlled by --encrypt or --decrypt options.
21
+ The mandatory password will be read from the standard input channel (pipe or keyboard).
22
+ }
23
+
24
+
25
+ def setup_rclone_task(x)
26
+ x.parameter 'SOURCE', 'Source endpoint specifier'
27
+ x.parameter 'DESTINATION', 'Destination endpoint specifier'
28
+ x.option '-e', :flag, 'Encrypt files in destination using default profile (alias for -E default)', attribute_name: :e do
29
+ $encryption = Bitferry::Rclone::Encrypt
30
+ $profile = :default
31
+ end
32
+ x.option '-d', :flag, 'Decrypt source files using default profile (alias for -D default)', attribute_name: :d do
33
+ $encryption = Bitferry::Rclone::Decrypt
34
+ $profile = :default
35
+ end
36
+ x.option '-x', :flag, 'Use extended encryption profile options (applies to -e, -d)', attribute_name: :x do
37
+ $extended = true
38
+ end
39
+ x.option ['--process', '-X'], 'OPTIONS', 'Extra task processing profile/options' do |opts|
40
+ $process = opts
41
+ end
42
+ x.option ['--encrypt', '-E'], 'OPTIONS', 'Encrypt files in destination using specified profile/options' do |opts|
43
+ $encryption = Bitferry::Rclone::Encrypt
44
+ $profile = opts
45
+ end
46
+ x.option ['--decrypt', '-D'], 'OPTIONS', 'Decrypt source files using specified profile/options' do |opts|
47
+ $encryption = Bitferry::Rclone::Decrypt
48
+ $profile = opts
49
+ end
50
+ end
51
+
52
+
53
+ def create_rclone_task(task, *args, **opts)
54
+ task.new(*args,
55
+ process: $process,
56
+ encryption: $encryption&.new(obtain_password, process: $extended ? :extended : $profile),
57
+ **opts
58
+ )
59
+ end
60
+
61
+
62
+ def bitferry(&code)
63
+ begin
64
+ Bitferry.restore
65
+ result = yield
66
+ exit(Bitferry.commit && result ? 0 : 1)
67
+ rescue => e
68
+ Bitferry.log.fatal(e.message)
69
+ exit(1)
70
+ end
71
+ end
72
+
73
+
74
+ def obtain_password
75
+ if $stdin.tty?
76
+ p1 = IO.console.getpass 'Enter password:'
77
+ p2 = IO.console.getpass 'Repeat password:'
78
+ raise 'passwords do not match' unless p1 == p2
79
+ p1
80
+ else
81
+ $stdin.readline.strip!
82
+ end
83
+ end
84
+
85
+
86
+ Bitferry.log.level = Logger::DEBUG if $DEBUG
87
+
88
+
89
+ Clamp do
90
+
91
+
92
+ self.default_subcommand = 'show'
93
+
94
+
95
+ option '--version', :flag, 'Print version' do
96
+ puts Bitferry::VERSION
97
+ exit
98
+ end
99
+
100
+
101
+ option ['--verbose', '-v'], :flag, 'Extensive logging' do
102
+ Bitferry.verbosity = :verbose
103
+ end
104
+
105
+
106
+ option ['--quiet', '-q'], :flag, 'Disable logging' do
107
+ Bitferry.verbosity = :quiet
108
+ end
109
+
110
+
111
+ option ['--dry-run', '-n'], :flag, 'Simulation mode (make no on-disk changes)' do
112
+ Bitferry.simulate = true
113
+ end
114
+
115
+
116
+ subcommand ['show', 'info', 'i'], 'Print state' do
117
+ def execute
118
+ Bitferry.restore
119
+ unless (xs = Bitferry::Volume.intact).empty?
120
+ puts '# Intact volumes'
121
+ puts
122
+ xs.each do |volume|
123
+ puts " #{volume.tag} #{volume.root}"
124
+ end
125
+ end
126
+ unless (xs = Bitferry::Task.intact).empty?
127
+ puts
128
+ puts '# Intact tasks'
129
+ puts
130
+ xs.each do |task|
131
+ puts " #{task.tag} #{task.show_status}"
132
+ end
133
+ end
134
+ unless (xs = Bitferry::Task.stale).empty?
135
+ puts
136
+ puts '# Stale tasks'
137
+ puts
138
+ xs.each do |task|
139
+ puts " #{task.tag} #{task.show_status}"
140
+ end
141
+ end
142
+ end
143
+ end
144
+
145
+
146
+ subcommand ['create', 'c'], 'Create entity' do
147
+
148
+
149
+ subcommand ['volume', 'v'], 'Create volume' do
150
+ banner %{
151
+ Create new volume in specified directory. Create directory if it does not exist.
152
+ Refuse to overwrite existing volume storage unless --force is specified.
153
+ }
154
+ option '--force', :flag, 'Overwrite existing volume storage in target directory'
155
+ parameter 'DIRECTORY', 'Target volume directory'
156
+ def execute
157
+ bitferry { Bitferry::Volume.new(directory, overwrite: force?) }
158
+ end
159
+ end
160
+
161
+
162
+ subcommand ['task', 't'], 'Create task' do
163
+
164
+
165
+ subcommand ['copy', 'c'], 'Create copy task' do
166
+ banner %{
167
+ Create source --> destination file copy task.
168
+
169
+ The task operates recursively on two specified endpoints.
170
+ This task unconditionally copies all source files overwriting existing files in destination.
171
+
172
+ #{Endpoint}
173
+
174
+ #{Encryption}
175
+
176
+ This task employs the Rclone worker.
177
+ }
178
+ setup_rclone_task(self)
179
+ def execute
180
+ bitferry { create_rclone_task(Bitferry::Rclone::Copy, source, destination) }
181
+ end
182
+ end
183
+
184
+
185
+ subcommand ['update', 'u'], 'Create update task' do
186
+ banner %{
187
+ Create source --> destination file update (freshen) task.
188
+
189
+ The task operates recursively on two specified endpoints.
190
+ This task copies newer source files while skipping unchanged files in destination.
191
+
192
+ #{Endpoint}
193
+
194
+ #{Encryption}
195
+
196
+ This task employs the Rclone worker.
197
+ }
198
+ setup_rclone_task(self)
199
+ def execute
200
+ bitferry { create_rclone_task(Bitferry::Rclone::Update, source, destination) }
201
+ end
202
+ end
203
+
204
+
205
+ subcommand ['synchronize', 'sync', 's'], 'Create one way sync task' do
206
+ banner %{
207
+ Create source --> destination one way file synchronization task.
208
+
209
+ The task operates recursively on two specified endpoints.
210
+ This task copies newer source files while skipping unchanged files in destination.
211
+ Also, it deletes destination files which are non-existent in source.
212
+
213
+ #{Endpoint}
214
+
215
+ #{Encryption}
216
+
217
+ This task employs the Rclone worker.
218
+ }
219
+ setup_rclone_task(self)
220
+ def execute
221
+ bitferry { create_rclone_task(Bitferry::Rclone::Synchronize, source, destination) }
222
+ end
223
+ end
224
+
225
+
226
+ subcommand ['equalize', 'bisync', 'e'], 'Create two way sync task' do
227
+ banner %{
228
+ Create source <-> destination two way file synchronization task.
229
+
230
+ The task operates recursively on two specified endpoints.
231
+ This task retains only the most recent versions of files on both endpoints.
232
+ Opon execution both endpoints are left identical.
233
+
234
+ #{Endpoint}
235
+
236
+ #{Encryption}
237
+
238
+ This task employs the Rclone worker.
239
+ }
240
+ setup_rclone_task(self)
241
+ def execute
242
+ bitferry { create_rclone_task(Bitferry::Rclone::Equalize, source, destination) }
243
+ end
244
+ end
245
+
246
+
247
+ subcommand ['backup', 'b'], 'Create backup task' do
248
+ banner %{
249
+ Create source --> repository incremental backup task.
250
+ This task employs the Restic worker.
251
+ }
252
+ option '--force', :flag, 'Force overwriting existing repository' do $format = true end
253
+ option ['--attach', '-a'], :flag, 'Attach to existing repository' do $format = false end
254
+ option '-f', :flag, 'Rig for application of the snapshot retention policy (alias for -F default)', attribute_name: :f do $forget = :default end
255
+ option '-c', :flag, 'Rig for repository checking (alias for -C default)', attribute_name: :c do $check = :default end
256
+ option ['--process', '-X'], 'OPTIONS', 'Extra task processing profile/options' do |opts| $process = opts end
257
+ option ['--forget', '-F'], 'OPTIONS', 'Rig for snapshot retention policy with profile/options' do |opts| $forget = opts end
258
+ option ['--check', '-C'], 'OPTIONS', 'Rig for repository checking with profile/options' do |opts| $check = opts end
259
+ parameter 'SOURCE', 'Source endpoint specifier'
260
+ parameter 'REPOSITORY', 'Destination repository endpoint specifier'
261
+ def execute
262
+ bitferry {
263
+ Bitferry::Restic::Backup.new(
264
+ source, repository, obtain_password,
265
+ format: $format,
266
+ process: $process,
267
+ check: $check,
268
+ forget: $forget
269
+ )
270
+ }
271
+ end
272
+ end
273
+
274
+
275
+ subcommand ['restore', 'r'], 'Create restore task' do
276
+ banner %{
277
+ Create repository --> destination restore task.
278
+ This task employs the Restic worker.
279
+ }
280
+ option ['--process', '-X'], 'OPTIONS', 'Extra task processing profile/options' do |opts| $process = opts end
281
+ parameter 'REPOSITORY', 'Source repository endpoint specifier'
282
+ parameter 'DESTINATION', 'Destination endpoint specifier'
283
+ def execute
284
+ bitferry {
285
+ Bitferry::Restic::Restore.new(
286
+ destination, repository, obtain_password,
287
+ process: $process,
288
+ )
289
+ }
290
+ end
291
+ end
292
+
293
+
294
+ end
295
+
296
+
297
+ end
298
+
299
+
300
+ subcommand ['delete', 'd'], 'Delete entity' do
301
+
302
+
303
+ subcommand ['volume', 'v'], 'Delete volume' do
304
+ banner %{
305
+ Delete volumes matched by specified (partial) tags.
306
+ There may be multiple tags but each tag must match at most one volume.
307
+ This command deletes the volume storage file only with the rest of data left intact.
308
+ }
309
+ option '--wipe', :flag, 'Wipe target directory upon deletion'
310
+ parameter 'TAG ...', 'Volume tags', attribute_name: :tags
311
+ def execute
312
+ bitferry { Bitferry::Volume.delete(*tags, wipe: wipe?) }
313
+ end
314
+ end
315
+
316
+
317
+ subcommand ['task', 't'], 'Delete task' do
318
+ banner %{
319
+ Delete tasks matched by specified (partial) tags.
320
+ There may be multiple tags but each tag must match at most one task.
321
+ }
322
+ parameter 'TAG ...', 'Task tags', attribute_name: :tags
323
+ def execute
324
+ bitferry { Bitferry::Task.delete(*tags) }
325
+ end
326
+ end
327
+
328
+
329
+ end
330
+
331
+
332
+ subcommand ['process', 'x'], 'Process tasks' do
333
+ banner %{
334
+ Process tasks matched by specified (partial) tags.
335
+ If no tags are given, process all intact tasks.
336
+ }
337
+ parameter '[TAG] ...', 'Task tags', attribute_name: :tags
338
+ def execute
339
+ bitferry { Bitferry.process(*tags) }
340
+ end
341
+ end
342
+
343
+
344
+ end