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.
@@ -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
+