Author: andar

Revision: 5654

Log:
        Add new maketorrent module

Diff:
Added: trunk/deluge/maketorrent.py
===================================================================
--- trunk/deluge/maketorrent.py                         (rev 0)
+++ trunk/deluge/maketorrent.py 2009-08-12 00:09:22 UTC (rev 5654)
@@ -0,0 +1,358 @@
+#
+# maketorrent.py
+#
+# Copyright (C) 2009 Andrew Resch <[email protected]>
+#
+# Deluge is free software.
+#
+# You may redistribute it and/or modify it under the terms of the
+# GNU General Public License, as published by the Free Software
+# Foundation; either version 3 of the License, or (at your option)
+# any later version.
+#
+# deluge is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+# See the GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with deluge.    If not, write to:
+#      The Free Software Foundation, Inc.,
+#      51 Franklin Street, Fifth Floor
+#      Boston, MA  02110-1301, USA.
+#
+#    In addition, as a special exception, the copyright holders give
+#    permission to link the code of portions of this program with the OpenSSL
+#    library.
+#    You must obey the GNU General Public License in all respects for all of
+#    the code used other than OpenSSL. If you modify file(s) with this
+#    exception, you may extend this exception to your version of the file(s),
+#    but you are not obligated to do so. If you do not wish to do so, delete
+#    this exception statement from your version. If you delete this exception
+#    statement from all source files in the program, then also delete it here.
+#
+#
+
+import sys
+import os
+from hashlib import sha1 as sha
+
+from deluge.common import get_path_size
+from deluge.bencode import bencode, bdecode
+
+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.__web_seeds = []
+        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
+        
+        """
+        if not self.data_path:
+            raise InvalidPath("Need to set a data_path!")
+
+        torrent = {
+            "info": {}
+            }
+            
+        if self.comment:
+            torrent["comment"] = self.comment.encode("UTF-8")
+        
+        if self.private:
+            torrent["info"]["private"] = True
+        
+        if self.trackers:
+            torrent["announce"] = self.trackers[0][0]
+            torrent["announce-list"] = self.trackers
+        else:
+            torrent["announce"] = ""
+        
+        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)
+
+        if not self.piece_size:
+            # We need to calculate a piece size
+            psize = 16384
+            while (datasize / psize) > 1024:
+                psize *= 2
+            
+            self.piece_size = psize
+
+        # Calculate the number of pieces we will require for the data
+        num_pieces = datasize / self.piece_size
+        torrent["info"]["piece length"] = self.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.lstrip(self.data_path)
+                    p = p.split("/")
+                    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 % self.piece_size
+                        if left:
+                            p = list(p)
+                            p[-1] = "_____padding_file_" + str(padding_count)
+                            files.append((self.piece_size - left, p))
+                            padding_count += 1
+            
+
+            # Run the progress function with 0 completed pieces
+            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(self.piece_size - len(buf))
+                    while r:
+                        buf += r
+                        if len(buf) == self.piece_size:
+                            pieces.append(sha(buf).digest())
+                            # Run the progress function if necessary
+                            if progress:
+                                progress(len(pieces), num_pieces)
+                            buf = ""
+                        else:
+                            break
+                        r = fd.read(self.piece_size - len(buf))
+                    fd.close()
+
+            if buf:
+                pieces.append(sha(buf).digest())
+                if progress:
+                    progress(len(pieces), num_pieces)
+                buf = ""
+            
+            torrent["info"]["pieces"] = "".join(pieces)
+            torrent["info"]["files"] = fs
+
+        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(self.piece_size)
+            while r:
+                pieces.append(sha(r).digest())
+                if progress:
+                    progress(len(pieces), num_pieces)
+
+                r = fd.read(self.piece_size)
+            
+            torrent["info"]["pieces"] = "".join(pieces)
+        
+        # Write out the torrent file
+        open(torrent_path, "wb").write(bencode(torrent))
+        
+    @property
+    def 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
+    
+    @data_path.setter
+    def data_path(self, path):
+        """
+        :param path: the path to the data
+        :type path: string
+        
+        :raises InvalidPath: if the path is not found
+        
+        """
+        if os.path.exists(path) and (os.path.isdir(path) or 
os.path.isfile(path)):
+            self.__data_path = path
+        else:
+            raise InvalidPath("No such file or directory: %s" % path)
+
+    @property
+    def 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.
+        
+        """
+        return self.__piece_size
+    
+    @piece_size.setter
+    def piece_size(self, size):
+        """
+        :param size: the desired piece size in bytes
+        
+        :raises InvalidPieceSize: if the piece size is not a multiple of 16KiB
+        
+        """
+        if size % 16384 and size:
+            raise InvalidPieceSize("Piece size must be a multiple of 16384")
+        self.__piece_size = size
+        
+    @property
+    def comment(self):
+        """
+        Comment is some extra info to be stored in the torrent.  This is
+        typically an informational string.
+        """
+        return self.__comment
+    
+    @comment.setter
+    def comment(self, comment):
+        """
+        :param comment: an informational string
+        :type comment: string
+        """
+        self.__comment = comment
+    
+    @property
+    def 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
+    
+    @private.setter
+    def private(self, private):
+        """
+        :param private: True if the torrent is to be private
+        :type private: bool
+        """
+        self.__private = private
+    
+    @property
+    def trackers(self):
+        """
+        The announce trackers is a list of lists.
+        
+        See: http://bittorrent.org/beps/bep_0012.html
+        
+        """
+        return self.__trackers
+    
+    @trackers.setter
+    def 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
+
+    @property
+    def 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.__web_seeds
+    
+    @webseeds.setter
+    def webseeds(self, webseeds):
+        """
+        :param webseeds: the webseeds which can be either Hoffman or GetRight 
style
+        :type webseeds: list of urls
+        """
+        self.__webseeds = webseeds
+
+    @property
+    def pad_files(self):
+        """
+        If this is True, padding files will be added to align files on piece
+        boundaries.
+        """
+        return self.__pad_files
+    
+    @pad_files.setter
+    def pad_files(self, pad):
+        """
+        :param pad: set True to align files on piece boundaries
+        :type pad: bool
+        """
+        self.__pad_files = pad
+

Added: trunk/tests/test_maketorrent.py
===================================================================
--- trunk/tests/test_maketorrent.py                             (rev 0)
+++ trunk/tests/test_maketorrent.py     2009-08-12 00:09:22 UTC (rev 5654)
@@ -0,0 +1,72 @@
+from twisted.trial import unittest
+from twisted.python.failure import Failure
+
+import os
+import tempfile
+
+from deluge import maketorrent
+
+
+def check_torrent(filename):
+    # Test loading with libtorrent to make sure it's valid
+    import libtorrent as lt
+    lt.torrent_info(filename)
+    
+    # Test loading with our internal TorrentInfo class
+    from deluge.ui.common import TorrentInfo
+    ti = TorrentInfo(filename)    
+    
+class MakeTorrentTestCase(unittest.TestCase):
+    def test_save_multifile(self):
+        # Create a temporary folder for torrent creation
+        tmp_path = tempfile.mkdtemp()
+        open(os.path.join(tmp_path, "file_A"), "wb").write("a" * (312 * 1024))
+        open(os.path.join(tmp_path, "file_B"), "wb").write("b" * (2354 * 1024))
+        open(os.path.join(tmp_path, "file_C"), "wb").write("c" * (11 * 1024))
+        
+        t = maketorrent.TorrentMetadata()
+        t.data_path = tmp_path
+        tmp_file = tempfile.mkstemp(".torrent")[1]
+        t.save(tmp_file)
+        
+        check_torrent(tmp_file)
+        
+        os.remove(os.path.join(tmp_path, "file_A"))
+        os.remove(os.path.join(tmp_path, "file_B"))
+        os.remove(os.path.join(tmp_path, "file_C"))
+        os.rmdir(tmp_path)
+        os.remove(tmp_file)
+
+    def test_save_singlefile(self):
+        tmp_data = tempfile.mkstemp("testdata")[1]
+        open(tmp_data, "wb").write("a"*(2314*1024))
+        t = maketorrent.TorrentMetadata()
+        t.data_path = tmp_data
+        tmp_file = tempfile.mkstemp(".torrent")[1]
+        t.save(tmp_file)
+        
+        check_torrent(tmp_file)
+
+        os.remove(tmp_data)
+        os.remove(tmp_file)
+
+    def test_save_multifile_padded(self):
+        # Create a temporary folder for torrent creation
+        tmp_path = tempfile.mkdtemp()
+        open(os.path.join(tmp_path, "file_A"), "wb").write("a" * (312 * 1024))
+        open(os.path.join(tmp_path, "file_B"), "wb").write("b" * (2354 * 1024))
+        open(os.path.join(tmp_path, "file_C"), "wb").write("c" * (11 * 1024))
+        
+        t = maketorrent.TorrentMetadata()
+        t.data_path = tmp_path
+        t.pad_files = True
+        tmp_file = tempfile.mkstemp(".torrent")[1]
+        t.save(tmp_file)
+        
+        check_torrent(tmp_file)
+        
+        os.remove(os.path.join(tmp_path, "file_A"))
+        os.remove(os.path.join(tmp_path, "file_B"))
+        os.remove(os.path.join(tmp_path, "file_C"))
+        os.rmdir(tmp_path)
+        os.remove(tmp_file)        



--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"deluge-commit" group.
To post to this group, send email to [email protected]
To unsubscribe from this group, send email to 
[email protected]
For more options, visit this group at 
http://groups.google.com/group/deluge-commit?hl=en
-~----------~----~----~----~------~----~------~--~---

Reply via email to