Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add progressbar support to most download actions #615

Merged
52 changes: 52 additions & 0 deletions snapcraft/internal/indicators.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4 -*-
#
# Copyright (C) 2016 Canonical Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3 as
# published by the Free Software Foundation.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.

import os

from progressbar import (
AnimatedMarker,
Bar,
Percentage,
ProgressBar,
UnknownLength,
)


def download_requests_stream(request_stream, destination, message=None):
"""This is a facility to download a request with nice progress bars."""
if not message:
message = 'Downloading {!r}'.format(os.path.basename(destination))

total_length = int(request_stream.headers.get('Content-Length', '0'))
if total_length:
progress_bar = ProgressBar(
widgets=[message,
Bar(marker='=', left='[', right=']'),
' ', Percentage()],
maxval=total_length)
else:
progress_bar = ProgressBar(
widgets=[message, AnimatedMarker()],
maxval=UnknownLength)

total_read = 0
progress_bar.start()
with open(destination, 'wb') as destination_file:
for buf in request_stream.iter_content(1024):
destination_file.write(buf)
total_read += len(buf)
progress_bar.update(total_read)
progress_bar.finish()
33 changes: 3 additions & 30 deletions snapcraft/internal/parts.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,9 @@

import requests
import yaml
from progressbar import (
AnimatedMarker,
Bar,
Percentage,
ProgressBar,
UnknownLength,
)
from xdg import BaseDirectory

from snapcraft.internal.indicators import download_requests_stream
from snapcraft.internal.common import get_terminal_width


Expand Down Expand Up @@ -67,31 +61,10 @@ def execute(self):
return
self._request.raise_for_status()

self._download()
download_requests_stream(self._request, self.parts_yaml,
'Downloading parts list')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is really nice.

self._save_headers()

def _download(self):
total_length = int(self._request.headers.get('Content-Length', '0'))
if total_length:
progress_bar = ProgressBar(
widgets=['Updating parts list',
Bar(marker='=', left='[', right=']'),
' ', Percentage()],
maxval=total_length)
else:
progress_bar = ProgressBar(
widgets=['Updating parts list... ', AnimatedMarker()],
maxval=UnknownLength)

total_read = 0
progress_bar.start()
with open(self.parts_yaml, 'wb') as parts_file:
for buf in self._request.iter_content(1024):
parts_file.write(buf)
total_read += len(buf)
progress_bar.update(total_read)
progress_bar.finish()

def _load_headers(self):
if not os.path.exists(self._headers_yaml):
return None
Expand Down
16 changes: 7 additions & 9 deletions snapcraft/internal/sources.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@
import zipfile

from snapcraft.internal import common
from snapcraft.internal.indicators import download_requests_stream


logging.getLogger('urllib3').setLevel(logging.CRITICAL)
Expand Down Expand Up @@ -103,15 +104,12 @@ def pull(self):
self.provision(self.source_dir)

def download(self):
req = requests.get(self.source, stream=True, allow_redirects=True)
if req.status_code is not 200:
raise EnvironmentError('unexpected http status code when '
'downloading {}'.format(req.status_code))

file = os.path.join(self.source_dir, os.path.basename(self.source))
with open(file, 'wb') as f:
for chunk in req.iter_content(1024):
f.write(chunk)
request = requests.get(self.source, stream=True, allow_redirects=True)
request.raise_for_status()

file_path = os.path.join(
self.source_dir, os.path.basename(self.source))
download_requests_stream(request, file_path)


class Bazaar(Base):
Expand Down
16 changes: 8 additions & 8 deletions snapcraft/storeapi/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@

import snapcraft
from snapcraft import config
from snapcraft.internal.indicators import download_requests_stream
from snapcraft.storeapi import (
_upload,
constants,
Expand Down Expand Up @@ -191,12 +192,10 @@ def _download_snap(self, name, channel, arch, download_path,
name, download_path))
return
logger.info('Downloading {}'.format(name, download_path))
# FIXME: Check the status code ! -- vila 2016-05-04
download = self.cpi.get(download_url)
with open(download_path, 'wb') as f:
# FIXME: Cough, we may want to buffer here (and a progress bar
# would be nice) -- vila 2016-04-26
f.write(download.content)
request = self.cpi.get(download_url, stream=True)
request.raise_for_status()
download_requests_stream(request, download_path)

if self._is_downloaded(download_path, expected_sha512):
logger.info('Successfully downloaded {} at {}'.format(
name, download_path))
Expand Down Expand Up @@ -275,11 +274,12 @@ def search_package(self, snap_name, channel, arch):
else:
return embedded['clickindex:package'][0]

def get(self, url, headers=None, params=None):
def get(self, url, headers=None, params=None, stream=False):
if headers is None:
headers = {}
headers.update({'Authorization': _macaroon_auth(self.conf)})
response = self.request('GET', url, headers=headers, params=params)
response = self.request('GET', url, stream=stream,
headers=headers, params=params)
return response


Expand Down
6 changes: 6 additions & 0 deletions snapcraft/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ def setUp(self):
self.cpu_count.return_value = 2
self.addCleanup(patcher.stop)

patcher = mock.patch(
'snapcraft.internal.indicators.ProgressBar',
new=SilentProgressBar)
patcher.start()
self.addCleanup(patcher.stop)

# These are what we expect by default
self.snap_dir = os.path.join(os.getcwd(), 'prime')
self.stage_dir = os.path.join(os.getcwd(), 'stage')
Expand Down
4 changes: 1 addition & 3 deletions snapcraft/tests/test_commands_define.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@ class DefineCommandTestCase(tests.TestCase):
def setUp(self):
super().setUp()
self.useFixture(fixture_setup.FakeParts())
with mock.patch('snapcraft.internal.parts.ProgressBar',
new=tests.SilentProgressBar):
parts.update()
parts.update()

@mock.patch('sys.stdout', new_callable=io.StringIO)
def test_defining_a_part_that_exists(self, mock_stdout):
Expand Down
5 changes: 1 addition & 4 deletions snapcraft/tests/test_commands_search.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import logging
from unittest import mock

import fixtures

Expand All @@ -29,9 +28,7 @@ class SearchCommandTestCase(tests.TestCase):
def setUp(self):
super().setUp()
self.useFixture(fixture_setup.FakeParts())
with mock.patch('snapcraft.internal.parts.ProgressBar',
new=tests.SilentProgressBar):
parts.update()
parts.update()

def test_searching_for_a_part_that_exists(self):
fake_terminal = fixture_setup.FakeTerminal()
Expand Down
6 changes: 0 additions & 6 deletions snapcraft/tests/test_commands_update.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@

import logging
import os
from unittest import mock

import fixtures
import yaml
Expand All @@ -32,11 +31,6 @@ class UpdateCommandTestCase(tests.TestCase):
def setUp(self):
super().setUp()
self.useFixture(fixture_setup.FakeParts())
patcher = mock.patch(
'snapcraft.internal.parts.ProgressBar',
new=tests.SilentProgressBar)
patcher.start()
self.addCleanup(patcher.stop)

self.parts_dir = os.path.join(BaseDirectory.xdg_data_home, 'snapcraft')
self.parts_yaml = os.path.join(self.parts_dir, 'parts.yaml')
Expand Down
20 changes: 0 additions & 20 deletions snapcraft/tests/test_yaml.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,11 +93,6 @@ def test_config_loads_with_different_encodings(
@unittest.mock.patch('snapcraft.internal.yaml.Config.load_plugin')
def test_config_composes_with_remote_parts(self, mock_loadPlugin):
self.useFixture(fixture_setup.FakeParts())
patcher = unittest.mock.patch(
'snapcraft.internal.parts.ProgressBar',
new=tests.SilentProgressBar)
patcher.start()
self.addCleanup(patcher.stop)

self.make_snapcraft_yaml("""name: test
version: "1"
Expand All @@ -119,11 +114,6 @@ def test_config_composes_with_remote_parts(self, mock_loadPlugin):

def test_config_composes_with_a_non_existent_remote_part(self):
self.useFixture(fixture_setup.FakeParts())
patcher = unittest.mock.patch(
'snapcraft.internal.parts.ProgressBar',
new=tests.SilentProgressBar)
patcher.start()
self.addCleanup(patcher.stop)

self.make_snapcraft_yaml("""name: test
version: "1"
Expand All @@ -148,11 +138,6 @@ def test_config_composes_with_a_non_existent_remote_part(self):

def test_config_after_is_an_undefined_part(self):
self.useFixture(fixture_setup.FakeParts())
patcher = unittest.mock.patch(
'snapcraft.internal.parts.ProgressBar',
new=tests.SilentProgressBar)
patcher.start()
self.addCleanup(patcher.stop)

self.make_snapcraft_yaml("""name: test
version: "1"
Expand All @@ -179,11 +164,6 @@ def test_config_after_is_an_undefined_part(self):
@unittest.mock.patch('snapcraft.internal.pluginhandler.load_plugin')
def test_config_uses_remote_part_from_after(self, mock_load):
self.useFixture(fixture_setup.FakeParts())
patcher = unittest.mock.patch(
'snapcraft.internal.parts.ProgressBar',
new=tests.SilentProgressBar)
patcher.start()
self.addCleanup(patcher.stop)

self.make_snapcraft_yaml("""name: test
version: "1"
Expand Down