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,128 @@
|
|
1
|
+
## message.rb -- peer wire protocol message parsing/composition
|
2
|
+
## Copyright 2004 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
|
+
## we violate the users' namespaces here. but it's not in too
|
15
|
+
## egregious of a way, and it's a royal pita to remove, so i'm keeping
|
16
|
+
## it in for the time being.
|
17
|
+
class String
|
18
|
+
def from_fbbe # four-byte big-endian integer
|
19
|
+
raise "fbbe must be four-byte string (got #{self.inspect})" unless length == 4
|
20
|
+
(self[0] << 24) + (self[1] << 16) + (self[2] << 8) + self[3]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class Integer
|
25
|
+
def to_fbbe # four-byte big-endian integer
|
26
|
+
raise "fbbe must be < 2^32" unless self <= 2**32
|
27
|
+
raise "fbbe must be >= 0" unless self >= 0
|
28
|
+
s = " "
|
29
|
+
s[0] = (self >> 24) % 256
|
30
|
+
s[1] = (self >> 16) % 256
|
31
|
+
s[2] = (self >> 8) % 256
|
32
|
+
s[3] = (self ) % 256
|
33
|
+
s
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
module RubyTorrent
|
38
|
+
|
39
|
+
module StringExpandBits
|
40
|
+
include StringMapBytes
|
41
|
+
|
42
|
+
def expand_bits # just for debugging purposes
|
43
|
+
self.map_bytes do |b|
|
44
|
+
(0 .. 7).map { |i| ((b & (1 << (7 - i))) == 0 ? "0" : "1") }
|
45
|
+
end.flatten.join
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class Message
|
50
|
+
WIRE_IDS = [:choke, :unchoke, :interested, :uninterested, :have, :bitfield,
|
51
|
+
:request, :piece, :cancel]
|
52
|
+
|
53
|
+
attr_accessor :id
|
54
|
+
|
55
|
+
def initialize(id, args=nil)
|
56
|
+
@id = id
|
57
|
+
@args = args
|
58
|
+
end
|
59
|
+
|
60
|
+
def method_missing(meth)
|
61
|
+
if @args.has_key? meth
|
62
|
+
@args[meth]
|
63
|
+
else
|
64
|
+
raise %{no such argument "#{meth}" to message #{self.to_s}}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def to_wire_form
|
69
|
+
case @id
|
70
|
+
when :keepalive
|
71
|
+
0.to_fbbe
|
72
|
+
when :choke, :unchoke, :interested, :uninterested
|
73
|
+
1.to_fbbe + WIRE_IDS.index(@id).chr
|
74
|
+
when :have
|
75
|
+
5.to_fbbe + 4.chr + @args[:index].to_fbbe
|
76
|
+
when :bitfield
|
77
|
+
(@args[:bitfield].length + 1).to_fbbe + 5.chr + @args[:bitfield]
|
78
|
+
when :request, :cancel
|
79
|
+
13.to_fbbe + WIRE_IDS.index(@id).chr + @args[:index].to_fbbe +
|
80
|
+
@args[:begin].to_fbbe + @args[:length].to_fbbe
|
81
|
+
when :piece
|
82
|
+
(@args[:length] + 9).to_fbbe + 7.chr + @args[:index].to_fbbe +
|
83
|
+
@args[:begin].to_fbbe
|
84
|
+
else
|
85
|
+
raise "unknown message type #{id}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.from_wire_form(idnum, argstr)
|
90
|
+
type = WIRE_IDS[idnum]
|
91
|
+
|
92
|
+
case type
|
93
|
+
when :choke, :unchoke, :interested, :uninterested
|
94
|
+
raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.nil? or (argstr.length == 0)
|
95
|
+
Message.new(type)
|
96
|
+
|
97
|
+
when :have
|
98
|
+
raise ProtocolError, "invalid length #{str.length} for #{type} message" unless argstr.length == 4
|
99
|
+
Message.new(type, {:index => argstr[0,4].from_fbbe})
|
100
|
+
|
101
|
+
when :bitfield
|
102
|
+
Message.new(type, {:bitfield => argstr})
|
103
|
+
|
104
|
+
when :request, :cancel
|
105
|
+
raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 12
|
106
|
+
Message.new(type, {:index => argstr[0,4].from_fbbe,
|
107
|
+
:begin => argstr[4,4].from_fbbe,
|
108
|
+
:length => argstr[8,4].from_fbbe})
|
109
|
+
when :piece
|
110
|
+
raise ProtocolError, "invalid length #{argstr.length} for #{type} message" unless argstr.length == 8
|
111
|
+
Message.new(type, {:index => argstr[0,4].from_fbbe,
|
112
|
+
:begin => argstr[4,4].from_fbbe})
|
113
|
+
else
|
114
|
+
raise "unknown message #{type.inspect}"
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def to_s
|
119
|
+
case @id
|
120
|
+
when :bitfield
|
121
|
+
%{bitfield <#{@args[:bitfield].extend(StringExpandBits).expand_bits}>}
|
122
|
+
else
|
123
|
+
%{#@id#{@args.nil? ? "" : "(" + @args.map { |k, v| "#{k}=#{v.to_s.inspect}" }.join(", ") + ")"}}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
## metainfo.rb -- parsed .torrent file
|
2
|
+
## Copyright 2004 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 "rubytorrent/typedstruct"
|
15
|
+
require 'uri'
|
16
|
+
require 'open-uri'
|
17
|
+
require 'digest/sha1'
|
18
|
+
|
19
|
+
## MetaInfo file is the parsed form of the .torrent file that people
|
20
|
+
## send around. It contains a MetaInfoInfo and possibly some
|
21
|
+
## MetaInfoInfoFile objects.
|
22
|
+
module RubyTorrent
|
23
|
+
|
24
|
+
class MetaInfoFormatError < StandardError; end
|
25
|
+
|
26
|
+
class MetaInfoInfoFile
|
27
|
+
def initialize(dict=nil)
|
28
|
+
@s = TypedStruct.new do |s|
|
29
|
+
s.field :length => Integer, :md5sum => String, :sha1 => String,
|
30
|
+
:path => String
|
31
|
+
s.array :path
|
32
|
+
s.required :length, :path
|
33
|
+
end
|
34
|
+
|
35
|
+
@dict = dict
|
36
|
+
unless dict.nil?
|
37
|
+
@s.parse dict
|
38
|
+
check
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def method_missing(meth, *args)
|
43
|
+
@s.send(meth, *args)
|
44
|
+
end
|
45
|
+
|
46
|
+
def check
|
47
|
+
raise MetaInfoFormatError, "invalid file length" unless @s.length >= 0
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_bencoding
|
51
|
+
check
|
52
|
+
(@dict || @s).to_bencoding
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class MetaInfoInfo
|
57
|
+
def initialize(dict=nil)
|
58
|
+
@s = TypedStruct.new do |s|
|
59
|
+
s.field :length => Integer, :md5sum => String, :name => String,
|
60
|
+
:piece_length => Integer, :pieces => String,
|
61
|
+
:files => MetaInfoInfoFile, :sha1 => String
|
62
|
+
s.label :piece_length => "piece length"
|
63
|
+
s.required :name, :piece_length, :pieces
|
64
|
+
s.array :files
|
65
|
+
s.coerce :files => lambda { |x| x.map { |y| MetaInfoInfoFile.new(y) } }
|
66
|
+
end
|
67
|
+
|
68
|
+
@dict = dict
|
69
|
+
unless dict.nil?
|
70
|
+
@s.parse dict
|
71
|
+
check
|
72
|
+
if dict["sha1"]
|
73
|
+
## this seems to always be off. don't know how it's supposed
|
74
|
+
## to be calculated, so fuck it.
|
75
|
+
# puts "we have #{sha1.inspect}, they have #{dict['sha1'].inspect}"
|
76
|
+
# rt_warning "info hash SHA1 mismatch" unless dict["sha1"] == sha1
|
77
|
+
# raise MetaInfoFormatError, "info hash SHA1 mismatch" unless dict["sha1"] == sha1
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def check
|
83
|
+
raise MetaInfoFormatError, "invalid file length" unless @s.length.nil? || @s.length >= 0
|
84
|
+
raise MetaInfoFormatError, "one (and only one) of 'length' (single-file torrent) or 'files' (multi-file torrent) must be specified" if (@s.length.nil? && @s.files.nil?) || (!@s.length.nil? && !@s.files.nil?)
|
85
|
+
if single?
|
86
|
+
length = @s.length
|
87
|
+
else
|
88
|
+
length = @s.files.inject(0) { |s, x| s + x.length }
|
89
|
+
end
|
90
|
+
raise MetaInfoFormatError, "invalid metainfo file: length #{length} > (#{@s.pieces.length / 20} pieces * #{@s.piece_length})" unless length <= (@s.pieces.length / 20) * @s.piece_length
|
91
|
+
raise MetaInfoFormatError, "invalid metainfo file: pieces length = #{@s.pieces.length} not a multiple of 20" unless (@s.pieces.length % 20) == 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def to_bencoding
|
95
|
+
check
|
96
|
+
(@dict || @s).to_bencoding
|
97
|
+
end
|
98
|
+
|
99
|
+
def sha1
|
100
|
+
if @s.dirty
|
101
|
+
@sha1 = Digest::SHA1.digest(self.to_bencoding)
|
102
|
+
@s.dirty = false
|
103
|
+
end
|
104
|
+
@sha1
|
105
|
+
end
|
106
|
+
|
107
|
+
def single?
|
108
|
+
!length.nil?
|
109
|
+
end
|
110
|
+
|
111
|
+
def multiple?
|
112
|
+
length.nil?
|
113
|
+
end
|
114
|
+
|
115
|
+
def total_length
|
116
|
+
if single?
|
117
|
+
length
|
118
|
+
else
|
119
|
+
files.inject(0) { |a, f| a + f.length }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def num_pieces
|
124
|
+
pieces.length / 20
|
125
|
+
end
|
126
|
+
|
127
|
+
def method_missing(meth, *args)
|
128
|
+
@s.send(meth, *args)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
class MetaInfo
|
133
|
+
def initialize(dict=nil)
|
134
|
+
raise TypeError, "argument must be a Hash (maybe see MetaInfo.from_location)" unless dict.is_a? Hash
|
135
|
+
@s = TypedStruct.new do |s|
|
136
|
+
s.field :info => MetaInfoInfo, :announce => URI::HTTP,
|
137
|
+
:announce_list => Array, :creation_date => Time,
|
138
|
+
:comment => String, :created_by => String, :encoding => String
|
139
|
+
s.label :announce_list => "announce-list", :creation_date => "creation date",
|
140
|
+
:created_by => "created by"
|
141
|
+
s.array :announce_list
|
142
|
+
s.coerce :info => lambda { |x| MetaInfoInfo.new(x) },
|
143
|
+
:creation_date => lambda { |x| Time.at(x) },
|
144
|
+
:announce => lambda { |x| URI.parse(x) },
|
145
|
+
:announce_list => lambda { |x| x.map { |y| y.map { |z| URI.parse(z) } } }
|
146
|
+
end
|
147
|
+
|
148
|
+
@dict = dict
|
149
|
+
unless dict.nil?
|
150
|
+
@s.parse dict
|
151
|
+
check
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def single?; info.single?; end
|
156
|
+
def multiple?; info.multiple?; end
|
157
|
+
|
158
|
+
def check
|
159
|
+
if @s.announce_list
|
160
|
+
@s.announce_list.each do |tier|
|
161
|
+
tier.each { |track| raise MetaInfoFormatError, "expecting HTTP URL in announce-list, got #{track} instead" unless track.is_a? URI::HTTP }
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def self.from_bstream(bs)
|
167
|
+
dict = nil
|
168
|
+
bs.each do |e|
|
169
|
+
if dict == nil
|
170
|
+
dict = e
|
171
|
+
else
|
172
|
+
raise MetaInfoFormatError, "too many bencoded elements for metainfo file (just need one)"
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
raise MetaInfoFormatError, "bencoded element must be a dictionary, got a #{dict.class}" unless dict.kind_of? ::Hash
|
177
|
+
|
178
|
+
MetaInfo.new(dict)
|
179
|
+
end
|
180
|
+
|
181
|
+
## either a filename or a URL
|
182
|
+
def self.from_location(fn, http_proxy=ENV["http_proxy"])
|
183
|
+
if http_proxy # lame!
|
184
|
+
open(fn, "rb", :proxy => http_proxy) { |f| from_bstream(BStream.new(f)) }
|
185
|
+
else
|
186
|
+
open(fn, "rb") { |f| from_bstream(BStream.new(f)) }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def self.from_stream(s)
|
191
|
+
from_bstream(BStream.new(s))
|
192
|
+
end
|
193
|
+
|
194
|
+
def method_missing(meth, *args)
|
195
|
+
@s.send(meth, *args)
|
196
|
+
end
|
197
|
+
|
198
|
+
def to_bencoding
|
199
|
+
check
|
200
|
+
(@dict || @s).to_bencoding
|
201
|
+
end
|
202
|
+
|
203
|
+
def trackers
|
204
|
+
if announce_list && (announce_list.length > 0)
|
205
|
+
announce_list.map do |tier|
|
206
|
+
tier.extend(ArrayShuffle).shuffle
|
207
|
+
end.flatten
|
208
|
+
else
|
209
|
+
[announce]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
end
|
@@ -0,0 +1,595 @@
|
|
1
|
+
## package.rb -- RubyTorrent <=> filesystem interface.
|
2
|
+
## Copyright 2004 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 'thread'
|
15
|
+
require 'digest/sha1'
|
16
|
+
|
17
|
+
## A Package is the connection between the network and the
|
18
|
+
## filesystem. There is one Package per torrent. Each Package is
|
19
|
+
## composed of one or more Pieces, as determined by the MetaInfoInfo
|
20
|
+
## object, and each Piece is composed of one or more Blocks, which are
|
21
|
+
## transmitted over the PeerConnection with :piece comments.
|
22
|
+
|
23
|
+
module RubyTorrent
|
24
|
+
|
25
|
+
## Range plus a lot of utility methods
|
26
|
+
class AwesomeRange < Range
|
27
|
+
def initialize(start, endd=nil, exclude_end=false)
|
28
|
+
case start
|
29
|
+
when Integer
|
30
|
+
raise ArgumentError, "both start and endd must be specified" if endd.nil?
|
31
|
+
super(start, endd, exclude_end)
|
32
|
+
when Range
|
33
|
+
super(start.first, start.last, start.exclude_end?)
|
34
|
+
else
|
35
|
+
raise ArgumentError, "start should be an Integer or a Range, is a #{start.class}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
## range super-set: does this range encompass 'o'?
|
40
|
+
def rss?(o)
|
41
|
+
(first <= o.first) &&
|
42
|
+
((last > o.last) || (o.exclude_end? && (last == o.last)))
|
43
|
+
end
|
44
|
+
|
45
|
+
## range intersection
|
46
|
+
def rint(o)
|
47
|
+
## three cases. either:
|
48
|
+
## a) our left endpoint is within o
|
49
|
+
if ((first >= o.first) &&
|
50
|
+
((first < o.last) || (!o.exclude_end? && (first == o.last))))
|
51
|
+
if last < o.last
|
52
|
+
AwesomeRange.new(first, last, exclude_end?)
|
53
|
+
elsif last > o.last
|
54
|
+
AwesomeRange.new(first, o.last, o.exclude_end?)
|
55
|
+
else # ==
|
56
|
+
AwesomeRange.new(first, last, exclude_end? || o.exclude_end?)
|
57
|
+
end
|
58
|
+
## b) our right endpoint is within o
|
59
|
+
elsif (((last > o.first) || (!exclude_end? && (last == o.first))) && ((last < o.last) || (!o.exclude_end? && (last == o.last))))
|
60
|
+
AwesomeRange.new([first, o.first].max, last, exclude_end?)
|
61
|
+
## c) we encompass o
|
62
|
+
elsif rss?(o)
|
63
|
+
o
|
64
|
+
else
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
## range continuity
|
70
|
+
def rcont?(o)
|
71
|
+
(first == o.last) || (last == o.first) || (rint(o) != nil)
|
72
|
+
end
|
73
|
+
|
74
|
+
## range union: only valid for continuous ranges
|
75
|
+
def runion(o)
|
76
|
+
if last > o.last
|
77
|
+
AwesomeRange.new([first, o.first].min, last, exclude_end?)
|
78
|
+
elsif o.last > last
|
79
|
+
AwesomeRange.new([first, o.first].min, o.last, o.exclude_end?)
|
80
|
+
else # equal
|
81
|
+
AwesomeRange.new([first, o.first].min, last, (exclude_end? && o.exclude_end?))
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
## range difference. returns an array of 0, 1 or 2 ranges.
|
86
|
+
def rdiff(o)
|
87
|
+
return [] if o == self
|
88
|
+
ret = []
|
89
|
+
int = rint o
|
90
|
+
return [] if int == self
|
91
|
+
return [self] if int == nil
|
92
|
+
raise RangeError, "can't subtract a range that doesn't have an exclusive end" unless int.exclude_end?
|
93
|
+
if int.first > first
|
94
|
+
ret << AwesomeRange.new(first, int.first, true)
|
95
|
+
end
|
96
|
+
ret + [AwesomeRange.new(int.last, last, exclude_end?)]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
## a Covering is a set of non-overlapping ranges within a given start
|
101
|
+
## point and endpoint.
|
102
|
+
class Covering
|
103
|
+
attr_accessor :domain, :ranges
|
104
|
+
|
105
|
+
## 'domain' should be an AwesomeRange determining the start and end
|
106
|
+
## point. 'ranges' should be an array of non-overlapping
|
107
|
+
## AwesomeRanges sorted by start point.
|
108
|
+
def initialize(domain, ranges=[])
|
109
|
+
@domain = domain
|
110
|
+
@ranges = ranges
|
111
|
+
end
|
112
|
+
|
113
|
+
def complete!; @ranges = [@domain]; self; end
|
114
|
+
def complete?; @ranges == [@domain]; end
|
115
|
+
def empty!; @ranges = []; self; end
|
116
|
+
def empty?; @ranges == []; end
|
117
|
+
|
118
|
+
## given a covering of size N and a new range 'r', returns a
|
119
|
+
## covering of size 0 <= s <= N + 1 that doesn't cover the range
|
120
|
+
## given by 'r'.
|
121
|
+
def poke(r)
|
122
|
+
raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r
|
123
|
+
Covering.new(@domain, @ranges.inject([]) do |set, x|
|
124
|
+
if x.rint(r) != nil
|
125
|
+
set + x.rdiff(r)
|
126
|
+
else
|
127
|
+
set + [x]
|
128
|
+
end
|
129
|
+
end)
|
130
|
+
end
|
131
|
+
|
132
|
+
## given a covering of size N and a new range 'r', returns a
|
133
|
+
## covering of size 0 < s <= N + 1 that also covers the range 'r'.
|
134
|
+
def fill(r)
|
135
|
+
raise ArgumentError, "#{r} outside of domain #@domain" unless @domain.rss? r
|
136
|
+
Covering.new(@domain, @ranges.inject([]) do |set, x|
|
137
|
+
## r contains the result of the continuing merge. if r is nil,
|
138
|
+
## then we've already added it, so we just copy x.
|
139
|
+
if r.nil? then set + [x] else
|
140
|
+
## otoh, if r is there, we try and merge in the current
|
141
|
+
## element.
|
142
|
+
if r.rcont? x
|
143
|
+
## if we can merge, keep the union in r and don't add
|
144
|
+
## anything
|
145
|
+
r = r.runion x
|
146
|
+
set
|
147
|
+
## if we can't merge it, we'll see if it's time to add it. we
|
148
|
+
## know that r and x don't overlap because r.mergable?(x) was
|
149
|
+
## false, so we can simply compare the start points to see
|
150
|
+
## whether it should come before x.
|
151
|
+
elsif r.first < x.first
|
152
|
+
s = set + [r, x] # add both
|
153
|
+
r = nil
|
154
|
+
s
|
155
|
+
else set + [x] ## no merging or adding, so we just copy x.
|
156
|
+
end
|
157
|
+
end
|
158
|
+
## if 'r' still hasn't been added, it should be the last element,
|
159
|
+
## we add it here.
|
160
|
+
end.push(r).compact)
|
161
|
+
end
|
162
|
+
|
163
|
+
## given an array of non-overlapping ranges sorted by start point,
|
164
|
+
## and a range 'domain', returns the first range from 'domain' not
|
165
|
+
## covered by any range in the array.
|
166
|
+
def first_gap(domain=@domain)
|
167
|
+
start = domain.first
|
168
|
+
endd = nil
|
169
|
+
excl = nil
|
170
|
+
@ranges.each do |r|
|
171
|
+
next if r.last < start
|
172
|
+
|
173
|
+
if r.first > start # found a gap
|
174
|
+
if r.first < domain.last
|
175
|
+
return AwesomeRange.new(start, r.first, false)
|
176
|
+
else # r.first >= domain.last, so use domain's exclusion
|
177
|
+
return AwesomeRange.new(start, domain.last, domain.exclude_end?)
|
178
|
+
end
|
179
|
+
else # r.first <= start
|
180
|
+
start = r.last unless r.last < start
|
181
|
+
break if start > domain.last
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
if (start >= domain.last)
|
186
|
+
## entire domain was covered
|
187
|
+
nil
|
188
|
+
else
|
189
|
+
## tail end of the domain uncovered
|
190
|
+
AwesomeRange.new(start, domain.last, domain.exclude_end?)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def ==(o); o.domain == self.domain && o.ranges == self.ranges; end
|
195
|
+
end
|
196
|
+
|
197
|
+
## Blocks are very simple chunks of data which exist solely in
|
198
|
+
## memory. they are the basic currency of the bittorrent protocol. a
|
199
|
+
## Block can be divided into "chunks" (no intelligence there; it's
|
200
|
+
## solely for the purposes of buffered reading/writing) and one or
|
201
|
+
## more Blocks comprises a Piece.
|
202
|
+
class Block
|
203
|
+
attr_accessor :pindex, :begin, :length, :data, :requested
|
204
|
+
|
205
|
+
def initialize(pindex, beginn, length)
|
206
|
+
@pindex = pindex
|
207
|
+
@begin = beginn
|
208
|
+
@length = length
|
209
|
+
@data = nil
|
210
|
+
@requested = false
|
211
|
+
@time = nil
|
212
|
+
end
|
213
|
+
|
214
|
+
def requested?; @requested; end
|
215
|
+
def have_length; @data.length; end
|
216
|
+
def complete?; @data && (@data.length == @length); end
|
217
|
+
def mark_time; @time = Time.now; end
|
218
|
+
def time_elapsed; Time.now - @time; end
|
219
|
+
|
220
|
+
def to_s
|
221
|
+
"<block: p[#{@pindex}], #@begin + #@length #{(data.nil? || (data.length == 0) ? 'emp' : (complete? ? 'cmp' : 'inc'))}>"
|
222
|
+
end
|
223
|
+
|
224
|
+
## chunk can only be added to blocks in order
|
225
|
+
def add_chunk(chunk)
|
226
|
+
@data = "" if @data.nil?
|
227
|
+
raise "adding chunk would result in too much data (#{@data.length} + #{chunk.length} > #@length)" if (@data.length + chunk.length) > @length
|
228
|
+
@data += chunk
|
229
|
+
self
|
230
|
+
end
|
231
|
+
|
232
|
+
def each_chunk(blocksize)
|
233
|
+
raise "each_chunk called on incomplete block" unless complete?
|
234
|
+
start = 0
|
235
|
+
while(start < @length)
|
236
|
+
yield data[start, [blocksize, @length - start].min]
|
237
|
+
start += blocksize
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
def ==(o)
|
242
|
+
o.is_a?(Block) && (o.pindex == self.pindex) && (o.begin == self.begin) &&
|
243
|
+
(o.length == self.length)
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
## a Piece is the basic unit of the .torrent metainfo file (though not
|
248
|
+
## of the bittorrent protocol). Pieces store their data directly on
|
249
|
+
## disk, so many operations here will be slow. each Piece stores data
|
250
|
+
## in one or more file pointers.
|
251
|
+
##
|
252
|
+
## unlike Blocks and Packages, which are either complete or
|
253
|
+
## incomplete, a Piece can be complete but not valid, if the SHA1
|
254
|
+
## check fails. thus, a call to piece.complete? is not sufficient to
|
255
|
+
## determine whether the data is ok to use or not.
|
256
|
+
##
|
257
|
+
## Pieces handle all the trickiness involved with Blocks: taking in
|
258
|
+
## Blocks from arbitrary locations, writing them out to the correct
|
259
|
+
## set of file pointers, keeping track of which sections of the data
|
260
|
+
## have been filled, claimed but not filled, etc.
|
261
|
+
class Piece
|
262
|
+
include EventSource
|
263
|
+
|
264
|
+
attr_reader :index, :start, :length
|
265
|
+
event :complete
|
266
|
+
|
267
|
+
def initialize(index, sha1, start, length, files, validity_assumption=nil)
|
268
|
+
@index = index
|
269
|
+
@sha1 = sha1
|
270
|
+
@start = start
|
271
|
+
@length = length
|
272
|
+
@files = files # array of [file pointer, mutex, file length]
|
273
|
+
@valid = nil
|
274
|
+
|
275
|
+
## calculate where we start and end in terms of the file pointers.
|
276
|
+
@start_index = 0
|
277
|
+
sum = 0
|
278
|
+
while(sum + @files[@start_index][2] <= @start)
|
279
|
+
sum += @files[@start_index][2]
|
280
|
+
@start_index += 1
|
281
|
+
end
|
282
|
+
## now sum + @files[@start_index][2] > start, and sum <= start
|
283
|
+
@start_offset = @start - sum
|
284
|
+
|
285
|
+
## sections of the data we have
|
286
|
+
@have = Covering.new(AwesomeRange.new(0 ... @length)).complete!
|
287
|
+
@valid = validity_assumption
|
288
|
+
@have.empty! unless valid?
|
289
|
+
|
290
|
+
## sections of the data someone has laid claim to but hasn't yet
|
291
|
+
## provided. a super-set of @have.
|
292
|
+
@claimed = Covering.new(AwesomeRange.new(0 ... @length))
|
293
|
+
|
294
|
+
## protects @claimed, @have
|
295
|
+
@state_m = Mutex.new
|
296
|
+
end
|
297
|
+
|
298
|
+
def to_s
|
299
|
+
"<piece #@index: #@start + #@length #{(complete? ? 'cmp' : 'inc')}>"
|
300
|
+
end
|
301
|
+
|
302
|
+
def complete?; @have.complete?; end
|
303
|
+
def started?; !@claimed.empty? || !@have.empty?; end
|
304
|
+
|
305
|
+
def discard # discard all data
|
306
|
+
@state_m.synchronize do
|
307
|
+
@have.empty!
|
308
|
+
@claimed.empty!
|
309
|
+
end
|
310
|
+
@valid = false
|
311
|
+
end
|
312
|
+
|
313
|
+
def valid?
|
314
|
+
return @valid unless @valid.nil?
|
315
|
+
return (@valid = false) unless complete?
|
316
|
+
|
317
|
+
data = read_bytes(0, @length)
|
318
|
+
if (data.length != @length)
|
319
|
+
@valid = false
|
320
|
+
else
|
321
|
+
@valid = (Digest::SHA1.digest(data) == @sha1)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def unclaimed_bytes
|
326
|
+
r = 0
|
327
|
+
each_gap(@claimed) { |start, len| r += len }
|
328
|
+
r
|
329
|
+
end
|
330
|
+
|
331
|
+
def empty_bytes
|
332
|
+
r = 0
|
333
|
+
each_gap(@have) { |start, len| r += len }
|
334
|
+
r
|
335
|
+
end
|
336
|
+
|
337
|
+
def percent_claimed; 100.0 * (@length.to_f - unclaimed_bytes) / @length; end
|
338
|
+
def percent_done; 100.0 * (@length.to_f - empty_bytes) / @length; end
|
339
|
+
|
340
|
+
def each_unclaimed_block(max_length)
|
341
|
+
raise "no unclaimed blocks in a complete piece" if complete?
|
342
|
+
|
343
|
+
each_gap(@claimed, max_length) do |start, len|
|
344
|
+
yield Block.new(@index, start, len)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
def each_empty_block(max_length)
|
349
|
+
raise "no empty blocks in a complete piece" if complete?
|
350
|
+
|
351
|
+
each_gap(@have, max_length) do |start, len|
|
352
|
+
yield Block.new(@index, start, len)
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
def claim_block(b)
|
357
|
+
@state_m.synchronize do
|
358
|
+
@claimed = @claimed.fill AwesomeRange.new(b.begin ... (b.begin + b.length))
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
def unclaim_block(b)
|
363
|
+
@state_m.synchronize do
|
364
|
+
@claimed = @claimed.poke AwesomeRange.new(b.begin ... (b.begin + b.length))
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
## for a complete Piece, returns a complete Block of specified size
|
369
|
+
## and location.
|
370
|
+
def get_complete_block(beginn, length)
|
371
|
+
raise "can't make block from incomplete piece" unless complete?
|
372
|
+
raise "invalid parameters #{beginn}, #{length}" unless (length > 0) && (beginn + length) <= @length
|
373
|
+
|
374
|
+
b = Block.new(@index, beginn, length)
|
375
|
+
b.add_chunk read_bytes(beginn, length) # returns b
|
376
|
+
end
|
377
|
+
|
378
|
+
## we don't do any checking that this block has been claimed or not.
|
379
|
+
def add_block(b)
|
380
|
+
@valid = nil
|
381
|
+
write = false
|
382
|
+
new_have = @state_m.synchronize { @have.fill AwesomeRange.new(b.begin ... (b.begin + b.length)) }
|
383
|
+
if new_have != @have
|
384
|
+
@have = new_have
|
385
|
+
write = true
|
386
|
+
end
|
387
|
+
|
388
|
+
write_bytes(b.begin, b.data) if write
|
389
|
+
send_event(:complete) if complete?
|
390
|
+
end
|
391
|
+
|
392
|
+
private
|
393
|
+
|
394
|
+
## yields successive gaps from 'array' between 0 and @length
|
395
|
+
def each_gap(covering, max_length=nil)
|
396
|
+
return if covering.complete?
|
397
|
+
|
398
|
+
range_first = 0
|
399
|
+
while true
|
400
|
+
range = covering.first_gap(range_first ... @length)
|
401
|
+
break if range.nil? || (range.first == range.last)
|
402
|
+
start = range.first
|
403
|
+
|
404
|
+
while start < range.last
|
405
|
+
len = range.last - start
|
406
|
+
len = max_length if max_length && (max_length < len)
|
407
|
+
yield start, len
|
408
|
+
start += len
|
409
|
+
end
|
410
|
+
|
411
|
+
range_first = range.last
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
def write_bytes(start, data); do_bytes(start, 0, data); end
|
416
|
+
def read_bytes(start, length); do_bytes(start, length, nil); end
|
417
|
+
|
418
|
+
## do the dirty work of splitting the read/writes across multiple
|
419
|
+
## file pointers to possibly incomplete, possibly overcomplete files
|
420
|
+
def do_bytes(start, length, data)
|
421
|
+
raise ArgumentError, "invalid start" if (start < 0) || (start > @length)
|
422
|
+
# raise "invalid length" if (length < 0) || (start + length > @length)
|
423
|
+
|
424
|
+
start += @start_offset
|
425
|
+
index = @start_index
|
426
|
+
sum = 0
|
427
|
+
while(sum + @files[index][2] <= start)
|
428
|
+
sum += @files[index][2]
|
429
|
+
index += 1
|
430
|
+
end
|
431
|
+
offset = start - sum
|
432
|
+
|
433
|
+
done = 0
|
434
|
+
abort = false
|
435
|
+
if data.nil?
|
436
|
+
want = length
|
437
|
+
ret = ""
|
438
|
+
else
|
439
|
+
want = data.length
|
440
|
+
ret = 0
|
441
|
+
end
|
442
|
+
while (done < want) && !abort
|
443
|
+
break if index > @files.length
|
444
|
+
fp, mutex, size = @files[index]
|
445
|
+
mutex.synchronize do
|
446
|
+
fp.seek offset
|
447
|
+
here = [want - done, size - offset].min
|
448
|
+
if data.nil?
|
449
|
+
# puts "> reading #{here} bytes from #{index} at #{offset}"
|
450
|
+
s = fp.read here
|
451
|
+
# puts "> got #{(s.nil? ? s.inspect : s.length)} bytes"
|
452
|
+
if s.nil?
|
453
|
+
abort = true
|
454
|
+
else
|
455
|
+
ret += s
|
456
|
+
abort = true if s.length < here
|
457
|
+
# puts "fp.tell is #{fp.tell}, size is #{size}, eof #{fp.eof?}"
|
458
|
+
if (fp.tell == size) && !fp.eof?
|
459
|
+
rt_warning "file #{index}: not at eof after #{size} bytes, truncating"
|
460
|
+
fp.truncate(size - 1)
|
461
|
+
end
|
462
|
+
end
|
463
|
+
else
|
464
|
+
# puts "> writing #{here} bytes to #{index} at #{offset}"
|
465
|
+
x = fp.write data[done, here]
|
466
|
+
ret += here
|
467
|
+
# @files[index][0].flush
|
468
|
+
end
|
469
|
+
done += here
|
470
|
+
end
|
471
|
+
index += 1
|
472
|
+
offset = 0
|
473
|
+
end
|
474
|
+
|
475
|
+
ret
|
476
|
+
end
|
477
|
+
end
|
478
|
+
|
479
|
+
## finally, the Package. one Package per Controller so we don't do any
|
480
|
+
## thread safety stuff in here.
|
481
|
+
class Package
|
482
|
+
include EventSource
|
483
|
+
|
484
|
+
attr_reader :pieces, :size
|
485
|
+
event :complete
|
486
|
+
|
487
|
+
def initialize(metainfo, out=nil, validity_assumption=nil)
|
488
|
+
info = metainfo.info
|
489
|
+
|
490
|
+
created = false
|
491
|
+
out ||= info.name
|
492
|
+
case out
|
493
|
+
when File
|
494
|
+
raise ArgumentError, "'out' cannot be a File for a multi-file .torrent" if info.multiple?
|
495
|
+
fstream = out
|
496
|
+
when Dir
|
497
|
+
raise ArgumentError, "'out' cannot be a Dir for a single-file .torrent" if info.single?
|
498
|
+
fstream = out
|
499
|
+
when String
|
500
|
+
if info.single?
|
501
|
+
rt_debug "output file is #{out}"
|
502
|
+
begin
|
503
|
+
fstream = File.open(out, "rb+")
|
504
|
+
rescue Errno::ENOENT
|
505
|
+
created = true
|
506
|
+
fstream = File.open(out, "wb+")
|
507
|
+
end
|
508
|
+
else
|
509
|
+
rt_debug "output directory is #{out}"
|
510
|
+
unless File.exists? out
|
511
|
+
Dir.mkdir(out)
|
512
|
+
created = true
|
513
|
+
end
|
514
|
+
fstream = Dir.open(out)
|
515
|
+
end
|
516
|
+
else
|
517
|
+
raise ArgumentError, "'out' should be a File, Dir or String object, is #{out.class}"
|
518
|
+
end
|
519
|
+
|
520
|
+
@ro = false
|
521
|
+
@size = info.total_length
|
522
|
+
if info.single?
|
523
|
+
@files = [[fstream, Mutex.new, info.length]]
|
524
|
+
else
|
525
|
+
@files = info.files.map do |finfo|
|
526
|
+
path = File.join(finfo.path[0, finfo.path.length - 1].inject(fstream.path) do |path, el|
|
527
|
+
dir = File.join(path, el)
|
528
|
+
unless File.exist? dir
|
529
|
+
rt_debug "making directory #{dir}"
|
530
|
+
Dir.mkdir dir
|
531
|
+
end
|
532
|
+
dir
|
533
|
+
end, finfo.path[finfo.path.length - 1])
|
534
|
+
rt_debug "opening #{path}..."
|
535
|
+
[open_file(path), Mutex.new, finfo.length]
|
536
|
+
end
|
537
|
+
end
|
538
|
+
|
539
|
+
i = 0
|
540
|
+
@pieces = info.pieces.unpack("a20" * (info.pieces.length / 20)).map do |hash|
|
541
|
+
start = (info.piece_length * i)
|
542
|
+
len = [info.piece_length, @size - start].min
|
543
|
+
p = Piece.new(i, hash, start, len, @files, (created ? false : validity_assumption))
|
544
|
+
p.on_event(self, :complete) { send_event(:complete) if complete? }
|
545
|
+
yield p if block_given?
|
546
|
+
(i += 1) && p
|
547
|
+
end
|
548
|
+
|
549
|
+
reopen_ro if complete?
|
550
|
+
end
|
551
|
+
|
552
|
+
def ro?; @ro; end
|
553
|
+
def reopen_ro
|
554
|
+
raise "called on incomplete package" unless complete?
|
555
|
+
return if @ro
|
556
|
+
|
557
|
+
rt_debug "reopening all files with mode r"
|
558
|
+
@files = @files.map do |fp, mutex, size|
|
559
|
+
[fp.reopen(fp.path, "rb"), mutex, size]
|
560
|
+
end
|
561
|
+
@ro = true
|
562
|
+
end
|
563
|
+
|
564
|
+
def complete?; @pieces.detect { |p| !p.complete? || !p.valid? } == nil; end
|
565
|
+
|
566
|
+
def bytes_completed
|
567
|
+
@pieces.inject(0) { |s, p| s + (p.complete? ? p.length : 0) }
|
568
|
+
end
|
569
|
+
|
570
|
+
def pieces_completed
|
571
|
+
@pieces.inject(0) { |s, p| s + (p.complete? ? 1 : 0) }
|
572
|
+
end
|
573
|
+
|
574
|
+
def percent_completed
|
575
|
+
100.0 * pieces_completed.to_f / @pieces.length.to_f
|
576
|
+
end
|
577
|
+
|
578
|
+
def num_pieces; @pieces.length; end
|
579
|
+
|
580
|
+
def to_s
|
581
|
+
"<#{self.class} size #@size>"
|
582
|
+
end
|
583
|
+
|
584
|
+
private
|
585
|
+
|
586
|
+
def open_file(path)
|
587
|
+
begin
|
588
|
+
File.open(path, "rb+")
|
589
|
+
rescue Errno::ENOENT
|
590
|
+
File.open(path, "wb+")
|
591
|
+
end
|
592
|
+
end
|
593
|
+
end
|
594
|
+
|
595
|
+
end
|