Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

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

# -*- coding: utf-8 -*- 

# 

# Copyright (C) 2009 Andrew Resch <andrewresch@gmail.com> 

# 

# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with 

# the additional special exception to link portions of this program with the OpenSSL library. 

# See LICENSE for more details. 

# 

 

import os 

import sys 

from hashlib import sha1 as sha 

 

from deluge.bencode import bencode 

from deluge.common import get_path_size 

 

 

class InvalidPath(Exception): 

    """ 

    Raised when an invalid path is supplied 

    """ 

    pass 

 

 

class InvalidPieceSize(Exception): 

    """ 

    Raised when an invalid piece size is set.  Piece sizes must be multiples of 

    16KiB. 

    """ 

    pass 

 

 

class TorrentMetadata(object): 

    """ 

    This class is used to create .torrent files. 

 

    ** Usage ** 

 

    >>> t = TorrentMetadata() 

    >>> t.data_path = "/tmp/torrent" 

    >>> t.comment = "My Test Torrent" 

    >>> t.trackers = [["http://tracker.openbittorent.com"]] 

    >>> t.save("/tmp/test.torrent") 

 

    """ 

    def __init__(self): 

        self.__data_path = None 

        self.__piece_size = 0 

        self.__comment = "" 

        self.__private = False 

        self.__trackers = [] 

        self.__webseeds = [] 

        self.__pad_files = False 

 

    def save(self, torrent_path, progress=None): 

        """ 

        Creates and saves the torrent file to `path`. 

 

        :param torrent_path: where to save the torrent file 

        :type torrent_path: string 

 

        :param progress: a function to be called when a piece is hashed 

        :type progress: function(num_completed, num_pieces) 

 

        :raises InvalidPath: if the data_path has not been set 

 

        """ 

69        if not self.data_path: 

            raise InvalidPath("Need to set a data_path!") 

 

        torrent = { 

            "info": {} 

        } 

 

76        if self.comment: 

            torrent["comment"] = self.comment.encode("UTF-8") 

 

79        if self.private: 

            torrent["info"]["private"] = True 

 

82        if self.trackers: 

            torrent["announce"] = self.trackers[0][0] 

            torrent["announce-list"] = self.trackers 

        else: 

            torrent["announce"] = "" 

 

88        if self.webseeds: 

            httpseeds = [] 

            webseeds = [] 

            for w in self.webseeds: 

                if w.endswith(".php"): 

                    httpseeds.append(w) 

                else: 

                    webseeds.append(w) 

 

            if httpseeds: 

                torrent["httpseeds"] = httpseeds 

            if webseeds: 

                torrent["url-list"] = webseeds 

 

        datasize = get_path_size(self.data_path) 

 

104        if self.piece_size: 

            piece_size = self.piece_size * 1024 

        else: 

            # We need to calculate a piece size 

            piece_size = 16384 

109            while (datasize / piece_size) > 1024 and piece_size < (8192 * 1024): 

                piece_size *= 2 

 

        # Calculate the number of pieces we will require for the data 

        num_pieces = datasize / piece_size 

116        if datasize % piece_size: 

            num_pieces += 1 

 

        torrent["info"]["piece length"] = piece_size 

 

        # Create the info 

        if os.path.isdir(self.data_path): 

            torrent["info"]["name"] = os.path.split(self.data_path)[1] 

            files = [] 

            padding_count = 0 

            # Collect a list of file paths and add padding files if necessary 

            for (dirpath, dirnames, filenames) in os.walk(self.data_path): 

                for index, filename in enumerate(filenames): 

                    size = get_path_size(os.path.join(self.data_path, dirpath, filename)) 

                    p = dirpath[len(self.data_path):] 

                    p = p.lstrip("/") 

                    p = p.split("/") 

131                    if p[0]: 

                        p += [filename] 

                    else: 

                        p = [filename] 

                    files.append((size, p)) 

                    # Add a padding file if necessary 

                    if self.pad_files and (index + 1) < len(filenames): 

                        left = size % piece_size 

125                        if left: 

                            p = list(p) 

                            p[-1] = "_____padding_file_" + str(padding_count) 

                            files.append((piece_size - left, p)) 

                            padding_count += 1 

 

            # Run the progress function with 0 completed pieces 

146            if progress: 

                progress(0, num_pieces) 

 

            fs = [] 

            pieces = [] 

            # Create the piece hashes 

            buf = "" 

            for size, path in files: 

                path = [s.decode(sys.getfilesystemencoding()).encode("UTF-8") for s in path] 

                fs.append({"length": size, "path": path}) 

                if path[-1].startswith("_____padding_file_"): 

                    buf += "\0" * size 

                    pieces.append(sha(buf).digest()) 

                    buf = "" 

                    fs[-1]["attr"] = "p" 

                else: 

                    fd = open(os.path.join(self.data_path, *path), "rb") 

                    r = fd.read(piece_size - len(buf)) 

174                    while r: 

                        buf += r 

                        if len(buf) == piece_size: 

                            pieces.append(sha(buf).digest()) 

                            # Run the progress function if necessary 

169                            if progress: 

                                progress(len(pieces), num_pieces) 

                            buf = "" 

                        else: 

                            break 

                        r = fd.read(piece_size - len(buf)) 

                    fd.close() 

 

182            if buf: 

                pieces.append(sha(buf).digest()) 

179                if progress: 

                    progress(len(pieces), num_pieces) 

                buf = "" 

 

            torrent["info"]["pieces"] = "".join(pieces) 

            torrent["info"]["files"] = fs 

 

202        elif os.path.isfile(self.data_path): 

            torrent["info"]["name"] = os.path.split(self.data_path)[1] 

            torrent["info"]["length"] = get_path_size(self.data_path) 

            pieces = [] 

 

            fd = open(self.data_path, "rb") 

            r = fd.read(piece_size) 

            while r: 

                pieces.append(sha(r).digest()) 

195                if progress: 

                    progress(len(pieces), num_pieces) 

 

                r = fd.read(piece_size) 

 

            torrent["info"]["pieces"] = "".join(pieces) 

 

        # Write out the torrent file 

        open(torrent_path, "wb").write(bencode(torrent)) 

 

    def get_data_path(self): 

        """ 

        The path to the files that the torrent will contain.  It can be either 

        a file or a folder.  This property needs to be set before the torrent 

        file can be created and saved. 

        """ 

        return self.__data_path 

 

    def set_data_path(self, path): 

        """ 

        :param path: the path to the data 

        :type path: string 

 

        :raises InvalidPath: if the path is not found 

 

        """ 

223        if os.path.exists(path) and (os.path.isdir(path) or os.path.isfile(path)): 

            self.__data_path = os.path.abspath(path) 

        else: 

            raise InvalidPath("No such file or directory: %s" % path) 

 

    def get_piece_size(self): 

        """ 

        The size of pieces in bytes.  The size must be a multiple of 16KiB. 

        If you don't set a piece size, one will be automatically selected to 

        produce a torrent with less than 1024 pieces or the smallest possible 

        with a 8192KiB piece size. 

 

        """ 

        return self.__piece_size 

 

    def set_piece_size(self, size): 

        """ 

        :param size: the desired piece size in KiBs 

        :type size: int 

 

        :raises InvalidPieceSize: if the piece size is not a multiple of 16 KiB 

 

        """ 

        if size % 16 and size: 

            raise InvalidPieceSize("Piece size must be a multiple of 16 KiB") 

        self.__piece_size = size 

 

    def get_comment(self): 

        """ 

        Comment is some extra info to be stored in the torrent.  This is 

        typically an informational string. 

        """ 

        return self.__comment 

 

    def set_comment(self, comment): 

        """ 

        :param comment: an informational string 

        :type comment: string 

        """ 

        self.__comment = comment 

 

    def get_private(self): 

        """ 

        Private torrents only announce to the tracker and will not use DHT or 

        Peer Exchange. 

 

        See: http://bittorrent.org/beps/bep_0027.html 

 

        """ 

        return self.__private 

 

    def set_private(self, private): 

        """ 

        :param private: True if the torrent is to be private 

        :type private: bool 

        """ 

        self.__private = private 

 

    def get_trackers(self): 

        """ 

        The announce trackers is a list of lists. 

 

        See: http://bittorrent.org/beps/bep_0012.html 

 

        """ 

        return self.__trackers 

 

    def set_trackers(self, trackers): 

        """ 

        :param trackers: a list of lists of trackers, each list is a tier 

        :type trackers: list of list of strings 

        """ 

        self.__trackers = trackers 

 

    def get_webseeds(self): 

        """ 

        The web seeds can either be: 

        Hoffman-style: http://bittorrent.org/beps/bep_0017.html 

        or, 

        GetRight-style: http://bittorrent.org/beps/bep_0019.html 

 

        If the url ends in '.php' then it will be considered Hoffman-style, if 

        not it will be considered GetRight-style. 

        """ 

        return self.__webseeds 

 

    def set_webseeds(self, webseeds): 

        """ 

        :param webseeds: the webseeds which can be either Hoffman or GetRight style 

        :type webseeds: list of urls 

        """ 

        self.__webseeds = webseeds 

 

    def get_pad_files(self): 

        """ 

        If this is True, padding files will be added to align files on piece 

        boundaries. 

        """ 

        return self.__pad_files 

 

    def set_pad_files(self, pad): 

        """ 

        :param pad: set True to align files on piece boundaries 

        :type pad: bool 

        """ 

        self.__pad_files = pad 

 

    data_path = property(get_data_path, set_data_path) 

    piece_size = property(get_piece_size, set_piece_size) 

    comment = property(get_comment, set_comment) 

    private = property(get_private, set_private) 

    trackers = property(get_trackers, set_trackers) 

    webseeds = property(get_webseeds, set_webseeds) 

    pad_files = property(get_pad_files, set_pad_files)