mast 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,158 @@
1
+ # Metaclass extensions for core File class.
2
+ #
3
+ class File #:nodoc:
4
+
5
+ # Is a file a gzip file?
6
+ #
7
+ def self.gzip?(file)
8
+ open(file,'rb') { |f|
9
+ return false unless f.getc == 0x1f
10
+ return false unless f.getc == 0x8b
11
+ }
12
+ true
13
+ end
14
+
15
+ # Reads in a file, removes blank lines and remarks
16
+ # (lines starting with '#') and then returns
17
+ # an array of all the remaining lines.
18
+ #
19
+ # CREDIT: Trans
20
+ def self.read_list(filepath, chomp_string='')
21
+ farr = nil
22
+ farr = read(filepath).split("\n")
23
+ farr.collect! { |line|
24
+ l = line.strip.chomp(chomp_string)
25
+ (l.empty? or l[0,1] == '#') ? nil : l
26
+ }
27
+ farr.compact
28
+ end
29
+
30
+ # Return the path shared.
31
+ def self.sharedpath(file1, file2)
32
+ afile1 = file1.split(/\/\\/)
33
+ afile2 = file2.split(/\/\\/)
34
+ overlap = []
35
+ i = 0; e1, e2 = afile1[i], afile2[i]
36
+ while e1 && e2 && e1 == e2
37
+ overlap << e1
38
+ i += 1; e1, e2 = afile1[i], afile2[i]
39
+ end
40
+ return overlap.empty? ? false : overlap
41
+ end
42
+
43
+ # Is path1 a parent directory of path2.
44
+ def self.parent?(file1, file2)
45
+ return false if File.identical?(file1, file2)
46
+ afile1 = file1.split(/(\/|\\)/)
47
+ afile2 = file2.split(/(\/|\\)/)
48
+ overlap = []
49
+ i = 0; e1, e2 = afile1[i], afile2[i]
50
+ while e1 && e2 && e1 == e2
51
+ overlap << e1
52
+ i += 1; e1, e2 = afile1[i], afile2[i]
53
+ end
54
+ return (overlap == afile1)
55
+ end
56
+
57
+ # Reduce a list of files so there is no overlapping
58
+ # directory entries. This is useful when recursively
59
+ # descending a directory listing, so as to avoid and
60
+ # repeat entries.
61
+ #
62
+ # TODO: Maybe globbing should occur in here too?
63
+ #
64
+ def self.reduce(*list)
65
+ # TODO: list = list.map{ |f| File.cleanpath(f) }
66
+ newlist = list.dup
67
+ list.each do |file1|
68
+ list.each do |file2|
69
+ if parent?(file1, file2)
70
+ newlist.delete(file2)
71
+ end
72
+ end
73
+ end
74
+ newlist
75
+ end
76
+
77
+ end
78
+
79
+ # Metaclass extensions for core Dir class.
80
+ #
81
+ class Dir #:nodoc:
82
+
83
+ # Like +glob+ but can take multiple patterns.
84
+ #
85
+ # Dir.multiglob( '*.rb', '*.py' )
86
+ #
87
+ # Rather then constants for options multiglob accepts a trailing options
88
+ # hash of symbol keys.
89
+ #
90
+ # :noescape File::FNM_NOESCAPE
91
+ # :casefold File::FNM_CASEFOLD
92
+ # :pathname File::FNM_PATHNAME
93
+ # :dotmatch File::FNM_DOTMATCH
94
+ # :strict File::FNM_PATHNAME && File::FNM_DOTMATCH
95
+ #
96
+ # It also has an option for recurse.
97
+ #
98
+ # :recurse Recurively include contents of directories.
99
+ #
100
+ # For example
101
+ #
102
+ # Dir.multiglob( '*', :recurse => true )
103
+ #
104
+ # would have the same result as
105
+ #
106
+ # Dir.multiglob('**/*')
107
+ #
108
+ def self.multiglob(*patterns)
109
+ options = (Hash === patterns.last ? patterns.pop : {})
110
+
111
+ if options.delete(:recurse)
112
+ #patterns += patterns.collect{ |f| File.join(f, '**', '**') }
113
+ multiglob_r(*patterns)
114
+ end
115
+
116
+ bitflags = 0
117
+ bitflags |= File::FNM_NOESCAPE if options[:noescape]
118
+ bitflags |= File::FNM_CASEFOLD if options[:casefold]
119
+ bitflags |= File::FNM_PATHNAME if options[:pathname] or options[:strict]
120
+ bitflags |= File::FNM_DOTMATCH if options[:dotmatch] or options[:strict]
121
+
122
+ patterns = [patterns].flatten.compact
123
+
124
+ if options[:recurse]
125
+ patterns += patterns.collect{ |f| File.join(f, '**', '**') }
126
+ end
127
+
128
+ files = []
129
+ files += patterns.collect{ |pattern| Dir.glob(pattern, bitflags) }.flatten.uniq
130
+
131
+ return files
132
+ end
133
+
134
+ # The same as +multiglob+, but recusively includes directories.
135
+ #
136
+ # Dir.multiglob_r( 'folder' )
137
+ #
138
+ # is equivalent to
139
+ #
140
+ # Dir.multiglob( 'folder', :recurse=>true )
141
+ #
142
+ # The effect of which is
143
+ #
144
+ # Dir.multiglob( 'folder', 'folder/**/**' )
145
+ #
146
+ def self.multiglob_r(*patterns)
147
+ options = (Hash === patterns.last ? patterns.pop : {})
148
+ matches = multiglob(*patterns)
149
+ directories = matches.select{ |m| File.directory?(m) }
150
+ matches += directories.collect{ |d| multiglob_r(File.join(d, '**'), options) }.flatten
151
+ matches.uniq
152
+ #options = (Hash === patterns.last ? patterns.pop : {})
153
+ #options[:recurse] = true
154
+ #patterns << options
155
+ #multiglob(*patterns)
156
+ end
157
+
158
+ end
@@ -0,0 +1,588 @@
1
+ module Mast
2
+ require 'fileutils'
3
+ require 'getoptlong'
4
+ require 'shellwords'
5
+ require 'mast/core_ext'
6
+
7
+ # Manifest stores a list of package files, and optionally checksums.
8
+ #
9
+ # The class can be used to create and compare package manifests and digests.
10
+ #
11
+ # TODO: Integrate file signing and general manifest better (?)
12
+ #
13
+ # TODO: Digester is in sign.rb too. Dry-up?
14
+ #
15
+ # TODO: The #diff method is shelling out; this needs to be internalized.
16
+ #
17
+ # TODO: Consider adding @include options rather then scanning entire directory.
18
+ # But this can't be done unless we can write a routine that can look at @include
19
+ # and reduce it to non-overlapping matches. Eg. [doc, doc/rdoc] should reduce
20
+ # to just [doc]. Otherwise we will get duplicate entries, b/c the #output
21
+ # method is written for speed and low memory footprint. This might mean @include
22
+ # can't use file globs.
23
+ #
24
+ class Manifest
25
+
26
+ # Manifest file overwrite error.
27
+ #
28
+ OverwriteError = Class.new(Exception)
29
+
30
+ # No Manifest File Error.
31
+ #
32
+ NoManifestError = Class.new(LoadError)
33
+
34
+ # By default mast will exclude any pathname matching
35
+ # 'CVS', '_darcs', '.git*' or '.config'.
36
+ DEFAULT_EXCLUDE = %w{CVS _darcs .git* .config} # InstalledFiles
37
+
38
+ # By default, mast will ignore any file with a name matching
39
+ # '.svn' or '*~', ie. ending with a tilde.
40
+ DEFAULT_IGNORE = %w{*~ .svn}
41
+
42
+ #
43
+ DEFAULT_FILE = '{manifest,digest}{,.txt,.list}'
44
+
45
+ # Possible file name (was for Fileable?).
46
+ #def self.filename
47
+ # DEFAULT_FILE
48
+ #end
49
+
50
+ def self.open(file=nil, options={})
51
+ unless file
52
+ file = Dir.glob(filename, File::FNM_CASEFOLD).first
53
+ raise LoadError, "Manifest file is required." unless file
54
+ end
55
+ options[:file] = file
56
+ new(options)
57
+ end
58
+
59
+ # Directory of manifest.
60
+ attr_accessor :directory
61
+
62
+ # File used to store manifest/digest file.
63
+ attr_accessor :file
64
+
65
+ # Encryption type
66
+ attr_accessor :digest
67
+
68
+ # Do not exclude standard exclusions.
69
+ attr_accessor :all
70
+
71
+ # What files to include. Defaults to ['*'].
72
+ # Note that Mast automatically recurses through
73
+ # directory entries, so using '**/*' would simply
74
+ # be a waste of of processing cycles.
75
+ attr_accessor :include
76
+
77
+ # What files to exclude.
78
+ attr_accessor :exclude
79
+
80
+ # Special files to ignore.
81
+ attr_accessor :ignore
82
+
83
+ # Layout of digest -- 'csf' or 'sfv'. Default is 'csf'.
84
+ attr_accessor :format
85
+
86
+ # Files and checksums listed in file.
87
+ #attr_reader :list
88
+
89
+ #
90
+ alias_method :all?, :all
91
+
92
+ # New Manifest object.
93
+ #
94
+ def initialize(options={})
95
+ @include = ['*']
96
+ @exclude = []
97
+ @ignore = []
98
+ @format = 'csf'
99
+ @all = false
100
+ @digest = nil
101
+ @directory = Dir.pwd
102
+
103
+ change_options(options)
104
+
105
+ #if @file
106
+ # read(@file)
107
+ #else
108
+ #if file = Dir.glob(self.class.filename)[0]
109
+ # @file = file
110
+ #else
111
+ # @file = DEFAULT_FILE
112
+ #end
113
+ #end
114
+ end
115
+
116
+ # Set options.
117
+ def change_options(opts)
118
+ opts.each do |k,v|
119
+ k = k.to_s.downcase
120
+ send("#{k}=",v||send(k))
121
+ end
122
+ #@file = options[:file] || @file
123
+ #@digest = options[:digest] || @digest
124
+ #@all = options[:all] || @all
125
+ #@exclude = options[:exclude] || options[:ignore] || @exclude
126
+ #@exclude = [@exclude].flatten.compact
127
+ end
128
+
129
+ def include=(inc)
130
+ @include = [inc].flatten.uniq
131
+ end
132
+
133
+ #
134
+ def file
135
+ @file ||= Dir[File.join(directory, DEFAULT_FILE)].first || 'MANIFEST'
136
+ end
137
+
138
+ #
139
+ def read?
140
+ @read
141
+ end
142
+
143
+ # Create a digest/manifest file. This saves the list of files
144
+ # and optionally their checksum.
145
+ #def create(options=nil)
146
+ # change_options(options) if options
147
+ # #@file ||= DEFAULT_FILE
148
+ # raise OverwriteError if FileTest.file?(file)
149
+ # save #(false)
150
+ #end
151
+
152
+ # Generate manifest.
153
+ def generate(out=$stdout)
154
+ out << topline_string
155
+ output(out)
156
+ end
157
+
158
+ # Update file.
159
+ def update
160
+ raise NoManifestError unless file and FileTest.file?(file)
161
+ parse_topline
162
+ save
163
+ end
164
+
165
+ # Save as file.
166
+ def save
167
+ File.open(file, 'w') do |file|
168
+ file << topline_string
169
+ output(file)
170
+ end
171
+ return file
172
+ end
173
+
174
+ # Diff file against actual files.
175
+ #
176
+ # TODO: Do not shell out for diff.
177
+ #
178
+ def diff
179
+ raise NoManifestError unless file and FileTest.file?(file)
180
+ parse_topline # parse_file unless read?
181
+ manifest = create_temporary_manifest
182
+ begin
183
+ result = `diff -du #{file} #{manifest.file}`
184
+ ensure
185
+ FileUtils.rm(manifest.file)
186
+ end
187
+ # pass = result.empty?
188
+ return result
189
+ end
190
+
191
+ # Files listed in the manifest file, but not found in file system.
192
+ #
193
+ def whatsold
194
+ parse_file unless read?
195
+ filelist - list
196
+ end
197
+
198
+ # Files found in file system, but not listed in the manifest file.
199
+ def whatsnew
200
+ parse_file unless read?
201
+ list - (filelist + [filename])
202
+ end
203
+
204
+ #
205
+ def verify
206
+ parse_file unless read?
207
+ chart == filechart
208
+ end
209
+
210
+ # Clean non-manifest files.
211
+ def clean
212
+ cfiles, cdirs = cleanlist.partition{ |f| !File.directory?(f) }
213
+ FileUtils.rm(cfiles)
214
+ FileUtils.rmdir(cdirs)
215
+ end
216
+
217
+ # List of current files.
218
+ def list
219
+ @list ||= chart.keys.sort
220
+ end
221
+
222
+ # Chart of current files (name => checksum).
223
+ def chart
224
+ @chart ||= parse_directory
225
+ end
226
+
227
+ # List of files as given in MANIFEST file.
228
+ def filelist
229
+ @filelist ||= filechart.keys.sort
230
+ end
231
+
232
+ # Chart of files as given in MANIFEST file (name => checksum).
233
+ def filechart
234
+ @filechart ||= parse_file
235
+ end
236
+
237
+ #
238
+ def cleanlist
239
+ list = []
240
+ Dir.chdir(directory) do
241
+ keep = Dir.glob('*').select{|f| File.directory?(f)} # keep top-level directories?
242
+ keep << filename # keep manifest file
243
+ list = Dir.glob('**/*') - (filelist + keep)
244
+ end
245
+ list
246
+ end
247
+
248
+ #
249
+ def showlist
250
+ parse_topline unless read?
251
+ list
252
+ end
253
+
254
+ # # Clobber non-manifest files.
255
+ # #
256
+ # def clobber
257
+ # clobber_files.each{ |f| rm_r(f) if File.exist?(f) }
258
+ # end
259
+ #
260
+ # #--
261
+ # # TODO Should clobber work off the manifest file itself?
262
+ # #++
263
+ # def clobber_files
264
+ # keep = filelist # + [info.manifest]
265
+ # Dir.glob('**/*') - keep
266
+ # end
267
+
268
+ # File's basename.
269
+ def filename
270
+ File.basename(file)
271
+ end
272
+
273
+ private
274
+
275
+ #
276
+ def output(out=$stdout)
277
+ Dir.chdir(directory) do
278
+ exclusions # seed exclusions
279
+ #rec_output('*', out)
280
+ inclusions.each do |inc|
281
+ rec_output(inc, out)
282
+ end
283
+ end
284
+ end
285
+
286
+ # Generate listing on the fly.
287
+ def rec_output(match, out=$stdout)
288
+ out.flush unless Array === out
289
+ #match = (location == dir ? '*' : File.join(dir,'*'))
290
+ files = Dir.glob(match, File::FNM_DOTMATCH) - exclusions
291
+ # TODO: Is there a more efficient way to reject ignored files?
292
+ #files = files.select{ |f| !ignores.any?{ |i| File.fnmatch(i, File.basename(f)) } }
293
+ files = files.reject{ |f| ignores.any?{ |i| File.fnmatch(i, File.basename(f)) } }
294
+ files = files.sort
295
+ files.each do |file|
296
+ sum = checksum(file,digest)
297
+ sum = sum + ' ' if sum
298
+ out << "#{sum}#{file}"
299
+ out << "\n" unless Array === out
300
+ if File.directory?(file)
301
+ rec_output(File.join(file,'*'), out)
302
+ end
303
+ end
304
+ #return out
305
+ end
306
+
307
+ #
308
+ def parse_directory
309
+ h = {}
310
+ files.each do |f|
311
+ h[f] = checksum(f)
312
+ end
313
+ h
314
+ end
315
+
316
+ # List of files.
317
+ #
318
+ def files #(update=false)
319
+ @files ||= (
320
+ r = []
321
+ output(r)
322
+ r
323
+ )
324
+ #files = []
325
+ #Dir.chdir(directory) do
326
+ # files += Dir.multiglob_r('**/*')
327
+ # files -= Dir.multiglob_r(exclusions)
328
+ #end
329
+ #return files
330
+ end
331
+
332
+ # Compute exclusions.
333
+ def inclusions
334
+ @_inclusions ||= (
335
+ e = [include].flatten
336
+ #e += DEFAULT_EXCLUDE unless all?
337
+ #e += [filename, filename.chomp('~')] if file
338
+ e = e.map{ |x| Dir.glob(x) }.flatten.uniq
339
+ e = File.reduce(*e)
340
+ e
341
+ )
342
+ end
343
+
344
+ # Compute exclusions.
345
+ def exclusions
346
+ @_exclusions ||= (
347
+ e = [exclude].flatten
348
+ e += DEFAULT_EXCLUDE unless all?
349
+ e += [filename, filename.chomp('~')] if file
350
+ e.map{ |x| Dir.glob(x) }.flatten.uniq
351
+ )
352
+ end
353
+
354
+ # Compute ignores.
355
+ def ignores
356
+ @_ignores ||= (
357
+ i = [ignore].flatten
358
+ i += [ '.', '..' ]
359
+ i += DEFAULT_IGNORE unless all?
360
+ i
361
+ )
362
+ end
363
+
364
+ public
365
+
366
+ # List of files in file system, but omit folders.
367
+ def list_without_folders
368
+ list.select{ |f| !File.directory?(f) }
369
+ end
370
+
371
+ # Produce textual listing less the manifest file.
372
+ #
373
+ def listing
374
+ str = ''
375
+ output(str)
376
+ str
377
+ end
378
+
379
+ #
380
+ def to_s
381
+ topline_string + listing
382
+ end
383
+
384
+ private
385
+
386
+ # Create temporary manifest (for comparison).
387
+
388
+ def create_temporary_manifest
389
+ temp_manifest = Manifest.new(
390
+ :file => file+"~",
391
+ :digest => digest,
392
+ :include => include,
393
+ :exclude => exclude,
394
+ :ignore => ignore,
395
+ :all => all
396
+ )
397
+ temp_manifest.save
398
+ #File.open(tempfile, 'w+') do |f|
399
+ # f << to_s(true)
400
+ #end
401
+ return temp_manifest
402
+ end
403
+
404
+ # Produce hexdigest/cheksum for a file.
405
+ # Default digest type is sha1.
406
+
407
+ def checksum(file, digest=nil)
408
+ return nil unless digest
409
+ if FileTest.directory?(file)
410
+ @null_string ||= digester(digest).hexdigest("")
411
+ else
412
+ digester(digest).hexdigest(File.read(file))
413
+ end
414
+ end
415
+
416
+ # Return a digest class for given +type+.
417
+ # Supported digests are:
418
+ #
419
+ # * md5
420
+ # * sha1
421
+ # * sha128 (same as sha1)
422
+ # * sha256
423
+ # * sha512
424
+ #
425
+ # Default digest type is sha256.
426
+
427
+ def digester(type=nil)
428
+ require 'openssl'
429
+ case type.to_s.downcase
430
+ when 'md5'
431
+ require 'digest/md5'
432
+ ::Digest::MD5
433
+ when 'sha128', 'sha1'
434
+ require 'digest/sha1' #need?
435
+ OpenSSL::Digest::SHA1
436
+ when 'sha256'
437
+ require 'digest/sha1' #need?
438
+ OpenSSL::Digest::SHA256
439
+ when 'sha512'
440
+ require 'digest/sha1' #need?
441
+ OpenSSL::Digest::SHA512
442
+ else
443
+ raise "unsupported digest #{type}"
444
+ end
445
+ end
446
+
447
+ # Read manifest file.
448
+
449
+ def parse_file
450
+ raise ManifestMissing unless file
451
+
452
+ parse_topline
453
+
454
+ #@file = file
455
+ #@location = File.dirname(File.expand_path(file))
456
+
457
+ chart = {}
458
+ flist = File.read_list(file)
459
+ flist.each do |line|
460
+ left, right = line.split(/\s+/)
461
+ if right
462
+ checksum = left
463
+ filename = right
464
+ chart[filename] = checksum
465
+ else
466
+ filename = left
467
+ chart[filename] = nil
468
+ end
469
+ end
470
+
471
+ @read = true
472
+ @filechart = chart
473
+ end
474
+
475
+ # Get topline of Manifest file, parse and cache.
476
+ #def topline
477
+ # @topline ||= topline_parse
478
+ #end
479
+
480
+ # Parse topline.
481
+ #
482
+ def parse_topline
483
+ if line = read_topline
484
+ argv = Shellwords.shellwords(line)
485
+ ARGV.replace(argv)
486
+ opts = GetoptLong.new(
487
+ [ '-g', '--digest' , GetoptLong::REQUIRED_ARGUMENT ],
488
+ [ '-x', '--exclude', GetoptLong::REQUIRED_ARGUMENT ],
489
+ [ '-i', '--ignore' , GetoptLong::REQUIRED_ARGUMENT ],
490
+ [ '-a', '--all' , GetoptLong::NO_ARGUMENT ]
491
+ )
492
+ a, d, x, i = false, nil, [], []
493
+ opts.each do |opt, arg|
494
+ case opt
495
+ when '-g': d = arg.downcase
496
+ when '-a': a = true
497
+ when '-x': x << arg
498
+ when '-i': i << arg
499
+ end
500
+ end
501
+
502
+ @all = a
503
+ @digest = d
504
+ @exclude = x
505
+ @ignore = i
506
+ @include = ARGV.empty? ? nil : ARGV.dup
507
+ end
508
+ end
509
+
510
+ # Read topline of MANIFEST file.
511
+ #
512
+ def read_topline
513
+ r = nil
514
+ #if file = locate(filename)
515
+ File.open(file) do |f|
516
+ s = f.readline
517
+ if s =~ /^#!mast\s*(.*?)\n/
518
+ r = $1
519
+ end
520
+ end
521
+ return r
522
+ #end
523
+ end
524
+
525
+ # Create topline of MANIFEST file.
526
+ #
527
+ def topline_string(update=false)
528
+ #if update
529
+ # a = all #|| topline.all
530
+ # d = digest #|| topline.digest
531
+ # x = exclude #+ topline.exclude
532
+ #else
533
+ # a, d, x = all, digest, exclude
534
+ #end
535
+ top = []
536
+ top << "-a" if all?
537
+ top << "-g #{digest.to_s.downcase}" if digest
538
+ exclude.each do |e|
539
+ top << "-x #{e}"
540
+ end
541
+ ignore.each do |e|
542
+ top << "-i #{e}"
543
+ end
544
+ include.each do |e|
545
+ top << e
546
+ end
547
+ return "#!mast #{top.join(' ')}\n" # FIXME: use proper executable
548
+ end
549
+
550
+ end
551
+
552
+ end
553
+
554
+
555
+ =begin
556
+ #
557
+ def manifest_file
558
+ apply_naming_policy(@file || DEFAULT_FILE, 'txt')
559
+ end
560
+
561
+ private
562
+
563
+ # Apply naming policy.
564
+ #
565
+ def apply_naming_policy(name, ext)
566
+ return name unless policy
567
+ policies = naming_policy.split(' ')
568
+ policies.each do |polic|
569
+ case polic
570
+ when 'downcase'
571
+ name = name.downcase
572
+ when 'upcase'
573
+ name = name.upcase
574
+ when 'capitalize'
575
+ name = name.capitalize
576
+ when 'extension'
577
+ name = name + ".#{ext}"
578
+ when 'plain'
579
+ name = name.chomp(File.extname(name))
580
+ else
581
+ name
582
+ end
583
+ end
584
+ return name
585
+ end
586
+ =end
587
+
588
+ #end # module Ratchets