qdumpfs 0.4.0

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