Skip to content

Commit

Permalink
Update downloader: modernized, includes hashlib and other fixes from …
Browse files Browse the repository at this point in the history
…recent releases. re nvaccess#3504

Modernized Jamie Teh's downloader class to include file hashes and other changes from recent releases. Testing shows it is working as advertised.
  • Loading branch information
josephsl committed Feb 10, 2017
1 parent 232f9c8 commit ee39ad2
Show file tree
Hide file tree
Showing 2 changed files with 16 additions and 135 deletions.
18 changes: 15 additions & 3 deletions source/downloader.py
Expand Up @@ -2,14 +2,16 @@
#A part of NonVisual Desktop Access (NVDA)
#This file is covered by the GNU General Public License.
#See the file COPYING for more details.
#Copyright (C) 2013 NV Access Limited
#Copyright (C) 2013-2017 NV Access Limited, Joseph Lee

"""Utilities for downloading files.
"""

import os
import tempfile
import urllib
import threading
import hashlib
import wx
import gui
from logHandler import log
Expand All @@ -36,14 +38,17 @@ class FileDownloader(object):
#: @type: basestring
errorMessage = None

def __init__(self, urls, parent=None):
def __init__(self, urls, fileHash=None, parent=None):
"""Constructor.
@param urls: URLs to try for the file.
@type urls: list of str
@param fileHash: The SHA-1 hash of the file as a hex string.
@type fileHash: basestring
@param parent: The parent window for any dialogs.
@type parent: C{wx.Window}
"""
self.urls = urls
self.fileHash = fileHash
if not parent:
parent = gui.mainFrame
self.parent = parent
Expand Down Expand Up @@ -74,7 +79,8 @@ def _guiExec(self, func, *args):
self._guiExecFunc = func
self._guiExecArgs = args
if not self._guiExecTimer.IsRunning():
self._guiExecTimer.Start(50, True)
# #6127: Timers must be manipulated from the main thread.
wx.CallAfter(self._guiExecTimer.Start, 50, True)

def _guiExecNotify(self):
self._guiExecFunc(*self._guiExecArgs)
Expand Down Expand Up @@ -112,6 +118,8 @@ def _download(self, url):
remote.fp._sock.settimeout(120)
size = int(remote.headers["content-length"])
local = file(self.destPath, "wb")
if self.fileHash:
hasher = hashlib.sha1()
self._guiExec(self._downloadReport, 0, size)
read = 0
chunk=DOWNLOAD_BLOCK_SIZE
Expand All @@ -127,9 +135,13 @@ def _download(self, url):
if self._shouldCancel:
return
local.write(block)
if self.fileHash:
hasher.update(block)
self._guiExec(self._downloadReport, read, size)
if read < size:
raise RuntimeError("Content too short")
if self.fileHash and hasher.hexdigest() != self.fileHash:
raise RuntimeError("Content has incorrect file hash")
self._guiExec(self._downloadReport, read, size)

def _downloadReport(self, read, size):
Expand Down
133 changes: 1 addition & 132 deletions source/updateCheck.py
Expand Up @@ -15,6 +15,7 @@
if not versionInfo.updateVersionType:
raise RuntimeError("No update version type, update checking not supported")

import ctypes
import winVersion
import os
import threading
Expand All @@ -37,8 +38,6 @@
CHECK_INTERVAL = 86400 # 1 day
#: The time to wait before retrying a failed check.
RETRY_INTERVAL = 600 # 10 min
#: The download block size in bytes.
DOWNLOAD_BLOCK_SIZE = 8192 # 8 kb

#: Persistent state information.
#: @type: dict
Expand Down Expand Up @@ -254,141 +253,11 @@ class UpdateDownloader(FileDownloader):
"""Download and start installation of an updated version of NVDA, presenting appropriate user interface.
"""

<<<<<<< 89e9721cfdeba19f0508a99f716c4b4f72cf74f9
def __init__(self, urls, fileHash=None):
"""Constructor.
@param urls: URLs to try for the update file.
@type urls: list of str
@param fileHash: The SHA-1 hash of the file as a hex string.
@type fileHash: basestring
"""
self.urls = urls
self.destPath = tempfile.mktemp(prefix="nvda_update_", suffix=".exe")
self.fileHash = fileHash

def start(self):
"""Start the download.
"""
self._shouldCancel = False
# Use a timer because timers aren't re-entrant.
self._guiExecTimer = wx.PyTimer(self._guiExecNotify)
gui.mainFrame.prePopup()
# Translators: The title of the dialog displayed while downloading an NVDA update.
self._progressDialog = wx.ProgressDialog(_("Downloading Update"),
# Translators: The progress message indicating that a connection is being established.
_("Connecting"),
# PD_AUTO_HIDE is required because ProgressDialog.Update blocks at 100%
# and waits for the user to press the Close button.
style=wx.PD_CAN_ABORT | wx.PD_ELAPSED_TIME | wx.PD_REMAINING_TIME | wx.PD_AUTO_HIDE,
parent=gui.mainFrame)
self._progressDialog.Raise()
t = threading.Thread(target=self._bg)
t.daemon = True
t.start()

def _guiExec(self, func, *args):
self._guiExecFunc = func
self._guiExecArgs = args
if not self._guiExecTimer.IsRunning():
# #6127: Timers must be manipulated from the main thread.
wx.CallAfter(self._guiExecTimer.Start, 50, True)

def _guiExecNotify(self):
self._guiExecFunc(*self._guiExecArgs)

def _bg(self):
success=False
for url in self.urls:
try:
self._download(url)
except:
log.debugWarning("Error downloading %s" % url, exc_info=True)
else: #Successfully downloaded or canceled
if not self._shouldCancel:
success=True
break
else:
# None of the URLs succeeded.
self._guiExec(self._error)
return
if not success:
try:
os.remove(self.destPath)
except OSError:
pass
return
self._guiExec(self._downloadSuccess)

def _download(self, url):
remote = urllib.urlopen(url)
if remote.code != 200:
raise RuntimeError("Download failed with code %d" % remote.code)
# #2352: Some security scanners such as Eset NOD32 HTTP Scanner
# cause huge read delays while downloading.
# Therefore, set a higher timeout.
remote.fp._sock.settimeout(120)
size = int(remote.headers["content-length"])
local = file(self.destPath, "wb")
if self.fileHash:
hasher = hashlib.sha1()
self._guiExec(self._downloadReport, 0, size)
read = 0
chunk=DOWNLOAD_BLOCK_SIZE
while True:
if self._shouldCancel:
return
if size -read <chunk:
chunk =size -read
block = remote.read(chunk)
if not block:
break
read += len(block)
if self._shouldCancel:
return
local.write(block)
if self.fileHash:
hasher.update(block)
self._guiExec(self._downloadReport, read, size)
if read < size:
raise RuntimeError("Content too short")
if self.fileHash and hasher.hexdigest() != self.fileHash:
raise RuntimeError("Content has incorrect file hash")
self._guiExec(self._downloadReport, read, size)

def _downloadReport(self, read, size):
if self._shouldCancel:
return
percent = int(float(read) / size * 100)
# Translators: The progress message indicating that a download is in progress.
cont, skip = self._progressDialog.Update(percent, _("Downloading"))
if not cont:
self._shouldCancel = True
self._stopped()

def _stopped(self):
self._guiExecTimer = None
self._guiExecFunc = None
self._guiExecArgs = None
self._progressDialog.Hide()
self._progressDialog.Destroy()
self._progressDialog = None
# Not sure why, but this doesn't work if we call it directly here.
wx.CallLater(50, gui.mainFrame.postPopup)

def _error(self):
self._stopped()
gui.messageBox(
# Translators: A message indicating that an error occurred while downloading an update to NVDA.
_("Error downloading update."),
_("Error"),
wx.OK | wx.ICON_ERROR)
=======
destSuffix = ".exe"
# Translators: The title of the dialog displayed while downloading an NVDA update.
progressTitle = _("Downloading Update")
# Translators: A message indicating that an error occurred while downloading an update to NVDA.
errorMessage = _("Error downloading update.")
>>>>>>> updateCheck.UpdateDownloader: Use new FileDownloader class.

def onSuccess(self):
# Translators: The message presented when the update has been successfully downloaded
Expand Down

0 comments on commit ee39ad2

Please sign in to comment.