snoopy 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/bin/snoopy +97 -0
- data/lib/ftp.rb +22 -0
- data/lib/http.rb +94 -0
- data/lib/https.rb +113 -0
- data/lib/snoopy.rb +22 -0
- data/lib/source.rb +50 -0
- data/lib/torrent.rb +90 -0
- metadata +76 -0
data/bin/snoopy
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
#
|
3
|
+
# Created by me on 2007-12-24.
|
4
|
+
# Copyright (c) 2007. All pwnage reserved.
|
5
|
+
|
6
|
+
require "rubygems"
|
7
|
+
require "snoopy"
|
8
|
+
|
9
|
+
##################################################################################
|
10
|
+
|
11
|
+
opts = Trollop::options do
|
12
|
+
version "snoopy 0.0.4 (c) Ari Brown"
|
13
|
+
banner <<-EOS
|
14
|
+
snoopy is a multithreaded golden retriever for the internet
|
15
|
+
--> handles http, ftp (single threaded), and torrent
|
16
|
+
|
17
|
+
Usage:
|
18
|
+
snoopy <URI of file> [options]
|
19
|
+
Example:
|
20
|
+
snoopy http://www.lighttpd.net -t 5 option
|
21
|
+
where [options] are:
|
22
|
+
EOS
|
23
|
+
|
24
|
+
opt :verbose, "Be as loud as possible"
|
25
|
+
opt :clean, "Remove the out file after finishing (debugging)"
|
26
|
+
opt :out, "Where to save the file", :type => String
|
27
|
+
opt :threads, "Set number of threads to use", :default => 5
|
28
|
+
opt :dots, "Display a dot every X times a block comes in", :default => 10
|
29
|
+
end
|
30
|
+
Trollop::die "need a file to download" if ARGV.empty?
|
31
|
+
|
32
|
+
##################################################################################
|
33
|
+
|
34
|
+
opts[:in] = ARGV[0]
|
35
|
+
|
36
|
+
url = URI.parse(opts[:in])
|
37
|
+
|
38
|
+
ball = Source.new(url, opts)
|
39
|
+
|
40
|
+
def start_report(opts)
|
41
|
+
puts Time.now
|
42
|
+
puts "Downloading #{opts[:in]}"
|
43
|
+
puts "with #{opts[:threads]} thread" + (opts[:threads] > 1 ? "s" : '')
|
44
|
+
puts
|
45
|
+
end
|
46
|
+
|
47
|
+
def determine_size(value)
|
48
|
+
if value >= 1073741824
|
49
|
+
end_value = value / 1073741824
|
50
|
+
unit = 'GB'
|
51
|
+
elsif value >= 1048576 and value < 1073741824
|
52
|
+
end_value = value / 1048576
|
53
|
+
unit = 'MB'
|
54
|
+
elsif value >= 1024 and value < 1048576
|
55
|
+
end_value = value / 1024
|
56
|
+
unit = 'KB'
|
57
|
+
else
|
58
|
+
end_value = value
|
59
|
+
unit = 'B'
|
60
|
+
end
|
61
|
+
|
62
|
+
[end_value, unit, value]
|
63
|
+
end
|
64
|
+
|
65
|
+
def end_report(source, opts)
|
66
|
+
puts
|
67
|
+
puts "Download file written to " + opts[:out] + (opts[:clean] ? ", and then deleted" : '')
|
68
|
+
puts "Elapsed time: " + $elapsed_time.to_s + " seconds"
|
69
|
+
|
70
|
+
end_value, unit, actual = determine_size(source.length)
|
71
|
+
|
72
|
+
if end_value.zero?
|
73
|
+
end_value, unit, actual = determine_size(File.size(opts[:out]))
|
74
|
+
end
|
75
|
+
|
76
|
+
puts "Total downloaded: " + end_value.to_s + " " + unit
|
77
|
+
res = determine_size(actual/$elapsed_time)
|
78
|
+
puts "At %.2f%s/s" % [res[0], res[1]]
|
79
|
+
|
80
|
+
source.finish
|
81
|
+
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
|
87
|
+
start_report(opts)
|
88
|
+
##################################################################################
|
89
|
+
$start_time = Time.now
|
90
|
+
##################################################################################
|
91
|
+
|
92
|
+
ball.roll
|
93
|
+
|
94
|
+
##################################################################################
|
95
|
+
$elapsed_time = Time.now - $start_time
|
96
|
+
##################################################################################
|
97
|
+
end_report(ball, opts)
|
data/lib/ftp.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
#
|
3
|
+
# Created by me on 2007-12-28.
|
4
|
+
# Copyright (c) 2007. All pwnage reserved.
|
5
|
+
|
6
|
+
module FTP
|
7
|
+
|
8
|
+
def setup(opts, uri)
|
9
|
+
@opts = opts
|
10
|
+
end
|
11
|
+
|
12
|
+
def prep
|
13
|
+
end
|
14
|
+
|
15
|
+
def fetch
|
16
|
+
Net::FTP.open(@opts[:in]) {|ftp| ftp.getbinaryfile(@opts[:in], @opts[:out]) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def clean
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
data/lib/http.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
#
|
3
|
+
# Created by me on 2007-12-28.
|
4
|
+
# Copyright (c) 2007. All pwnage reserved.
|
5
|
+
|
6
|
+
module HTTP
|
7
|
+
class << self
|
8
|
+
def setup(opts, uri)
|
9
|
+
@opts = opts
|
10
|
+
@url = uri
|
11
|
+
end
|
12
|
+
|
13
|
+
def prep
|
14
|
+
req = Net::HTTP.new(@url.host, @url.port)
|
15
|
+
len = req.request_head(@url.page)['content-length']
|
16
|
+
@url.length = len.to_i
|
17
|
+
|
18
|
+
if len.nil? or @url.length == 0
|
19
|
+
@opts[:threads] = 1
|
20
|
+
puts "--> Uhoh! Server isn't cooperating..."
|
21
|
+
puts "--> Changing threadcount to 1"
|
22
|
+
alias_method :fetch_multi, :fetch
|
23
|
+
alias_method :fetch, :fetch_single
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def fetch_single
|
28
|
+
p single
|
29
|
+
open(@opts[:out], "w") do |f|
|
30
|
+
http = Net::HTTP.new(@url.host, @url.port)
|
31
|
+
http.get(@url.page) do |str|
|
32
|
+
f.write str
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch
|
38
|
+
i = 0
|
39
|
+
array = []
|
40
|
+
start = 0
|
41
|
+
jump = @url.length / @opts[:threads]
|
42
|
+
finish = -1 # -1 so that start becomes 0
|
43
|
+
|
44
|
+
@opts[:threads].times do |t|
|
45
|
+
start = finish + 1
|
46
|
+
finish = (start + jump) - 1
|
47
|
+
array << [start, finish, t + 1]
|
48
|
+
end
|
49
|
+
|
50
|
+
array.each_simultaneously { |d, b, w|
|
51
|
+
puts "[LOG] Setting up a new thread..." if @opts[:verbose]
|
52
|
+
open(@opts[:out], "a+") do |f|
|
53
|
+
begin
|
54
|
+
f.seek(d)
|
55
|
+
Net::HTTP.new(@url.host, @url.port).start do |http|
|
56
|
+
http.get(@url.page, {'range' => "bytes=#{d}-#{b}"}) do |str|
|
57
|
+
i += 1
|
58
|
+
print '.' if i % @opts[:dots] == 0
|
59
|
+
STDOUT.flush
|
60
|
+
f.write str if str
|
61
|
+
end
|
62
|
+
end
|
63
|
+
rescue Timeout::Error => e
|
64
|
+
print "\n[Error] Error in server response in thread #{w}. Retrying!\n"
|
65
|
+
puts e
|
66
|
+
STDOUT.flush
|
67
|
+
sleep 5
|
68
|
+
retry
|
69
|
+
rescue => e
|
70
|
+
print "\n[Error] Error in server response in thread #{w}. Retrying!\n"
|
71
|
+
puts e
|
72
|
+
STDOUT.flush
|
73
|
+
sleep 5
|
74
|
+
retry
|
75
|
+
end
|
76
|
+
end
|
77
|
+
puts "[LOG] Thread #{w} finished!" if @opts[:verbose]
|
78
|
+
}
|
79
|
+
end
|
80
|
+
|
81
|
+
def clean
|
82
|
+
remove_extra_files
|
83
|
+
end
|
84
|
+
|
85
|
+
def remove_extra_files
|
86
|
+
puts "[LOG] Writing #{@opts[:out]}" if @opts[:verbose]
|
87
|
+
|
88
|
+
if @opts[:clean]
|
89
|
+
File.delete(@opts[:out])
|
90
|
+
puts "[LOG] Removing #{@opts[:out]}" if @opts[:verbose]
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
data/lib/https.rb
ADDED
@@ -0,0 +1,113 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
#
|
3
|
+
# Created by me on 2007-12-28.
|
4
|
+
# Copyright (c) 2007. All pwnage reserved.
|
5
|
+
|
6
|
+
module HTTPS
|
7
|
+
class << self
|
8
|
+
def setup(opts, uri)
|
9
|
+
@opts = opts
|
10
|
+
@url = uri
|
11
|
+
end
|
12
|
+
|
13
|
+
def prep
|
14
|
+
req = Net::HTTP.new(@url.host, @url.port)
|
15
|
+
req.use_ssl = true
|
16
|
+
len = req.request_head(@url.page)['content-length']
|
17
|
+
@url.length = len.to_i
|
18
|
+
|
19
|
+
if len.nil?
|
20
|
+
@opts[:threads] = 1
|
21
|
+
puts "--> Uhoh! Server isn't cooperating..."
|
22
|
+
puts "--> Changing threadcount to 1"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def fetch
|
27
|
+
array = []
|
28
|
+
start = 0
|
29
|
+
jump = @url.length / @opts[:threads]
|
30
|
+
finish = -1 # -1 so that start becomes 0
|
31
|
+
|
32
|
+
@opts[:threads].times do |t|
|
33
|
+
start = finish + 1
|
34
|
+
finish = (start + jump) - 1
|
35
|
+
array << [start, finish, t + 1]
|
36
|
+
end
|
37
|
+
|
38
|
+
array.each_simultaneously { |d, b, w|
|
39
|
+
puts "[LOG] Setting up a new thread..." if @opts[:verbose]
|
40
|
+
p @opts[:out]
|
41
|
+
open(@opts[:out], "a+") do |f|
|
42
|
+
f.seek(d)
|
43
|
+
http = Net::HTTP.new(@url.host, @url.port)
|
44
|
+
http.use_ssl = true
|
45
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
46
|
+
c = http.start do |http|
|
47
|
+
a = Net::HTTP::Get.new(@url.page)
|
48
|
+
a.range = (d..b)
|
49
|
+
http.request(a)
|
50
|
+
end
|
51
|
+
f.write(c.body)
|
52
|
+
end
|
53
|
+
}
|
54
|
+
end
|
55
|
+
=begin
|
56
|
+
def fetch_fat
|
57
|
+
array = []
|
58
|
+
start = 0
|
59
|
+
jump = @url.length / @opts[:threads]
|
60
|
+
finish = -1 # -1 so that start becomes 0
|
61
|
+
|
62
|
+
@opts[:threads].times do |t|
|
63
|
+
start = finish + 1
|
64
|
+
finish = (start + jump) - 1
|
65
|
+
array << [start, finish, t + 1]
|
66
|
+
end
|
67
|
+
|
68
|
+
array.each_simultaneously { |d, b, w|
|
69
|
+
puts "[LOG] Setting up a new thread..." if @opts[:verbose]
|
70
|
+
open("/tmp/part#{w}", "w") do |f|
|
71
|
+
http = Net::HTTP.new(@url.host, @url.port)
|
72
|
+
c = http.start do |http|
|
73
|
+
a = Net::HTTP::Get.new(@url.page)
|
74
|
+
a.range = (d..b)
|
75
|
+
http.request(a)
|
76
|
+
end
|
77
|
+
f.write(c.body)
|
78
|
+
end
|
79
|
+
}
|
80
|
+
end
|
81
|
+
=end
|
82
|
+
def clean
|
83
|
+
mash_files
|
84
|
+
remove_extra_files
|
85
|
+
end
|
86
|
+
|
87
|
+
def remove_extra_files
|
88
|
+
puts "[LOG] Writing #{@opts[:out]}" if @opts[:verbose]
|
89
|
+
|
90
|
+
Dir["/tmp/part*"].each do |part|
|
91
|
+
File.delete(part)
|
92
|
+
puts "[LOG] Removing #{part}" if @opts[:verbose]
|
93
|
+
end
|
94
|
+
|
95
|
+
if @opts[:clean]
|
96
|
+
File.delete(@opts[:out])
|
97
|
+
puts "[LOG] Removing #{@opts[:out]}" if @opts[:verbose]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def mash_files
|
102
|
+
open(@opts[:out], "a") do |f|
|
103
|
+
files = Dir["/tmp/part*"].sort_by { |word|
|
104
|
+
word.scan(/\d+/)[0].to_i
|
105
|
+
}
|
106
|
+
files.each do |part|
|
107
|
+
puts "[LOG] Reading #{part}" if @opts[:verbose]
|
108
|
+
f.print open(part, "r").read
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/lib/snoopy.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "trollop"
|
3
|
+
require "net/http"
|
4
|
+
require "net/https"
|
5
|
+
require "net/ftp"
|
6
|
+
require "rubytorrent"
|
7
|
+
require "tempfile"
|
8
|
+
require "open-uri"
|
9
|
+
|
10
|
+
require File.join(File.dirname(__FILE__), "source")
|
11
|
+
require File.join(File.dirname(__FILE__), "http")
|
12
|
+
require File.join(File.dirname(__FILE__), "https")
|
13
|
+
require File.join(File.dirname(__FILE__), "ftp")
|
14
|
+
|
15
|
+
|
16
|
+
module Enumerable
|
17
|
+
def each_simultaneously
|
18
|
+
threads = []
|
19
|
+
each { |e, f, i| threads << Thread.new { yield e, f, i } }
|
20
|
+
return threads.each {|t| t.join }
|
21
|
+
end
|
22
|
+
end
|
data/lib/source.rb
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
#
|
3
|
+
# Created by me on 2007-12-28.
|
4
|
+
# Copyright (c) 2007. All pwnage reserved.
|
5
|
+
|
6
|
+
class Source
|
7
|
+
|
8
|
+
attr_accessor :port, :scheme, :host, :path, :length, :page, :full
|
9
|
+
|
10
|
+
def initialize(uri_obj, opts)
|
11
|
+
@uri = uri_obj
|
12
|
+
@host = uri_obj.host
|
13
|
+
@port = uri_obj.port
|
14
|
+
@scheme = uri_obj.scheme
|
15
|
+
@page = (uri_obj.path =~ /^$/ ? "/" : uri_obj.path)
|
16
|
+
@full = @scheme + '://' + @host + @page
|
17
|
+
@in = opts[:in]
|
18
|
+
@out = opts[:out]
|
19
|
+
opts[:out] = opts[:out] || (File.basename(@page) =~ /^\/$/ ? 'default' : File.basename(@page))
|
20
|
+
opts[:out] = (File.directory?(opts[:out]) ? File.join(opts[:out], File.basename(@page)) : opts[:out])
|
21
|
+
|
22
|
+
case @scheme
|
23
|
+
when "http" then @prefix = HTTP
|
24
|
+
when "https" then @prefix = HTTPS
|
25
|
+
when "ftp" then @prefix = FTP
|
26
|
+
when "torrent" then @prefix = Torrent
|
27
|
+
else
|
28
|
+
abort "Unknown URL scheme!\nsnoopy --help for help and usage"
|
29
|
+
end
|
30
|
+
|
31
|
+
prep(opts)
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
def start
|
36
|
+
@prefix.prep
|
37
|
+
@prefix.fetch
|
38
|
+
end
|
39
|
+
alias_method :roll, :start
|
40
|
+
|
41
|
+
def finish
|
42
|
+
@prefix.clean
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def prep(opts)
|
47
|
+
@prefix.setup(opts, self)
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
data/lib/torrent.rb
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
#!/usr/local/bin/ruby
|
2
|
+
#
|
3
|
+
# Created by me on 2007-12-28.
|
4
|
+
# Copyright (c) 2007. All pwnage reserved.
|
5
|
+
|
6
|
+
module Torrent
|
7
|
+
class << self
|
8
|
+
|
9
|
+
def setup(opts, uri)
|
10
|
+
@opts = opts
|
11
|
+
@url = uri
|
12
|
+
end
|
13
|
+
|
14
|
+
def prep
|
15
|
+
begin
|
16
|
+
mi = RubyTorrent::MetaInfo.from_location(File.join(@url.host, @url.path), ENV["http_proxy"])
|
17
|
+
rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
|
18
|
+
abort "[Error] Can't parse metainfo file '#{torrent}' -- maybe not a .torrent?"
|
19
|
+
rescue RubyTorrent::TypedStructError => e
|
20
|
+
raise e
|
21
|
+
rescue IOError, SystemCallError => e
|
22
|
+
abort "[Error] Can't read file '#{torrent}': #{e.message}"
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
if FileTest.directory?(@opts[:out]) && mi.info.single?
|
27
|
+
dest = File.join(@opts[:out], mi.info.name)
|
28
|
+
elsif FileTest.file?(@opts[:out]) && mi.info.multiple?
|
29
|
+
abort "[Error] .torrent contains multiple files, but '#{dest}' is a single file (must be a directory)"
|
30
|
+
end
|
31
|
+
dest ||= @opts[:out]
|
32
|
+
|
33
|
+
print "[LOG] Checking file status: " if @opts[:verbose]
|
34
|
+
package = RubyTorrent::Package.new(mi, dest) do |piece|
|
35
|
+
print(piece.complete? && piece.valid? ? "#" : ".") if @opts[:verbose]
|
36
|
+
end
|
37
|
+
puts " done" if @opts[:verbose]
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch
|
41
|
+
puts "[Log] Starting peer..." if @opts[:verbose]
|
42
|
+
bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy)
|
43
|
+
|
44
|
+
if @opts[:verbose]
|
45
|
+
bt.on_event(self, :added_peer) { |s, p| puts "[Log] Connected to peer #{p}" }
|
46
|
+
bt.on_event(self, :received_block) { |s, b, peer| puts "<-- got block #{b} from peer #{peer}, now #{package.pieces[b.pindex].percent_done.f}% done and #{package.pieces[b.pindex].percent_claimed.f}% claimed" }
|
47
|
+
bt.on_event(self, :sent_block) { |s, b, peer| puts "--> Sent block #{b} to peer #{peer}" }
|
48
|
+
bt.on_event(self, :discarded_piece) { |s, p| puts "[Log] Checksum error on piece #{p}, discarded" }
|
49
|
+
bt.on_event(self, :tracker_connected) { |s, url| puts "--> Connected to tracker #{url}" }
|
50
|
+
bt.on_event(self, :tracker_lost) { |s, url| puts "--> Couldn't connect to tracker #{url}" }
|
51
|
+
end
|
52
|
+
|
53
|
+
# Always register this
|
54
|
+
bt.on_event(self, :complete) do
|
55
|
+
@url.length = bt.dlamt
|
56
|
+
puts "--> Download complete!"
|
57
|
+
end
|
58
|
+
|
59
|
+
puts "[Log] Listening on #{bt.ip}:#{bt.port}" if @opts[:verbose]
|
60
|
+
|
61
|
+
|
62
|
+
thread = Thread.new do
|
63
|
+
until bt.complete?
|
64
|
+
previous = ''
|
65
|
+
|
66
|
+
if bt.tracker
|
67
|
+
|
68
|
+
current = '%s: %dk of %dk (%.2f%% complete)' % [Time.now,
|
69
|
+
bt.bytes_completed / 1024,
|
70
|
+
bt.total_bytes / 1024,
|
71
|
+
bt.percent_completed]
|
72
|
+
print "\b" * previous.size
|
73
|
+
print curent
|
74
|
+
previous = ''
|
75
|
+
|
76
|
+
sleep(5)
|
77
|
+
else
|
78
|
+
sleep(5)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
thread.join
|
84
|
+
end
|
85
|
+
|
86
|
+
def clean
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
end
|
metadata
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: snoopy
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Ari Brown
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-01-14 00:00:00 -05:00
|
13
|
+
default_executable: snoopy
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rubytorrent
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: "0.3"
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: trollop
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 0.0.1
|
32
|
+
version:
|
33
|
+
description: Segmented downloader for HTTP(S)/FTP/Bit Torrent.
|
34
|
+
email: ari@aribrown.com
|
35
|
+
executables:
|
36
|
+
- snoopy
|
37
|
+
extensions: []
|
38
|
+
|
39
|
+
extra_rdoc_files: []
|
40
|
+
|
41
|
+
files:
|
42
|
+
- lib/ftp.rb
|
43
|
+
- lib/http.rb
|
44
|
+
- lib/https.rb
|
45
|
+
- lib/snoopy.rb
|
46
|
+
- lib/source.rb
|
47
|
+
- lib/torrent.rb
|
48
|
+
- bin/snoopy
|
49
|
+
has_rdoc: false
|
50
|
+
homepage: http://snoopy.rubyforge.org/
|
51
|
+
post_install_message:
|
52
|
+
rdoc_options: []
|
53
|
+
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: "0"
|
61
|
+
version:
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: "0"
|
67
|
+
version:
|
68
|
+
requirements: []
|
69
|
+
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.0.0
|
72
|
+
signing_key:
|
73
|
+
specification_version: 2
|
74
|
+
summary: Snoopy is a golden retriever for the internet. It's a great big world and there... And you need a great big dog to do your bidding.
|
75
|
+
test_files: []
|
76
|
+
|