Skip to content

Commit

Permalink
Merge pull request #2321 from devos50/start_stop_remove_download
Browse files Browse the repository at this point in the history
Implemented endpoints to remove, resume and stop downloads
  • Loading branch information
whirm authored Jun 24, 2016
2 parents 7eacca9 + 04511e1 commit e0b9d4f
Show file tree
Hide file tree
Showing 3 changed files with 214 additions and 5 deletions.
109 changes: 105 additions & 4 deletions Tribler/Core/Modules/restapi/downloads_endpoint.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,38 @@
import json

from twisted.web import resource
from twisted.web import http, resource
from Tribler.Core.Libtorrent.LibtorrentDownloadImpl import LibtorrentStatisticsResponse

from Tribler.Core.simpledefs import DOWNLOAD, UPLOAD, dlstatus_strings


class DownloadsEndpoint(resource.Resource):
class DownloadBaseEndpoint(resource.Resource):
"""
This endpoint is responsible for all requests regarding downloads. Examples include getting all downloads,
starting, pausing and stopping downloads.
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

@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})


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)

def render_GET(self, request):
"""
.. http:get:: /downloads
Expand Down Expand Up @@ -105,3 +122,87 @@ def render_GET(self, request):
"destination": download.get_dest_dir()}
downloads_json.append(download_json)
return json.dumps({"downloads": downloads_json})


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'))

def render_DELETE(self, request):
"""
.. http:delete:: /download/(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}
"""
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] is True
self.session.remove_download(download, removecontent=remove_data)

return json.dumps({"removed": True})

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) or "stop" (to
stop a running download).
**Example request**:
.. sourcecode:: none
curl -X PATCH http://localhost:8085/download/4344503b7e797ebf31582327a5baae35b11bda01
--data "state=resume"
**Example response**:
.. sourcecode:: javascript
{"modified": True}
"""
download = self.session.get_download(self.infohash)
if not download:
return DownloadSpecificEndpoint.return_404(request)

parameters = http.parse_qs(request.content.read(), 1)

if 'state' in parameters and len(parameters['state']) > 0:
state = parameters['state'][0]
if state == "resume":
download.restart()
elif state == "stop":
download.stop()
else:
request.setResponseCode(http.BAD_REQUEST)
return json.dumps({"error": "unknown state parameter"})

return json.dumps({"modified": True})
109 changes: 108 additions & 1 deletion Tribler/Test/Core/Modules/RestApi/test_downloads_endpoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,117 @@ def verify_download(downloads):
downloads_json = json.loads(downloads)
self.assertEqual(len(downloads_json['downloads']), 2)

video_tdef, self.torrent_path = self.create_local_torrent(os.path.join(TESTS_DATA_DIR, 'video.avi'))
video_tdef, _ = self.create_local_torrent(os.path.join(TESTS_DATA_DIR, 'video.avi'))
self.session.start_download_from_tdef(video_tdef, DownloadStartupConfig())
self.session.start_download_from_uri("file:" + pathname2url(
os.path.join(TESTS_DATA_DIR, "bak_single.torrent")))

self.should_check_equality = False
return self.do_request('downloads', expected_code=200).addCallback(verify_download)

@deferred(timeout=10)
def test_remove_download_no_remove_data_param(self):
"""
Testing whether the API returns error 400 if the remove_data parameter is not passed
"""
self.should_check_equality = False
return self.do_request('downloads/abcd', expected_code=400, request_type='DELETE')

@deferred(timeout=10)
def test_remove_download_wrong_infohash(self):
"""
Testing whether the API returns error 404 if a non-existent download is removed
"""
self.should_check_equality = False
return self.do_request('downloads/abcd', post_data={"remove_data": True},
expected_code=404, request_type='DELETE')

@deferred(timeout=10)
def test_remove_download(self):
"""
Testing whether the API returns 200 if a download is being removed
"""
def verify_removed(_):
self.assertEqual(len(self.session.get_downloads()), 0)

video_tdef, _ = self.create_local_torrent(os.path.join(TESTS_DATA_DIR, 'video.avi'))
self.session.start_download_from_tdef(video_tdef, DownloadStartupConfig())
infohash = video_tdef.get_infohash().encode('hex')

request_deferred = self.do_request('downloads/%s' % infohash, post_data={"remove_data": True},
expected_code=200, expected_json={"removed": True}, request_type='DELETE')
return request_deferred.addCallback(verify_removed)

@deferred(timeout=10)
def test_stop_download_wrong_infohash(self):
"""
Testing whether the API returns error 404 if a non-existent download is stopped
"""
self.should_check_equality = False
return self.do_request('downloads/abcd', expected_code=404, post_data={"state": "stop"}, request_type='PATCH')

@deferred(timeout=10)
def test_stop_download(self):
"""
Testing whether the API returns 200 if a download is being stopped
"""
video_tdef, _ = self.create_local_torrent(os.path.join(TESTS_DATA_DIR, 'video.avi'))
download = self.session.start_download_from_tdef(video_tdef, DownloadStartupConfig())
infohash = video_tdef.get_infohash().encode('hex')

def mocked_stop():
download.should_stop = True

def verify_removed(_):
self.assertEqual(len(self.session.get_downloads()), 1)
download = self.session.get_downloads()[0]
self.assertTrue(download.should_stop)

download.stop = mocked_stop

request_deferred = self.do_request('downloads/%s' % infohash, post_data={"state": "stop"},
expected_code=200, expected_json={"modified": True}, request_type='PATCH')
return request_deferred.addCallback(verify_removed)

@deferred(timeout=10)
def test_resume_download_wrong_infohash(self):
"""
Testing whether the API returns error 404 if a non-existent download is resumed
"""
self.should_check_equality = False
return self.do_request('downloads/abcd', expected_code=404, post_data={"state": "resume"}, request_type='PATCH')

@deferred(timeout=10)
def test_resume_download(self):
"""
Testing whether the API returns 200 if a download is being resumed
"""
video_tdef, _ = self.create_local_torrent(os.path.join(TESTS_DATA_DIR, 'video.avi'))
download = self.session.start_download_from_tdef(video_tdef, DownloadStartupConfig())
infohash = video_tdef.get_infohash().encode('hex')

def mocked_restart():
download.should_restart = True

def verify_resumed(_):
self.assertEqual(len(self.session.get_downloads()), 1)
download = self.session.get_downloads()[0]
self.assertTrue(download.should_restart)

download.restart = mocked_restart

request_deferred = self.do_request('downloads/%s' % infohash, post_data={"state": "resume"},
expected_code=200, expected_json={"modified": True}, request_type='PATCH')
return request_deferred.addCallback(verify_resumed)

@deferred(timeout=10)
def test_download_unknown_state(self):
"""
Testing whether the API returns error 400 if an unknown state is passed when modifying a download
"""
video_tdef, _ = self.create_local_torrent(os.path.join(TESTS_DATA_DIR, 'video.avi'))
self.session.start_download_from_tdef(video_tdef, DownloadStartupConfig())

self.should_check_equality = False
return self.do_request('downloads/%s' % video_tdef.get_infohash().encode('hex'), expected_code=400,
post_data={"state": "abc"}, request_type='PATCH')
1 change: 1 addition & 0 deletions doc/restapi/downloads.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ Downloads

.. automodule:: Tribler.Core.Modules.restapi.downloads_endpoint
:members:
:exclude-members: DownloadBaseEndpoint, DownloadSpecificEndpoint

0 comments on commit e0b9d4f

Please sign in to comment.