fixi 0.0.1
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/.gitignore +6 -0
- data/Gemfile +4 -0
- data/Rakefile +1 -0
- data/bin/fixi +50 -0
- data/fixi.gemspec +26 -0
- data/lib/fixi/command/add.rb +33 -0
- data/lib/fixi/command/check.rb +66 -0
- data/lib/fixi/command/commit.rb +62 -0
- data/lib/fixi/command/info.rb +20 -0
- data/lib/fixi/command/init.rb +24 -0
- data/lib/fixi/command/ls.rb +70 -0
- data/lib/fixi/command/rm.rb +40 -0
- data/lib/fixi/command/sum.rb +34 -0
- data/lib/fixi/command.rb +10 -0
- data/lib/fixi/index.rb +203 -0
- data/lib/fixi/patch/string_pack.rb +5 -0
- data/lib/fixi/version.rb +3 -0
- data/lib/fixi.rb +42 -0
- metadata +82 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/bin/fixi
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'trollop'
|
5
|
+
require 'fixi'
|
6
|
+
|
7
|
+
name = ARGV.shift
|
8
|
+
if name == "--help" || name == "-h" || name == "help"
|
9
|
+
name = ARGV.shift
|
10
|
+
if name.nil?
|
11
|
+
puts <<-EOS
|
12
|
+
usage: fixi [--version] [--help] <command> [<options>] [path]
|
13
|
+
|
14
|
+
All commands are scoped to the current directory or the given path, if specified.
|
15
|
+
|
16
|
+
Commands:
|
17
|
+
add: #{Fixi::Command::Add.synopsis}
|
18
|
+
check: #{Fixi::Command::Check.synopsis}
|
19
|
+
commit: #{Fixi::Command::Commit.synopsis}
|
20
|
+
info: #{Fixi::Command::Info.synopsis}
|
21
|
+
init: #{Fixi::Command::Init.synopsis}
|
22
|
+
ls: #{Fixi::Command::Ls.synopsis}
|
23
|
+
rm: #{Fixi::Command::Rm.synopsis}
|
24
|
+
sum: #{Fixi::Command::Sum.synopsis}
|
25
|
+
|
26
|
+
See 'fixi help <command>' for more information on a specific command.
|
27
|
+
EOS
|
28
|
+
exit 0
|
29
|
+
else
|
30
|
+
ARGV.insert(0, "--help")
|
31
|
+
end
|
32
|
+
elsif name == "--version" || name == "-v"
|
33
|
+
puts "fixi version #{Fixi::VERSION}"
|
34
|
+
exit 0
|
35
|
+
end
|
36
|
+
|
37
|
+
command = Fixi::command name
|
38
|
+
if command.nil?
|
39
|
+
puts "Error: No such command: #{name}"
|
40
|
+
exit 1
|
41
|
+
end
|
42
|
+
begin
|
43
|
+
command.execute ARGV
|
44
|
+
rescue RuntimeError => msg
|
45
|
+
puts "Error: #{msg}"
|
46
|
+
exit 1
|
47
|
+
#rescue => msg
|
48
|
+
# puts "Error: #{msg}"
|
49
|
+
# exit 1
|
50
|
+
end
|
data/fixi.gemspec
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "fixi/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "fixi"
|
7
|
+
s.version = Fixi::VERSION
|
8
|
+
s.authors = ["Chris Wilper"]
|
9
|
+
s.email = ["cwilper@gmail.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = "A fixity tracker utility"
|
12
|
+
s.description = "Keeps an index of checksums and lets you update and verify them"
|
13
|
+
|
14
|
+
s.rubyforge_project = "fixi"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
# s.add_development_dependency "rspec"
|
23
|
+
# s.add_runtime_dependency "rest-client"
|
24
|
+
|
25
|
+
s.add_runtime_dependency "trollop"
|
26
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
require 'fixi/index'
|
3
|
+
|
4
|
+
class Fixi::Command::Add
|
5
|
+
def self.synopsis
|
6
|
+
"Add new files to the index"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.details
|
10
|
+
"This command is scoped to the current directory or the given path,
|
11
|
+
if specified.".pack
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute args
|
15
|
+
opts = Trollop::options args do
|
16
|
+
banner Fixi::Command.banner "add"
|
17
|
+
opt :absolute, "Show absolute paths. By default, paths are reported
|
18
|
+
relative to the index root.".pack
|
19
|
+
opt :dry_run, "Don't do anything; just report what would be done"
|
20
|
+
end
|
21
|
+
path = File.expand_path(args[0] || ".")
|
22
|
+
index = Fixi::Index.new(path)
|
23
|
+
|
24
|
+
index.find(path) do |abspath|
|
25
|
+
relpath = index.relpath(abspath)
|
26
|
+
unless index.contains?(relpath)
|
27
|
+
puts "A #{opts[:absolute] ? abspath : relpath}"
|
28
|
+
index.add(relpath) unless opts[:dry_run]
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
|
3
|
+
class Fixi::Command::Check
|
4
|
+
def self.synopsis
|
5
|
+
"Verify the fixity of files in the index"
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.details
|
9
|
+
"This command is scoped to the current directory or the given path,
|
10
|
+
if specified.".pack
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute args
|
14
|
+
opts = Trollop::options args do
|
15
|
+
banner Fixi::Command.banner "check"
|
16
|
+
opt :absolute, "Show absolute paths. By default, paths are reported
|
17
|
+
relative to the index root.".pack
|
18
|
+
opt :shallow, "Do shallow comparisons when determining which files have
|
19
|
+
changed. If specified, only file sizes and mtimes will be used. By
|
20
|
+
default, checksums will also be computed and compared if necessary.".pack
|
21
|
+
opt :verbose, "For modified files, show which attribute changed.
|
22
|
+
By default, only the path is shown.".pack
|
23
|
+
end
|
24
|
+
path = File.expand_path(args[0] || ".")
|
25
|
+
index = Fixi::Index.new(path)
|
26
|
+
|
27
|
+
index.each(args[0]) do |hash|
|
28
|
+
relpath = hash['relpath']
|
29
|
+
abspath = index.rootpath + '/' + relpath
|
30
|
+
if index.file_in_scope(relpath)
|
31
|
+
if File.exists?(abspath)
|
32
|
+
size = File.size(abspath)
|
33
|
+
mtime = File.mtime(abspath).to_i
|
34
|
+
if size != hash['size']
|
35
|
+
detail = opts[:verbose] ? "size=#{size} " : ""
|
36
|
+
puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
|
37
|
+
elsif File.mtime(abspath).to_i != hash['mtime']
|
38
|
+
detail = opts[:verbose] ? "mtime=#{Time.at(mtime).utc.iso8601} " : ""
|
39
|
+
puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
|
40
|
+
elsif not opts[:shallow]
|
41
|
+
hexdigests = Fixi::hexdigests(Fixi::digests(index.algorithms), abspath)
|
42
|
+
i = 0
|
43
|
+
index.algorithms.split(',').each do |algorithm|
|
44
|
+
if hexdigests[i] != hash[algorithm]
|
45
|
+
detail = opts[:verbose] ? "#{algorithm}=#{hexdigests[i]} " : ""
|
46
|
+
puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
|
47
|
+
end
|
48
|
+
i += 1
|
49
|
+
end
|
50
|
+
end
|
51
|
+
else
|
52
|
+
puts "D #{opts[:absolute] ? abspath : relpath}"
|
53
|
+
end
|
54
|
+
else
|
55
|
+
puts "X #{opts[:absolute] ? abspath : relpath}"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
index.find(path) do |abspath|
|
60
|
+
relpath = index.relpath(abspath)
|
61
|
+
unless index.contains?(relpath)
|
62
|
+
puts "A #{opts[:absolute] ? abspath : relpath}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
require 'find'
|
3
|
+
require 'fixi/index'
|
4
|
+
|
5
|
+
class Fixi::Command::Commit
|
6
|
+
def self.synopsis
|
7
|
+
"Commit modified files to the index"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.details
|
11
|
+
"This command is scoped to the current directory or the given path,
|
12
|
+
if specified.".pack
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute args
|
16
|
+
opts = Trollop::options args do
|
17
|
+
banner Fixi::Command.banner "commit"
|
18
|
+
opt :absolute, "Show absolute paths. By default, paths are reported
|
19
|
+
relative to the index root.".pack
|
20
|
+
opt :dry_run, "Don't do anything; just report what would be done"
|
21
|
+
opt :shallow, "Do shallow comparisons when determining which files have
|
22
|
+
changed. If specified, only file sizes and mtimes will be used. By
|
23
|
+
default, checksums will also be computed and compared if necessary.".pack
|
24
|
+
opt :verbose, "For modified files, show which attribute changed.
|
25
|
+
By default, only the path is shown.".pack
|
26
|
+
end
|
27
|
+
path = File.expand_path(args[0] || ".")
|
28
|
+
index = Fixi::Index.new(path)
|
29
|
+
|
30
|
+
index.each(args[0]) do |hash|
|
31
|
+
relpath = hash['relpath']
|
32
|
+
abspath = index.rootpath + '/' + relpath
|
33
|
+
if index.file_in_scope(relpath) && File.exists?(abspath)
|
34
|
+
size = File.size(abspath)
|
35
|
+
mtime = File.mtime(abspath).to_i
|
36
|
+
if size != hash['size']
|
37
|
+
detail = opts[:verbose] ? "size=#{size} " : ""
|
38
|
+
puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
|
39
|
+
index.update(relpath) unless opts[:dry_run]
|
40
|
+
elsif mtime != hash['mtime']
|
41
|
+
detail = opts[:verbose] ? "mtime=#{Time.at(mtime).utc.iso8601} " : ""
|
42
|
+
puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
|
43
|
+
index.update(relpath) unless opts[:dry_run]
|
44
|
+
elsif not opts[:shallow]
|
45
|
+
hexdigests = Fixi::hexdigests(Fixi::digests(index.algorithms), abspath)
|
46
|
+
i = 0
|
47
|
+
need_update = false
|
48
|
+
index.algorithms.split(',').each do |algorithm|
|
49
|
+
if not(need_update) && (hexdigests[i] != hash[algorithm])
|
50
|
+
need_update = true
|
51
|
+
detail = opts[:verbose] ? "#{algorithm}=#{hexdigests[i]} " : ""
|
52
|
+
puts "M #{detail}#{opts[:absolute] ? abspath : relpath}"
|
53
|
+
end
|
54
|
+
hash[algorithm] = hexdigests[i]
|
55
|
+
i += 1
|
56
|
+
end
|
57
|
+
index.update(relpath, hash) if need_update && not(opts[:dry_run])
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
require 'fixi/index'
|
3
|
+
|
4
|
+
class Fixi::Command::Info
|
5
|
+
def self.synopsis
|
6
|
+
"Display a summary of the index"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.details
|
10
|
+
"This command is scoped to the current directory or the given path,
|
11
|
+
if specified.".pack
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute args
|
15
|
+
opts = Trollop::options args do
|
16
|
+
banner Fixi::Command::banner "info"
|
17
|
+
end
|
18
|
+
Fixi::Index.new(args[0]).describe
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
require 'fixi/index'
|
3
|
+
|
4
|
+
class Fixi::Command::Init
|
5
|
+
def self.synopsis
|
6
|
+
"Create a new, empty index"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.details
|
10
|
+
"This command is scoped to the current directory or the given path,
|
11
|
+
if specified.".pack
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute args
|
15
|
+
opts = Trollop::options args do
|
16
|
+
banner Fixi::Command::banner "init"
|
17
|
+
opt :algorithms, "Checksum algorithm(s) to use for the index. This is
|
18
|
+
a comma-separated list, which may include md5, sha1, sha256, sha384, and
|
19
|
+
sha512.".pack, :default => "sha256", :short => 'l'
|
20
|
+
end
|
21
|
+
index = Fixi::Index.new(args[0], true, opts[:algorithms])
|
22
|
+
puts "Initialized empty index at #{index.dotpath}"
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'time'
|
2
|
+
require 'trollop'
|
3
|
+
|
4
|
+
class Fixi::Command::Ls
|
5
|
+
def self.synopsis
|
6
|
+
"List contents of the index"
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.details
|
10
|
+
"This command is scoped to the current directory or the given path,
|
11
|
+
if specified.".pack
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute args
|
15
|
+
opts = Trollop::options args do
|
16
|
+
banner Fixi::Command.banner "ls"
|
17
|
+
opt :absolute, "Show absolute paths. By default, paths are reported
|
18
|
+
relative to the index root.".pack
|
19
|
+
opt :json, "Like --verbose, but outputs the result as a json array."
|
20
|
+
opt :md5, "Restrict list to files with the given md5 checksum",
|
21
|
+
:type => :string, :short => :none
|
22
|
+
opt :sha1, "Restrict list to files with the given sha1 checksum",
|
23
|
+
:type => :string, :short => :none
|
24
|
+
opt :sha256, "Restrict list to files with the given sha256 checksum",
|
25
|
+
:type => :string, :short => :none
|
26
|
+
opt :sha384, "Restrict list to files with the given sha384 checksum",
|
27
|
+
:type => :string, :short => :none
|
28
|
+
opt :sha512, "Restrict list to files with the given sha512 checksum",
|
29
|
+
:type => :string, :short => :none
|
30
|
+
opt :verbose, "Include all information known about each file. By default,
|
31
|
+
only paths will be listed.".pack
|
32
|
+
end
|
33
|
+
index = Fixi::Index.new(args[0])
|
34
|
+
if opts[:json]
|
35
|
+
print "["
|
36
|
+
end
|
37
|
+
i = 0
|
38
|
+
index.each(args[0], opts) do |hash|
|
39
|
+
path = hash['relpath']
|
40
|
+
path = index.rootpath + '/' + path if opts[:absolute]
|
41
|
+
if opts[:verbose]
|
42
|
+
print "size=#{hash['size']},mtime=#{Time.at(hash['mtime']).utc.iso8601}"
|
43
|
+
print ",md5=#{hash['md5']}" if hash['md5']
|
44
|
+
print ",sha1=#{hash['sha1']}" if hash['sha1']
|
45
|
+
print ",sha256=#{hash['sha256']}" if hash['sha256']
|
46
|
+
print ",sha384=#{hash['sha384']}" if hash['sha384']
|
47
|
+
print ",sha512=#{hash['sha512']}" if hash['sha512']
|
48
|
+
puts " #{path}"
|
49
|
+
elsif opts[:json]
|
50
|
+
print "," if i > 0
|
51
|
+
puts "\n { path: \"#{path}\","
|
52
|
+
puts " size: \"#{hash['size']}\","
|
53
|
+
print " mtime: \"#{Time.at(hash['mtime']).utc.iso8601}\""
|
54
|
+
print ",\n md5: \"#{hash['md5']}\"" if hash['md5']
|
55
|
+
print ",\n sha1: \"#{hash['sha1']}\"" if hash['md5']
|
56
|
+
print ",\n sha256: \"#{hash['sha256']}\"" if hash['sha256']
|
57
|
+
print ",\n sha384: \"#{hash['sha384']}\"" if hash['sha384']
|
58
|
+
print ",\n sha512: \"#{hash['sha512']}\"" if hash['sha512']
|
59
|
+
print " }"
|
60
|
+
else
|
61
|
+
puts path
|
62
|
+
end
|
63
|
+
i += 1
|
64
|
+
end
|
65
|
+
if opts[:json]
|
66
|
+
print "\n" if i > 0
|
67
|
+
puts "]"
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
require 'find'
|
3
|
+
require 'fixi/index'
|
4
|
+
|
5
|
+
class Fixi::Command::Rm
|
6
|
+
def self.synopsis
|
7
|
+
"Delete old files from the index"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.details
|
11
|
+
"This command is scoped to the current directory or the given path,
|
12
|
+
if specified.".pack
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute args
|
16
|
+
opts = Trollop::options args do
|
17
|
+
banner Fixi::Command.banner "rm"
|
18
|
+
opt :absolute, "Show absolute paths. By default, paths are reported
|
19
|
+
relative to the index root.".pack
|
20
|
+
opt :dry_run, "Don't do anything; just report what would be done"
|
21
|
+
end
|
22
|
+
path = File.expand_path(args[0] || ".")
|
23
|
+
index = Fixi::Index.new(path)
|
24
|
+
|
25
|
+
index.each(args[0]) do |hash|
|
26
|
+
relpath = hash['relpath']
|
27
|
+
abspath = index.rootpath + '/' + relpath
|
28
|
+
if index.file_in_scope(relpath)
|
29
|
+
unless File.exists?(abspath)
|
30
|
+
puts "D #{opts[:absolute] ? abspath : relpath}"
|
31
|
+
index.delete relpath unless opts[:dry_run]
|
32
|
+
end
|
33
|
+
else
|
34
|
+
puts "X #{opts[:absolute] ? abspath : relpath}"
|
35
|
+
index.delete relpath unless opts[:dry_run]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'trollop'
|
2
|
+
require 'find'
|
3
|
+
require 'fixi/index'
|
4
|
+
|
5
|
+
class Fixi::Command::Sum
|
6
|
+
def self.synopsis
|
7
|
+
"Calculate checksum(s) of a file"
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.details
|
11
|
+
"This command operates on files and does not require an index to exist."
|
12
|
+
end
|
13
|
+
|
14
|
+
def execute args
|
15
|
+
opts = Trollop::options args do
|
16
|
+
banner Fixi::Command.banner "sum"
|
17
|
+
opt :algorithms, "Checksum algorithm(s) to use. This is a comma-separated
|
18
|
+
list, which may include md5, sha1, sha256, sha384, and sha512. At least
|
19
|
+
one must be specified.".pack, :short => 'l', :type => :string,
|
20
|
+
:required => true
|
21
|
+
end
|
22
|
+
unless args[0]
|
23
|
+
raise "Must specify a file."
|
24
|
+
exit 1
|
25
|
+
end
|
26
|
+
path = args[0]
|
27
|
+
unless File.exists?(path)
|
28
|
+
raise "No such file: #{path}"
|
29
|
+
exit 1
|
30
|
+
end
|
31
|
+
hexdigests = Fixi::hexdigests(Fixi::digests(opts[:algorithms]), path)
|
32
|
+
hexdigests.each { |hexdigest| puts "#{hexdigest}" }
|
33
|
+
end
|
34
|
+
end
|
data/lib/fixi/command.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
module Fixi::Command
|
2
|
+
def self.banner(name)
|
3
|
+
"fixi-#{name}: #{const_get(name.capitalize).synopsis}\n\n" +
|
4
|
+
"usage: fixi #{name} [<options>] [path]\n\n" +
|
5
|
+
"#{const_get(name.capitalize).details}\n\n" +
|
6
|
+
"Options:"
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
Dir.glob(File.dirname(__FILE__) + '/command/*') {|file| require file}
|
data/lib/fixi/index.rb
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
require 'sqlite3'
|
2
|
+
|
3
|
+
class Fixi::Index
|
4
|
+
attr_reader :dotpath, :rootpath, :dbversion, :algorithms
|
5
|
+
|
6
|
+
def initialize(startpath, create=false, algorithms=nil)
|
7
|
+
startpath = File.expand_path(startpath || ".")
|
8
|
+
unless File.directory?(startpath)
|
9
|
+
raise "No such file or directory: #{startpath}" unless File.exist?(startpath)
|
10
|
+
startpath = File.dirname(startpath)
|
11
|
+
end
|
12
|
+
if create
|
13
|
+
Fixi::digests(algorithms)
|
14
|
+
@dotpath = File.join(startpath, ".fixi")
|
15
|
+
raise "Index already exists at #{@dotpath}" if Dir.exist? @dotpath
|
16
|
+
Dir.mkdir @dotpath
|
17
|
+
@db = SQLite3::Database.new(File.join(@dotpath, "fixi.db"))
|
18
|
+
@dbversion = 1
|
19
|
+
@algorithms = algorithms
|
20
|
+
ddl = <<-EOS
|
21
|
+
create table fixi (
|
22
|
+
dbversion text,
|
23
|
+
algorithms text
|
24
|
+
);
|
25
|
+
create table file (
|
26
|
+
relpath text primary key,
|
27
|
+
size integer not null,
|
28
|
+
mtime integer not null,
|
29
|
+
md5 text,
|
30
|
+
sha1 text,
|
31
|
+
sha256 text,
|
32
|
+
sha384 text,
|
33
|
+
sha512 text
|
34
|
+
);
|
35
|
+
insert into fixi (dbversion, algorithms)
|
36
|
+
values (#{@dbversion}, "#{@algorithms}");
|
37
|
+
EOS
|
38
|
+
@db.execute_batch ddl
|
39
|
+
open(File.join(@dotpath, "includes"), "w") { |f| f.puts ".*" }
|
40
|
+
open(File.join(@dotpath, "excludes"), "w") { |f| f.puts "^\\.fixi\\/" }
|
41
|
+
else
|
42
|
+
@dotpath = find_dotpath(startpath)
|
43
|
+
@db = SQLite3::Database.new(File.join(@dotpath, "fixi.db"))
|
44
|
+
@db.execute("select dbversion, algorithms from fixi") do |row|
|
45
|
+
@dbversion = row[0]
|
46
|
+
@algorithms = row[1]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
@includes = load_patterns("includes")
|
50
|
+
@excludes = load_patterns("excludes")
|
51
|
+
@rootpath = File.expand_path(File.join(@dotpath, ".."))
|
52
|
+
@db.results_as_hash = true
|
53
|
+
end
|
54
|
+
|
55
|
+
# Traverse the given path within @rootdir and return all files
|
56
|
+
# of interest (those matching @includes and not matching @excludes)
|
57
|
+
def find(path)
|
58
|
+
Find.find(path) do |abspath|
|
59
|
+
relpath = relpath(abspath)
|
60
|
+
if relpath.length > 0
|
61
|
+
if matches_any?(relpath, @includes)
|
62
|
+
if matches_any?(relpath, @excludes)
|
63
|
+
Find.prune
|
64
|
+
else
|
65
|
+
yield abspath unless File.directory?(abspath)
|
66
|
+
end
|
67
|
+
else
|
68
|
+
Find.prune unless File.directory?(abspath)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def file_in_scope(relpath)
|
75
|
+
return matches_any?(relpath, @includes) &&
|
76
|
+
not(matches_any?(relpath, @excludes))
|
77
|
+
end
|
78
|
+
|
79
|
+
def each(path=nil, attribs={})
|
80
|
+
sql = "select * from file"
|
81
|
+
conditions = []
|
82
|
+
if path && File.expand_path(path) != @rootpath
|
83
|
+
relpath = relpath(File.expand_path(path))
|
84
|
+
if File.directory?(path)
|
85
|
+
conditions << " relpath like '#{relpath}/%'"
|
86
|
+
else
|
87
|
+
conditions << " relpath = '#{relpath}'"
|
88
|
+
end
|
89
|
+
end
|
90
|
+
attribs.each do |name,value|
|
91
|
+
if value && (name == :size || name == :mtime ||
|
92
|
+
name == :md5 || name == :sha1 || name == :sha256 ||
|
93
|
+
name == :sha384 || name == :sha512)
|
94
|
+
c = "#{name} = "
|
95
|
+
c += value.is_a?(Numeric) ? "#{value}" : "'#{value}'"
|
96
|
+
conditions << c
|
97
|
+
end
|
98
|
+
end
|
99
|
+
unless conditions.size == 0
|
100
|
+
sql += " where"
|
101
|
+
conditions.each do |c|
|
102
|
+
sql += " and" unless c == conditions.first
|
103
|
+
sql += " #{c}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
@db.execute(sql) do |hash|
|
107
|
+
yield hash
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def size
|
112
|
+
@db.get_first_value("select count(*) from file")
|
113
|
+
end
|
114
|
+
|
115
|
+
def contains?(relpath)
|
116
|
+
@db.get_first_value("select count(*) from file where relpath = ?", relpath) > 0
|
117
|
+
end
|
118
|
+
|
119
|
+
def relpath(abspath)
|
120
|
+
return "" if abspath == @rootpath
|
121
|
+
abspath.slice(@rootpath.length + 1..-1)
|
122
|
+
end
|
123
|
+
|
124
|
+
def update(relpath, hash=nil)
|
125
|
+
abspath = File.join(@rootpath, relpath)
|
126
|
+
unless hash
|
127
|
+
hash = Hash.new
|
128
|
+
hash['size'] = File.size(abspath)
|
129
|
+
hash['mtime'] = File.mtime(abspath).to_i
|
130
|
+
hexdigests = Fixi::hexdigests(Fixi::digests(@algorithms), abspath)
|
131
|
+
i = 0
|
132
|
+
@algorithms.split(',').each do |algorithm|
|
133
|
+
hash[algorithm] = hexdigests[i]
|
134
|
+
end
|
135
|
+
end
|
136
|
+
sql = "update file set size = #{hash['size']}, mtime = #{hash['mtime']}"
|
137
|
+
@algorithms.split(',').each do |algorithm|
|
138
|
+
sql += ", #{algorithm} = '#{hash[algorithm]}'"
|
139
|
+
end
|
140
|
+
sql += " where relpath = ?"
|
141
|
+
@db.execute(sql, relpath)
|
142
|
+
end
|
143
|
+
|
144
|
+
def add(relpath)
|
145
|
+
abspath = File.join(@rootpath, relpath)
|
146
|
+
sql = "insert into file (relpath, size, mtime, "
|
147
|
+
sql += @algorithms
|
148
|
+
sql += ") values (:relpath, :size, :mtime, "
|
149
|
+
values = Hash.new
|
150
|
+
values[:relpath] = relpath
|
151
|
+
values[:size] = File.size abspath
|
152
|
+
values[:mtime] = File.mtime(abspath).to_i
|
153
|
+
hexdigests = Fixi::hexdigests(Fixi::digests(@algorithms), abspath)
|
154
|
+
i = 0
|
155
|
+
@algorithms.split(",").each do |alg|
|
156
|
+
sql += ", " if i > 0
|
157
|
+
sql += "'" + hexdigests[i] + "'"
|
158
|
+
i += 1
|
159
|
+
end
|
160
|
+
sql += ")"
|
161
|
+
@db.execute(sql, values)
|
162
|
+
end
|
163
|
+
|
164
|
+
def delete(relpath)
|
165
|
+
@db.execute("delete from file where relpath = ?", relpath)
|
166
|
+
end
|
167
|
+
|
168
|
+
def describe
|
169
|
+
puts "#{size} files indexed at #{@dotpath}"
|
170
|
+
puts "Using checksum algorithm(s) [#{@algorithms}]"
|
171
|
+
puts "Fixi database version #{dbversion}"
|
172
|
+
end
|
173
|
+
|
174
|
+
private
|
175
|
+
|
176
|
+
def load_patterns(name)
|
177
|
+
result = []
|
178
|
+
f = File.join(@dotpath, name)
|
179
|
+
if File.exists? f
|
180
|
+
File.foreach(f) do |line|
|
181
|
+
line = line.chomp
|
182
|
+
result << Regexp.new(line) if line.length > 0
|
183
|
+
end
|
184
|
+
end
|
185
|
+
result
|
186
|
+
end
|
187
|
+
|
188
|
+
def matches_any?(path, patterns)
|
189
|
+
patterns.each do |pattern|
|
190
|
+
return true if pattern.match(path)
|
191
|
+
end
|
192
|
+
return false
|
193
|
+
end
|
194
|
+
|
195
|
+
# Return the first .fixi directory we find while traversing up the tree
|
196
|
+
def find_dotpath(path, startpath=path)
|
197
|
+
dotpath = File.join(path, ".fixi")
|
198
|
+
return dotpath if Dir.exist?(dotpath)
|
199
|
+
parent = File.dirname(path)
|
200
|
+
return find_dotpath(parent, startpath) unless parent == path
|
201
|
+
raise "No index at #{startpath} or any parent"
|
202
|
+
end
|
203
|
+
end
|
data/lib/fixi/version.rb
ADDED
data/lib/fixi.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "digest"
|
2
|
+
require "fixi/version"
|
3
|
+
require "fixi/patch/string_pack"
|
4
|
+
require "fixi/command"
|
5
|
+
|
6
|
+
module Fixi
|
7
|
+
# Get an instance of the command with the given name
|
8
|
+
def self.command(name)
|
9
|
+
return nil unless Command.const_defined? name.capitalize
|
10
|
+
Command.const_get(name.capitalize).new
|
11
|
+
end
|
12
|
+
|
13
|
+
# Validate the given comma-separated list of checksum algorithms
|
14
|
+
# and return and array of matching Digest implementations
|
15
|
+
def self.digests(checksums)
|
16
|
+
digests = []
|
17
|
+
checksums.split(",").each do |checksum|
|
18
|
+
begin
|
19
|
+
digests << Digest(checksum.upcase).new
|
20
|
+
rescue LoadError
|
21
|
+
raise "No such algorithm: #{checksum}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
digests
|
25
|
+
end
|
26
|
+
|
27
|
+
# Read the file once while computing any number of digests
|
28
|
+
def self.hexdigests(digests, file)
|
29
|
+
File.open(file, "rb") {|f|
|
30
|
+
buf = ""
|
31
|
+
while f.read(16384, buf)
|
32
|
+
digests.each {|digest| digest.update buf}
|
33
|
+
end
|
34
|
+
}
|
35
|
+
hds = []
|
36
|
+
digests.each {|digest|
|
37
|
+
hd = digest.hexdigest
|
38
|
+
hds << hd
|
39
|
+
}
|
40
|
+
hds
|
41
|
+
end
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fixi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Chris Wilper
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-12-08 00:00:00 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: trollop
|
17
|
+
prerelease: false
|
18
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ">="
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: "0"
|
24
|
+
type: :runtime
|
25
|
+
version_requirements: *id001
|
26
|
+
description: Keeps an index of checksums and lets you update and verify them
|
27
|
+
email:
|
28
|
+
- cwilper@gmail.com
|
29
|
+
executables:
|
30
|
+
- fixi
|
31
|
+
extensions: []
|
32
|
+
|
33
|
+
extra_rdoc_files: []
|
34
|
+
|
35
|
+
files:
|
36
|
+
- .gitignore
|
37
|
+
- Gemfile
|
38
|
+
- Rakefile
|
39
|
+
- bin/fixi
|
40
|
+
- fixi.gemspec
|
41
|
+
- lib/fixi.rb
|
42
|
+
- lib/fixi/command.rb
|
43
|
+
- lib/fixi/command/add.rb
|
44
|
+
- lib/fixi/command/check.rb
|
45
|
+
- lib/fixi/command/commit.rb
|
46
|
+
- lib/fixi/command/info.rb
|
47
|
+
- lib/fixi/command/init.rb
|
48
|
+
- lib/fixi/command/ls.rb
|
49
|
+
- lib/fixi/command/rm.rb
|
50
|
+
- lib/fixi/command/sum.rb
|
51
|
+
- lib/fixi/index.rb
|
52
|
+
- lib/fixi/patch/string_pack.rb
|
53
|
+
- lib/fixi/version.rb
|
54
|
+
homepage: ""
|
55
|
+
licenses: []
|
56
|
+
|
57
|
+
post_install_message:
|
58
|
+
rdoc_options: []
|
59
|
+
|
60
|
+
require_paths:
|
61
|
+
- lib
|
62
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: "0"
|
68
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
69
|
+
none: false
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: "0"
|
74
|
+
requirements: []
|
75
|
+
|
76
|
+
rubyforge_project: fixi
|
77
|
+
rubygems_version: 1.8.11
|
78
|
+
signing_key:
|
79
|
+
specification_version: 3
|
80
|
+
summary: A fixity tracker utility
|
81
|
+
test_files: []
|
82
|
+
|