mast 1.0.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.
- 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
|