mast 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/HISTORY.rdoc +12 -0
- data/LICENSE +677 -0
- data/MANIFEST +23 -0
- data/README.rdoc +57 -0
- data/TUTORIAL.rdoc +83 -0
- data/bin/mast +4 -0
- data/lib/mast.rb +1 -0
- data/lib/mast/cli.rb +480 -0
- data/lib/mast/core_ext.rb +158 -0
- data/lib/mast/manifest.rb +588 -0
- data/meta/authors +1 -0
- data/meta/contact +1 -0
- data/meta/description +1 -0
- data/meta/package +1 -0
- data/meta/project +1 -0
- data/meta/ruby +2 -0
- data/meta/sitemap +1 -0
- data/meta/version +1 -0
- metadata +79 -0
@@ -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
|