Source code for Tribler.Core.Modules.restapi.downloads_endpoint

import logging
from urllib import unquote_plus, url2pathname

from twisted.web import http, resource
from twisted.web.server import NOT_DONE_YET

from Tribler.Core.DownloadConfig import DownloadStartupConfig
from Tribler.Core.Modules.restapi.util import return_handled_exception
from Tribler.Core.simpledefs import DOWNLOAD, UPLOAD, dlstatus_strings, DLMODE_VOD
import Tribler.Core.Utilities.json_util as json


def _safe_extended_peer_info(ext_peer_info):
    """
    Given a string describing peer info, return a JSON.dumps() safe representation.

    :param ext_peer_info: the string to convert to a dumpable format
    :return: the safe string
    """
    # First see if we can use this as-is
    if not ext_peer_info:
        ext_peer_info = u''
    try:
        json.dumps(ext_peer_info)
        return ext_peer_info
    except UnicodeDecodeError:
        # We might have some special unicode characters in here
        return u''.join([unichr(ord(c)) for c in ext_peer_info])


class DownloadBaseEndpoint(resource.Resource):
    """
    Base class for all endpoints related to fetching information about downloads or a specific download.
    """

    def __init__(self, session):
        resource.Resource.__init__(self)
        self.session = session
        self._logger = logging.getLogger(self.__class__.__name__)

    @staticmethod
    def return_404(request, message="this download does not exist"):
        """
        Returns a 404 response code if your channel has not been created.
        """
        request.setResponseCode(http.NOT_FOUND)
        return json.dumps({"error": message})

    @staticmethod
    def create_dconfig_from_params(parameters):
        """
        Create a download configuration based on some given parameters. Possible parameters are:
        - anon_hops: the number of hops for the anonymous download. 0 hops is equivalent to a plain download
        - safe_seeding: whether the seeding of the download should be anonymous or not (0 = off, 1 = on)
        - destination: the destination path of the torrent (where it is saved on disk)
        """
        download_config = DownloadStartupConfig()

        anon_hops = 0
        if 'anon_hops' in parameters and len(parameters['anon_hops']) > 0:
            if parameters['anon_hops'][0].isdigit():
                anon_hops = int(parameters['anon_hops'][0])

        safe_seeding = False
        if 'safe_seeding' in parameters and len(parameters['safe_seeding']) > 0 \
                and parameters['safe_seeding'][0] == "1":
            safe_seeding = True

        if anon_hops > 0 and not safe_seeding:
            return None, "Cannot set anonymous download without safe seeding enabled"

        if anon_hops > 0:
            download_config.set_hops(anon_hops)

        if safe_seeding:
            download_config.set_safe_seeding(True)

        if 'destination' in parameters and len(parameters['destination']) > 0:
            dest_dir = unicode(parameters['destination'][0], 'utf-8')
            download_config.set_dest_dir(dest_dir)

        if 'selected_files[]' in parameters:
            selected_files_list = [unicode(f, 'utf-8') for f in parameters['selected_files[]']]
            download_config.set_selected_files(selected_files_list)

        return download_config, None

    def get_files_info_json(self, download):
        """
        Return file information as JSON from a specified download.
        """
        files_json = []
        files_completion = dict((name, progress) for name, progress in download.get_state().get_files_completion())
        selected_files = download.get_selected_files()
        file_index = 0
        for fn, size in download.get_def().get_files_with_length():
            files_json.append({
                "index": file_index,
                "name": fn,
                "size": size,
                "included": (fn in selected_files or not selected_files),
                "progress": files_completion.get(fn, 0.0)
            })
            file_index += 1
        return files_json


[docs]class DownloadsEndpoint(DownloadBaseEndpoint): """ This endpoint is responsible for all requests regarding downloads. Examples include getting all downloads, starting, pausing and stopping downloads. """ def getChild(self, path, request): return DownloadSpecificEndpoint(self.session, path)
[docs] def render_GET(self, request): """ .. http:get:: /downloads?get_peers=(boolean: get_peers)&get_pieces=(boolean: get_pieces) A GET request to this endpoint returns all downloads in Tribler, both active and inactive. The progress is a number ranging from 0 to 1, indicating the progress of the specific state (downloading, checking etc). The download speeds have the unit bytes/sec. The size of the torrent is given in bytes. The estimated time assumed is given in seconds. A description of the possible download statuses can be found in the REST API documentation. Detailed information about peers and pieces is only requested when the get_peers and/or get_pieces flag is set. Note that setting this flag has a negative impact on performance and should only be used in situations where this data is required. **Example request**: .. sourcecode:: none curl -X GET http://localhost:8085/downloads?get_peers=1&get_pieces=1 **Example response**: .. sourcecode:: javascript { "downloads": [{ "name": "Ubuntu-16.04-desktop-amd64", "progress": 0.31459265, "infohash": "4344503b7e797ebf31582327a5baae35b11bda01", "speed_down": 4938.83, "speed_up": 321.84, "status": "DLSTATUS_DOWNLOADING", "size": 89432483, "eta": 38493, "num_peers": 53, "num_seeds": 93, "total_up": 10000, "total_down": 100000, "ratio": 0.1, "files": [{ "index": 0, "name": "ubuntu.iso", "size": 89432483, "included": True }, ...], "trackers": [{ "url": "http://ipv6.torrent.ubuntu.com:6969/announce", "status": "Working", "peers": 42 }, ...], "hops": 1, "anon_download": True, "safe_seeding": True, "max_upload_speed": 0, "max_download_speed": 0, "destination": "/home/user/file.txt", "availability": 1.234, "peers": [{ "ip": "123.456.789.987", "dtotal": 23, "downrate": 0, "uinterested": False, "wstate": "\x00", "optimistic": False, ... }, ...], "total_pieces": 420, "vod_mod": True, "vod_prebuffering_progress": 0.89, "vod_prebuffering_progress_consec": 0.86, "error": "", "time_added": 1484819242, } }, ...] """ get_peers = False if 'get_peers' in request.args and len(request.args['get_peers']) > 0 \ and request.args['get_peers'][0] == "1": get_peers = True get_pieces = False if 'get_pieces' in request.args and len(request.args['get_pieces']) > 0 \ and request.args['get_pieces'][0] == "1": get_pieces = True get_files = 'get_files' in request.args and request.args['get_files'] and request.args['get_files'][0] == "1" downloads_json = [] downloads = self.session.get_downloads() for download in downloads: state = download.get_state() tdef = download.get_def() # Create tracker information of the download tracker_info = [] for url, url_info in download.get_tracker_status().iteritems(): tracker_info.append({"url": url, "peers": url_info[0], "status": url_info[1]}) num_seeds, num_peers = state.get_num_seeds_peers() num_connected_seeds, num_connected_peers = download.get_num_connected_seeds_peers() download_json = {"name": tdef.get_name_utf8(), "progress": state.get_progress(), "infohash": tdef.get_infohash().encode('hex'), "speed_down": state.get_current_payload_speed(DOWNLOAD), "speed_up": state.get_current_payload_speed(UPLOAD), "status": dlstatus_strings[state.get_status()], "size": tdef.get_length(), "eta": state.get_eta(), "num_peers": num_peers, "num_seeds": num_seeds, "num_connected_peers": num_connected_peers, "num_connected_seeds": num_connected_seeds, "total_up": state.get_total_transferred(UPLOAD), "total_down": state.get_total_transferred(DOWNLOAD), "ratio": state.get_seeding_ratio(), "trackers": tracker_info, "hops": download.get_hops(), "anon_download": download.get_anon_mode(), "safe_seeding": download.get_safe_seeding(), # Maximum upload/download rates are set for entire sessions "max_upload_speed": self.session.config.get_libtorrent_max_upload_rate(), "max_download_speed": self.session.config.get_libtorrent_max_download_rate(), "destination": download.get_dest_dir(), "availability": state.get_availability(), "total_pieces": tdef.get_nr_pieces(), "vod_mode": download.get_mode() == DLMODE_VOD, "vod_prebuffering_progress": state.get_vod_prebuffering_progress(), "vod_prebuffering_progress_consec": state.get_vod_prebuffering_progress_consec(), "error": repr(state.get_error()) if state.get_error() else "", "time_added": download.get_time_added(), "credit_mining": download.get_credit_mining()} # Add peers information if requested if get_peers: peer_list = state.get_peerlist() for peer_info in peer_list: # Remove have field since it is very large to transmit. del peer_info['have'] if 'extended_version' in peer_info: peer_info['extended_version'] = _safe_extended_peer_info(peer_info['extended_version']) peer_info['id'] = peer_info['id'].encode('hex') download_json["peers"] = peer_list # Add piece information if requested if get_pieces: download_json["pieces"] = download.get_pieces_base64() # Add files if requested if get_files: download_json["files"] = self.get_files_info_json(download) downloads_json.append(download_json) return json.dumps({"downloads": downloads_json})
[docs] def render_PUT(self, request): """ .. http:put:: /downloads A PUT request to this endpoint will start a download from a provided URI. This URI can either represent a file location, a magnet link or a HTTP(S) url. - anon_hops: the number of hops for the anonymous download. 0 hops is equivalent to a plain download - safe_seeding: whether the seeding of the download should be anonymous or not (0 = off, 1 = on) - destination: the download destination path of the torrent - torrent: the URI of the torrent file that should be downloaded. This parameter is required. **Example request**: .. sourcecode:: none curl -X PUT http://localhost:8085/downloads --data "anon_hops=2&safe_seeding=1&destination=/my/dest/on/disk/&uri=file:/home/me/test.torrent **Example response**: .. sourcecode:: javascript {"started": True, "infohash": "4344503b7e797ebf31582327a5baae35b11bda01"} """ parameters = http.parse_qs(request.content.read(), 1) if 'uri' not in parameters or len(parameters['uri']) == 0: request.setResponseCode(http.BAD_REQUEST) return json.dumps({"error": "uri parameter missing"}) download_config, error = DownloadsEndpoint.create_dconfig_from_params(parameters) if error: request.setResponseCode(http.BAD_REQUEST) return json.dumps({"error": error}) def download_added(download): request.write(json.dumps({"started": True, "infohash": download.get_def().get_infohash().encode('hex')})) request.finish() def on_error(error): request.setResponseCode(http.INTERNAL_SERVER_ERROR) request.write(json.dumps({"error": error.getErrorMessage()})) request.finish() uri = parameters['uri'][0] if uri.startswith("file:"): download_uri = u"file:%s" % url2pathname(unicode(uri[5:], 'utf-8')) else: download_uri = unquote_plus(unicode(uri, 'utf-8')) download_deferred = self.session.start_download_from_uri(download_uri, download_config) download_deferred.addCallback(download_added) download_deferred.addErrback(on_error) return NOT_DONE_YET
class DownloadSpecificEndpoint(DownloadBaseEndpoint): """ This class is responsible for dispatching requests to perform operations in a specific discovered channel. """ def __init__(self, session, infohash): DownloadBaseEndpoint.__init__(self, session) self.infohash = bytes(infohash.decode('hex')) self.putChild("torrent", DownloadExportTorrentEndpoint(session, self.infohash)) self.putChild("files", DownloadFilesEndpoint(session, self.infohash)) def render_DELETE(self, request): """ .. http:delete:: /downloads/(string: infohash) A DELETE request to this endpoint removes a specific download from Tribler. You can specify whether you only want to remove the download or the download and the downloaded data using the remove_data parameter. **Example request**: .. sourcecode:: none curl -X DELETE http://localhost:8085/download/4344503b7e797ebf31582327a5baae35b11bda01 --data "remove_data=1" **Example response**: .. sourcecode:: javascript {"removed": True, "infohash": "4344503b7e797ebf31582327a5baae35b11bda01"} """ parameters = http.parse_qs(request.content.read(), 1) if 'remove_data' not in parameters or len(parameters['remove_data']) == 0: request.setResponseCode(http.BAD_REQUEST) return json.dumps({"error": "remove_data parameter missing"}) download = self.session.get_download(self.infohash) if not download: return DownloadSpecificEndpoint.return_404(request) remove_data = parameters['remove_data'][0] == "1" def _on_torrent_removed(_): """ Success callback """ request.write(json.dumps({"removed": True, "infohash": download.get_def().get_infohash().encode('hex')})) request.finish() def _on_remove_failure(failure): """ Error callback :param failure: from remove_download """ self._logger.exception(failure) request.write(return_handled_exception(request, failure.value)) # If the above request.write failed, the request will have already been finished if not request.finished: request.finish() deferred = self.session.remove_download(download, remove_content=remove_data) deferred.addCallback(_on_torrent_removed) deferred.addErrback(_on_remove_failure) return NOT_DONE_YET def render_PATCH(self, request): """ .. http:patch:: /download/(string: infohash) A PATCH request to this endpoint will update a download in Tribler. A state parameter can be passed to modify the state of the download. Valid states are "resume" (to resume a stopped/paused download), "stop" (to stop a running download) and "recheck" (to force a recheck of the hashes of a download). Another possible parameter is selected_files which manipulates which files are included in the download. The selected_files parameter is an array with the file indices as values. The anonymity of a download can be changed at runtime by passing the anon_hops parameter, however, this must be the only parameter in this request. **Example request**: .. sourcecode:: none curl -X PATCH http://localhost:8085/downloads/4344503b7e797ebf31582327a5baae35b11bda01 --data "state=resume&selected_files[]=file1.iso&selected_files[]=1" **Example response**: .. sourcecode:: javascript {"modified": True, "infohash": "4344503b7e797ebf31582327a5baae35b11bda01"} """ download = self.session.get_download(self.infohash) if not download: return DownloadSpecificEndpoint.return_404(request) parameters = http.parse_qs(request.content.read(), 1) if len(parameters) > 1 and 'anon_hops' in parameters: request.setResponseCode(http.BAD_REQUEST) return json.dumps({"error": "anon_hops must be the only parameter in this request"}) elif 'anon_hops' in parameters: anon_hops = int(parameters['anon_hops'][0]) deferred = self.session.lm.update_download_hops(download, anon_hops) def _on_download_readded(_): """ Success callback """ request.write(json.dumps({"modified": True, "infohash": download.get_def().get_infohash().encode('hex')})) request.finish() def _on_download_readd_failure(failure): """ Error callback :param failure: from LibtorrentDownloadImp.setup() """ self._logger.exception(failure) request.write(return_handled_exception(request, failure.value)) # If the above request.write failed, the request will have already been finished if not request.finished: request.finish() deferred.addCallback(_on_download_readded) deferred.addErrback(_on_download_readd_failure) # As we already checked for len(parameters) > 1, we know there are no other parameters. # As such, we can return immediately. return NOT_DONE_YET if 'selected_files[]' in parameters: selected_files_list = [] for ind in parameters['selected_files[]']: try: selected_files_list.append(download.tdef.get_files()[int(ind)]) except IndexError: # File could not be found request.setResponseCode(http.BAD_REQUEST) return json.dumps({"error": "index %s out of range" % ind}) download.set_selected_files(selected_files_list) if 'state' in parameters and len(parameters['state']) > 0: state = parameters['state'][0] if state == "resume": download.restart() elif state == "stop": download.stop() elif state == "recheck": download.force_recheck() else: request.setResponseCode(http.BAD_REQUEST) return json.dumps({"error": "unknown state parameter"}) return json.dumps({"modified": True, "infohash": download.get_def().get_infohash().encode('hex')})
[docs]class DownloadExportTorrentEndpoint(DownloadBaseEndpoint): """ This class is responsible for requests that are exporting a download to a .torrent file. """ def __init__(self, session, infohash): DownloadBaseEndpoint.__init__(self, session) self.infohash = infohash
[docs] def render_GET(self, request): """ .. http:get:: /download/(string: infohash)/torrent A GET request to this endpoint returns the .torrent file associated with the specified download. **Example request**: .. sourcecode:: none curl -X GET http://localhost:8085/downloads/4344503b7e797ebf31582327a5baae35b11bda01/torrent **Example response**: The contents of the .torrent file. """ torrent = self.session.get_collected_torrent(self.infohash) if not torrent: return DownloadExportTorrentEndpoint.return_404(request) request.setHeader(b'content-type', 'application/x-bittorrent') request.setHeader(b'Content-Disposition', 'attachment; filename=%s.torrent' % self.infohash.encode('hex')) return torrent
[docs]class DownloadFilesEndpoint(DownloadBaseEndpoint): """ This class is responsible for requests that request the files of a specific torrent. """ def __init__(self, session, infohash): DownloadBaseEndpoint.__init__(self, session) self.infohash = infohash
[docs] def render_GET(self, request): """ .. http:get:: /download/(string: infohash)/files A GET request to this endpoint returns the file information of a specific download. **Example request**: .. sourcecode:: none curl -X GET http://localhost:8085/downloads/4344503b7e797ebf31582327a5baae35b11bda01/files **Example response**: .. sourcecode:: javascript { "files": [{ "index": 1, "name": "test.txt", "size": 12345, "included": True, "progress": 0.5448 }, ...] } """ download = self.session.get_download(self.infohash) if not download: return DownloadExportTorrentEndpoint.return_404(request) return json.dumps({"files": self.get_files_info_json(download)})