rubytorrent 0.3
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/COPYING +340 -0
- data/README +21 -0
- data/ReleaseNotes.txt +25 -0
- data/doc/api.txt +289 -0
- data/doc/design.txt +59 -0
- data/dump-metainfo.rb +55 -0
- data/dump-peers.rb +45 -0
- data/lib/rubytorrent.rb +94 -0
- data/lib/rubytorrent/bencoding.rb +174 -0
- data/lib/rubytorrent/controller.rb +610 -0
- data/lib/rubytorrent/message.rb +128 -0
- data/lib/rubytorrent/metainfo.rb +214 -0
- data/lib/rubytorrent/package.rb +595 -0
- data/lib/rubytorrent/peer.rb +536 -0
- data/lib/rubytorrent/server.rb +166 -0
- data/lib/rubytorrent/tracker.rb +225 -0
- data/lib/rubytorrent/typedstruct.rb +132 -0
- data/lib/rubytorrent/util.rb +186 -0
- data/make-metainfo.rb +211 -0
- data/rtpeer-ncurses.rb +340 -0
- data/rtpeer.rb +125 -0
- metadata +78 -0
@@ -0,0 +1,186 @@
|
|
1
|
+
## util.rb -- miscellaneous RubyTorrent utility modules.
|
2
|
+
## Copyright 2005 William Morgan.
|
3
|
+
##
|
4
|
+
## This file is part of RubyTorrent. RubyTorrent is free software;
|
5
|
+
## you can redistribute it and/or modify it under the terms of version
|
6
|
+
## 2 of the GNU General Public License as published by the Free
|
7
|
+
## Software Foundation.
|
8
|
+
##
|
9
|
+
## RubyTorrent is distributed in the hope that it will be useful, but
|
10
|
+
## WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
12
|
+
## General Public License (in the file COPYING) for more details.
|
13
|
+
|
14
|
+
def rt_debug(*args)
|
15
|
+
if $DEBUG || RubyTorrent.log
|
16
|
+
stream = RubyTorrent.log || $stdout
|
17
|
+
stream << args.join << "\n"
|
18
|
+
stream.flush
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def rt_warning(*args)
|
23
|
+
if $DEBUG || RubyTorrent.log
|
24
|
+
stream = RubyTorrent.log || $stderr
|
25
|
+
stream << "warning: " << args.join << "\n"
|
26
|
+
stream.flush
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
module RubyTorrent
|
31
|
+
|
32
|
+
@log = nil
|
33
|
+
def log_output_to(fn)
|
34
|
+
@log = File.open(fn, "w")
|
35
|
+
end
|
36
|
+
attr_reader :log
|
37
|
+
module_function :log_output_to, :log
|
38
|
+
|
39
|
+
|
40
|
+
## parse final hash of pseudo-keyword arguments
|
41
|
+
def get_args(rest, *names)
|
42
|
+
hash = rest.find { |x| x.is_a? Hash }
|
43
|
+
if hash
|
44
|
+
rest.delete hash
|
45
|
+
hash.each { |k, v| raise ArgumentError, %{unknown argument "#{k}"} unless names.include?(k) }
|
46
|
+
end
|
47
|
+
|
48
|
+
[hash || {}, rest]
|
49
|
+
end
|
50
|
+
module_function :get_args
|
51
|
+
|
52
|
+
## "events": very similar to Observable, but cleaner, IMO. events are
|
53
|
+
## listened to and sent in instance space, but registered in class
|
54
|
+
## space. example:
|
55
|
+
##
|
56
|
+
## class C
|
57
|
+
## include EventSource
|
58
|
+
## event :goat, :boat
|
59
|
+
##
|
60
|
+
## def send_events
|
61
|
+
## send_event :goat
|
62
|
+
## send_event(:boat, 3)
|
63
|
+
## end
|
64
|
+
## end
|
65
|
+
##
|
66
|
+
## c = C.new
|
67
|
+
## c.on_event(:goat) { puts "got goat!" }
|
68
|
+
## c.on_event(:boat) { |x| puts "got boat: #{x}" }
|
69
|
+
##
|
70
|
+
## Defining them in class space is not really necessary, except as an
|
71
|
+
## error-checking mechanism.
|
72
|
+
module EventSource
|
73
|
+
def on_event(who, *events, &b)
|
74
|
+
@event_handlers ||= Hash.new { [] }
|
75
|
+
events.each do |e|
|
76
|
+
raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
|
77
|
+
@event_handlers[e] <<= [who, b]
|
78
|
+
end
|
79
|
+
nil
|
80
|
+
end
|
81
|
+
|
82
|
+
def send_event(e, *args)
|
83
|
+
raise ArgumentError, "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
|
84
|
+
@event_handlers ||= Hash.new { [] }
|
85
|
+
@event_handlers[e].each { |who, proc| proc[self, *args] }
|
86
|
+
nil
|
87
|
+
end
|
88
|
+
|
89
|
+
def unregister_events(who, *events)
|
90
|
+
@event_handlers.each do |event, handlers|
|
91
|
+
handlers.each do |ewho, proc|
|
92
|
+
if (ewho == who) && (events.empty? || events.member?(event))
|
93
|
+
@event_handlers[event].delete [who, proc]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def relay_event(who, *events)
|
101
|
+
@event_handlers ||= Hash.new { [] }
|
102
|
+
events.each do |e|
|
103
|
+
raise "unknown event #{e} for #{self.class}" unless (self.class.class_eval "@@event_has")[e]
|
104
|
+
raise "unknown event #{e} for #{who.class}" unless (who.class.class_eval "@@event_has")[e]
|
105
|
+
@event_handlers[e] <<= [who, lambda { |s, *a| who.send_event e, *a }]
|
106
|
+
end
|
107
|
+
nil
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.append_features(mod)
|
111
|
+
super(mod)
|
112
|
+
mod.class_eval %q{
|
113
|
+
@@event_has ||= Hash.new(false)
|
114
|
+
def self.event(*args)
|
115
|
+
args.each { |a| @@event_has[a] = true }
|
116
|
+
end
|
117
|
+
}
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
## ensure that a method doesn't execute more frequently than some
|
122
|
+
## number of seconds. e.g.:
|
123
|
+
##
|
124
|
+
## def meth
|
125
|
+
## ...
|
126
|
+
## end
|
127
|
+
## min_iterval :meth, 10
|
128
|
+
##
|
129
|
+
## ensures that "meth" won't be executed more than once every 10
|
130
|
+
## seconds.
|
131
|
+
module MinIntervalMethods
|
132
|
+
def min_interval(meth, int)
|
133
|
+
class_eval %{
|
134
|
+
@@min_interval ||= {}
|
135
|
+
@@min_interval[:#{meth}] = [nil, #{int.to_i}]
|
136
|
+
alias :min_interval_#{meth} :#{meth}
|
137
|
+
def #{meth}(*a, &b)
|
138
|
+
last, int = @@min_interval[:#{meth}]
|
139
|
+
unless last && ((Time.now - last) < int)
|
140
|
+
min_interval_#{meth}(*a, &b)
|
141
|
+
@@min_interval[:#{meth}][0] = Time.now
|
142
|
+
end
|
143
|
+
end
|
144
|
+
}
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
## boolean attributes now get question marks in their accessors
|
149
|
+
## don't forget to 'extend' rather than 'include' this one
|
150
|
+
module AttrReaderQ
|
151
|
+
def attr_reader_q(*args)
|
152
|
+
args.each { |v| class_eval "def #{v}?; @#{v}; end" }
|
153
|
+
end
|
154
|
+
|
155
|
+
def attr_writer_q(*args)
|
156
|
+
args.each { |v| attr_writer v }
|
157
|
+
end
|
158
|
+
|
159
|
+
def attr_accessor_q(*args)
|
160
|
+
attr_reader_q args
|
161
|
+
attr_writer_q args
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
module ArrayShuffle
|
166
|
+
def shuffle!
|
167
|
+
each_index do |i|
|
168
|
+
j = i + rand(self.size - i);
|
169
|
+
self[i], self[j] = self[j], self[i]
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def shuffle
|
174
|
+
self.clone.shuffle! # dup doesn't preserve shuffle! method
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
module StringMapBytes
|
179
|
+
def map_bytes
|
180
|
+
ret = []
|
181
|
+
each_byte { |x| ret.push(yield(x)) }
|
182
|
+
ret
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
end
|
data/make-metainfo.rb
ADDED
@@ -0,0 +1,211 @@
|
|
1
|
+
## make-metainfo.rb -- interactive .torrent creater
|
2
|
+
## Copyright 2005 William Morgan.
|
3
|
+
##
|
4
|
+
## This file is part of RubyTorrent. RubyTorrent is free software;
|
5
|
+
## you can redistribute it and/or modify it under the terms of version
|
6
|
+
## 2 of the GNU General Public License as published by the Free
|
7
|
+
## Software Foundation.
|
8
|
+
##
|
9
|
+
## RubyTorrent is distributed in the hope that it will be useful, but
|
10
|
+
## WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
12
|
+
## General Public License (in the file COPYING) for more details.
|
13
|
+
|
14
|
+
require 'digest/sha1'
|
15
|
+
require "rubytorrent"
|
16
|
+
|
17
|
+
def die(x); $stderr << "#{x}\n" && exit(-1); end
|
18
|
+
def syntax
|
19
|
+
%{
|
20
|
+
Syntax: make-metainfo.rb [<file or directory>]+
|
21
|
+
|
22
|
+
make-metainfo is an interactive program for creating .torrent files from a set
|
23
|
+
of files or directories. any directories specified will be scanned recursively.
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def find_files(f)
|
28
|
+
if FileTest.directory? f
|
29
|
+
Dir.new(f).entries.map { |x| find_files(File.join(f, x)) unless x =~ /^\.[\.\/]*$/}.compact
|
30
|
+
else
|
31
|
+
f
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Numeric
|
36
|
+
def to_size_s
|
37
|
+
if self < 1024
|
38
|
+
"#{self.round}b"
|
39
|
+
elsif self < 1024**2
|
40
|
+
"#{(self / 1024.0).round}kb"
|
41
|
+
elsif self < 1024**3
|
42
|
+
"#{(self / (1024.0**2)).round}mb"
|
43
|
+
else
|
44
|
+
"#{(self / (1024.0**3)).round}gb"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_pieces(files, length)
|
50
|
+
buf = ""
|
51
|
+
files.each do |f|
|
52
|
+
File.open(f) do |fh|
|
53
|
+
begin
|
54
|
+
read = fh.read(length - buf.length)
|
55
|
+
if (buf.length + read.length) == length
|
56
|
+
yield(buf + read)
|
57
|
+
buf = ""
|
58
|
+
else
|
59
|
+
buf += read
|
60
|
+
end
|
61
|
+
end until fh.eof?
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
yield buf
|
66
|
+
end
|
67
|
+
|
68
|
+
die syntax if ARGV.length == 0
|
69
|
+
|
70
|
+
puts "Scanning..."
|
71
|
+
files = ARGV.map { |f| find_files f }.flatten
|
72
|
+
single = files.length == 1
|
73
|
+
|
74
|
+
puts "Building #{(single ? 'single' : 'multi')}-file .torrent for #{files.length} file#{(single ? '' : 's')}."
|
75
|
+
|
76
|
+
mi = RubyTorrent::MetaInfo.new
|
77
|
+
mii = RubyTorrent::MetaInfoInfo.new
|
78
|
+
|
79
|
+
maybe_name = if single
|
80
|
+
ARGV[0]
|
81
|
+
else
|
82
|
+
(File.directory?(ARGV[0]) ? File.basename(ARGV[0]) : File.basename(File.dirname(ARGV[0])))
|
83
|
+
end
|
84
|
+
puts
|
85
|
+
print %{Default output file/directory name (enter for "#{maybe_name}"): }
|
86
|
+
name = $stdin.gets.chomp
|
87
|
+
mii.name = (name == "" ? maybe_name : name)
|
88
|
+
puts %{We'll use "#{mii.name}".}
|
89
|
+
|
90
|
+
puts
|
91
|
+
puts "Measuring..."
|
92
|
+
length = nil
|
93
|
+
if single
|
94
|
+
length = mii.length = files.inject(0) { |s, f| s + File.size(f) }
|
95
|
+
else
|
96
|
+
mii.files = []
|
97
|
+
length = files.inject(0) do |s, f|
|
98
|
+
miif = RubyTorrent::MetaInfoInfoFile.new
|
99
|
+
miif.length = File.size f
|
100
|
+
miif.path = f.split File::SEPARATOR
|
101
|
+
miif.path = miif.path[1, miif.path.length - 1] if miif.path[0] == mii.name
|
102
|
+
mii.files << miif
|
103
|
+
s + miif.length
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
puts <<EOS
|
108
|
+
|
109
|
+
The file is #{length.to_size_s}. What piece size would you like? A smaller piece size
|
110
|
+
will result in a larger .torrent file; a larger piece size may cause
|
111
|
+
transfer inefficiency. Common sizes are 256, 512, and 1024kb.
|
112
|
+
|
113
|
+
Hint: for this .torrent,
|
114
|
+
EOS
|
115
|
+
|
116
|
+
size = nil
|
117
|
+
[64, 128, 256, 512, 1024, 2048, 4096].each do |size|
|
118
|
+
num_pieces = (length.to_f / size / 1024.0).ceil
|
119
|
+
tsize = num_pieces.to_f * 20.0 + 100
|
120
|
+
puts " - piece size of #{size}kb => #{num_pieces} pieces and .torrent size of approx. #{tsize.to_size_s}."
|
121
|
+
break if tsize < 10240
|
122
|
+
end
|
123
|
+
|
124
|
+
maybe_plen = [size, 256].min
|
125
|
+
begin
|
126
|
+
print "Piece size in kb (enter for #{maybe_plen}k): "
|
127
|
+
plen = $stdin.gets.chomp
|
128
|
+
end while plen !~ /^\d*$/
|
129
|
+
|
130
|
+
plen = (plen == "" ? maybe_plen : plen.to_i)
|
131
|
+
|
132
|
+
mii.piece_length = plen * 1024
|
133
|
+
num_pieces = (length.to_f / mii.piece_length.to_f).ceil
|
134
|
+
puts "Using piece size of #{plen}kb => .torrent size of approx. #{(num_pieces * 20.0).to_size_s}."
|
135
|
+
|
136
|
+
print "Calculating #{num_pieces} piece SHA1s... " ; $stdout.flush
|
137
|
+
|
138
|
+
mii.pieces = ""
|
139
|
+
i = 0
|
140
|
+
read_pieces(files, mii.piece_length) do |piece|
|
141
|
+
mii.pieces += Digest::SHA1.digest(piece)
|
142
|
+
i += 1
|
143
|
+
if (i % 100) == 0
|
144
|
+
print "#{(i.to_f / num_pieces * 100.0).round}%... "; $stdout.flush
|
145
|
+
end
|
146
|
+
end
|
147
|
+
puts "done"
|
148
|
+
|
149
|
+
mi.info = mii
|
150
|
+
puts <<EOS
|
151
|
+
|
152
|
+
Enter the tracker URL or URLs that will be hosting the .torrent
|
153
|
+
file. These are typically of the form:
|
154
|
+
|
155
|
+
http://tracker.example.com:6969/announce
|
156
|
+
|
157
|
+
Multiple trackers may be partitioned into tiers; clients will try all
|
158
|
+
servers (in random order) from an earlier tier before trying those of
|
159
|
+
a later tier. See http://home.elp.rr.com/tur/multitracker-spec.txt
|
160
|
+
for details.
|
161
|
+
|
162
|
+
Enter the tracker URL(s) now. Separate multiple tracker URLs on the
|
163
|
+
same tier with spaces. Enter a blank line when you're done.
|
164
|
+
|
165
|
+
(Note that if you have multiple trackers, some clients may only use
|
166
|
+
the first one, so that should be the one capable of handling the most
|
167
|
+
traffic.)
|
168
|
+
EOS
|
169
|
+
|
170
|
+
tier = 0
|
171
|
+
trackers = []
|
172
|
+
begin
|
173
|
+
print "Tier #{tier} tracker(s): "
|
174
|
+
these = $stdin.gets.chomp.split(/\s+/)
|
175
|
+
trackers.push these unless these.length == 0
|
176
|
+
tier += 1 unless these.length == 0
|
177
|
+
end while (these.length != 0) || (tier == 0)
|
178
|
+
|
179
|
+
mi.announce = URI.parse(trackers[0][0])
|
180
|
+
mi.announce_list = trackers.map do |tier|
|
181
|
+
tier.map { |x| URI.parse(x) }
|
182
|
+
end unless (trackers.length == 1) && (trackers[0].length == 1)
|
183
|
+
|
184
|
+
puts <<EOS
|
185
|
+
|
186
|
+
Enter any comments. No one will probably ever see these. End with a blank line.
|
187
|
+
EOS
|
188
|
+
comm = ""
|
189
|
+
while true
|
190
|
+
s = $stdin.gets.chomp
|
191
|
+
break if s == ""
|
192
|
+
comm += s + "\n"
|
193
|
+
end
|
194
|
+
mi.comment = comm.chomp unless comm == ""
|
195
|
+
|
196
|
+
mi.created_by = "RubyTorrent make-metainfo (http://rubytorrent.rubyforge.org)"
|
197
|
+
mi.creation_date = Time.now
|
198
|
+
|
199
|
+
maybe_name = "#{mii.name}.torrent"
|
200
|
+
begin
|
201
|
+
print "Output filename (enter for #{maybe_name}): "
|
202
|
+
name = $stdin.gets.chomp
|
203
|
+
end while name.length == ""
|
204
|
+
|
205
|
+
name = (name == "" ? maybe_name : name)
|
206
|
+
File.open(name, "w") do |f|
|
207
|
+
f.write mi.to_bencoding
|
208
|
+
end
|
209
|
+
|
210
|
+
puts "Succesfully created #{name}"
|
211
|
+
|
data/rtpeer-ncurses.rb
ADDED
@@ -0,0 +1,340 @@
|
|
1
|
+
## rtpeer-ncurses.rb -- RubyTorrent ncurses BitTorrent peer.
|
2
|
+
## Copyright 2005 William Morgan.
|
3
|
+
##
|
4
|
+
## This file is part of RubyTorrent. RubyTorrent is free software;
|
5
|
+
## you can redistribute it and/or modify it under the terms of version
|
6
|
+
## 2 of the GNU General Public License as published by the Free
|
7
|
+
## Software Foundation.
|
8
|
+
##
|
9
|
+
## RubyTorrent is distributed in the hope that it will be useful, but
|
10
|
+
## WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
12
|
+
## General Public License (in the file COPYING) for more details.
|
13
|
+
|
14
|
+
require "rubygems"
|
15
|
+
require "rubytorrent"
|
16
|
+
require "ncurses"
|
17
|
+
require "optparse"
|
18
|
+
|
19
|
+
def die(x); $stderr << "#{x}\n" && exit(-1); end
|
20
|
+
|
21
|
+
dlratelim = nil
|
22
|
+
ulratelim = nil
|
23
|
+
|
24
|
+
opts = OptionParser.new do |opts|
|
25
|
+
opts.banner =
|
26
|
+
%{Usage: rtpeer-ncurses [options] <torrent> [<target>]
|
27
|
+
|
28
|
+
rtpeer-ncurses is a very simple ncurses-based BitTorrent peer. You can use it
|
29
|
+
to download .torrents or to seed them.
|
30
|
+
|
31
|
+
<torrent> is a .torrent filename or URL.
|
32
|
+
<target> is a file or directory on disk. If not specified, the default value
|
33
|
+
from <torrent> will be used.
|
34
|
+
[options] are:
|
35
|
+
}
|
36
|
+
|
37
|
+
opts.on("-l", "--log FILENAME",
|
38
|
+
"Log events to FILENAME (for debugging)") do |fn|
|
39
|
+
RubyTorrent::log_output_to(fn)
|
40
|
+
end
|
41
|
+
|
42
|
+
opts.on("-d", "--downlimit LIMIT", Integer,
|
43
|
+
"Limit download rate to LIMIT kb/s") do |x|
|
44
|
+
dlratelim = x * 1024
|
45
|
+
end
|
46
|
+
|
47
|
+
opts.on("-u", "--uplimit LIMIT", Integer,
|
48
|
+
"Limit upload rate to LIMIT kb/s") do |x|
|
49
|
+
ulratelim = x * 1024
|
50
|
+
end
|
51
|
+
|
52
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
53
|
+
puts opts
|
54
|
+
exit
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
opts.parse!(ARGV)
|
59
|
+
proxy = ENV["http_proxy"]
|
60
|
+
torrent = ARGV.shift or (puts opts; exit)
|
61
|
+
dest = ARGV.shift
|
62
|
+
|
63
|
+
class Numeric
|
64
|
+
def to_sz
|
65
|
+
if self < 1024
|
66
|
+
"#{self.round}b"
|
67
|
+
elsif self < 1024 ** 2
|
68
|
+
"#{(self / 1024 ).round}k"
|
69
|
+
elsif self < 1024 ** 3
|
70
|
+
sprintf("%.1fm", self.to_f / (1024 ** 2))
|
71
|
+
else
|
72
|
+
sprintf("%.2fg", self.to_f / (1024 ** 3))
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
MIN = 60
|
77
|
+
HOUR = 60 * MIN
|
78
|
+
DAY = 24 * HOUR
|
79
|
+
|
80
|
+
def to_time
|
81
|
+
if self < MIN
|
82
|
+
sprintf("0:%02d", self)
|
83
|
+
elsif self < HOUR
|
84
|
+
sprintf("%d:%02d", self / MIN, self % MIN)
|
85
|
+
elsif self < DAY
|
86
|
+
sprintf("%d:%02d:%02d", self / HOUR, (self % HOUR) / MIN, (self % HOUR) % MIN)
|
87
|
+
else
|
88
|
+
sprintf("%dd %d:%02d:%02d", self / DAY, (self % DAY) / HOUR, ((self % DAY) % HOUR) / MIN, ((self % DAY) % HOUR) % MIN)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
class NilClass
|
94
|
+
def to_time; "--:--"; end
|
95
|
+
def to_sz; "-"; end
|
96
|
+
end
|
97
|
+
|
98
|
+
class Display
|
99
|
+
STALL_SECS = 15
|
100
|
+
|
101
|
+
attr_accessor :fn, :dest, :status,:dlamt, :ulamt, :dlrate, :ulrate,
|
102
|
+
:conn_peers, :fail_peers, :untried_peers, :tracker,
|
103
|
+
:errcount, :completed, :total, :rate, :use_rate
|
104
|
+
|
105
|
+
def initialize(window)
|
106
|
+
@window = window
|
107
|
+
@need_update = true
|
108
|
+
|
109
|
+
@fn = ""
|
110
|
+
@dest = ""
|
111
|
+
@status = ""
|
112
|
+
@completed = 0
|
113
|
+
@total = 0
|
114
|
+
@dlamt = 0
|
115
|
+
@dlrate = 0
|
116
|
+
@ulamt = 0
|
117
|
+
@ulrate = 0
|
118
|
+
@rate = 0
|
119
|
+
@conn_peers = 0
|
120
|
+
@fail_peers = 0
|
121
|
+
@untried_peers = 0
|
122
|
+
@tracker = "not connected"
|
123
|
+
@errcount = 0
|
124
|
+
@dlblocks = 0
|
125
|
+
@ulblocks = 0
|
126
|
+
|
127
|
+
@got_blocks = 0
|
128
|
+
@sent_blocks = 0
|
129
|
+
@last_got_block = nil
|
130
|
+
@last_sent_block = nil
|
131
|
+
@start_time = nil
|
132
|
+
|
133
|
+
@use_rate = false
|
134
|
+
end
|
135
|
+
|
136
|
+
def got_block
|
137
|
+
@got_blocks += 1
|
138
|
+
@last_got_block = Time.now
|
139
|
+
end
|
140
|
+
|
141
|
+
def sent_block
|
142
|
+
@sent_blocks += 1
|
143
|
+
@last_sent_block = Time.now
|
144
|
+
end
|
145
|
+
|
146
|
+
def sigwinch_handler(sig = nil)
|
147
|
+
@need_update = true
|
148
|
+
end
|
149
|
+
|
150
|
+
def start_timer
|
151
|
+
@start_time = Time.now
|
152
|
+
end
|
153
|
+
|
154
|
+
def draw
|
155
|
+
if @need_update
|
156
|
+
update_size
|
157
|
+
@window.erase
|
158
|
+
end
|
159
|
+
|
160
|
+
complete_width = [@cols - 23, 0].max
|
161
|
+
complete_ticks = ((@completed.to_f / @total) * complete_width)
|
162
|
+
|
163
|
+
elapsed = (@start_time ? Time.now - @start_time : nil)
|
164
|
+
rate = (use_rate ? @rate : @dlrate)
|
165
|
+
remaining = rate && (rate > 0 ? (@total - @completed).to_f / rate : nil)
|
166
|
+
|
167
|
+
dlstall = @last_got_block && ((Time.now - @last_got_block) > STALL_SECS)
|
168
|
+
ulstall = @last_sent_block && ((Time.now - @last_sent_block) > STALL_SECS)
|
169
|
+
|
170
|
+
line = 1
|
171
|
+
[
|
172
|
+
"Contents: #@fn",
|
173
|
+
" Dest: #@dest",
|
174
|
+
"",
|
175
|
+
" Status: #@status",
|
176
|
+
"Progress: [" + ("#" * complete_ticks),
|
177
|
+
" Time: elapsed #{elapsed.to_time}, remaining #{remaining.to_time}",
|
178
|
+
"Download: #{@dlamt.to_sz} at #{dlstall ? '(stalled)' : @dlrate.to_sz + '/s'}",
|
179
|
+
" Upload: #{@ulamt.to_sz} at #{ulstall ? '(stalled)' : @ulrate.to_sz + '/s'}",
|
180
|
+
" Peers: connected to #@conn_peers (#@fail_peers failed, #@untried_peers untried)",
|
181
|
+
" Tracker: #@tracker",
|
182
|
+
" Errors: #@errcount",
|
183
|
+
].each do |s|
|
184
|
+
break if line > @rows
|
185
|
+
@window.mvaddnstr(line, 2, s + (" " * @cols), @cols - 4)
|
186
|
+
line += 1
|
187
|
+
end
|
188
|
+
|
189
|
+
## progress bar tail
|
190
|
+
@window.mvaddstr(5, @cols - 11, sprintf("] %.2f%% ", (@completed.to_f / @total) * 100.0))
|
191
|
+
@window.mvaddnstr(7, 31, "|" + ("#" * (@dlrate / 1024)) + (" " * @cols), @cols - 31 - 2)
|
192
|
+
@window.mvaddnstr(8, 31, "|" + ('#' * (@ulrate / 1024)) + (" " * @cols), @cols - 31 - 2)
|
193
|
+
|
194
|
+
@window.box(0,0)
|
195
|
+
|
196
|
+
# @got_blocks -= 1 unless @got_blocks == 0
|
197
|
+
# @sent_blocks -= 1 unless @sent_blocks == 0
|
198
|
+
end
|
199
|
+
|
200
|
+
private
|
201
|
+
|
202
|
+
def update_size
|
203
|
+
rows = []
|
204
|
+
cols = []
|
205
|
+
## jesus CHRIST this is a shitty interface.
|
206
|
+
@window.getmaxyx(rows, cols)
|
207
|
+
@rows = rows[0]
|
208
|
+
@cols = cols[0]
|
209
|
+
@need_update = false
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
begin
|
214
|
+
mi = RubyTorrent::MetaInfo.from_location(torrent, proxy)
|
215
|
+
rescue RubyTorrent::MetaInfoFormatError, RubyTorrent::BEncodingError => e
|
216
|
+
die %{Error: can\'t parse metainfo file "#{torrent}"---maybe not a .torrent?}
|
217
|
+
rescue RubyTorrent::TypedStructError => e
|
218
|
+
$stderr << <<EOS
|
219
|
+
error parsing metainfo file, and it's likely something I should know about.
|
220
|
+
please email the torrent file to wmorgan-rubytorrent-bug@masanjin.net,
|
221
|
+
along with this backtrace: (this is RubyTorrent version #{RubyTorrent::VERSION})
|
222
|
+
EOS
|
223
|
+
|
224
|
+
raise e
|
225
|
+
rescue IOError, SystemCallError => e
|
226
|
+
$stderr.puts %{Error: can't read file "#{torrent}": #{e.message}}
|
227
|
+
exit
|
228
|
+
end
|
229
|
+
|
230
|
+
unless dest.nil?
|
231
|
+
if FileTest.directory?(dest) && mi.info.single?
|
232
|
+
dest = File.join(dest, mi.info.name)
|
233
|
+
elsif FileTest.file?(dest) && mi.info.multiple?
|
234
|
+
die %{Error: .torrent contains multiple files, but "#{dest}" is a single file (must be a directory)}
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def handle_any_input(display)
|
239
|
+
case(Ncurses.getch())
|
240
|
+
when ?q, ?Q
|
241
|
+
Ncurses.curs_set(1)
|
242
|
+
Ncurses.endwin()
|
243
|
+
exit
|
244
|
+
when Ncurses::KEY_RESIZE
|
245
|
+
display.sigwinch_handler
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
Ncurses.initscr
|
250
|
+
|
251
|
+
begin
|
252
|
+
Ncurses.nl()
|
253
|
+
Ncurses.noecho()
|
254
|
+
Ncurses.curs_set(0)
|
255
|
+
Ncurses.stdscr.nodelay(true)
|
256
|
+
Ncurses.timeout(0)
|
257
|
+
|
258
|
+
display = Display.new Ncurses.stdscr
|
259
|
+
display.status = "checking file on disk..."
|
260
|
+
display.dest = File.expand_path(dest || mi.info.name) + (mi.single? ? "" : "/")
|
261
|
+
if mi.single?
|
262
|
+
display.fn = "#{mi.info.name} (#{mi.info.length.to_sz} in one file)"
|
263
|
+
else
|
264
|
+
display.fn = "#{mi.info.name}/ (#{mi.info.total_length.to_sz} in #{mi.info.files.length} files)"
|
265
|
+
end
|
266
|
+
display.total = mi.info.num_pieces * mi.info.piece_length
|
267
|
+
display.completed = 0
|
268
|
+
display.draw; Ncurses.refresh
|
269
|
+
|
270
|
+
display.use_rate = true
|
271
|
+
display.start_timer
|
272
|
+
num_pieces = 0
|
273
|
+
start = Time.now
|
274
|
+
every = 10
|
275
|
+
package = RubyTorrent::Package.new(mi, dest) do |piece|
|
276
|
+
num_pieces += 1
|
277
|
+
if (num_pieces % every) == 0
|
278
|
+
display.completed = (num_pieces * mi.info.piece_length)
|
279
|
+
display.rate = display.completed.to_f / (Time.now - start)
|
280
|
+
handle_any_input display
|
281
|
+
display.draw; Ncurses.refresh
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
display.status = "starting peer..."
|
286
|
+
display.use_rate = false
|
287
|
+
display.draw; Ncurses.refresh
|
288
|
+
bt = RubyTorrent::BitTorrent.new(mi, package, :http_proxy => proxy, :dlratelim => dlratelim, :ulratelim => ulratelim)
|
289
|
+
|
290
|
+
connecting = true
|
291
|
+
bt.on_event(self, :received_block) do |s, b, peer|
|
292
|
+
display.got_block
|
293
|
+
connecting = false
|
294
|
+
end
|
295
|
+
bt.on_event(self, :sent_block) do |s, b, peer|
|
296
|
+
display.sent_block
|
297
|
+
connecting = false
|
298
|
+
end
|
299
|
+
bt.on_event(self, :discarded_piece) { |s, p| display.errcount += 1 }
|
300
|
+
bt.on_event(self, :tracker_connected) do |s, url|
|
301
|
+
display.tracker = url
|
302
|
+
display.untried_peers = bt.num_possible_peers
|
303
|
+
end
|
304
|
+
bt.on_event(self, :tracker_lost) { |s, url| display.tracker = "can't connect to #{url}" }
|
305
|
+
bt.on_event(self, :forgetting_peer) { |s, p| display.fail_peers += 1 }
|
306
|
+
bt.on_event(self, :removed_peer, :added_peer) do |s, p|
|
307
|
+
if (display.conn_peers = bt.num_active_peers) == 0
|
308
|
+
connecting = true
|
309
|
+
end
|
310
|
+
end
|
311
|
+
bt.on_event(self, :added_peer) { |s, p| display.conn_peers += 1 }
|
312
|
+
bt.on_event(self, :trying_peer) { |s, p| display.untried_peers -= 1 unless display.untried_peers == 0 }
|
313
|
+
|
314
|
+
display.total = bt.total_bytes
|
315
|
+
display.start_timer
|
316
|
+
|
317
|
+
while true
|
318
|
+
handle_any_input(display)
|
319
|
+
|
320
|
+
display.status = if bt.complete?
|
321
|
+
"seeding (download complete)"
|
322
|
+
elsif connecting
|
323
|
+
"connecting to peers"
|
324
|
+
else
|
325
|
+
"downloading"
|
326
|
+
end
|
327
|
+
display.draw; Ncurses.refresh
|
328
|
+
|
329
|
+
display.dlamt = bt.dlamt
|
330
|
+
display.dlrate = bt.dlrate
|
331
|
+
display.ulamt = bt.ulamt
|
332
|
+
display.ulrate = bt.ulrate
|
333
|
+
display.completed = bt.bytes_completed
|
334
|
+
display.draw; Ncurses.refresh
|
335
|
+
sleep(0.5)
|
336
|
+
end
|
337
|
+
ensure
|
338
|
+
Ncurses.curs_set(1)
|
339
|
+
Ncurses.endwin()
|
340
|
+
end
|