bitferry 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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