qdumpfs 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
@@ -0,0 +1,302 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
module Qdumpfs
|
3
|
+
# 日毎のバックアップフォルダに対応
|
4
|
+
class BackupDir
|
5
|
+
def self.scan_backup_dirs(target_dir)
|
6
|
+
backup_dirs = []
|
7
|
+
Dir.glob("#{target_dir}/[0-9][0-9][0-9][0-9]/[0-1][0-9]/[0-3][0-9]").sort.each do |path|
|
8
|
+
if File.directory?(path) && path =~ /(\d\d\d\d)\/(\d\d)\/(\d\d)/
|
9
|
+
# puts "Backup dir: #{path}"
|
10
|
+
backup_dir = BackupDir.new
|
11
|
+
backup_dir.path = path
|
12
|
+
backup_dir.date = Date.new($1.to_i, $2.to_i, $3.to_i)
|
13
|
+
backup_dirs << backup_dir
|
14
|
+
end
|
15
|
+
end
|
16
|
+
backup_dirs
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.find(backup_dirs, from_date, to_date)
|
20
|
+
backup_dirs.select{|backup_dir| backup_dir.date >= from_date && backup_dir.date <= to_date}
|
21
|
+
end
|
22
|
+
|
23
|
+
def initialize
|
24
|
+
@keep = false
|
25
|
+
end
|
26
|
+
attr_accessor :path, :date, :keep
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
class NullLogger
|
31
|
+
def close
|
32
|
+
end
|
33
|
+
def print(msg)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
class SimpleLogger
|
39
|
+
def initialize(filename)
|
40
|
+
@file = File.open(filename, "a")
|
41
|
+
end
|
42
|
+
|
43
|
+
def close
|
44
|
+
@file.close
|
45
|
+
end
|
46
|
+
|
47
|
+
def print(msg)
|
48
|
+
@file.puts msg
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
class Error < StandardError
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
class NullMatcher
|
58
|
+
def initialize(options = {})
|
59
|
+
end
|
60
|
+
def exclude?(path)
|
61
|
+
false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
|
66
|
+
class FileMatcher
|
67
|
+
def initialize(options = {})
|
68
|
+
@patterns = options[:patterns] || []
|
69
|
+
@globs = options[:globs] || []
|
70
|
+
@size = calc_size(options[:size])
|
71
|
+
end
|
72
|
+
|
73
|
+
def calc_size(size)
|
74
|
+
table = { "K" => 1, "M" => 2, "G" => 3, "T" => 4, "P" => 5 }
|
75
|
+
pattern = table.keys.join('')
|
76
|
+
case size
|
77
|
+
when nil
|
78
|
+
-1
|
79
|
+
when /^(\d+)([#{pattern}]?)$/i
|
80
|
+
num = Regexp.last_match[1].to_i
|
81
|
+
unit = Regexp.last_match[2]
|
82
|
+
num * 1024 ** (table[unit] or 0)
|
83
|
+
else
|
84
|
+
raise "Invalid size: #{size}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def exclude?(path)
|
89
|
+
stat = File.lstat(path)
|
90
|
+
if @size >= 0 and stat.file? and stat.size >= @size
|
91
|
+
return true
|
92
|
+
elsif @patterns.find {|pattern| pattern.match(path) }
|
93
|
+
return true
|
94
|
+
elsif stat.file? and
|
95
|
+
@globs.find {|glob| File.fnmatch(glob, File.basename(path)) }
|
96
|
+
return true
|
97
|
+
end
|
98
|
+
return false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
|
103
|
+
class Option
|
104
|
+
def initialize(opts, dirs)
|
105
|
+
@opts = opts
|
106
|
+
@dirs = dirs
|
107
|
+
@src = dirs[0] if dirs.size > 0
|
108
|
+
@dst = dirs[1] if dirs.size > 1
|
109
|
+
@cmd = @opts[:c] || 'backup'
|
110
|
+
|
111
|
+
# @logger = NullLogger.new
|
112
|
+
logfile = @opts[:logfile] || 'log.txt'
|
113
|
+
#ログディレクトリの作成
|
114
|
+
@logdir = File.expand_path('.')
|
115
|
+
Dir.mkdir(@logdir) unless FileTest.directory?(@logdir)
|
116
|
+
@logpath = File.join(@logdir, logfile)
|
117
|
+
@logger = SimpleLogger.new(@logpath)
|
118
|
+
|
119
|
+
verifyfile = 'verify.txt'
|
120
|
+
@verifypath = File.join(@logdir, verifyfile)
|
121
|
+
|
122
|
+
@matcher = NullMatcher.new
|
123
|
+
|
124
|
+
size = @opts[:es]
|
125
|
+
globs = @opts[:eg]
|
126
|
+
patterns = @opts[:ep]
|
127
|
+
if size || globs || patterns
|
128
|
+
@matcher = FileMatcher.new(:size => size, :globs => globs, :patterns => patterns)
|
129
|
+
end
|
130
|
+
|
131
|
+
# 同期用のオプションは日常使いのpdumpfs-cleanのオプションより期間短めに設定
|
132
|
+
@limit = @opts[:limit]
|
133
|
+
@keep_year = 100
|
134
|
+
@keep_month = 12
|
135
|
+
@keep_week = 12
|
136
|
+
@keep_day = 30
|
137
|
+
keep = @opts[:keep]
|
138
|
+
@keep_year = $1.to_i if keep =~ /(\d+)Y/
|
139
|
+
@keep_month = $1.to_i if keep =~ /(\d+)M/
|
140
|
+
@keep_week = $1.to_i if keep =~ /(\d+)W/
|
141
|
+
@keep_day = $1.to_i if keep =~ /(\d+)D/
|
142
|
+
@today = Date.today
|
143
|
+
end
|
144
|
+
attr_reader :dirs, :src, :dst, :cmd
|
145
|
+
attr_reader :keep_year, :keep_month, :keep_week, :keep_day
|
146
|
+
attr_reader :logdir, :logfile, :verifyfile
|
147
|
+
attr_reader :logger, :matcher, :reporter, :interval_proc
|
148
|
+
|
149
|
+
def report(type, filename)
|
150
|
+
if @opts[:v]
|
151
|
+
stat = File.stat(filename)
|
152
|
+
size = stat.size
|
153
|
+
format_size = convert_bytes(size)
|
154
|
+
msg = format_report_with_size(type, filename, size, format_size)
|
155
|
+
elsif @opts[:r]
|
156
|
+
if type =~ /^new_file/
|
157
|
+
stat = File.stat(filename)
|
158
|
+
size = stat.size
|
159
|
+
format_size = convert_bytes(size)
|
160
|
+
msg = format_report_with_size(type, filename, size, format_size)
|
161
|
+
end
|
162
|
+
else
|
163
|
+
# 何も指定されていない場合
|
164
|
+
if type == 'new_file'
|
165
|
+
msg = format_report(type, filename)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
log(msg)
|
169
|
+
end
|
170
|
+
|
171
|
+
def log(msg, console = true)
|
172
|
+
return if (msg.nil? || msg == '')
|
173
|
+
puts msg if console
|
174
|
+
@logger.print(msg)
|
175
|
+
end
|
176
|
+
|
177
|
+
def dry_run
|
178
|
+
@opts[:n]
|
179
|
+
end
|
180
|
+
|
181
|
+
def limit_sec
|
182
|
+
@limit.to_i * 3600
|
183
|
+
end
|
184
|
+
|
185
|
+
def validate_directory(dir)
|
186
|
+
if dir.nil? || !File.directory?(dir)
|
187
|
+
raise ArgumentError, "No such directory: #{dir}"
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def validate_directories(min_count)
|
192
|
+
@dirs.each do |dir|
|
193
|
+
validate_directory(dir)
|
194
|
+
end
|
195
|
+
if @dirs.size == 2 && windows?
|
196
|
+
# ディレクトリが2つだけ指定されている場合、コピー先はntfsである必要がある
|
197
|
+
unless ntfs?(dst)
|
198
|
+
fstype = get_filesystem_type(dst)
|
199
|
+
raise sprintf("only NTFS is supported but %s is %s.", dst, fstype)
|
200
|
+
end
|
201
|
+
end
|
202
|
+
if @dirs.size < min_count
|
203
|
+
raise "#{min_count} directories required."
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
def detect_keep_dirs(backup_dirs)
|
208
|
+
detect_year_keep_dirs(backup_dirs)
|
209
|
+
detect_month_keep_dirs(backup_dirs)
|
210
|
+
detect_week_keep_dirs(backup_dirs)
|
211
|
+
detect_day_keep_dirs(backup_dirs)
|
212
|
+
end
|
213
|
+
|
214
|
+
def open_verifyfile
|
215
|
+
if FileTest.file?(@verifypath)
|
216
|
+
File.unlink(@verifypath)
|
217
|
+
end
|
218
|
+
File.open(@verifypath, 'a')
|
219
|
+
end
|
220
|
+
|
221
|
+
def open_listfile
|
222
|
+
filename = File.join(@logdir, "list_" + @src.gsub(/[:\/]/, '_') + '.txt')
|
223
|
+
if FileTest.file?(filename)
|
224
|
+
File.unlink(filename)
|
225
|
+
end
|
226
|
+
File.open(filename, 'a')
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
def format_report(type, filename)
|
231
|
+
sprintf("%-12s %s\n", type, filename)
|
232
|
+
end
|
233
|
+
|
234
|
+
def format_report_with_size(type, filename, size, format_size)
|
235
|
+
sprintf("%s\t%s\t%d\t%s\n", type, filename, size, format_size)
|
236
|
+
end
|
237
|
+
|
238
|
+
def format_report_with_size_as_csv(type, filename, size, format_size)
|
239
|
+
sprintf("%s,%s,%d,%s\n", type, filename.encode('cp932', undef: :replace), size, format_size)
|
240
|
+
end
|
241
|
+
|
242
|
+
def format_report_as_csv(type, filename)
|
243
|
+
sprintf("%s,%s\n", type, filename.encode('cp932', undef: :replace))
|
244
|
+
end
|
245
|
+
|
246
|
+
def convert_bytes(bytes)
|
247
|
+
if bytes < 1024
|
248
|
+
sprintf("%dB", bytes)
|
249
|
+
elsif bytes < 1024 * 1000 # 1000kb
|
250
|
+
sprintf("%.1fKB", bytes.to_f / 1024)
|
251
|
+
elsif bytes < 1024 * 1024 * 1000 # 1000mb
|
252
|
+
sprintf("%.1fMB", bytes.to_f / 1024 / 1024)
|
253
|
+
else
|
254
|
+
sprintf("%.1fGB", bytes.to_f / 1024 / 1024 / 1024)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
def keep_dirs(backup_dirs, num)
|
259
|
+
num.downto(0) do |i|
|
260
|
+
from_date, to_date = yield(i)
|
261
|
+
dirs = BackupDir.find(backup_dirs, from_date, to_date)
|
262
|
+
dirs[0].keep = true if dirs.size > 0
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
def detect_year_keep_dirs(backup_dirs)
|
267
|
+
keep_dirs(backup_dirs, @keep_year) do |i|
|
268
|
+
from_date = Date.new(@today.year - i, 1, 1)
|
269
|
+
to_date = Date.new(@today.year - i, 12, 31)
|
270
|
+
[from_date, to_date]
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def detect_month_keep_dirs(backup_dirs)
|
275
|
+
keep_dirs(backup_dirs, @keep_month) do |i|
|
276
|
+
base_date = @today << i
|
277
|
+
from_date = Date.new(base_date.year, base_date.month, 1)
|
278
|
+
to_date = from_date >> 1
|
279
|
+
[from_date, to_date]
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def detect_week_keep_dirs(backup_dirs)
|
284
|
+
keep_dirs(backup_dirs, @keep_week) do |i|
|
285
|
+
base_date = @today - 7 * i
|
286
|
+
from_date = base_date - base_date.cwday # 1
|
287
|
+
to_date = from_date + 6
|
288
|
+
[from_date, to_date]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def detect_day_keep_dirs(backup_dirs)
|
293
|
+
keep_dirs(backup_dirs, @keep_day) do |i|
|
294
|
+
base_date = @today - i
|
295
|
+
from_date = base_date
|
296
|
+
to_date = base_date
|
297
|
+
[from_date, to_date]
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
end
|
data/lib/qdumpfs/util.rb
ADDED
@@ -0,0 +1,283 @@
|
|
1
|
+
def wprintf(format, *args)
|
2
|
+
STDERR.printf("pdumpfs: " + format + "\n", *args)
|
3
|
+
end
|
4
|
+
|
5
|
+
#https://stackoverflow.com/questions/2108727/which-in-ruby-checking-if-program-exists-in-path-from-ruby
|
6
|
+
# Cross-platform way of finding an executable in the $PATH.
|
7
|
+
#
|
8
|
+
# which('ruby') #=> /usr/bin/ruby
|
9
|
+
def which(cmd)
|
10
|
+
exts = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
|
11
|
+
ENV['PATH'].split(File::PATH_SEPARATOR).each do |path|
|
12
|
+
exts.each do |ext|
|
13
|
+
exe = File.join(path, "#{cmd}#{ext}")
|
14
|
+
return exe if File.executable?(exe) && !File.directory?(exe)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
class File
|
21
|
+
def self.real_file?(path)
|
22
|
+
FileTest.file?(path) and not FileTest.symlink?(path)
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.anything_exist?(path)
|
26
|
+
FileTest.exist?(path) or FileTest.symlink?(path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.real_directory?(path)
|
30
|
+
FileTest.directory?(path) and not FileTest.symlink?(path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.force_symlink(src, dest)
|
34
|
+
begin
|
35
|
+
File.unlink(dest) if File.anything_exist?(dest)
|
36
|
+
File.symlink(src, dest)
|
37
|
+
rescue
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.force_link(src, dest)
|
42
|
+
File.unlink(dest) if File.anything_exist?(dest)
|
43
|
+
File.link(src, dest)
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.readable_file?(path)
|
47
|
+
FileTest.file?(path) and FileTest.readable?(path)
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.split_all(path)
|
51
|
+
parts = []
|
52
|
+
while true
|
53
|
+
dirname, basename = File.split(path)
|
54
|
+
break if path == dirname
|
55
|
+
parts.unshift(basename) unless basename == "."
|
56
|
+
path = dirname
|
57
|
+
end
|
58
|
+
return parts
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
module QdumpfsFind
|
64
|
+
def find(logger, *paths)
|
65
|
+
block_given? or return enum_for(__method__, *paths)
|
66
|
+
paths.collect!{|d|
|
67
|
+
raise Errno::ENOENT unless File.exist?(d);
|
68
|
+
d.dup
|
69
|
+
}
|
70
|
+
while file = paths.shift
|
71
|
+
catch(:prune) do
|
72
|
+
yield file.dup.taint
|
73
|
+
begin
|
74
|
+
s = File.lstat(file)
|
75
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG => e
|
76
|
+
logger.print("File.lstat path=#{file} error=#{e.message}")
|
77
|
+
next
|
78
|
+
end
|
79
|
+
if s.directory? then
|
80
|
+
begin
|
81
|
+
fs = Dir.entries(file, :encoding=>'UTF-8')
|
82
|
+
rescue Errno::ENOENT, Errno::EACCES, Errno::ENOTDIR, Errno::ELOOP, Errno::ENAMETOOLONG => e
|
83
|
+
logger.print("Dir.entries path=#{file} error=#{e.message}")
|
84
|
+
next
|
85
|
+
end
|
86
|
+
fs.sort!
|
87
|
+
fs.reverse_each {|f|
|
88
|
+
next if f == "." or f == ".."
|
89
|
+
f = File.join(file, f)
|
90
|
+
paths.unshift f.untaint
|
91
|
+
}
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def prune
|
98
|
+
throw :prune
|
99
|
+
end
|
100
|
+
|
101
|
+
module_function :find, :prune
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
module QdumpfsUtils
|
106
|
+
|
107
|
+
# We don't use File.copy for calling @interval_proc.
|
108
|
+
def copy_file(src, dest)
|
109
|
+
File.open(src, 'rb') {|r|
|
110
|
+
File.open(dest, 'wb') {|w|
|
111
|
+
block_size = (r.stat.blksize or 8192)
|
112
|
+
begin
|
113
|
+
i = 0
|
114
|
+
while true
|
115
|
+
block = r.sysread(block_size)
|
116
|
+
w.syswrite(block)
|
117
|
+
i += 1
|
118
|
+
@written_bytes += block.size
|
119
|
+
# @interval_proc.call if i % 10 == 0
|
120
|
+
end
|
121
|
+
rescue EOFError => e
|
122
|
+
# puts e.message, e.backtrace
|
123
|
+
end
|
124
|
+
}
|
125
|
+
}
|
126
|
+
unless FileTest.file?(dest)
|
127
|
+
raise "copy_file fails #{dest}"
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# incomplete substitute for cp -p
|
132
|
+
def copy(src, dest)
|
133
|
+
stat = File.stat(src)
|
134
|
+
copy_file(src, dest)
|
135
|
+
File.chmod(0200, dest) if windows?
|
136
|
+
File.utime(stat.atime, stat.mtime, dest)
|
137
|
+
File.chmod(stat.mode, dest) # not necessary. just to make sure
|
138
|
+
end
|
139
|
+
|
140
|
+
def convert_bytes(bytes)
|
141
|
+
if bytes < 1024
|
142
|
+
sprintf("%dB", bytes)
|
143
|
+
elsif bytes < 1024 * 1000 # 1000kb
|
144
|
+
sprintf("%.1fKB", bytes.to_f / 1024)
|
145
|
+
elsif bytes < 1024 * 1024 * 1000 # 1000mb
|
146
|
+
sprintf("%.1fMB", bytes.to_f / 1024 / 1024)
|
147
|
+
else
|
148
|
+
sprintf("%.1fGB", bytes.to_f / 1024 / 1024 / 1024)
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def same_file?(f1, f2)
|
153
|
+
# File.real_file?(f1) and File.real_file?(f2) and
|
154
|
+
# File.size(f1) == File.size(f2) and File.mtime(f1) == File.mtime(f2)
|
155
|
+
real_file = File.real_file?(f1) and File.real_file?(f2)
|
156
|
+
same_size = File.size(f1) == File.size(f2)
|
157
|
+
|
158
|
+
# mtime1 = File.mtime(f1).strftime('%F %T.%N')
|
159
|
+
# mtime2 = File.mtime(f2).strftime('%F %T.%N')
|
160
|
+
same_mtime = File.mtime(f1).to_i == File.mtime(f2).to_i
|
161
|
+
# p "#{real_file} #{same_size} #{same_mtime}(#{mtime1}<=>#{mtime2})"
|
162
|
+
real_file and same_size and same_mtime
|
163
|
+
end
|
164
|
+
|
165
|
+
def detect_type(src, latest = nil)
|
166
|
+
type = "unsupported"
|
167
|
+
if File.real_directory?(src)
|
168
|
+
type = "directory"
|
169
|
+
else
|
170
|
+
if latest and File.real_file?(latest)
|
171
|
+
case File.ftype(src)
|
172
|
+
when "file"
|
173
|
+
same_file = same_file?(src, latest)
|
174
|
+
# p "same_file? #{src} #{latest} result=#{same_file}"
|
175
|
+
if same_file
|
176
|
+
type = "unchanged"
|
177
|
+
else
|
178
|
+
type = "updated"
|
179
|
+
end
|
180
|
+
when "link"
|
181
|
+
# the latest backup file is a real file but the
|
182
|
+
# current source file is changed to symlink.
|
183
|
+
type = "symlink"
|
184
|
+
end
|
185
|
+
else
|
186
|
+
case File.ftype(src)
|
187
|
+
when "file"
|
188
|
+
type = "new_file"
|
189
|
+
when "link"
|
190
|
+
type = "symlink"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
return type
|
195
|
+
end
|
196
|
+
|
197
|
+
def fmt(time)
|
198
|
+
time.strftime('%Y/%m/%d %H:%M:%S')
|
199
|
+
end
|
200
|
+
|
201
|
+
def chown_if_root(type, src, today)
|
202
|
+
return unless Process.uid == 0 and type != "unsupported"
|
203
|
+
if type == "symlink"
|
204
|
+
if File.respond_to?(:lchown)
|
205
|
+
stat = File.lstat(src)
|
206
|
+
File.lchown(stat.uid, stat.gid, today)
|
207
|
+
end
|
208
|
+
else
|
209
|
+
stat = File.stat(src)
|
210
|
+
File.chown(stat.uid, stat.gid, today)
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def restore_dir_attributes(dirs)
|
215
|
+
dirs.each {|dir, stat|
|
216
|
+
File.utime(stat.atime, stat.mtime, dir)
|
217
|
+
File.chmod(stat.mode, dir)
|
218
|
+
}
|
219
|
+
end
|
220
|
+
|
221
|
+
def make_relative_path(path, base)
|
222
|
+
pattern = sprintf("^%s%s?", Regexp.quote(base), File::SEPARATOR)
|
223
|
+
path.sub(Regexp.new(pattern), "")
|
224
|
+
end
|
225
|
+
|
226
|
+
def fputs(file, msg)
|
227
|
+
puts msg
|
228
|
+
file.puts msg
|
229
|
+
end
|
230
|
+
|
231
|
+
def time_diff(start_time, end_time)
|
232
|
+
seconds_diff = (start_time - end_time).to_i.abs
|
233
|
+
|
234
|
+
hours = seconds_diff / 3600
|
235
|
+
seconds_diff -= hours * 3600
|
236
|
+
|
237
|
+
minutes = seconds_diff / 60
|
238
|
+
seconds_diff -= minutes * 60
|
239
|
+
|
240
|
+
seconds = seconds_diff
|
241
|
+
|
242
|
+
'%02d:%02d:%02d' % [hours, minutes, seconds]
|
243
|
+
end
|
244
|
+
|
245
|
+
def create_latest_symlink(dest, today)
|
246
|
+
# 最新のバックアップに"latest"というシンボリックリンクをはる(Windowsだと動かない)
|
247
|
+
latest_day = File.dirname(make_relative_path(today, dest))
|
248
|
+
latest_symlink = File.join(dest, "latest")
|
249
|
+
# puts "force_symlink #{latest_day} #{latest_symlink}"
|
250
|
+
File.force_symlink(latest_day, latest_symlink)
|
251
|
+
end
|
252
|
+
|
253
|
+
def same_directory?(src, dest)
|
254
|
+
src = File.expand_path(src)
|
255
|
+
dest = File.expand_path(dest)
|
256
|
+
return src == dest
|
257
|
+
end
|
258
|
+
|
259
|
+
def sub_directory?(src, dest)
|
260
|
+
src = File.expand_path(src)
|
261
|
+
dest = File.expand_path(dest)
|
262
|
+
src += File::SEPARATOR unless /#{File::SEPARATOR}$/.match(src)
|
263
|
+
return /^#{Regexp.quote(src)}/.match(dest)
|
264
|
+
end
|
265
|
+
|
266
|
+
def datedir(date)
|
267
|
+
s = File::SEPARATOR
|
268
|
+
sprintf "%d%s%02d%s%02d", date.year, s, date.month, s, date.day
|
269
|
+
end
|
270
|
+
|
271
|
+
def past_date?(year, month, day, t)
|
272
|
+
([year, month, day] <=> [t.year, t.month, t.day]) < 0
|
273
|
+
end
|
274
|
+
|
275
|
+
def to_win_path(path)
|
276
|
+
path.gsub(/\//, '\\')
|
277
|
+
end
|
278
|
+
|
279
|
+
def to_unix_path(path)
|
280
|
+
path.gsub(/\\/, '/')
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|