mirror of
https://git.deluge-torrent.org/deluge
synced 2025-08-07 00:48:41 +00:00
[TrackerIcons] Cleanup tmp files created by downloading page
Temporary files created while download host html page are no cleaned up if the download fails. Fixed by adding a 'finally' step in the callback chain to delete any created temporary files. Added tests to ensure the temporary files are deleted, using a fixture that creates a known filename for the test. Trac: https://dev.deluge-torrent.org/ticket/3167
This commit is contained in:
parent
96a0825add
commit
68c75ccc05
3 changed files with 53 additions and 22 deletions
|
@ -4,8 +4,9 @@
|
||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
|
||||||
import unittest.mock
|
import tempfile
|
||||||
import warnings
|
import warnings
|
||||||
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_twisted
|
import pytest_twisted
|
||||||
|
@ -49,7 +50,7 @@ def mock_callback():
|
||||||
mock.side_effect = lambda *args, **kw: deferred.callback((args, kw))
|
mock.side_effect = lambda *args, **kw: deferred.callback((args, kw))
|
||||||
mock.deferred = deferred
|
mock.deferred = deferred
|
||||||
|
|
||||||
mock = unittest.mock.Mock()
|
mock = Mock()
|
||||||
original_reset_mock = mock.reset_mock
|
original_reset_mock = mock.reset_mock
|
||||||
mock.reset_mock = reset
|
mock.reset_mock = reset
|
||||||
mock.reset_mock()
|
mock.reset_mock()
|
||||||
|
@ -181,3 +182,11 @@ class BaseTestCase:
|
||||||
have finished.
|
have finished.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_mkstemp(tmp_path):
|
||||||
|
"""Return known tempfile location to verify file deleted"""
|
||||||
|
tmp_file = tempfile.mkstemp(dir=tmp_path)
|
||||||
|
with patch('tempfile.mkstemp', return_value=tmp_file):
|
||||||
|
yield tmp_file
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
# the additional special exception to link portions of this program with the OpenSSL library.
|
# the additional special exception to link portions of this program with the OpenSSL library.
|
||||||
# See LICENSE for more details.
|
# See LICENSE for more details.
|
||||||
#
|
#
|
||||||
|
import os.path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import pytest_twisted
|
import pytest_twisted
|
||||||
|
@ -28,11 +29,12 @@ class TestTrackerIcons(BaseTestCase):
|
||||||
return component.shutdown()
|
return component.shutdown()
|
||||||
|
|
||||||
@pytest_twisted.ensureDeferred
|
@pytest_twisted.ensureDeferred
|
||||||
async def test_get_deluge_png(self):
|
async def test_get_deluge_png(self, mock_mkstemp):
|
||||||
# Deluge has a png favicon link
|
# Deluge has a png favicon link
|
||||||
icon = TrackerIcon(common.get_test_data_file('deluge.png'))
|
icon = TrackerIcon(common.get_test_data_file('deluge.png'))
|
||||||
result = await self.icons.fetch('deluge-torrent.org')
|
result = await self.icons.fetch('deluge-torrent.org')
|
||||||
assert result == icon
|
assert result == icon
|
||||||
|
assert not os.path.isfile(mock_mkstemp[1])
|
||||||
|
|
||||||
@pytest_twisted.ensureDeferred
|
@pytest_twisted.ensureDeferred
|
||||||
async def test_get_google_ico(self):
|
async def test_get_google_ico(self):
|
||||||
|
@ -68,3 +70,10 @@ class TestTrackerIcons(BaseTestCase):
|
||||||
async def test_get_empty_string_tracker(self):
|
async def test_get_empty_string_tracker(self):
|
||||||
result = await self.icons.fetch('')
|
result = await self.icons.fetch('')
|
||||||
assert result is None
|
assert result is None
|
||||||
|
|
||||||
|
@pytest_twisted.ensureDeferred
|
||||||
|
async def test_invalid_host(self, mock_mkstemp):
|
||||||
|
"""Test that TrackerIcon can handle invalid hostname"""
|
||||||
|
result = await self.icons.fetch('deluge.example.com')
|
||||||
|
assert not result
|
||||||
|
assert not os.path.isfile(mock_mkstemp[1])
|
||||||
|
|
|
@ -8,8 +8,8 @@
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import tempfile
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
from tempfile import mkstemp
|
|
||||||
from urllib.parse import urljoin, urlparse
|
from urllib.parse import urljoin, urlparse
|
||||||
|
|
||||||
from twisted.internet import defer, threads
|
from twisted.internet import defer, threads
|
||||||
|
@ -203,8 +203,10 @@ class TrackerIcons(Component):
|
||||||
else:
|
else:
|
||||||
# We need to fetch it
|
# We need to fetch it
|
||||||
self.pending[host] = []
|
self.pending[host] = []
|
||||||
|
tmp_file = tempfile.mkstemp(prefix='deluge_trackericon_html.')
|
||||||
|
filename = tmp_file[1]
|
||||||
# Start callback chain
|
# Start callback chain
|
||||||
d = self.download_page(host)
|
d = self.download_page(host, filename)
|
||||||
d.addCallbacks(
|
d.addCallbacks(
|
||||||
self.on_download_page_complete,
|
self.on_download_page_complete,
|
||||||
self.on_download_page_fail,
|
self.on_download_page_fail,
|
||||||
|
@ -213,6 +215,7 @@ class TrackerIcons(Component):
|
||||||
d.addCallbacks(
|
d.addCallbacks(
|
||||||
self.on_parse_complete, self.on_parse_fail, callbackArgs=(host,)
|
self.on_parse_complete, self.on_parse_fail, callbackArgs=(host,)
|
||||||
)
|
)
|
||||||
|
d.addBoth(self.del_tmp_file, tmp_file)
|
||||||
d.addCallback(self.download_icon, host)
|
d.addCallback(self.download_icon, host)
|
||||||
d.addCallbacks(
|
d.addCallbacks(
|
||||||
self.on_download_icon_complete,
|
self.on_download_icon_complete,
|
||||||
|
@ -224,24 +227,38 @@ class TrackerIcons(Component):
|
||||||
d.addCallback(self.store_icon, host)
|
d.addCallback(self.store_icon, host)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def download_page(self, host, url=None):
|
@staticmethod
|
||||||
"""
|
def del_tmp_file(result, tmp_file):
|
||||||
Downloads a tracker host's page
|
"""Remove tmp_file created when downloading tracker page"""
|
||||||
|
fd, filename = tmp_file
|
||||||
|
try:
|
||||||
|
os.close(fd)
|
||||||
|
os.remove(filename)
|
||||||
|
except OSError:
|
||||||
|
log.debug(f'Unable to delete temporary file: {filename}')
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def download_page(
|
||||||
|
self, host: str, filename: str, url: str = None
|
||||||
|
) -> 'defer.Deferred[str]':
|
||||||
|
"""Downloads a tracker host's page
|
||||||
|
|
||||||
If no url is provided, it bases the url on the host
|
If no url is provided, it bases the url on the host
|
||||||
|
|
||||||
:param host: the tracker host
|
Args:
|
||||||
:type host: string
|
host: The tracker host
|
||||||
:param url: the (optional) url of the host
|
filename: Location to download page
|
||||||
:type url: string
|
url: The url of the host
|
||||||
:returns: the filename of the tracker host's page
|
|
||||||
:rtype: Deferred
|
Returns:
|
||||||
|
The filename of the tracker host's page
|
||||||
"""
|
"""
|
||||||
if not url:
|
if not url:
|
||||||
url = self.host_to_url(host)
|
url = self.host_to_url(host)
|
||||||
log.debug('Downloading %s %s', host, url)
|
|
||||||
tmp_fd, tmp_file = mkstemp(prefix='deluge_ticon.')
|
log.debug(f'Downloading {host} {url} to {filename}')
|
||||||
os.close(tmp_fd)
|
return download_file(url, filename, force_filename=True)
|
||||||
return download_file(url, tmp_file, force_filename=True)
|
|
||||||
|
|
||||||
def on_download_page_complete(self, page):
|
def on_download_page_complete(self, page):
|
||||||
"""
|
"""
|
||||||
|
@ -291,10 +308,6 @@ class TrackerIcons(Component):
|
||||||
if parser.left_head:
|
if parser.left_head:
|
||||||
break
|
break
|
||||||
parser.close()
|
parser.close()
|
||||||
try:
|
|
||||||
os.remove(page)
|
|
||||||
except OSError as ex:
|
|
||||||
log.warning('Could not remove temp file: %s', ex)
|
|
||||||
|
|
||||||
return parser.get_icons()
|
return parser.get_icons()
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue