qdumpfs 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ # Qdumpfs
2
+
3
+ qdumpfs is a modified version of pdumpfs.
4
+
5
+ ## Installation
6
+
7
+ gem install qdumpfs
8
+
9
+ ```ruby
10
+ gem 'qdumpfs'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ $ bundle
16
+
17
+ Or install it yourself as:
18
+
19
+ $ gem install qdumpfs
20
+
21
+ ## Usage
22
+
23
+ qdumpfs srcdir dstdir
24
+
25
+ ## License
26
+
27
+ qdumpfs is a free software with ABSOLUTELY NO WARRANTY under the terms of the GNU General Public License version 2.
28
+
29
+
30
+
31
+
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
@@ -0,0 +1,2 @@
1
+ ・引数なしで実行したときに例外が表示される
2
+ ・自動テストを追加する
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "qdumpfs"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,14 @@
1
+ @echo off
2
+
3
+ call uru 265p114
4
+
5
+ set bundle_dir=./vendor/bundle
6
+
7
+ IF EXIST "vendor/bundle/" (
8
+ echo update
9
+ rmdir /s /q vendor\bundle
10
+ bundle update
11
+ ) ELSE (
12
+ echo install
13
+ bundle install --path %bundle_dir%
14
+ )
@@ -0,0 +1,19 @@
1
+ #!/bin/sh
2
+ #bundle config build.libv8 --with-system-v8
3
+ #bundle config build.therubyracer --with-v8-dir
4
+ set -x
5
+ #export NOKOGIRI_USE_SYSTEM_LIBRARIES=1
6
+
7
+ bundle_dir=./vendor/bundle
8
+ if [ -d "$bundle_dir" ] ; then
9
+ /bin/rm -rf "$bundle_dir"
10
+ bundle update
11
+ else
12
+ /bin/rm -rf "$bundle_dir"
13
+ bundle install --path "$bundle_dir"
14
+ fi
15
+
16
+
17
+
18
+
19
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "qdumpfs"
4
+
5
+ Qdumpfs::Command.run(ARGV)
6
+
@@ -0,0 +1,540 @@
1
+ # coding: utf-8
2
+ require "qdumpfs/version"
3
+ require "qdumpfs/win32"
4
+ require "qdumpfs/util"
5
+ require "qdumpfs/option"
6
+ require 'find'
7
+ require 'optparse'
8
+ require 'date'
9
+ require 'fileutils'
10
+
11
+
12
+ module Qdumpfs
13
+
14
+ class Command
15
+ include QdumpfsUtils
16
+
17
+ def self.run(argv)
18
+ STDOUT.sync = true
19
+ opts = {}
20
+ opt = OptionParser.new(argv)
21
+ opt.version = VERSION
22
+ opt.banner = "Usage: #{opt.program_name} [-h|--help]"
23
+ opt.separator('')
24
+ opt.on_head('-h', '--help', 'show this message') do |v|
25
+ puts opt.help
26
+ exit
27
+ end
28
+ opt.on('-v', '--verbose', 'verbose message') {|v| opts[:v] = v}
29
+ opt.on('-r', '--report', 'report message') {|v| opts[:r] = v}
30
+ opt.on('-n', '--dry-run', "don't actually run any commands") {|v| opts[:n] = v}
31
+ opt.on('-e PATTERN', '--exclude=PATTERN', 'exclude files/directories matching PATTERN') {|v|
32
+ opts[:ep] = [] if opts[:ep].nil?
33
+ opts[:ep] << Regexp.new(v)
34
+ }
35
+ opt.on('-s SIZE', '--exclude-by-size=SIZE', 'exclude files larger than SIZE') {|v| opts[:es] = v }
36
+ opt.on('-w GLOB', '--exclude-by-glob=GLOB', 'exclude files matching GLOB') {|v| opts[:ep] = v }
37
+ commands = ['backup', 'sync', 'list', 'expire', 'verify', 'test']
38
+ opt.on('-c COMMAND', '--command=COMMAND', commands, commands.join('|')) {|v| opts[:c] = v}
39
+ opt.on('-l HOURS', '--limit=HOURS', 'limit hours') {|v| opts[:limit] = v}
40
+ opt.on('-k KEEPARG', '--keep=KEEPARG', 'ex: --keep 100Y12M12W30D (100years, 12months, 12weeks, 30days, default)') {|v| opts[:keep] = v}
41
+
42
+
43
+ opt.parse!(argv)
44
+ option = Option.new(opts, ARGV)
45
+
46
+ begin
47
+ command = Command.new(option)
48
+ command.run
49
+ rescue ArgumentError => e
50
+ puts e.message, e.backtrace
51
+ puts opt.help
52
+ exit
53
+ rescue => e
54
+ puts e.message, e.backtrace
55
+ end
56
+ end
57
+
58
+ def initialize(opt)
59
+ @opt = opt
60
+ end
61
+
62
+ def run
63
+ if @opt.cmd == 'backup'
64
+ backup
65
+ elsif @opt.cmd == 'sync'
66
+ sync
67
+ elsif @opt.cmd == 'list'
68
+ list
69
+ elsif @opt.cmd == 'expire'
70
+ expire
71
+ elsif @opt.cmd == 'verify'
72
+ verify
73
+ elsif @opt.cmd == 'test'
74
+ test
75
+ else
76
+ raise RuntimeError, "unknown command: #{cmd}"
77
+ end
78
+ end
79
+
80
+ private
81
+ def log_result(src, today, elapsed)
82
+ time = Time.now.strftime("%Y-%m-%dT%H:%M:%S")
83
+ bytes = convert_bytes(@written_bytes)
84
+ msg = sprintf("%s: %s -> %s (in %.2f sec, %s written)\n",
85
+ time, src, today, elapsed, bytes)
86
+ log(msg)
87
+ end
88
+
89
+ def log(msg, console = true)
90
+ @opt.log(msg, console)
91
+ end
92
+
93
+ def report(type, file_name)
94
+ @opt.report(type, file_name)
95
+ end
96
+
97
+ def update_file(src, latest, today)
98
+ type = detect_type(src, latest)
99
+ report(type, src)
100
+ return if @opt.dry_run
101
+ case type
102
+ when "directory"
103
+ FileUtils.mkpath(today)
104
+ when "unchanged"
105
+ File.force_link(latest, today)
106
+ when "updated"
107
+ copy(src, today)
108
+ when "new_file"
109
+ copy(src, today)
110
+ when "symlink"
111
+ File.force_symlink(File.readlink(src), today)
112
+ when "unsupported"
113
+ # just ignore it
114
+ else
115
+ raise "#{type}: shouldn't be reached here"
116
+ end
117
+ chown_if_root(type, src, today)
118
+ end
119
+
120
+ def filecount(dir)
121
+ pscmd = 'Get-ChildItem -Recurse -File | Measure-Object | %{$_.Count}'
122
+ cmd = "powershell -Command \"#{pscmd}\""
123
+ result = nil
124
+ Dir.chdir(dir) do
125
+ result = `#{cmd}`
126
+ result.chomp!
127
+ end
128
+ result.to_i
129
+ end
130
+
131
+ def do_verify(src, dst)
132
+ src_count = filecount(src)
133
+ dst_count= filecount(dst)
134
+ return src_count, dst_count
135
+ end
136
+
137
+ def get_snapshots(target_dir)
138
+ # 指定したディレクトリに含まれるバックアップフォルダ(日付つき)を全て取得
139
+ dd = "[0-9][0-9]"
140
+ dddd = dd + dd
141
+ # FIXME: Y10K problem.
142
+ dirs = []
143
+ glob_path = File.join(target_dir, dddd, dd, dd)
144
+ Dir.glob(glob_path).sort.find {|dir|
145
+ day, month, year = File.split_all(dir).reverse.map {|x| x.to_i }
146
+ path = dir
147
+ if File.directory?(path) and Date.valid_date?(year, month, day) and
148
+ dirs << path
149
+ end
150
+ }
151
+ dirs
152
+ end
153
+
154
+ def get_snapshot_date(snapshot)
155
+ # バックアップディレクトリのパス(日付つき)から日付を取得して返す
156
+ day, month, year = File.split_all(snapshot).reverse.map {|x| x.to_i }
157
+ Time.new(year, month, day)
158
+ end
159
+
160
+ def update_snapshot(src, latest, today)
161
+ # バックアップの差分コピーを実行
162
+ # src: コピー元ディレクトリ ex) i:/from/home
163
+ # latest: 最新のバックアップディレクトリ ex)j:/to/backup1/2019/05/09/home
164
+ # today: 差分バックアップ先ディレクトリ ex)j:/to/backup1/2019/05/10/home
165
+ dirs = {};
166
+ QdumpfsFind.find(@opt.logger, src) do |s| # path of the source file
167
+ if @opt.matcher.exclude?(s)
168
+ if File.lstat(s).directory? then Find.prune() else next end
169
+ end
170
+ # バックアップ元ファイルのパスからディレクトリ部分を削除
171
+ r = make_relative_path(s, src)
172
+ # 既存バックアップファイルのパス
173
+ l = File.join(latest, r) # path of the latest snapshot
174
+ # 新規バックアップファイルのパス
175
+ t = File.join(today, r) # path of the today's snapshot
176
+ begin
177
+ # ファイルのアップデート
178
+ update_file(s, l, t)
179
+ dirs[t] = File.stat(s) if File.ftype(s) == "directory"
180
+ rescue Errno::ENOENT, Errno::EACCES => e
181
+ wprintf("%s: %s", src, e.message)
182
+ next
183
+ end
184
+ end
185
+ return if @opt.dry_run
186
+ restore_dir_attributes(dirs)
187
+ end
188
+
189
+ def recursive_copy(src, dst)
190
+ dirs = {}
191
+ QdumpfsFind.find(@opt.logger, src) do |s|
192
+ if @opt.matcher.exclude?(s)
193
+ if File.lstat(s).directory? then Find.prune() else next end
194
+ end
195
+ r = make_relative_path(s, src)
196
+ t = File.join(dst, r)
197
+ begin
198
+ type = detect_type(s)
199
+ report(type, s)
200
+ next if @opt.dry_run
201
+ case type
202
+ when "directory"
203
+ FileUtils.mkpath(t)
204
+ when "new_file"
205
+ copy(s, t)
206
+ when "symlink"
207
+ File.force_symlink(File.readlink(s), t)
208
+ when "unsupported"
209
+ # just ignore it
210
+ else
211
+ raise "#{type}: shouldn't be reached here"
212
+ end
213
+ chown_if_root(type, s, t)
214
+ dirs[t] = File.stat(s) if File.ftype(s) == "directory"
215
+ rescue Errno::ENOENT, Errno::EACCES => e
216
+ wprintf("%s: %s", s, e.message)
217
+ next
218
+ end
219
+ end
220
+ restore_dir_attributes(dirs) unless @opt.dry_run
221
+ end
222
+
223
+ def sync_latest(src, dst, base = nil)
224
+ # pdumpfsのバックアップフォルダを同期する
225
+
226
+ #コピー元のスナップショット
227
+ src_snapshots = BackupDir.scan_backup_dirs(src)
228
+ @opt.detect_keep_dirs(src_snapshots)
229
+
230
+ # コピー先の最新スナップショット
231
+ dst_snapshots = BackupDir.scan_backup_dirs(dst)
232
+ dst_snapshot = dst_snapshots[-1]
233
+
234
+ # コピー元フォルダの決定
235
+ src_snapshot = nil
236
+ src_snapshots.each do |snapshot|
237
+ next if dst_snapshot && snapshot.date <= dst_snapshot.date
238
+ if snapshot.keep
239
+ src_snapshot = snapshot
240
+ break
241
+ end
242
+ end
243
+
244
+ if src_snapshot.nil?
245
+ return false, nil, nil
246
+ end
247
+
248
+ # 今回コピーするフォルダの名前
249
+ src = src_snapshot.path
250
+ today = File.join(dst, datedir(src_snapshot.date))
251
+ latest = dst_snapshot ? File.join(dst_snapshot.path) : nil
252
+
253
+ # src: j: /to/backup1/2019/05/10/home/"
254
+ # latest:
255
+ # today: j:/sync/backup1/2019/05/10/home"
256
+ log("sync_latest src=#{src} latest=#{latest} today=#{today}")
257
+
258
+ if latest
259
+ log("update_snapshot #{src} #{latest} #{today}")
260
+ # バックアップがすでに存在する場合差分コピー
261
+ update_snapshot(src, latest, today)
262
+ else
263
+ log("recursive_copy #{src}=>#{today}")
264
+ # 初回は単純に再帰コピー
265
+ recursive_copy(src, today)
266
+ end
267
+
268
+ return true, src, today
269
+ end
270
+
271
+ def latest_snapshot(start_time, src, dst, base)
272
+ # バックアップ先の日付ディレクトリを取得
273
+ # 現在の日付より過去のもののなかで最新を取得する(なければnil。現在の日付しかなくてもnil)
274
+ dd = "[0-9][0-9]"
275
+ dddd = dd + dd
276
+ # FIXME: Y10K problem.
277
+ glob_path = File.join(dst, dddd, dd, dd)
278
+ Dir.glob(glob_path).sort {|a, b| b <=> a }.find {|dir|
279
+ day, month, year = File.split_all(dir).reverse.map {|x| x.to_i }
280
+ path = File.join(dir, base)
281
+ if File.directory?(path) and Date.valid_date?(year, month, day) and
282
+ past_date?(year, month, day, start_time)
283
+ return path
284
+ end
285
+ }
286
+ return nil
287
+ end
288
+
289
+ def backup
290
+ ##### オリジナルのバックアップルーチン
291
+ @opt.validate_directories(2)
292
+
293
+ log("##### backup start #####")
294
+
295
+ @written_bytes = 0
296
+ start_time = Time.now
297
+ src = @opt.src
298
+ dst = @opt.dst
299
+
300
+ # Windowsの場合
301
+ if windows?
302
+ src = expand_special_folders(src)
303
+ dst = expand_special_folders(dst)
304
+ end
305
+
306
+ # 指定されたディレクトリの整合性チェック
307
+ if same_directory?(src, dst) or sub_directory?(src, dst)
308
+ raise "cannot copy a directory, `#{src}', into itself, `#{dst}'"
309
+ end
310
+
311
+ # Ruby 1.6.xではbasename(src) == ''となるため最後の'/'を除去
312
+ src = src.sub(%r!/+$!, "") unless src == '/' #'
313
+ base = File.basename(src)
314
+ dirname = File.dirname(src)
315
+ raise RuntimeError unless FileTest.exist?(dirname + '/' + base)
316
+
317
+ # 存在するバックアップの最新を取得
318
+ latest = latest_snapshot(start_time, src, dst, base)
319
+ # 現在の日付フォルダを取得j:/to/backup1/2019/05/10/home
320
+ today = File.join(dst, datedir(start_time), base)
321
+ File.umask(0077)
322
+ FileUtils.mkpath(today) unless @opt.dry_run
323
+ if windows?
324
+ src = src.sub( /^[A-Za-z]:$/, src + "/" )
325
+ end
326
+ if latest
327
+ # バックアップがすでに存在する場合差分コピー
328
+ log("## update_snapshot #{src} #{latest}=>#{today} ##")
329
+ update_snapshot(src, latest, today)
330
+ else
331
+ # 初回は単純に再帰コピー
332
+ log("## recursive_copy #{src}=>#{today} ##")
333
+ recursive_copy(src, today)
334
+ end
335
+ unless @opt.dry_run
336
+ create_latest_symlink(dst, today)
337
+ elapsed = Time.now - start_time
338
+ log_result(src, today, elapsed)
339
+ end
340
+ log("##### backup end #####")
341
+ end
342
+
343
+ def sync
344
+ ##### バックアップフォルダの同期ルーチン(バックアップディスクを他のディスクと同じ状態にする)
345
+ @opt.validate_directories(2)
346
+
347
+ start_time = Time.now
348
+ @written_bytes = 0
349
+ src = @opt.src
350
+ dst = @opt.dst
351
+
352
+ # 制限時間まで繰り返す(指定がない場合1回で終了)
353
+ limit_time = start_time + (@opt.limit_sec)
354
+ log("##### sync start #{fmt(start_time)} => limit_time=#{fmt(limit_time)} #####")
355
+ count = 0
356
+ last_sync_complete = false
357
+ while true
358
+ count += 1
359
+ log("## sync_latest count=#{count} ##")
360
+ latest_start = Time.now
361
+ sync_result, from, to = sync_latest(src, dst)
362
+ latest_end = Time.now
363
+
364
+ log("## sync_latest result=#{sync_result} from=#{from} to=#{to} ##")
365
+ unless sync_result
366
+ # 同期結果がtrueでない場合ここで終了。ただしsync_result=falseになるのはコピー元フォルダが存在しない場合なので、
367
+ # 中途半端な結果にはならない
368
+ last_sync_complete = true
369
+ break
370
+ end
371
+
372
+ from_count, to_count = do_verify(from, to)
373
+ log("## from_count=#{from_count} to_count=#{to_count} equals=#{from_count == to_count} ##")
374
+ unless from_count == to_count
375
+ # ファイル数が同じでない場合ここで終了
376
+ last_sync_complete = false
377
+ break
378
+ end
379
+
380
+ # 次回同期にかかる時間を最終同期時間の半分と予想
381
+ next_sync = (latest_end - latest_start) / 2
382
+
383
+ cur_time = Time.now
384
+ in_limit = (cur_time + next_sync) < limit_time
385
+ log("## cur_time=#{fmt(cur_time)} + next_sync=#{next_sync} < limit_time=#{fmt(limit_time)} in_limit=#{in_limit} ## ")
386
+ unless in_limit
387
+ # 指定時間内ではない場合ここで終了(ただし最終同期は成功)
388
+ last_sync_complete = true
389
+ break
390
+ end
391
+ end
392
+
393
+ end_time = Time.now
394
+ diff = time_diff(start_time, end_time)
395
+ log("##### sync end #{fmt(end_time)} diff=#{diff} last_sync_complete=#{last_sync_complete} #####")
396
+ end
397
+
398
+ def open_verify_file
399
+ filename = File.join(@log_dir, 'verify.txt')
400
+ if FileTest.file?(filename)
401
+ File.unlink(filename)
402
+ end
403
+ File.open(filename, 'a')
404
+ end
405
+
406
+ def verify
407
+ file = @opt.open_verifyfile
408
+
409
+ start_time = Time.now
410
+ add_log("##### verify start #{fmt(start_time)} #####")
411
+
412
+ src_count, dst_count = do_verify(src, dst)
413
+
414
+ fputs(file, "#{src}: #{src_count}")
415
+ fputs(file, "#{dst}: #{dst_count}")
416
+ result = src_count == dst_count
417
+ fputs(file, "result=#{result}")
418
+
419
+ end_time = Time.now
420
+ diff = time_diff(start_time, end_time)
421
+ add_log("##### list end #{fmt(end_time)} diff=#{diff} #####")
422
+
423
+ file.close
424
+ end
425
+
426
+ def list
427
+ file = @opt.open_listfile
428
+
429
+ start_time = Time.now
430
+ log("##### list start #{fmt(start_time)} #####")
431
+
432
+ src = @opt.src
433
+ QdumpfsFind.find(@opt.logger, src) do |path|
434
+ short_path = path.sub(/^#{src}/, '.')
435
+ log("#{File.ftype(path)} #{path}")
436
+ if FileTest.file?(path)
437
+ file.puts short_path
438
+ end
439
+ end
440
+
441
+ end_time = Time.now
442
+ diff = time_diff(start_time, end_time)
443
+ log("##### list end #{fmt(end_time)} diff=#{diff} #####")
444
+
445
+ file.close
446
+ end
447
+
448
+ def expire
449
+ @opt.validate_directories(1)
450
+
451
+ start_time = Time.now
452
+ limit_time = start_time + (@opt.limit_sec)
453
+ log("##### expire start #{fmt(start_time)} => limit_time=#{fmt(limit_time)} #####")
454
+
455
+ @opt.dirs.each do |target_dir|
456
+
457
+ target_start = Time.now
458
+ expire_target_dir(target_dir)
459
+ target_end = Time.now
460
+
461
+ # 次回expireにかかる時間を最終expire時間の半分と予想
462
+ next_expire = (target_end - target_start) / 2
463
+
464
+ cur_time = Time.now
465
+ in_imit = (cur_time + next_expire) < limit_time
466
+
467
+ log("## cur_time=#{fmt(cur_time)} + next_expire=#{next_expire} < limit_time=#{fmt(limit_time)} in_limit=#{in_limit} ## ")
468
+ unless in_limit
469
+ break
470
+ end
471
+ end
472
+
473
+ log("##### expire end #####")
474
+ end
475
+
476
+ def expire_target_dir(target_dir)
477
+ target_dir = to_unix_path(target_dir)
478
+ puts "<<<<< Target dir: #{target_dir} >>>>>"
479
+
480
+ snapshots = BackupDir.scan_backup_dirs(target_dir)
481
+ @opt.detect_keep_dirs(snapshots)
482
+
483
+ # p @opt.keep_year
484
+ # p @opt.keep_month
485
+ # p @opt.keep_day
486
+
487
+ snapshots.each do |snapshot|
488
+ next if snapshot.keep
489
+ t_start = Time.now
490
+ print "Deleting #{snapshot.path} ..."
491
+
492
+ unless @opt.dry_run
493
+ #http://superuser.com/questions/19762/mass-deleting-files-in-windows/289399#289399
494
+ ##### here
495
+ #bundle exec ruby exe/qdumpfs --dry-run --keep=100Y36M30W30D --command expire f:/pc1/pdumpfs/users f:/pc1/pdumpfs/opt f:/pc1/pdumpfs/d
496
+
497
+ if windows?
498
+ # Windowsの場合
499
+ win_backup_path = to_win_path(snapshot.path)
500
+
501
+ # byenow = "byenow"
502
+ # if which(byenow)
503
+ # print " bynow"
504
+ # system("byenow -y --delete-ntapi --one-liner #{snapshot.path}")
505
+ # else
506
+ # print " pass1"
507
+ # system("del /F /S /Q #{win_backup_path} > nul")
508
+ # print " pass2"
509
+ # system("rmdir /S /Q #{win_backup_path}")
510
+ # end
511
+ system("rmdir /S /Q #{win_backup_path}")
512
+ else
513
+ # Linux/macOSの場合
514
+ system("rm -rf #{snapshot.path}")
515
+ end
516
+ end
517
+
518
+ t_end = Time.now
519
+ diff = (t_end - t_start).to_i
520
+ diff_hours = diff / 3600
521
+ puts " done[#{diff} seconds = #{diff_hours} hours]."
522
+ end
523
+
524
+ Dir.glob("#{target_dir}/[0-9][0-9][0-9][0-9]/[0-1][0-9] #{target_dir}/[0-9][0-9][0-9][0-9]").each do |dir|
525
+ if File.directory?(dir) && Dir.entries(dir).size <= 2
526
+ win_dir = to_win_path(dir)
527
+ print "Deleting #{win_dir} ..."
528
+ Dir.rmdir(win_dir) unless @opt.dry_run
529
+ puts " done."
530
+ end
531
+ end
532
+
533
+ puts "Keep dirs:"
534
+ snapshots.each do |snapshot|
535
+ puts snapshot.path if snapshot.keep
536
+ end
537
+
538
+ end
539
+ end
540
+ end