forked from Tribler/tribler
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmaketorrent.py
371 lines (297 loc) · 11.3 KB
/
maketorrent.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
# Written by Arno Bakker, Bram Cohen
# multitracker extensions by John Hoffman
# modified for Merkle hashes and digital signatures by Arno Bakker
# see LICENSE.txt for license information
import sys
import os
import logging
from hashlib import sha1
from copy import copy
from time import time
from types import LongType
from libtorrent import bencode
from Tribler.Core.Utilities.unicode import bin2unicode
from Tribler.Core.APIImplementation.miscutils import offset2piece
from Tribler.Core.osutils import fix_filebasename
from Tribler.Core.defaults import tdefdictdefaults
from Tribler.Core.Utilities.utilities import validTorrentFile
logger = logging.getLogger(__name__)
def make_torrent_file(input, userabortflag=None, userprogresscallback=lambda x: None):
""" Create a torrent file from the supplied input.
Returns a (infohash,metainfo) pair, or (None,None) on userabort. """
(info, piece_length) = makeinfo(input, userabortflag, userprogresscallback)
if userabortflag is not None and userabortflag.isSet():
return None, None
if info is None:
return None, None
metainfo = {'info': info, 'encoding': input['encoding'], 'creation date': long(time())}
validTorrentFile(metainfo)
# http://www.bittorrent.org/DHT_protocol.html says both announce and nodes
# are not allowed, but some torrents (Azureus?) apparently violate this.
if input['nodes'] is None and input['announce'] is None:
raise ValueError('No tracker set')
for key in ['announce', 'announce-list', 'nodes', 'comment', 'created by', 'httpseeds', 'url-list']:
if input[key] is not None and len(input[key]) > 0:
metainfo[key] = input[key]
if key == 'comment':
metainfo['comment.utf-8'] = uniconvert(input['comment'], 'utf-8')
if 'private' in input:
metainfo['info']['private'] = input['private']
if 'anonymous' in input:
metainfo['info']['anonymous'] = input['anonymous']
# Two places where infohash calculated, here and in TorrentDef.
# Elsewhere: must use TorrentDef.get_infohash() to allow P2PURLs.
infohash = sha1(bencode(info)).digest()
return infohash, metainfo
def uniconvertl(l, e):
""" Convert a pathlist to a list of strings encoded in encoding "e" using
uniconvert. """
r = []
try:
for s in l:
r.append(uniconvert(s, e))
except UnicodeError:
raise UnicodeError('bad filename: ' + os.path.join(l))
return r
def uniconvert(s, enc):
""" Convert 's' to a string containing a Unicode sequence encoded using
encoding "enc". If 's' is not a Unicode object, we first try to convert
it to one, guessing the encoding if necessary. """
if not isinstance(s, unicode):
try:
s = bin2unicode(s, enc)
except UnicodeError:
raise UnicodeError('bad filename: ' + s)
return s.encode(enc)
def makeinfo(input, userabortflag, userprogresscallback):
""" Calculate hashes and create torrent file's 'info' part """
encoding = input['encoding']
pieces = []
sh = sha1()
done = 0
fs = []
totalsize = 0
totalhashed = 0
# 1. Determine which files should go into the torrent (=expand any dirs
# specified by user in input['files']
subs = []
for f in input['files']:
inpath = f['inpath']
outpath = f['outpath']
logger.debug("makeinfo: inpath=%s, outpath=%s", inpath, outpath)
if os.path.isdir(inpath):
dirsubs = subfiles(inpath)
subs.extend(dirsubs)
else:
if outpath is None:
subs.append(([os.path.basename(inpath)], inpath))
else:
subs.append((filename2pathlist(outpath, skipfirst=True), inpath))
subs.sort()
# 2. Calc total size
newsubs = []
for p, f in subs:
size = os.path.getsize(f)
totalsize += size
newsubs.append((p, f, size))
subs = newsubs
# 3. Calc piece length from totalsize if not set
if input['piece length'] == 0:
# Niels we want roughly between 1000-2000 pieces
# This results in the following logic:
# We start with 32K pieces
piece_length = 2 ** 15
while totalsize / piece_length > 2000:
# too many piece, double piece_size
piece_length *= 2
else:
piece_length = input['piece length']
# 4. Read files and calc hashes
for p, f, size in subs:
pos = 0
h = open(f, 'rb')
while pos < size:
a = min(size - pos, piece_length - done)
# See if the user cancelled
if userabortflag is not None and userabortflag.isSet():
return None, None
readpiece = h.read(a)
# See if the user cancelled
if userabortflag is not None and userabortflag.isSet():
return None, None
sh.update(readpiece)
done += a
pos += a
totalhashed += a
if done == piece_length:
pieces.append(sh.digest())
done = 0
sh = sha1()
if userprogresscallback is not None:
userprogresscallback(float(totalhashed) / float(totalsize))
newdict = {'length': num2num(size),
'path': uniconvertl(p, encoding),
'path.utf-8': uniconvertl(p, 'utf-8')}
fs.append(newdict)
h.close()
if done > 0:
pieces.append(sh.digest())
# 5. Create info dict
if len(subs) == 1:
flkey = 'length'
flval = num2num(totalsize)
name = subs[0][0][0]
else:
flkey = 'files'
flval = fs
if 'name' in input: # allow someone to overrule the default name if multifile
name = input['name']
else:
outpath = input['files'][0]['outpath']
l = filename2pathlist(outpath)
name = l[0]
infodict = {'piece length': num2num(piece_length),
flkey: flval,
'name': uniconvert(name, encoding),
'name.utf-8': uniconvert(name, 'utf-8')}
infodict.update({'pieces': ''.join(pieces)})
return infodict, piece_length
def subfiles(d):
""" Return list of (pathlist,local filename) tuples for all the files in
directory 'd' """
r = []
stack = [([], d)]
while stack:
p, n = stack.pop()
if os.path.isdir(n):
for s in os.listdir(n):
if s[:1] != '.':
stack.append((copy(p) + [s], os.path.join(n, s)))
else:
r.append((p, n))
return r
def filename2pathlist(path, skipfirst=False):
""" Convert a filename to a 'path' entry suitable for a multi-file torrent
file """
h = path
l = []
while True:
(h, t) = os.path.split(h)
if h == '' and t == '':
break
if h == '' and skipfirst:
continue
if t != '': # handle case where path ends in / (=path separator)
l.append(t)
l.reverse()
return l
def pathlist2filename(pathlist):
""" Convert a multi-file torrent file 'path' entry to a filename. """
fullpath = ''
for elem in pathlist:
fullpath = os.path.join(fullpath, elem)
return fullpath.decode('utf-8')
def pathlist2savefilename(pathlist, encoding):
fullpath = u''
for elem in pathlist:
u = bin2unicode(elem, encoding)
b = fix_filebasename(u)
fullpath = os.path.join(fullpath, b)
return fullpath
def num2num(num):
""" Converts long to int if small enough to fit """
if isinstance(num, LongType) and num < sys.maxint:
return int(num)
else:
return num
def get_length_from_metainfo(metainfo, selectedfiles):
if 'files' not in metainfo['info']:
# single-file torrent
return metainfo['info']['length']
else:
# multi-file torrent
files = metainfo['info']['files']
total = 0
for i in xrange(len(files)):
path = files[i]['path']
length = files[i]['length']
if length > 0 and (not selectedfiles or pathlist2filename(path) in selectedfiles):
total += length
return total
def get_length_filepieceranges_from_metainfo(metainfo, selectedfiles):
if 'files' not in metainfo['info']:
# single-file torrent
return metainfo['info']['length'], None
else:
# multi-file torrent
files = metainfo['info']['files']
piecesize = metainfo['info']['piece length']
offset = 0
total = 0
filepieceranges = []
for i in xrange(len(files)):
path = files[i]['path']
length = files[i]['length']
filename = pathlist2filename(path)
if length > 0 and (not selectedfiles or (selectedfiles and filename in selectedfiles)):
range = (offset2piece(offset, piecesize, False),
offset2piece(offset + length, piecesize),
(offset - offset2piece(offset, piecesize, False) * piecesize),
filename)
filepieceranges.append(range)
total += length
offset += length
return total, filepieceranges
def copy_metainfo_to_input(metainfo, input):
keys = tdefdictdefaults.keys()
# Arno: For magnet link support
keys.append("initial peers")
for key in keys:
if key in metainfo:
input[key] = metainfo[key]
infokeys = ['name', 'piece length']
for key in infokeys:
if key in metainfo['info']:
input[key] = metainfo['info'][key]
# Note: don't know inpath, set to outpath
if 'length' in metainfo['info']:
outpath = metainfo['info']['name']
length = metainfo['info']['length']
d = {'inpath': outpath, 'outpath': outpath, 'length': length}
input['files'].append(d)
else: # multi-file torrent
files = metainfo['info']['files']
for file in files:
outpath = pathlist2filename(file['path'])
length = file['length']
d = {'inpath': outpath, 'outpath': outpath, 'length': length}
input['files'].append(d)
# Diego : we want web seeding
if 'url-list' in metainfo:
input['url-list'] = metainfo['url-list']
if 'httpseeds' in metainfo:
input['httpseeds'] = metainfo['httpseeds']
def get_files(metainfo, exts):
# 01/02/10 Boudewijn: now returns (file, length) tuples instead of files
videofiles = []
if 'files' in metainfo['info']:
# Multi-file torrent
files = metainfo['info']['files']
for file in files:
p = file['path']
filename = ''
for elem in p:
filename = os.path.join(filename, elem)
(prefix, ext) = os.path.splitext(filename)
if ext != '' and ext[0] == '.':
ext = ext[1:]
if exts is None or ext.lower() in exts:
videofiles.append((filename, file['length']))
else:
filename = metainfo['info']['name'] # don't think we need fixed name here
(prefix, ext) = os.path.splitext(filename)
if ext != '' and ext[0] == '.':
ext = ext[1:]
if exts is None or ext.lower() in exts:
videofiles.append((filename, metainfo['info']['length']))
return videofiles