murder 0.0.0.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. data/.gitignore +1 -0
  2. data/LICENSE +17 -0
  3. data/README +224 -0
  4. data/Rakefile +52 -0
  5. data/VERSION +1 -0
  6. data/dist/BitTornado/BT1/Choker.py +128 -0
  7. data/dist/BitTornado/BT1/Connecter.py +288 -0
  8. data/dist/BitTornado/BT1/Downloader.py +594 -0
  9. data/dist/BitTornado/BT1/DownloaderFeedback.py +155 -0
  10. data/dist/BitTornado/BT1/Encrypter.py +333 -0
  11. data/dist/BitTornado/BT1/FileSelector.py +245 -0
  12. data/dist/BitTornado/BT1/Filter.py +12 -0
  13. data/dist/BitTornado/BT1/HTTPDownloader.py +251 -0
  14. data/dist/BitTornado/BT1/NatCheck.py +95 -0
  15. data/dist/BitTornado/BT1/PiecePicker.py +320 -0
  16. data/dist/BitTornado/BT1/Rerequester.py +426 -0
  17. data/dist/BitTornado/BT1/Statistics.py +177 -0
  18. data/dist/BitTornado/BT1/Storage.py +584 -0
  19. data/dist/BitTornado/BT1/StorageWrapper.py +1045 -0
  20. data/dist/BitTornado/BT1/StreamCheck.py +135 -0
  21. data/dist/BitTornado/BT1/T2T.py +193 -0
  22. data/dist/BitTornado/BT1/Uploader.py +145 -0
  23. data/dist/BitTornado/BT1/__init__.py +1 -0
  24. data/dist/BitTornado/BT1/btformats.py +100 -0
  25. data/dist/BitTornado/BT1/fakeopen.py +89 -0
  26. data/dist/BitTornado/BT1/makemetafile.py +263 -0
  27. data/dist/BitTornado/BT1/track.py +1067 -0
  28. data/dist/BitTornado/ConfigDir.py +401 -0
  29. data/dist/BitTornado/ConfigReader.py +1068 -0
  30. data/dist/BitTornado/ConnChoice.py +31 -0
  31. data/dist/BitTornado/CreateIcons.py +105 -0
  32. data/dist/BitTornado/CurrentRateMeasure.py +37 -0
  33. data/dist/BitTornado/HTTPHandler.py +167 -0
  34. data/dist/BitTornado/PSYCO.py +5 -0
  35. data/dist/BitTornado/RateLimiter.py +153 -0
  36. data/dist/BitTornado/RateMeasure.py +75 -0
  37. data/dist/BitTornado/RawServer.py +195 -0
  38. data/dist/BitTornado/ServerPortHandler.py +188 -0
  39. data/dist/BitTornado/SocketHandler.py +375 -0
  40. data/dist/BitTornado/__init__.py +63 -0
  41. data/dist/BitTornado/bencode.py +319 -0
  42. data/dist/BitTornado/bitfield.py +162 -0
  43. data/dist/BitTornado/clock.py +27 -0
  44. data/dist/BitTornado/download_bt1.py +882 -0
  45. data/dist/BitTornado/inifile.py +169 -0
  46. data/dist/BitTornado/iprangeparse.py +194 -0
  47. data/dist/BitTornado/launchmanycore.py +381 -0
  48. data/dist/BitTornado/natpunch.py +254 -0
  49. data/dist/BitTornado/parseargs.py +137 -0
  50. data/dist/BitTornado/parsedir.py +150 -0
  51. data/dist/BitTornado/piecebuffer.py +86 -0
  52. data/dist/BitTornado/selectpoll.py +109 -0
  53. data/dist/BitTornado/subnetparse.py +218 -0
  54. data/dist/BitTornado/torrentlistparse.py +38 -0
  55. data/dist/BitTornado/zurllib.py +100 -0
  56. data/dist/murder_client.py +291 -0
  57. data/dist/murder_make_torrent.py +46 -0
  58. data/dist/murder_tracker.py +28 -0
  59. data/doc/examples/Capfile +28 -0
  60. data/lib/capistrano/recipes/deploy/strategy/murder.rb +52 -0
  61. data/lib/murder.rb +43 -0
  62. data/lib/murder/admin.rb +47 -0
  63. data/lib/murder/murder.rb +121 -0
  64. data/murder.gemspec +101 -0
  65. metadata +129 -0
@@ -0,0 +1,169 @@
1
+ # Written by John Hoffman
2
+ # see LICENSE.txt for license information
3
+
4
+ '''
5
+ reads/writes a Windows-style INI file
6
+ format:
7
+
8
+ aa = "bb"
9
+ cc = 11
10
+
11
+ [eee]
12
+ ff = "gg"
13
+
14
+ decodes to:
15
+ d = { '': {'aa':'bb','cc':'11'}, 'eee': {'ff':'gg'} }
16
+
17
+ the encoder can also take this as input:
18
+
19
+ d = { 'aa': 'bb, 'cc': 11, 'eee': {'ff':'gg'} }
20
+
21
+ though it will only decode in the above format. Keywords must be strings.
22
+ Values that are strings are written surrounded by quotes, and the decoding
23
+ routine automatically strips any.
24
+ Booleans are written as integers. Anything else aside from string/int/float
25
+ may have unpredictable results.
26
+ '''
27
+
28
+ from cStringIO import StringIO
29
+ from traceback import print_exc
30
+ from types import DictType, StringType
31
+ try:
32
+ from types import BooleanType
33
+ except ImportError:
34
+ BooleanType = None
35
+
36
+ try:
37
+ True
38
+ except:
39
+ True = 1
40
+ False = 0
41
+
42
+ DEBUG = False
43
+
44
+ def ini_write(f, d, comment=''):
45
+ try:
46
+ a = {'':{}}
47
+ for k,v in d.items():
48
+ assert type(k) == StringType
49
+ k = k.lower()
50
+ if type(v) == DictType:
51
+ if DEBUG:
52
+ print 'new section:' +k
53
+ if k:
54
+ assert not a.has_key(k)
55
+ a[k] = {}
56
+ aa = a[k]
57
+ for kk,vv in v:
58
+ assert type(kk) == StringType
59
+ kk = kk.lower()
60
+ assert not aa.has_key(kk)
61
+ if type(vv) == BooleanType:
62
+ vv = int(vv)
63
+ if type(vv) == StringType:
64
+ vv = '"'+vv+'"'
65
+ aa[kk] = str(vv)
66
+ if DEBUG:
67
+ print 'a['+k+']['+kk+'] = '+str(vv)
68
+ else:
69
+ aa = a['']
70
+ assert not aa.has_key(k)
71
+ if type(v) == BooleanType:
72
+ v = int(v)
73
+ if type(v) == StringType:
74
+ v = '"'+v+'"'
75
+ aa[k] = str(v)
76
+ if DEBUG:
77
+ print 'a[\'\']['+k+'] = '+str(v)
78
+ r = open(f,'w')
79
+ if comment:
80
+ for c in comment.split('\n'):
81
+ r.write('# '+c+'\n')
82
+ r.write('\n')
83
+ l = a.keys()
84
+ l.sort()
85
+ for k in l:
86
+ if k:
87
+ r.write('\n['+k+']\n')
88
+ aa = a[k]
89
+ ll = aa.keys()
90
+ ll.sort()
91
+ for kk in ll:
92
+ r.write(kk+' = '+aa[kk]+'\n')
93
+ success = True
94
+ except:
95
+ if DEBUG:
96
+ print_exc()
97
+ success = False
98
+ try:
99
+ r.close()
100
+ except:
101
+ pass
102
+ return success
103
+
104
+
105
+ if DEBUG:
106
+ def errfunc(lineno, line, err):
107
+ print '('+str(lineno)+') '+err+': '+line
108
+ else:
109
+ errfunc = lambda lineno, line, err: None
110
+
111
+ def ini_read(f, errfunc = errfunc):
112
+ try:
113
+ r = open(f,'r')
114
+ ll = r.readlines()
115
+ d = {}
116
+ dd = {'':d}
117
+ for i in xrange(len(ll)):
118
+ l = ll[i]
119
+ l = l.strip()
120
+ if not l:
121
+ continue
122
+ if l[0] == '#':
123
+ continue
124
+ if l[0] == '[':
125
+ if l[-1] != ']':
126
+ errfunc(i,l,'syntax error')
127
+ continue
128
+ l1 = l[1:-1].strip().lower()
129
+ if not l1:
130
+ errfunc(i,l,'syntax error')
131
+ continue
132
+ if dd.has_key(l1):
133
+ errfunc(i,l,'duplicate section')
134
+ d = dd[l1]
135
+ continue
136
+ d = {}
137
+ dd[l1] = d
138
+ continue
139
+ try:
140
+ k,v = l.split('=',1)
141
+ except:
142
+ try:
143
+ k,v = l.split(':',1)
144
+ except:
145
+ errfunc(i,l,'syntax error')
146
+ continue
147
+ k = k.strip().lower()
148
+ v = v.strip()
149
+ if len(v) > 1 and ( (v[0] == '"' and v[-1] == '"') or
150
+ (v[0] == "'" and v[-1] == "'") ):
151
+ v = v[1:-1]
152
+ if not k:
153
+ errfunc(i,l,'syntax error')
154
+ continue
155
+ if d.has_key(k):
156
+ errfunc(i,l,'duplicate entry')
157
+ continue
158
+ d[k] = v
159
+ if DEBUG:
160
+ print dd
161
+ except:
162
+ if DEBUG:
163
+ print_exc()
164
+ dd = None
165
+ try:
166
+ r.close()
167
+ except:
168
+ pass
169
+ return dd
@@ -0,0 +1,194 @@
1
+ # Written by John Hoffman
2
+ # see LICENSE.txt for license information
3
+
4
+ from bisect import bisect, insort
5
+
6
+ try:
7
+ True
8
+ except:
9
+ True = 1
10
+ False = 0
11
+ bool = lambda x: not not x
12
+
13
+
14
+ def to_long_ipv4(ip):
15
+ ip = ip.split('.')
16
+ if len(ip) != 4:
17
+ raise ValueError, "bad address"
18
+ b = 0L
19
+ for n in ip:
20
+ b *= 256
21
+ b += int(n)
22
+ return b
23
+
24
+
25
+ def to_long_ipv6(ip):
26
+ if ip == '':
27
+ raise ValueError, "bad address"
28
+ if ip == '::': # boundary handling
29
+ ip = ''
30
+ elif ip[:2] == '::':
31
+ ip = ip[1:]
32
+ elif ip[0] == ':':
33
+ raise ValueError, "bad address"
34
+ elif ip[-2:] == '::':
35
+ ip = ip[:-1]
36
+ elif ip[-1] == ':':
37
+ raise ValueError, "bad address"
38
+
39
+ b = []
40
+ doublecolon = False
41
+ for n in ip.split(':'):
42
+ if n == '': # double-colon
43
+ if doublecolon:
44
+ raise ValueError, "bad address"
45
+ doublecolon = True
46
+ b.append(None)
47
+ continue
48
+ if n.find('.') >= 0: # IPv4
49
+ n = n.split('.')
50
+ if len(n) != 4:
51
+ raise ValueError, "bad address"
52
+ for i in n:
53
+ b.append(int(i))
54
+ continue
55
+ n = ('0'*(4-len(n))) + n
56
+ b.append(int(n[:2],16))
57
+ b.append(int(n[2:],16))
58
+ bb = 0L
59
+ for n in b:
60
+ if n is None:
61
+ for i in xrange(17-len(b)):
62
+ bb *= 256
63
+ continue
64
+ bb *= 256
65
+ bb += n
66
+ return bb
67
+
68
+ ipv4addrmask = 65535L*256*256*256*256
69
+
70
+ class IP_List:
71
+ def __init__(self):
72
+ self.ipv4list = [] # starts of ranges
73
+ self.ipv4dict = {} # start: end of ranges
74
+ self.ipv6list = [] # "
75
+ self.ipv6dict = {} # "
76
+
77
+ def __nonzero__(self):
78
+ return bool(self.ipv4list or self.ipv6list)
79
+
80
+
81
+ def append(self, ip_beg, ip_end = None):
82
+ if ip_end is None:
83
+ ip_end = ip_beg
84
+ else:
85
+ assert ip_beg <= ip_end
86
+ if ip_beg.find(':') < 0: # IPv4
87
+ ip_beg = to_long_ipv4(ip_beg)
88
+ ip_end = to_long_ipv4(ip_end)
89
+ l = self.ipv4list
90
+ d = self.ipv4dict
91
+ else:
92
+ ip_beg = to_long_ipv6(ip_beg)
93
+ ip_end = to_long_ipv6(ip_end)
94
+ bb = ip_beg % (256*256*256*256)
95
+ if bb == ipv4addrmask:
96
+ ip_beg -= bb
97
+ ip_end -= bb
98
+ l = self.ipv4list
99
+ d = self.ipv4dict
100
+ else:
101
+ l = self.ipv6list
102
+ d = self.ipv6dict
103
+
104
+ pos = bisect(l,ip_beg)-1
105
+ done = pos < 0
106
+ while not done:
107
+ p = pos
108
+ while p < len(l):
109
+ range_beg = l[p]
110
+ if range_beg > ip_end+1:
111
+ done = True
112
+ break
113
+ range_end = d[range_beg]
114
+ if range_end < ip_beg-1:
115
+ p += 1
116
+ if p == len(l):
117
+ done = True
118
+ break
119
+ continue
120
+ # if neither of the above conditions is true, the ranges overlap
121
+ ip_beg = min(ip_beg, range_beg)
122
+ ip_end = max(ip_end, range_end)
123
+ del l[p]
124
+ del d[range_beg]
125
+ break
126
+
127
+ insort(l,ip_beg)
128
+ d[ip_beg] = ip_end
129
+
130
+
131
+ def includes(self, ip):
132
+ if not (self.ipv4list or self.ipv6list):
133
+ return False
134
+ if ip.find(':') < 0: # IPv4
135
+ ip = to_long_ipv4(ip)
136
+ l = self.ipv4list
137
+ d = self.ipv4dict
138
+ else:
139
+ ip = to_long_ipv6(ip)
140
+ bb = ip % (256*256*256*256)
141
+ if bb == ipv4addrmask:
142
+ ip -= bb
143
+ l = self.ipv4list
144
+ d = self.ipv4dict
145
+ else:
146
+ l = self.ipv6list
147
+ d = self.ipv6dict
148
+ for ip_beg in l[bisect(l,ip)-1:]:
149
+ if ip == ip_beg:
150
+ return True
151
+ ip_end = d[ip_beg]
152
+ if ip > ip_beg and ip <= ip_end:
153
+ return True
154
+ return False
155
+
156
+
157
+ # reads a list from a file in the format 'whatever:whatever:ip-ip'
158
+ # (not IPv6 compatible at all)
159
+ def read_rangelist(self, file):
160
+ f = open(file, 'r')
161
+ while True:
162
+ line = f.readline()
163
+ if not line:
164
+ break
165
+ line = line.strip()
166
+ if not line or line[0] == '#':
167
+ continue
168
+ line = line.split(':')[-1]
169
+ try:
170
+ ip1,ip2 = line.split('-')
171
+ except:
172
+ ip1 = line
173
+ ip2 = line
174
+ try:
175
+ self.append(ip1.strip(),ip2.strip())
176
+ except:
177
+ print '*** WARNING *** could not parse IP range: '+line
178
+ f.close()
179
+
180
+ def is_ipv4(ip):
181
+ return ip.find(':') < 0
182
+
183
+ def is_valid_ip(ip):
184
+ try:
185
+ if is_ipv4(ip):
186
+ a = ip.split('.')
187
+ assert len(a) == 4
188
+ for i in a:
189
+ chr(int(i))
190
+ return True
191
+ to_long_ipv6(ip)
192
+ return True
193
+ except:
194
+ return False
@@ -0,0 +1,381 @@
1
+ #!/usr/bin/env python
2
+
3
+ # Written by John Hoffman
4
+ # see LICENSE.txt for license information
5
+
6
+ from BitTornado import PSYCO
7
+ if PSYCO.psyco:
8
+ try:
9
+ import psyco
10
+ assert psyco.__version__ >= 0x010100f0
11
+ psyco.full()
12
+ except:
13
+ pass
14
+
15
+ from download_bt1 import BT1Download
16
+ from RawServer import RawServer, UPnP_ERROR
17
+ from RateLimiter import RateLimiter
18
+ from ServerPortHandler import MultiHandler
19
+ from parsedir import parsedir
20
+ from natpunch import UPnP_test
21
+ from random import seed
22
+ from socket import error as socketerror
23
+ from threading import Event
24
+ from sys import argv, exit
25
+ import sys, os
26
+ from clock import clock
27
+ from __init__ import createPeerID, mapbase64, version
28
+ from cStringIO import StringIO
29
+ from traceback import print_exc
30
+
31
+ try:
32
+ True
33
+ except:
34
+ True = 1
35
+ False = 0
36
+
37
+
38
+ def fmttime(n):
39
+ try:
40
+ n = int(n) # n may be None or too large
41
+ assert n < 5184000 # 60 days
42
+ except:
43
+ return 'downloading'
44
+ m, s = divmod(n, 60)
45
+ h, m = divmod(m, 60)
46
+ return '%d:%02d:%02d' % (h, m, s)
47
+
48
+ class SingleDownload:
49
+ def __init__(self, controller, hash, response, config, myid):
50
+ self.controller = controller
51
+ self.hash = hash
52
+ self.response = response
53
+ self.config = config
54
+
55
+ self.doneflag = Event()
56
+ self.waiting = True
57
+ self.checking = False
58
+ self.working = False
59
+ self.seed = False
60
+ self.closed = False
61
+
62
+ self.status_msg = ''
63
+ self.status_err = ['']
64
+ self.status_errtime = 0
65
+ self.status_done = 0.0
66
+
67
+ self.rawserver = controller.handler.newRawServer(hash, self.doneflag)
68
+
69
+ d = BT1Download(self.display, self.finished, self.error,
70
+ controller.exchandler, self.doneflag, config, response,
71
+ hash, myid, self.rawserver, controller.listen_port)
72
+ self.d = d
73
+
74
+ def start(self):
75
+ if not self.d.saveAs(self.saveAs):
76
+ self._shutdown()
77
+ return
78
+ self._hashcheckfunc = self.d.initFiles()
79
+ if not self._hashcheckfunc:
80
+ self._shutdown()
81
+ return
82
+ self.controller.hashchecksched(self.hash)
83
+
84
+
85
+ def saveAs(self, name, length, saveas, isdir):
86
+ return self.controller.saveAs(self.hash, name, saveas, isdir)
87
+
88
+ def hashcheck_start(self, donefunc):
89
+ if self.is_dead():
90
+ self._shutdown()
91
+ return
92
+ self.waiting = False
93
+ self.checking = True
94
+ self._hashcheckfunc(donefunc)
95
+
96
+ def hashcheck_callback(self):
97
+ self.checking = False
98
+ if self.is_dead():
99
+ self._shutdown()
100
+ return
101
+ if not self.d.startEngine(ratelimiter = self.controller.ratelimiter):
102
+ self._shutdown()
103
+ return
104
+ self.d.startRerequester()
105
+ self.statsfunc = self.d.startStats()
106
+ self.rawserver.start_listening(self.d.getPortHandler())
107
+ self.working = True
108
+
109
+ def is_dead(self):
110
+ return self.doneflag.isSet()
111
+
112
+ def _shutdown(self):
113
+ self.shutdown(False)
114
+
115
+ def shutdown(self, quiet=True):
116
+ if self.closed:
117
+ return
118
+ self.doneflag.set()
119
+ self.rawserver.shutdown()
120
+ if self.checking or self.working:
121
+ self.d.shutdown()
122
+ self.waiting = False
123
+ self.checking = False
124
+ self.working = False
125
+ self.closed = True
126
+ self.controller.was_stopped(self.hash)
127
+ if not quiet:
128
+ self.controller.died(self.hash)
129
+
130
+
131
+ def display(self, activity = None, fractionDone = None):
132
+ # really only used by StorageWrapper now
133
+ if activity:
134
+ self.status_msg = activity
135
+ if fractionDone is not None:
136
+ self.status_done = float(fractionDone)
137
+
138
+ def finished(self):
139
+ self.seed = True
140
+
141
+ def error(self, msg):
142
+ if self.doneflag.isSet():
143
+ self._shutdown()
144
+ self.status_err.append(msg)
145
+ self.status_errtime = clock()
146
+
147
+
148
+ class LaunchMany:
149
+ def __init__(self, config, Output):
150
+ try:
151
+ self.config = config
152
+ self.Output = Output
153
+
154
+ self.torrent_dir = config['torrent_dir']
155
+ self.torrent_cache = {}
156
+ self.file_cache = {}
157
+ self.blocked_files = {}
158
+ self.scan_period = config['parse_dir_interval']
159
+ self.stats_period = config['display_interval']
160
+
161
+ self.torrent_list = []
162
+ self.downloads = {}
163
+ self.counter = 0
164
+ self.doneflag = Event()
165
+
166
+ self.hashcheck_queue = []
167
+ self.hashcheck_current = None
168
+
169
+ self.rawserver = RawServer(self.doneflag, config['timeout_check_interval'],
170
+ config['timeout'], ipv6_enable = config['ipv6_enabled'],
171
+ failfunc = self.failed, errorfunc = self.exchandler)
172
+ upnp_type = UPnP_test(config['upnp_nat_access'])
173
+ while True:
174
+ try:
175
+ self.listen_port = self.rawserver.find_and_bind(
176
+ config['minport'], config['maxport'], config['bind'],
177
+ ipv6_socket_style = config['ipv6_binds_v4'],
178
+ upnp = upnp_type, randomizer = config['random_port'])
179
+ break
180
+ except socketerror, e:
181
+ if upnp_type and e == UPnP_ERROR:
182
+ self.Output.message('WARNING: COULD NOT FORWARD VIA UPnP')
183
+ upnp_type = 0
184
+ continue
185
+ self.failed("Couldn't listen - " + str(e))
186
+ return
187
+
188
+ self.ratelimiter = RateLimiter(self.rawserver.add_task,
189
+ config['upload_unit_size'])
190
+ self.ratelimiter.set_upload_rate(config['max_upload_rate'])
191
+
192
+ self.handler = MultiHandler(self.rawserver, self.doneflag)
193
+ seed(createPeerID())
194
+ self.rawserver.add_task(self.scan, 0)
195
+ self.rawserver.add_task(self.stats, 0)
196
+
197
+ self.handler.listen_forever()
198
+
199
+ self.Output.message('shutting down')
200
+ self.hashcheck_queue = []
201
+ for hash in self.torrent_list:
202
+ self.Output.message('dropped "'+self.torrent_cache[hash]['path']+'"')
203
+ self.downloads[hash].shutdown()
204
+ self.rawserver.shutdown()
205
+
206
+ except:
207
+ data = StringIO()
208
+ print_exc(file = data)
209
+ Output.exception(data.getvalue())
210
+
211
+
212
+ def scan(self):
213
+ self.rawserver.add_task(self.scan, self.scan_period)
214
+
215
+ r = parsedir(self.torrent_dir, self.torrent_cache,
216
+ self.file_cache, self.blocked_files,
217
+ return_metainfo = True, errfunc = self.Output.message)
218
+
219
+ ( self.torrent_cache, self.file_cache, self.blocked_files,
220
+ added, removed ) = r
221
+
222
+ for hash, data in removed.items():
223
+ self.Output.message('dropped "'+data['path']+'"')
224
+ self.remove(hash)
225
+ for hash, data in added.items():
226
+ self.Output.message('added "'+data['path']+'"')
227
+ self.add(hash, data)
228
+
229
+ def stats(self):
230
+ self.rawserver.add_task(self.stats, self.stats_period)
231
+ data = []
232
+ for hash in self.torrent_list:
233
+ cache = self.torrent_cache[hash]
234
+ if self.config['display_path']:
235
+ name = cache['path']
236
+ else:
237
+ name = cache['name']
238
+ size = cache['length']
239
+ d = self.downloads[hash]
240
+ progress = '0.0%'
241
+ peers = 0
242
+ seeds = 0
243
+ seedsmsg = "S"
244
+ dist = 0.0
245
+ uprate = 0.0
246
+ dnrate = 0.0
247
+ upamt = 0
248
+ dnamt = 0
249
+ t = 0
250
+ if d.is_dead():
251
+ status = 'stopped'
252
+ elif d.waiting:
253
+ status = 'waiting for hash check'
254
+ elif d.checking:
255
+ status = d.status_msg
256
+ progress = '%.1f%%' % (d.status_done*100)
257
+ else:
258
+ stats = d.statsfunc()
259
+ s = stats['stats']
260
+ if d.seed:
261
+ status = 'seeding'
262
+ progress = '100.0%'
263
+ seeds = s.numOldSeeds
264
+ seedsmsg = "s"
265
+ dist = s.numCopies
266
+ else:
267
+ if s.numSeeds + s.numPeers:
268
+ t = stats['time']
269
+ if t == 0: # unlikely
270
+ t = 0.01
271
+ status = fmttime(t)
272
+ else:
273
+ t = -1
274
+ status = 'connecting to peers'
275
+ progress = '%.1f%%' % (int(stats['frac']*1000)/10.0)
276
+ seeds = s.numSeeds
277
+ dist = s.numCopies2
278
+ dnrate = stats['down']
279
+ peers = s.numPeers
280
+ uprate = stats['up']
281
+ upamt = s.upTotal
282
+ dnamt = s.downTotal
283
+
284
+ if d.is_dead() or d.status_errtime+300 > clock():
285
+ msg = d.status_err[-1]
286
+ else:
287
+ msg = ''
288
+
289
+ data.append(( name, status, progress, peers, seeds, seedsmsg, dist,
290
+ uprate, dnrate, upamt, dnamt, size, t, msg ))
291
+ stop = self.Output.display(data)
292
+ if stop:
293
+ self.doneflag.set()
294
+
295
+ def remove(self, hash):
296
+ self.torrent_list.remove(hash)
297
+ self.downloads[hash].shutdown()
298
+ del self.downloads[hash]
299
+
300
+ def add(self, hash, data):
301
+ c = self.counter
302
+ self.counter += 1
303
+ x = ''
304
+ for i in xrange(3):
305
+ x = mapbase64[c & 0x3F]+x
306
+ c >>= 6
307
+ peer_id = createPeerID(x)
308
+ d = SingleDownload(self, hash, data['metainfo'], self.config, peer_id)
309
+ self.torrent_list.append(hash)
310
+ self.downloads[hash] = d
311
+ d.start()
312
+
313
+
314
+ def saveAs(self, hash, name, saveas, isdir):
315
+ x = self.torrent_cache[hash]
316
+ style = self.config['saveas_style']
317
+ if style == 1 or style == 3:
318
+ if saveas:
319
+ saveas = os.path.join(saveas,x['file'][:-1-len(x['type'])])
320
+ else:
321
+ saveas = x['path'][:-1-len(x['type'])]
322
+ if style == 3:
323
+ if not os.path.isdir(saveas):
324
+ try:
325
+ os.mkdir(saveas)
326
+ except:
327
+ raise OSError("couldn't create directory for "+x['path']
328
+ +" ("+saveas+")")
329
+ if not isdir:
330
+ saveas = os.path.join(saveas, name)
331
+ else:
332
+ if saveas:
333
+ saveas = os.path.join(saveas, name)
334
+ else:
335
+ saveas = os.path.join(os.path.split(x['path'])[0], name)
336
+
337
+ if isdir and not os.path.isdir(saveas):
338
+ try:
339
+ os.mkdir(saveas)
340
+ except:
341
+ raise OSError("couldn't create directory for "+x['path']
342
+ +" ("+saveas+")")
343
+ return saveas
344
+
345
+
346
+ def hashchecksched(self, hash = None):
347
+ if hash:
348
+ self.hashcheck_queue.append(hash)
349
+ if not self.hashcheck_current:
350
+ self._hashcheck_start()
351
+
352
+ def _hashcheck_start(self):
353
+ self.hashcheck_current = self.hashcheck_queue.pop(0)
354
+ self.downloads[self.hashcheck_current].hashcheck_start(self.hashcheck_callback)
355
+
356
+ def hashcheck_callback(self):
357
+ self.downloads[self.hashcheck_current].hashcheck_callback()
358
+ if self.hashcheck_queue:
359
+ self._hashcheck_start()
360
+ else:
361
+ self.hashcheck_current = None
362
+
363
+ def died(self, hash):
364
+ if self.torrent_cache.has_key(hash):
365
+ self.Output.message('DIED: "'+self.torrent_cache[hash]['path']+'"')
366
+
367
+ def was_stopped(self, hash):
368
+ try:
369
+ self.hashcheck_queue.remove(hash)
370
+ except:
371
+ pass
372
+ if self.hashcheck_current == hash:
373
+ self.hashcheck_current = None
374
+ if self.hashcheck_queue:
375
+ self._hashcheck_start()
376
+
377
+ def failed(self, s):
378
+ self.Output.message('FAILURE: '+s)
379
+
380
+ def exchandler(self, s):
381
+ self.Output.exception(s)