qdumpfs 0.4.0
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 +7 -0
- data/.gitignore +14 -0
- data/.travis.yml +7 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +23 -0
- data/LICENSE.txt +339 -0
- data/README.md +31 -0
- data/Rakefile +10 -0
- data/TODO.txt +2 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/build.cmd +14 -0
- data/build.sh +19 -0
- data/exe/qdumpfs +6 -0
- data/lib/qdumpfs.rb +540 -0
- data/lib/qdumpfs/option.rb +302 -0
- data/lib/qdumpfs/util.rb +283 -0
- data/lib/qdumpfs/version.rb +3 -0
- data/lib/qdumpfs/win32.rb +146 -0
- data/qdumpfs.gemspec +43 -0
- data/run2_qdumpfs.cmd +6 -0
- data/run_list.cmd +7 -0
- data/run_qdumpfs.cmd +5 -0
- data/run_qdumpfs.sh +4 -0
- data/run_sync.cmd +6 -0
- data/test_pdumpfs/data/bar +1 -0
- data/test_pdumpfs/data/baz/quux +1 -0
- data/test_pdumpfs/data/file +1 -0
- data/test_pdumpfs/data/foo +1 -0
- data/test_pdumpfs/data/secret/secret +1 -0
- data/test_pdumpfs/pdumpfs-test +86 -0
- metadata +120 -0
data/README.md
ADDED
@@ -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
|
+
|
data/Rakefile
ADDED
data/TODO.txt
ADDED
data/bin/console
ADDED
@@ -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__)
|
data/bin/setup
ADDED
data/build.cmd
ADDED
data/build.sh
ADDED
@@ -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
|
+
|
data/exe/qdumpfs
ADDED
data/lib/qdumpfs.rb
ADDED
@@ -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
|