checksum-tools 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +10 -0
- data/Gemfile.lock +51 -0
- data/README.rdoc +109 -0
- data/Rakefile +38 -0
- data/bin/checksum-tools +172 -0
- data/lib/checksum-tools.rb +180 -0
- data/lib/checksum-tools/local.rb +122 -0
- data/lib/checksum-tools/remote.rb +148 -0
- metadata +170 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
checksum-tools (1.1.0)
|
5
|
+
net-sftp
|
6
|
+
net-ssh
|
7
|
+
progressbar
|
8
|
+
|
9
|
+
GEM
|
10
|
+
remote: https://rubygems.org/
|
11
|
+
specs:
|
12
|
+
diff-lcs (1.2.4)
|
13
|
+
ffi (1.9.0)
|
14
|
+
gssapi (1.1.2)
|
15
|
+
ffi (>= 1.0.1)
|
16
|
+
json (1.8.0)
|
17
|
+
net-sftp (2.1.2)
|
18
|
+
net-ssh (>= 2.6.5)
|
19
|
+
net-ssh (2.7.0)
|
20
|
+
net-ssh-kerberos (0.2.7)
|
21
|
+
net-ssh (>= 2.0)
|
22
|
+
net-ssh-krb (0.3.0)
|
23
|
+
gssapi (~> 1.1.2)
|
24
|
+
net-ssh (>= 2.0)
|
25
|
+
progressbar (0.21.0)
|
26
|
+
rake (10.1.0)
|
27
|
+
rcov (1.0.0)
|
28
|
+
rdoc (4.0.1)
|
29
|
+
json (~> 1.4)
|
30
|
+
rspec (2.14.1)
|
31
|
+
rspec-core (~> 2.14.0)
|
32
|
+
rspec-expectations (~> 2.14.0)
|
33
|
+
rspec-mocks (~> 2.14.0)
|
34
|
+
rspec-core (2.14.5)
|
35
|
+
rspec-expectations (2.14.3)
|
36
|
+
diff-lcs (>= 1.1.3, < 2.0)
|
37
|
+
rspec-mocks (2.14.3)
|
38
|
+
yard (0.8.7.2)
|
39
|
+
|
40
|
+
PLATFORMS
|
41
|
+
ruby
|
42
|
+
|
43
|
+
DEPENDENCIES
|
44
|
+
checksum-tools!
|
45
|
+
net-ssh-kerberos
|
46
|
+
net-ssh-krb
|
47
|
+
rake (>= 0.8.7)
|
48
|
+
rcov
|
49
|
+
rdoc
|
50
|
+
rspec
|
51
|
+
yard
|
data/README.rdoc
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
= Name
|
2
|
+
|
3
|
+
checksum-tools --- Generate or verify checksums for a set of files
|
4
|
+
|
5
|
+
= Setup
|
6
|
+
|
7
|
+
== Requirements
|
8
|
+
|
9
|
+
* Ruby 1.8.7
|
10
|
+
* Rubygems 1.3.6 or greater
|
11
|
+
|
12
|
+
== Installation
|
13
|
+
|
14
|
+
gem install checksum-tools
|
15
|
+
|
16
|
+
= Synopsis
|
17
|
+
|
18
|
+
Usage: checksum-tools [options] [path]
|
19
|
+
-a, --action ACTION Specify action to take
|
20
|
+
generate|verify
|
21
|
+
-c, --config FILE Load configuration from FILE
|
22
|
+
-d, --digest DIGEST Generate checksums of type DIGEST
|
23
|
+
-e, --extension EXT File extension for digest files
|
24
|
+
-f, --filemask MASK Include files matching MASK
|
25
|
+
-n, --no-action Dry run; don't execute
|
26
|
+
-o, --overwrite Overwrite existing digest files
|
27
|
+
-q, --quiet Hide the progress bar
|
28
|
+
-r, --recursive Recurse into subdirectories
|
29
|
+
-x, --exclude MASK Exclude files matching MASK
|
30
|
+
-D, --digest-types Show digest types
|
31
|
+
-v, --version Print version and exit
|
32
|
+
-h, --help Show this help message
|
33
|
+
|
34
|
+
= Description
|
35
|
+
|
36
|
+
checksum-tools is a dual-purpose tool to generate or verify checksums of various types for a set of files in a given directory.
|
37
|
+
|
38
|
+
= Options
|
39
|
+
|
40
|
+
<tt>-a, --action ACTION</tt>
|
41
|
+
|
42
|
+
Valid choices are <tt>generate</tt> and <tt>verify</tt>. In <tt>generate</tt> mode, a digest file is created for each content file processed. In <tt>verify</tt> mode, the digests in digest files are verified against the content files and PASS/FAIL information written to standard output. Default: <tt>verify</tt>.
|
43
|
+
|
44
|
+
<tt>-c, --config FILE</tt>
|
45
|
+
|
46
|
+
Load default configuration values from <tt>FILE</tt>. Default: a file called <tt>.checksum-tools</tt> in the user's home directory. Options specified on the command line will override any defaults in the configuration file.
|
47
|
+
|
48
|
+
<tt>-d, --digest DIGEST</tt>
|
49
|
+
|
50
|
+
In <tt>generate</tt> mode, specifies the type of digest to generate. Valid options depend on the Ruby libraries available on the system, but usually include <tt>md5</tt>, <tt>sha1</tt>, <tt>sha256</tt>, <tt>sha384</tt>, and <tt>sha512</tt>. Default: <tt>md5</tt>. Including this option multiple times causes multiple digests to be generated for each file.
|
51
|
+
|
52
|
+
<tt>-D, --digest-types</tt>
|
53
|
+
|
54
|
+
Display the available digest types for the specified target directory/path/system.
|
55
|
+
|
56
|
+
<tt>-e, --extension EXT</tt>
|
57
|
+
|
58
|
+
The file extension to be appended to the names of content files when generating digest files. Default: <tt>.digest</tt>.
|
59
|
+
|
60
|
+
<tt>-f, --filemask MASK</tt>
|
61
|
+
|
62
|
+
Generate or verify checksums for files matching <tt>MASK</tt>. Include this option multiple times to match multiple filemasks. Default: <tt>*</tt> (all files)
|
63
|
+
|
64
|
+
<tt>-n, --no-action</tt>
|
65
|
+
|
66
|
+
Dry run — display configuration options and exit.
|
67
|
+
|
68
|
+
<tt>-o, --overwrite</tt>
|
69
|
+
|
70
|
+
Overwrite existing digest files with newly-generated ones.
|
71
|
+
|
72
|
+
<tt>-q, --quiet</tt>
|
73
|
+
|
74
|
+
Do not display a progress bar for each file generated.
|
75
|
+
|
76
|
+
<tt>-r, --recursive</tt>
|
77
|
+
|
78
|
+
Recurse into subdirectories.
|
79
|
+
|
80
|
+
<tt>-v, --version</tt>
|
81
|
+
|
82
|
+
Print version number and exit.
|
83
|
+
|
84
|
+
<tt>-x, --exclude MASK</tt>
|
85
|
+
|
86
|
+
Exclude files matching <tt>MASK</tt> from the operation. Can be used to skip a certain subset of files otherwise included using the <tt>--filemask</tt> option.
|
87
|
+
|
88
|
+
<tt>-h, --help</tt>
|
89
|
+
|
90
|
+
Display a simple help message and exit.
|
91
|
+
|
92
|
+
== Releases
|
93
|
+
- <b>0.6.6</b> Initial release
|
94
|
+
- <b>0.7.0</b> Added ability to verify against digest files created by other tools
|
95
|
+
- <b>0.7.1</b> Added -v / --version switch
|
96
|
+
- <b>0.7.2</b> Fixed shift error when evaluating .digest files
|
97
|
+
- <b>0.9.0</b> Added remote openssl invocation capability to generate digests on non-Ruby systems
|
98
|
+
- <b>0.9.1</b> Slight refactoring of bin/checksum-tools; removed debugging code
|
99
|
+
- <b>0.9.2</b> Fix gem deployment problem with bundler
|
100
|
+
- <b>0.9.3</b> Hotfix to revert SFTP/SSH object changes in 0.9.2
|
101
|
+
- <b>0.9.4</b> [HYDRALABWARE-59] Checksum-tools anomalies
|
102
|
+
- <b>0.9.5</b> Minor bug fixes and spec tests
|
103
|
+
- <b>1.0.0</b> Add --openssl command line parameter for self-configuration of remotes
|
104
|
+
- <b>1.0.1</b> Hotfix for broken spec test
|
105
|
+
- <b>1.0.3</b> Hotfix for remote paths with spaces in them
|
106
|
+
- <b>1.0.4</b> Hotfix for remote paths with singles quotes in them
|
107
|
+
- <b>1.0.5</b> Ensure remote commands are always invoked through bash
|
108
|
+
- <b>1.0.6</b> Fix remote digest type introspection bug introduced in 1.0.5
|
109
|
+
- <b>1.0.7</b> Compensate for differences between OpenSSL 0.9.8 and OpenSSL 1.0.0
|
data/Rakefile
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'bundler'
|
4
|
+
require 'bundler/gem_tasks'
|
5
|
+
|
6
|
+
Dir.glob('lib/tasks/*.rake').each { |r| import r }
|
7
|
+
|
8
|
+
begin
|
9
|
+
Bundler.setup(:default, :development)
|
10
|
+
rescue Bundler::BundlerError => e
|
11
|
+
$stderr.puts e.message
|
12
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
13
|
+
exit e.status_code
|
14
|
+
end
|
15
|
+
|
16
|
+
require 'rspec/core/rake_task'
|
17
|
+
RSpec::Core::RakeTask.new(:spec)
|
18
|
+
|
19
|
+
RSpec::Core::RakeTask.new(:features) do |spec|
|
20
|
+
spec.libs << 'lib' << 'spec'
|
21
|
+
spec.spec_files = FileList['spec/features/**/*_spec.rb']
|
22
|
+
end
|
23
|
+
|
24
|
+
RSpec::Core::RakeTask.new(:rcov) do |spec|
|
25
|
+
spec.libs << 'lib' << 'spec'
|
26
|
+
spec.pattern = 'spec/unit_tests/**/*.rb'
|
27
|
+
spec.rcov = true
|
28
|
+
spec.rcov_opts = %w{--exclude spec\/*,gems\/*,ruby\/* --aggregate coverage.data}
|
29
|
+
end
|
30
|
+
|
31
|
+
|
32
|
+
task :clean do
|
33
|
+
puts 'Cleaning old coverage.data'
|
34
|
+
FileUtils.rm('coverage.data') if(File.exists? 'coverage.data')
|
35
|
+
end
|
36
|
+
|
37
|
+
task :default => [:spec]
|
38
|
+
|
data/bin/checksum-tools
ADDED
@@ -0,0 +1,172 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
begin
|
5
|
+
require 'checksum-tools'
|
6
|
+
rescue LoadError
|
7
|
+
require 'bundler/setup'
|
8
|
+
require 'checksum-tools'
|
9
|
+
end
|
10
|
+
require 'optparse'
|
11
|
+
require 'progressbar'
|
12
|
+
require 'yaml'
|
13
|
+
|
14
|
+
DEFAULT_OPTIONS = {
|
15
|
+
:action => 'verify',
|
16
|
+
:digests => [],
|
17
|
+
:filemasks => [],
|
18
|
+
:progress => true,
|
19
|
+
:exclude => [],
|
20
|
+
:extension => '.digest',
|
21
|
+
:openssl => nil,
|
22
|
+
:overwrite => false,
|
23
|
+
:recursive => false
|
24
|
+
}
|
25
|
+
|
26
|
+
@pbar = nil
|
27
|
+
def progress(filename, size, pos)
|
28
|
+
unless size.nil?
|
29
|
+
if pos == -1
|
30
|
+
@pbar.finish unless @pbar.nil?
|
31
|
+
@pbar = nil
|
32
|
+
else
|
33
|
+
if @pbar.nil?
|
34
|
+
@pbar = ProgressBar.new(File.basename(filename), size)
|
35
|
+
end
|
36
|
+
@pbar.set(pos)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def generate(tool, path, options)
|
42
|
+
tool.create_digest_files(path,options[:filemasks]) do |filename, size, pos|
|
43
|
+
progress(filename, size, pos) if options[:progress]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def verify(tool, path, options)
|
48
|
+
tool.verify_digest_files(path,options[:filemasks]) do |filename, size, pos, result|
|
49
|
+
if result.nil?
|
50
|
+
progress(filename, size, pos) if options[:progress]
|
51
|
+
else
|
52
|
+
progress(filename, -1, -1) if options[:progress]
|
53
|
+
result.delete_if { |k,v| v == true }
|
54
|
+
if result.length == 0
|
55
|
+
print "PASS"
|
56
|
+
else
|
57
|
+
print "FAIL [#{result.keys.join(',')}]"
|
58
|
+
end
|
59
|
+
puts " #{filename}"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
VALID_ACTIONS = ['generate','verify','console']
|
65
|
+
|
66
|
+
config_file = File.join(ENV['HOME'], '.checksum-tools')
|
67
|
+
config_file_options = begin
|
68
|
+
YAML.load(File.read(config_file))
|
69
|
+
rescue
|
70
|
+
{}
|
71
|
+
end
|
72
|
+
|
73
|
+
cmdline_options = {}
|
74
|
+
dry_run = false
|
75
|
+
|
76
|
+
optparse = OptionParser.new do |opts|
|
77
|
+
opts.banner = "Usage: #{File.basename($0)} [options] [path]"
|
78
|
+
|
79
|
+
opts.on('-a', '--action ACTION', VALID_ACTIONS, "Specify action to take", "#{VALID_ACTIONS.join('|')}") do |action|
|
80
|
+
cmdline_options[:action] = action
|
81
|
+
end
|
82
|
+
|
83
|
+
opts.on('-c', '--config FILE', "Load configuration from FILE") do |filename|
|
84
|
+
config_file = filename
|
85
|
+
config_file_options = YAML.load(File.read(config_file))
|
86
|
+
end
|
87
|
+
|
88
|
+
opts.on('-d', '--digest DIGEST', "Generate checksums of type DIGEST") do |digest|
|
89
|
+
(cmdline_options[:digests] ||= []) << digest.to_sym
|
90
|
+
end
|
91
|
+
|
92
|
+
opts.on('-e', '--extension EXT', "File extension for digest files") do |ext|
|
93
|
+
cmdline_options[:extension] = ext
|
94
|
+
end
|
95
|
+
|
96
|
+
opts.on('-f', '--filemask MASK', "Include files matching MASK") do |mask|
|
97
|
+
(cmdline_options[:filemasks] ||= []) << mask
|
98
|
+
end
|
99
|
+
|
100
|
+
opts.on('-n', '--no-action', "Dry run; don't execute") do
|
101
|
+
dry_run = true
|
102
|
+
end
|
103
|
+
|
104
|
+
opts.on('-o', '--overwrite', "Overwrite existing digest files") do
|
105
|
+
cmdline_options[:overwrite] = true
|
106
|
+
end
|
107
|
+
|
108
|
+
opts.on('-q', '--quiet', "Hide the progress bar") do
|
109
|
+
cmdline_options[:progress] = false
|
110
|
+
end
|
111
|
+
|
112
|
+
opts.on('-r', '--recursive', "Recurse into subdirectories") do
|
113
|
+
cmdline_options[:recursive] = true
|
114
|
+
end
|
115
|
+
|
116
|
+
opts.on('-s', '--openssl PATH', "Specify location of openssl on remote server") do |arg|
|
117
|
+
cmdline_options[:openssl] = arg
|
118
|
+
end
|
119
|
+
|
120
|
+
opts.on('-x', '--exclude MASK', "Exclude files matching MASK") do |mask|
|
121
|
+
cmdline_options[:exclude] << mask
|
122
|
+
end
|
123
|
+
|
124
|
+
opts.on_tail('-D', '--digest-types', "Show digest types") do
|
125
|
+
path_info = Checksum::Tools.parse_path(ARGV.pop || '.')
|
126
|
+
tool = Checksum::Tools.new(path_info, cmdline_options)
|
127
|
+
puts "Available digest types for #{tool.host || 'local system'}:"
|
128
|
+
puts tool.digests.join(' ')
|
129
|
+
exit
|
130
|
+
end
|
131
|
+
|
132
|
+
opts.on_tail('-v', '--version', "Print version and exit") do
|
133
|
+
puts "checksum-tools #{Checksum::Tools::VERSION}"
|
134
|
+
exit
|
135
|
+
end
|
136
|
+
|
137
|
+
opts.on_tail('-h', '--help', "Show this help message") do
|
138
|
+
puts opts
|
139
|
+
exit
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
optparse.parse!
|
144
|
+
|
145
|
+
options = DEFAULT_OPTIONS.merge(config_file_options).merge(cmdline_options)
|
146
|
+
options[:filemasks] << '*' if options[:filemasks].length == 0
|
147
|
+
options[:digests] << :md5 if options[:digests].length == 0
|
148
|
+
options[:digests].uniq!
|
149
|
+
options[:filemasks].uniq!
|
150
|
+
options[:exclude].uniq!
|
151
|
+
|
152
|
+
if options[:extension] !~ /[A-Za-z0-9]/
|
153
|
+
puts optparse
|
154
|
+
exit
|
155
|
+
end
|
156
|
+
|
157
|
+
if dry_run
|
158
|
+
puts "Defaults loaded from #{config_file}"
|
159
|
+
puts YAML.dump(options)
|
160
|
+
exit
|
161
|
+
end
|
162
|
+
|
163
|
+
args = options[:digests].dup
|
164
|
+
args << options.dup.reject { |k,v| not [:exclude,:extension,:openssl,:overwrite,:recursive].include?(k) }
|
165
|
+
path_info = Checksum::Tools.parse_path(ARGV.pop || '.')
|
166
|
+
tool = Checksum::Tools.new(path_info, *args)
|
167
|
+
|
168
|
+
case options[:action]
|
169
|
+
when 'generate' then generate(tool, path_info[:dir], options)
|
170
|
+
when 'verify' then verify(tool, path_info[:dir], options)
|
171
|
+
when 'console' then require 'ripl'; Ripl.start :binding => binding
|
172
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Checksum
|
2
|
+
module Tools
|
3
|
+
VERSION = "1.1.0"
|
4
|
+
|
5
|
+
CHUNK_SIZE = 1048576 # 1M blocks
|
6
|
+
DEFAULT_OPTS = { :overwrite => false, :recursive => false, :exclude => [], :extension => '.digest' }
|
7
|
+
|
8
|
+
autoload :Local, File.join(File.dirname(__FILE__), File.basename(__FILE__, '.rb'), 'local')
|
9
|
+
autoload :Remote, File.join(File.dirname(__FILE__), File.basename(__FILE__, '.rb'), 'remote')
|
10
|
+
|
11
|
+
class Exception < ::Exception; end
|
12
|
+
class ConfigurationError < Exception; end
|
13
|
+
|
14
|
+
class << self
|
15
|
+
def new(path_info, *args)
|
16
|
+
remote = path_info[:remote]
|
17
|
+
if remote.nil? or remote[:host].nil?
|
18
|
+
Local.new(*args)
|
19
|
+
else
|
20
|
+
Remote.new(remote[:host],remote[:user],*args)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def parse_path(path)
|
25
|
+
user,host,dir = path.to_s.scan(/^(?:(.+)@)?(?:(.+):)?(?:(.+))?$/).flatten
|
26
|
+
dir ||= '.'
|
27
|
+
result = { :remote => { :user => user, :host => host }, :dir => dir }
|
28
|
+
return result
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
class Base
|
33
|
+
attr_reader :host
|
34
|
+
|
35
|
+
def initialize(*args)
|
36
|
+
@opts = DEFAULT_OPTS
|
37
|
+
if args.last.is_a?(Hash)
|
38
|
+
@opts.merge!(args.pop)
|
39
|
+
end
|
40
|
+
@opts[:exclude] << digest_filename("*")
|
41
|
+
@opts[:exclude].uniq!
|
42
|
+
@digest_types = args
|
43
|
+
end
|
44
|
+
|
45
|
+
def digest_filename(filename)
|
46
|
+
"#{filename}.#{opts[:extension].sub(/^\.+/,'')}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def create_digest_files(base_dir, file_masks, &block)
|
50
|
+
process_files(base_dir, file_masks) do |filename|
|
51
|
+
yield(filename, -1, -1) if block_given?
|
52
|
+
create_digest_file(filename, &block)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_digest_file(filename, &block)
|
57
|
+
digest_file = digest_filename(filename)
|
58
|
+
if File.expand_path(filename) == File.expand_path(digest_file)
|
59
|
+
raise ArgumentError, "Digest file #{digest_file} will clobber content file #{filename}!"
|
60
|
+
end
|
61
|
+
if opts[:overwrite] or not file_exists?(digest_file)
|
62
|
+
result = digest_file(filename, &block)
|
63
|
+
file_open(digest_file,'w') do |cksum|
|
64
|
+
result.each_pair do |type,hexdigest|
|
65
|
+
cksum.puts("#{type.to_s.upcase}(#{File.basename(filename)})= #{hexdigest}")
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def digest_files(base_dir, file_masks, &block)
|
72
|
+
process_files(base_dir, file_masks) do |filename|
|
73
|
+
yield(filename, -1, -1) if block_given?
|
74
|
+
digest_file(filename, &block)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def verify_digest_files(base_dir, file_masks, &block)
|
79
|
+
process_files(base_dir, file_masks) do |filename|
|
80
|
+
yield(filename, -1, -1) if block_given?
|
81
|
+
result = verify_digest_file(filename, &block)
|
82
|
+
yield(filename, -1, 0, result)
|
83
|
+
result
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def verify_digest_file(filename, &block)
|
88
|
+
digest_file = digest_filename(filename)
|
89
|
+
unless file_exists?(digest_file)
|
90
|
+
return { :digest_file => false }
|
91
|
+
end
|
92
|
+
|
93
|
+
hashes = {}
|
94
|
+
digest_data = file_read(digest_file)
|
95
|
+
|
96
|
+
ext_type = File.extname(digest_file)[1..-1].downcase.to_sym
|
97
|
+
# Check to see if the digest file extension implies a specific digest type (e.g., .md5)
|
98
|
+
if digests.include?(ext_type)
|
99
|
+
# Find a hex value of the correct length in the digest file
|
100
|
+
len = self.digest_length(ext_type)
|
101
|
+
digest = digest_data.scan(/\b((?:[0-9a-f]{4})+)\b/im).flatten.find { |d| d.length == len }
|
102
|
+
hashes[ext_type] = digest
|
103
|
+
else
|
104
|
+
digest_data = digest_data.split(/\n/)
|
105
|
+
hashes = digest_data.inject({}) do |collector,sum|
|
106
|
+
(hash,hashed_fname,digest) = sum.scan(/(.+)\((.+)\)= (.+)/).flatten
|
107
|
+
unless File.basename(hashed_fname) == File.basename(filename)
|
108
|
+
warn "WARNING: Filename mismatch in #{digest_file}: #{File.basename(hashed_fname)}"
|
109
|
+
end
|
110
|
+
collector[hash.downcase.to_sym] = digest
|
111
|
+
collector
|
112
|
+
end
|
113
|
+
end
|
114
|
+
verify_file(filename, hashes, &block)
|
115
|
+
end
|
116
|
+
|
117
|
+
def verify_file(filename, hashes, &block)
|
118
|
+
with_types(hashes.keys) do
|
119
|
+
actual = digest_file(filename, &block)
|
120
|
+
result = {}
|
121
|
+
actual.each_pair do |hash,digest|
|
122
|
+
result[hash] = hashes[hash].downcase == digest.downcase
|
123
|
+
end
|
124
|
+
result
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
protected
|
129
|
+
|
130
|
+
def glob_to_re(glob)
|
131
|
+
replacements = [
|
132
|
+
[/\./,'##DOT##'],[/\\\*/,'##STAR##'],[/\\\?/,'##QUEST##'],
|
133
|
+
[/\*/,'.*'],[/\?/,'.'],
|
134
|
+
[/##STAR##/,'\*'],[/##QUEST##/,'\?'],[/##DOT##/,'\.']
|
135
|
+
]
|
136
|
+
result = glob.dup
|
137
|
+
replacements.each { |args| result.gsub!(*args) }
|
138
|
+
Regexp.new(result)
|
139
|
+
end
|
140
|
+
|
141
|
+
def file_read(filename)
|
142
|
+
if file_exists?(filename)
|
143
|
+
result = ''
|
144
|
+
file_open(filename) { |io| result = io.read }
|
145
|
+
return result
|
146
|
+
else
|
147
|
+
raise Errno::ENOENT, filename
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def process_files(base_dir, include_masks = ["*"])
|
152
|
+
unless block_given?
|
153
|
+
raise ArgumentError, "no block given"
|
154
|
+
end
|
155
|
+
|
156
|
+
excludes = Array(opts[:exclude]).collect { |ex| glob_to_re(ex) }
|
157
|
+
targets = file_list(base_dir, *include_masks).reject { |f| excludes.any? { |re| f.match(re) } }
|
158
|
+
targets.sort!
|
159
|
+
targets.uniq!
|
160
|
+
|
161
|
+
result = {}
|
162
|
+
targets.each do |filename|
|
163
|
+
result[filename] = yield(filename)
|
164
|
+
end
|
165
|
+
return result
|
166
|
+
end
|
167
|
+
|
168
|
+
def with_types(new_types)
|
169
|
+
old_types = @digest_types
|
170
|
+
@digest_types = new_types
|
171
|
+
begin
|
172
|
+
return(yield)
|
173
|
+
ensure
|
174
|
+
@digest_types = old_types
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
end
|
180
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
module Checksum::Tools
|
2
|
+
|
3
|
+
class Local < Base
|
4
|
+
|
5
|
+
class << self
|
6
|
+
@@digests = {}
|
7
|
+
|
8
|
+
def register_digest(key, file, &block)
|
9
|
+
if @@digests.has_key?(key)
|
10
|
+
return false
|
11
|
+
else
|
12
|
+
begin
|
13
|
+
require file
|
14
|
+
@@digests[key] = block
|
15
|
+
return key
|
16
|
+
rescue LoadError
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def digest_for(key)
|
23
|
+
handler = @@digests[key]
|
24
|
+
if handler.nil?
|
25
|
+
raise ArgumentError, "undefined digest class: #{key.to_s}"
|
26
|
+
end
|
27
|
+
return handler.call()
|
28
|
+
end
|
29
|
+
|
30
|
+
def digests
|
31
|
+
@@digests.keys
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
attr :digest_types
|
36
|
+
attr :opts
|
37
|
+
|
38
|
+
def initialize(*args)
|
39
|
+
super(*args)
|
40
|
+
@digest_types.each { |type| self.class.digest_for(type) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def digests
|
44
|
+
self.class.digests
|
45
|
+
end
|
46
|
+
|
47
|
+
def digest_length(type)
|
48
|
+
self.class.digest_for(type).hexdigest.length
|
49
|
+
end
|
50
|
+
|
51
|
+
def digest_file(filename)
|
52
|
+
if File.exists?(filename)
|
53
|
+
size = file_size(filename)
|
54
|
+
block = block_given? ? lambda { |pos| yield(filename, size, pos) } : nil
|
55
|
+
File.open(filename, 'r') do |io|
|
56
|
+
yield(filename, size, 0) if block_given?
|
57
|
+
return digest_stream(io, &block)
|
58
|
+
end
|
59
|
+
else
|
60
|
+
raise Errno::ENOENT, filename
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def digest_stream(io)
|
65
|
+
output = digest_types.inject({}) { |collector,key| collector[key] = self.class.digest_for(key); collector }
|
66
|
+
io.rewind
|
67
|
+
while chunk = io.read(CHUNK_SIZE)
|
68
|
+
output.values.each do |digest|
|
69
|
+
digest << chunk
|
70
|
+
end
|
71
|
+
if block_given?
|
72
|
+
yield(io.pos)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
output.each_pair do |type,digest|
|
76
|
+
output[type] = digest.hexdigest
|
77
|
+
end
|
78
|
+
return output
|
79
|
+
end
|
80
|
+
|
81
|
+
def verify_stream(io, hashes, &block)
|
82
|
+
with_types(hashes.keys) do
|
83
|
+
actual = digest_stream(io, &block)
|
84
|
+
result = {}
|
85
|
+
actual.each_pair do |hash,digest|
|
86
|
+
result[hash] = hashes[hash] == digest
|
87
|
+
end
|
88
|
+
result
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
protected
|
93
|
+
|
94
|
+
def file_list(base_dir, *file_masks)
|
95
|
+
path = [base_dir, '*']
|
96
|
+
path.insert(1,'**') if opts[:recursive]
|
97
|
+
result = Dir[File.join(*path)]
|
98
|
+
result.reject! { |f| File.directory?(f) or (not file_masks.any? { |m| File.fnmatch?(m,File.basename(f)) }) }
|
99
|
+
result
|
100
|
+
end
|
101
|
+
|
102
|
+
def file_open(*args, &block)
|
103
|
+
File.open(*args, &block)
|
104
|
+
end
|
105
|
+
|
106
|
+
def file_exists?(filename)
|
107
|
+
File.exists?(filename)
|
108
|
+
end
|
109
|
+
|
110
|
+
def file_size(filename)
|
111
|
+
File.size(filename)
|
112
|
+
end
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
Checksum::Tools::Local.register_digest(:md5, 'digest/md5') { Digest::MD5.new }
|
119
|
+
Checksum::Tools::Local.register_digest(:sha1, 'digest/sha1') { Digest::SHA1.new }
|
120
|
+
Checksum::Tools::Local.register_digest(:sha256, 'digest/sha2') { Digest::SHA2.new(256) }
|
121
|
+
Checksum::Tools::Local.register_digest(:sha384, 'digest/sha2') { Digest::SHA2.new(384) }
|
122
|
+
Checksum::Tools::Local.register_digest(:sha512, 'digest/sha2') { Digest::SHA2.new(512) }
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'net/ssh'
|
2
|
+
require 'net/sftp'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'net/ssh/kerberos'
|
6
|
+
rescue LoadError
|
7
|
+
raise LoadError.new "Include 'net-ssh-kerberos' (ruby 1.8) or 'net-ssh-krb' (ruby 1.9) in your Gemfile"
|
8
|
+
end
|
9
|
+
|
10
|
+
module Checksum::Tools
|
11
|
+
|
12
|
+
class Remote < Base
|
13
|
+
|
14
|
+
attr :digest_types
|
15
|
+
attr :opts
|
16
|
+
|
17
|
+
def initialize(host, user, *args)
|
18
|
+
super(*args)
|
19
|
+
@host = host
|
20
|
+
@user = user
|
21
|
+
@digest_length_cache = {}
|
22
|
+
unless (ssl = opts.delete(:openssl)).nil?
|
23
|
+
begin
|
24
|
+
remote_properties[:openssl] = ssl
|
25
|
+
rescue ConfigurationError
|
26
|
+
@remote_properties = { :openssl => ssl }
|
27
|
+
end
|
28
|
+
write_remote_properties
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def openssl
|
33
|
+
remote_properties[:openssl]
|
34
|
+
end
|
35
|
+
|
36
|
+
def sftp
|
37
|
+
@sftp ||= begin
|
38
|
+
auth_methods = %w(gssapi-with-mic publickey hostbased) if defined? Net::SSH::Kerberos
|
39
|
+
auth_methods ||= %w(publickey hostbased)
|
40
|
+
Net::SFTP.start(@host, @user, :auth_methods => auth_methods)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def ssh
|
45
|
+
result = sftp.session
|
46
|
+
if block_given?
|
47
|
+
channel = result.open_channel do |ch|
|
48
|
+
yield(ch)
|
49
|
+
end
|
50
|
+
channel.wait
|
51
|
+
end
|
52
|
+
result
|
53
|
+
end
|
54
|
+
|
55
|
+
def exec!(cmd)
|
56
|
+
result = ''
|
57
|
+
ssh do |ch|
|
58
|
+
ch.exec("bash -l") do |ch2, success|
|
59
|
+
ch2.on_data { |c,data| result += data }
|
60
|
+
ch2.send_data "#{cmd}\n"
|
61
|
+
ch2.send_data "exit\n"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
result.chomp
|
65
|
+
end
|
66
|
+
|
67
|
+
def digests
|
68
|
+
resp = ''
|
69
|
+
resp = exec! "#{openssl} dgst -h 2>&1"
|
70
|
+
resp.scan(/-(.+?)\s+to use the .+ message digest algorithm/).flatten.collect { |d| d.to_sym }
|
71
|
+
end
|
72
|
+
|
73
|
+
def digest_length(type)
|
74
|
+
if @digest_length_cache[type].nil?
|
75
|
+
resp = exec! "echo - | #{openssl} dgst -#{type}"
|
76
|
+
@digest_length_cache[type] = resp.split(/\s/).last.chomp.length
|
77
|
+
end
|
78
|
+
@digest_length_cache[type]
|
79
|
+
end
|
80
|
+
|
81
|
+
def digest_file(filename)
|
82
|
+
if file_exists?(filename)
|
83
|
+
size = file_size(filename)
|
84
|
+
yield(filename, size, 0) if block_given?
|
85
|
+
output = {}
|
86
|
+
digest_types.each { |key|
|
87
|
+
resp = exec! "#{openssl} dgst -#{key} $'#{filename.gsub(/[']/,'\\\\\'')}'"
|
88
|
+
output[key] = resp.split(/\= /).last
|
89
|
+
}
|
90
|
+
yield(filename, size, size) if block_given?
|
91
|
+
return output
|
92
|
+
else
|
93
|
+
raise Errno::ENOENT, filename
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
protected
|
98
|
+
|
99
|
+
def file_list(base_dir, *file_masks)
|
100
|
+
path = ['*']
|
101
|
+
path.unshift('**') if opts[:recursive]
|
102
|
+
result = sftp.dir[base_dir, File.join(*path)]
|
103
|
+
result.reject! { |f| f.directory? or (not file_masks.any? { |m| File.fnmatch?(m,File.basename(f.name)) }) }
|
104
|
+
result.collect { |f| File.expand_path(File.join(base_dir, f.name)) }
|
105
|
+
end
|
106
|
+
|
107
|
+
def file_open(*args, &block)
|
108
|
+
sftp.file.open(*args, &block)
|
109
|
+
end
|
110
|
+
|
111
|
+
def file_exists?(filename)
|
112
|
+
sftp.stat!(filename)
|
113
|
+
return true
|
114
|
+
rescue Net::SFTP::StatusException
|
115
|
+
return false
|
116
|
+
end
|
117
|
+
|
118
|
+
def file_size(filename)
|
119
|
+
if file_exists?(filename)
|
120
|
+
sftp.stat!(filename).size
|
121
|
+
else
|
122
|
+
raise Errno::ENOENT, filename
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def remote_properties
|
127
|
+
if @remote_properties.nil?
|
128
|
+
home_dir = exec!('echo $HOME')
|
129
|
+
settings_file = File.join(home_dir,".checksum-tools-system")
|
130
|
+
if file_exists?(settings_file)
|
131
|
+
@remote_properties = YAML.load(file_read(settings_file))
|
132
|
+
else
|
133
|
+
raise ConfigurationError, "Checksum Tools not configured for #{@user}@#{host}. Please use the --openssl parameter to specify the location of the remote openssl binary."
|
134
|
+
end
|
135
|
+
end
|
136
|
+
@remote_properties
|
137
|
+
end
|
138
|
+
|
139
|
+
def write_remote_properties
|
140
|
+
home_dir = exec!('echo $HOME')
|
141
|
+
settings_file = File.join(home_dir,".checksum-tools-system")
|
142
|
+
file_open(settings_file, 'w') { |io| YAML.dump(remote_properties, io) }
|
143
|
+
remote_properties
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
metadata
ADDED
@@ -0,0 +1,170 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: checksum-tools
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Michael Klein
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-10-15 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: progressbar
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: net-ssh
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: net-sftp
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rake
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 0.8.7
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 0.8.7
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: rdoc
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rspec
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: yard
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: Contains classes and executable files to generate and verify checksums
|
127
|
+
email:
|
128
|
+
- mbklein@stanford.edu
|
129
|
+
executables:
|
130
|
+
- checksum-tools
|
131
|
+
extensions: []
|
132
|
+
extra_rdoc_files: []
|
133
|
+
files:
|
134
|
+
- lib/checksum-tools/local.rb
|
135
|
+
- lib/checksum-tools/remote.rb
|
136
|
+
- lib/checksum-tools.rb
|
137
|
+
- bin/checksum-tools
|
138
|
+
- Gemfile
|
139
|
+
- Gemfile.lock
|
140
|
+
- Rakefile
|
141
|
+
- README.rdoc
|
142
|
+
homepage:
|
143
|
+
licenses: []
|
144
|
+
post_install_message:
|
145
|
+
rdoc_options: []
|
146
|
+
require_paths:
|
147
|
+
- lib
|
148
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
149
|
+
none: false
|
150
|
+
requirements:
|
151
|
+
- - ! '>='
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
segments:
|
155
|
+
- 0
|
156
|
+
hash: -3039575937498161987
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
158
|
+
none: false
|
159
|
+
requirements:
|
160
|
+
- - ! '>='
|
161
|
+
- !ruby/object:Gem::Version
|
162
|
+
version: 1.3.6
|
163
|
+
requirements: []
|
164
|
+
rubyforge_project:
|
165
|
+
rubygems_version: 1.8.23
|
166
|
+
signing_key:
|
167
|
+
specification_version: 3
|
168
|
+
summary: Checksum creation and verification tools
|
169
|
+
test_files: []
|
170
|
+
has_rdoc:
|