[postr] add a more verbose upload progress bar (bug 529504)



commit 36ec2467cd99abe3cce18347760188651505126a
Author: David Ignacio <dignacio litl com>
Date:   Tue Dec 15 19:53:06 2009 -0500

    add a more verbose upload progress bar (bug 529504)
    
    the hack is almost too embarassing to detail how it is done
    in the commit message.  if you want to know what is going on
    have to go a step further.
    
    in short, as the image writes bytes out to the socket, update
    an image-specific progress bar

 src/ProgressDialog.py |    8 +-
 src/flickrest.py      |   10 ++-
 src/postr.py          |    8 +-
 src/proxyclient.py    |  175 +++++++++++++++++++++++++++++++++++++++++++------
 4 files changed, 169 insertions(+), 32 deletions(-)
---
diff --git a/src/ProgressDialog.py b/src/ProgressDialog.py
index b4313b7..7fab482 100644
--- a/src/ProgressDialog.py
+++ b/src/ProgressDialog.py
@@ -40,8 +40,8 @@ class ProgressDialog(gtk.Dialog):
         self.label.set_alignment (0.0, 0.0)
         hbox.pack_start (self.label, True, True, 0)
         
-        self.progress = gtk.ProgressBar()
-        vbox.add(self.progress)
+        self.image_progress = gtk.ProgressBar()
+        vbox.add(self.image_progress)
 
         vbox.show_all()
 
@@ -53,9 +53,9 @@ if __name__ == "__main__":
     import gobject
     d = ProgressDialog()
     d.thumbnail.set_from_icon_name ("stock_internet", gtk.ICON_SIZE_DIALOG)
-    d.label.set_text("Uploading")
+    d.label.set_text(_("Uploading"))
     def pulse():
-        d.progress.pulse()
+        d.image_progress.pulse()
         return True
     gobject.timeout_add(200, pulse)
     d.show()
diff --git a/src/flickrest.py b/src/flickrest.py
index fb298f3..abf256a 100644
--- a/src/flickrest.py
+++ b/src/flickrest.py
@@ -165,7 +165,8 @@ class Flickr:
     def upload(self, uri=None, imageData=None,
                title=None, desc=None, tags=None,
                is_public=None, is_family=None, is_friend=None,
-               safety=None, search_hidden=None, content_type=None):
+               safety=None, search_hidden=None, content_type=None,
+               progress_tracker=None):
         # Sanity check the arguments
         if uri is None and imageData is None:
             raise ValueError("Need to pass either uri or imageData")
@@ -206,9 +207,10 @@ class Flickr:
             }
 
         self.logger.info("Calling upload")
-        return client.getPage("http://api.flickr.com/services/upload/";,
-                              proxy=self.proxy, method="POST",
-                              headers=headers, postdata=form).addCallback(self.__cb, "upload")
+        return client.upload("http://api.flickr.com/services/upload/";,
+                             proxy=self.proxy, method="POST",
+                             headers=headers, postdata=form,
+                             progress_tracker=progress_tracker).addCallback(self.__cb, "upload")
 
     def authenticate_2(self, state):
         def gotToken(e):
diff --git a/src/postr.py b/src/postr.py
index afbf28f..b41b437 100644
--- a/src/postr.py
+++ b/src/postr.py
@@ -33,6 +33,7 @@ from AuthenticationDialog import AuthenticationDialog
 from ProgressDialog import ProgressDialog
 from ErrorDialog import ErrorDialog
 import ImageStore, ImageList, StatusBar, PrivacyCombo, SafetyCombo, GroupSelector, ContentTypeCombo
+from proxyclient import UploadProgressTracker
 
 from flickrest import Flickr
 import EXIF
@@ -180,6 +181,7 @@ class Postr(UniqueApp):
             self.cancel_upload = True
         self.progress_dialog = ProgressDialog(cancel)
         self.progress_dialog.set_transient_for(self.window)
+        self.upload_progress_tracker = UploadProgressTracker(self.progress_dialog.image_progress)
         self.avatar_image.clear()
         # Disable the Upload menu until the user has authenticated
         self.update_upload()
@@ -885,8 +887,6 @@ class Postr(UniqueApp):
             self.progress_dialog.thumbnail.set_from_pixbuf(None)
             self.progress_dialog.thumbnail.hide()
 
-        self.progress_dialog.progress.set_fraction(float(self.upload_index) / float(self.upload_count))
-
         # Use named args for i18n
         data = {
             "index": self.upload_index+1,
@@ -1009,7 +1009,7 @@ class Postr(UniqueApp):
                                    title=title, desc=desc,
                                    tags=tags, search_hidden=not visible, safety=safety,
                                    is_public=is_public, is_family=is_family, is_friend=is_friend,
-                                   content_type=content_type)
+                                   content_type=content_type, progress_tracker=self.upload_progress_tracker)
         elif pixbuf:
             # This isn't very nice, but might be the best way
             data = []
@@ -1018,7 +1018,7 @@ class Postr(UniqueApp):
                                    title=title, desc=desc, tags=tags,
                                    search_hidden=not visible, safety=safety,
                                    is_public=is_public, is_family=is_family, is_friend=is_friend,
-                                   content_type=content_type)
+                                   content_type=content_type, progress_tracker=self.upload_progress_tracker)
         else:
             print "No filename or pixbuf stored"
 
diff --git a/src/proxyclient.py b/src/proxyclient.py
index f0784f9..1ff589d 100644
--- a/src/proxyclient.py
+++ b/src/proxyclient.py
@@ -14,18 +14,17 @@ import urlparse, os, types
 
 from twisted.web import http
 from twisted.internet import defer, protocol, reactor
+from twisted.internet.main import CONNECTION_LOST
 from twisted.python import failure
 from twisted.python.util import InsensitiveDict
 from twisted.web import error
 
-
 class PartialDownloadError(error.Error):
     """Page was only partially downloaded, we got disconnected in middle.
 
     The bit that was downloaded is in the response attribute.
     """
 
-
 class HTTPPageGetter(http.HTTPClient):
 
     quietLoss = 0
@@ -51,7 +50,9 @@ class HTTPPageGetter(http.HTTPClient):
                 self.sendHeader(key, value)
         self.endHeaders()
         self.headers = {}
-        
+        self.writeTheData(data)
+
+    def writeTheData(self, data):
         if data is not None:
             self.transport.write(data)
 
@@ -389,20 +390,23 @@ def _parse(url, defaultPort=None):
 
 
 def getPage(url, contextFactory=None, proxy=None, *args, **kwargs):
-    """Download a web page as a string.
+    """
+    This method has been refactored to _makeDeferredRequest, for more
+    information, see the comment there.
+    """
+    return _makeDeferredRequest(url, contextFactory=contextFactory,
+                                proxy=proxy, *args, **kwargs)
 
-    Download a page. Return a deferred, which will callback with a
-    page (as a string) or errback with a description of the error.
 
-    See HTTPClientFactory to see what extra args can be passed.
-    """
-    if proxy:
-        scheme, host, port, path = _parse(proxy)
-        kwargs['proxy'] = proxy
-    else:
-        scheme, host, port, path = _parse(url)
+def downloadPage(url, file, contextFactory=None, *args, **kwargs):
+    """Download a web page to a file.
+
+    @param file: path to file on filesystem, or file-like object.
     
-    factory = HTTPClientFactory(url, *args, **kwargs)
+    See HTTPDownloader to see what extra args can be passed.
+    """
+    scheme, host, port, path = _parse(url)
+    factory = HTTPDownloader(url, file, *args, **kwargs)
     if scheme == 'https':
         from twisted.internet import ssl
         if contextFactory is None:
@@ -412,16 +416,147 @@ def getPage(url, contextFactory=None, proxy=None, *args, **kwargs):
         reactor.connectTCP(host, port, factory)
     return factory.deferred
 
+class UploadProgressTracker(object):
+    """
+    This object takes a gtk.ProgressBar object as a parameter
+    and appropriately calls progress.set_fraction() as more
+    data gets written to the pipe.
+    """
+    def __init__(self, progress):
+        self._progress = progress
+        self._write_size = 1
+        self._write_progress = 0
 
-def downloadPage(url, file, contextFactory=None, *args, **kwargs):
-    """Download a web page to a file.
+    def set_write_size(self, size):
+        """
+        Resets the progress count and records the next image's size
+        """
+        self._write_progress = 0
+        self._write_size = size
+
+    def _onDataWritten(self, size):
+        """ increments the write_progress """
+        self._write_progress += size
+        self._update_progress()
+
+    def _onConnectionLost(self):
+        """ connection lost, same as done writing """
+        self._onWriteDone()
+
+    def _onWriteDone(self):
+        """ done writing, zero out progress """
+        self._write_progress = 0
+        self._write_size = 1
+        self._update_progress()
+
+    def _update_progress(self):
+        """ updates the progress bar, capping at 100% """
+        self._progress.set_fraction(min(float(self._write_progress) / float(self._write_size),
+                                        1))
+
+    def wrap_writeSomeData(self, func):
+        """
+        the horrible decorator to wrap
+        twisted.internet.tcp.Connection's implementation of writeSomeData
+        standard conventions fd->write return value conventions of
+        0 -> done, CONNECTION_LOST -> connection lost, N -> N bytes written
+        """
+        def inner_writeSomeData(data):
+            size = func(data)
+            if size == 0:
+                self._onWriteDone()
+            elif size == CONNECTION_LOST:
+                self._onConnectionLost()
+            else:
+                self._onDataWritten(size)
+            return size
+        return inner_writeSomeData
 
-    @param file: path to file on filesystem, or file-like object.
+class UploadHTTPPageGetter(HTTPPageGetter):
+    """
+    Subclass of HTTPPageGetter with one gruesome hack.
+    Looking at the twisted.internet.tcp.Connection class (which
+    this class subclasses) the actual data gets put on the wire
+    on writeSomeData.
+    http://twistedmatrix.com/documents/current/api/twisted.internet.tcp.Connection.html#writeSomeData
+    Since twisted is so abstracted out and simple, we never really
+    see the calls of self.transport.writeSomeData, but we obviously
+    know it is getting called.  This happens in response to a call to
+    self.transport.write.  So we can ensure that writeSomeData is wrapped
+    when write is called.
+    """
+    def set_progress_tracker(self, progress_tracker):
+        self._progress_tracker = progress_tracker
+
+    def writeTheData(self, data):
+        if data is not None:
+            if self._progress_tracker:
+                if not hasattr(self.transport, '__has_wrapped_writeSomeData'):
+                    self.transport.writeSomeData = self._progress_tracker.wrap_writeSomeData(self.transport.writeSomeData)
+                    self.transport.__has_wrapped_writeSomeData = True
+                self._progress_tracker.set_write_size(len(data))
+            self.transport.write(data)
+
+class UploadHTTPClientFactory(HTTPClientFactory):
+    """
+    Subclass of HTTPClientFactory that contains a method
+    set_progress_tracker that allows the user to specify
+    an UploadProgressTracker object to display the uploads
+    send percentage.
+    """
+    protocol = UploadHTTPPageGetter
+    _progress_tracker = None
+
+    def __init__(self, *args, **kwargs):
+        HTTPClientFactory.__init__(self, *args, **kwargs)
+
+    def buildProtocol(self, addr):
+        p = HTTPClientFactory.buildProtocol(self, addr)
+        if self._progress_tracker:
+            p.set_progress_tracker(self._progress_tracker)
+        return p
+
+    def set_progress_tracker(self, progress_tracker):
+        self._progress_tracker = progress_tracker
     
-    See HTTPDownloader to see what extra args can be passed.
+def upload(url, contextFactory=None, proxy=None,
+           progress_tracker=None, *args, **kwargs):
     """
-    scheme, host, port, path = _parse(url)
-    factory = HTTPDownloader(url, file, *args, **kwargs)
+    This is a horrible hacked version of getPage.  After
+    this method overrides the client_factory_class to
+    UploadHTTPClientFactory, which has the method 'set_progress_tracker'
+    that allows for tracking the progress of the upload.
+    """
+    return _makeDeferredRequest(url, contextFactory=contextFactory,
+                                progress_tracker=progress_tracker,
+                                clientFactoryClass=UploadHTTPClientFactory,
+                                proxy=proxy, *args, **kwargs)
+
+
+def _makeDeferredRequest(url, contextFactory=None, proxy=None,
+                         progress_tracker=None,
+                         clientFactoryClass=None,
+                         *args, **kwargs):
+    """Download a web page as a string.
+
+    Download a page. Return a deferred, which will callback with a
+    page (as a string) or errback with a description of the error.
+
+    See HTTPClientFactory to see what extra args can be passed.
+    """
+    if proxy:
+        scheme, host, port, path = _parse(proxy)
+        kwargs['proxy'] = proxy
+    else:
+        scheme, host, port, path = _parse(url)
+
+    if not clientFactoryClass:
+        clientFactoryClass = HTTPClientFactory
+    factory = clientFactoryClass(url, *args, **kwargs)
+
+    if progress_tracker is not None and hasattr(factory, 'set_progress_tracker'):
+        factory.set_progress_tracker(progress_tracker)
+
     if scheme == 'https':
         from twisted.internet import ssl
         if contextFactory is None:



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]