Michael Terry has proposed merging lp:~mterry/duplicity/encode-exceptions into 
lp:duplicity.

Requested reviews:
  duplicity-team (duplicity-team)
Related bugs:
  Bug #1311176 in duplicity (Ubuntu): "crash deja-dup backup ascii' codec can't 
decode byte 0xc3"
  https://bugs.launchpad.net/ubuntu/+source/duplicity/+bug/1311176
  Bug #1313966 in duplicity (Ubuntu): "duplicity cannot back up files from 
Russian file directory"
  https://bugs.launchpad.net/ubuntu/+source/duplicity/+bug/1313966

For more details, see:
https://code.launchpad.net/~mterry/duplicity/encode-exceptions/+merge/217699

I've noticed several bugs being filed lately for UnicodeDecodeErrors during 
exception handling.  Because exceptions often contain file paths, they have the 
same problem with Python 2.x's implicit decoding using the 'ascii' encoding 
that we've experienced before.

So I added a new util.uexc() method that uses the util.ufn() method to convert 
an exception to a unicode string and used it around the place.
-- 
https://code.launchpad.net/~mterry/duplicity/encode-exceptions/+merge/217699
Your team duplicity-team is requested to review the proposed merge of 
lp:~mterry/duplicity/encode-exceptions into lp:duplicity.
=== modified file 'bin/duplicity'
--- bin/duplicity	2014-04-22 15:01:38 +0000
+++ bin/duplicity	2014-04-29 23:53:13 +0000
@@ -1042,7 +1042,7 @@
         try:
             util.ignore_missing(os.unlink, del_name)
         except Exception as e:
-            log.Warn(_("Unable to delete %s: %s") % (util.ufn(del_name), str(e)))
+            log.Warn(_("Unable to delete %s: %s") % (util.ufn(del_name), util.uexc(e)))
 
     def copy_to_local(fn):
         """
@@ -1531,7 +1531,7 @@
         # default. But do with sufficient verbosity.
         log.Info(_("User error detail: %s")
                  % (u''.join(traceback.format_exception(*sys.exc_info()))))
-        log.FatalError(u"%s: %s" % (e.__class__.__name__, unicode(e)),
+        log.FatalError(u"%s: %s" % (e.__class__.__name__, util.uexc(e)),
                        log.ErrorCode.user_error,
                        e.__class__.__name__)
 
@@ -1541,14 +1541,14 @@
         # default. But do with sufficient verbosity.
         log.Info(_("Backend error detail: %s")
                  % (u''.join(traceback.format_exception(*sys.exc_info()))))
-        log.FatalError(u"%s: %s" % (e.__class__.__name__, unicode(e)),
+        log.FatalError(u"%s: %s" % (e.__class__.__name__, util.uexc(e)),
                        log.ErrorCode.user_error,
                        e.__class__.__name__)
 
     except Exception as e:
         util.release_lockfile()
         if "Forced assertion for testing" in str(e):
-            log.FatalError(u"%s: %s" % (e.__class__.__name__, unicode(e)),
+            log.FatalError(u"%s: %s" % (e.__class__.__name__, util.uexc(e)),
                            log.ErrorCode.exception,
                            e.__class__.__name__)
         else:

=== modified file 'duplicity/backend.py'
--- duplicity/backend.py	2014-04-28 02:49:39 +0000
+++ duplicity/backend.py	2014-04-29 23:53:13 +0000
@@ -383,10 +383,10 @@
                         extra = ' '.join([operation] + [make_filename(x) for x in args if x])
                         log.FatalError(_("Giving up after %s attempts. %s: %s")
                                        % (n, e.__class__.__name__,
-                                          str(e)), code=code, extra=extra)
+                                          util.uexc(e)), code=code, extra=extra)
                     else:
                         log.Warn(_("Attempt %s failed. %s: %s")
-                                 % (n, e.__class__.__name__, str(e)))
+                                 % (n, e.__class__.__name__, util.uexc(e)))
                     if not at_end:
                         if isinstance(e, TemporaryLoadException):
                             time.sleep(90) # wait longer before trying again
@@ -495,7 +495,7 @@
 
     def __do_put(self, source_path, remote_filename):
         if hasattr(self.backend, '_put'):
-            log.Info(_("Writing %s") % remote_filename)
+            log.Info(_("Writing %s") % util.ufn(remote_filename))
             self.backend._put(source_path, remote_filename)
         else:
             raise NotImplementedError()

=== modified file 'duplicity/backends/_cf_cloudfiles.py'
--- duplicity/backends/_cf_cloudfiles.py	2014-04-28 02:49:39 +0000
+++ duplicity/backends/_cf_cloudfiles.py	2014-04-29 23:53:13 +0000
@@ -22,6 +22,7 @@
 
 import duplicity.backend
 from duplicity import log
+from duplicity import util
 from duplicity.errors import BackendException
 
 class CloudFilesBackend(duplicity.backend.Backend):
@@ -61,7 +62,7 @@
             conn = Connection(**conn_kwargs)
         except Exception as e:
             log.FatalError("Connection failed, please check your credentials: %s %s"
-                           % (e.__class__.__name__, str(e)),
+                           % (e.__class__.__name__, util.uexc(e)),
                            log.ErrorCode.connection_failed)
         self.container = conn.create_container(container)
 

=== modified file 'duplicity/backends/_cf_pyrax.py'
--- duplicity/backends/_cf_pyrax.py	2014-04-28 02:49:39 +0000
+++ duplicity/backends/_cf_pyrax.py	2014-04-29 23:53:13 +0000
@@ -22,6 +22,7 @@
 
 import duplicity.backend
 from duplicity import log
+from duplicity import util
 from duplicity.errors import BackendException
 
 
@@ -61,7 +62,7 @@
             pyrax.set_credentials(**conn_kwargs)
         except Exception as e:
             log.FatalError("Connection failed, please check your credentials: %s %s"
-                           % (e.__class__.__name__, str(e)),
+                           % (e.__class__.__name__, util.uexc(e)),
                            log.ErrorCode.connection_failed)
 
         self.client_exc = pyrax.exceptions.ClientException

=== modified file 'duplicity/backends/dpbxbackend.py'
--- duplicity/backends/dpbxbackend.py	2014-04-28 02:49:39 +0000
+++ duplicity/backends/dpbxbackend.py	2014-04-29 23:53:13 +0000
@@ -35,6 +35,7 @@
 
 import duplicity.backend
 from duplicity import log
+from duplicity import util
 from duplicity.errors import BackendException
 
 
@@ -81,7 +82,7 @@
                 log_exception(e)
                 raise BackendException('dpbx type error "%s"' % (e,))
             except rest.ErrorResponse as e:
-                msg = e.user_error_msg or str(e)
+                msg = e.user_error_msg or util.uexc(e)
                 log.Error('dpbx error: %s' % (msg,), log.ErrorCode.backend_command_error)
                 raise e
             except Exception as e:
@@ -157,7 +158,7 @@
           try: # to login to the box
             self.sess.link()
           except rest.ErrorResponse as e:
-            log.FatalError('dpbx Error: %s\n' % str(e), log.ErrorCode.dpbx_nologin)
+            log.FatalError('dpbx Error: %s\n' % util.uexc(e), log.ErrorCode.dpbx_nologin)
           if not self.sess.is_linked(): # stil not logged in
             log.FatalError("dpbx Cannot login: check your credentials",log.ErrorCode.dpbx_nologin)
 

=== modified file 'duplicity/backends/giobackend.py'
--- duplicity/backends/giobackend.py	2014-04-28 02:49:39 +0000
+++ duplicity/backends/giobackend.py	2014-04-29 23:53:13 +0000
@@ -25,6 +25,7 @@
 
 import duplicity.backend
 from duplicity import log
+from duplicity import util
 
 def ensure_dbus():
     # GIO requires a dbus session bus which can start the gvfs daemons
@@ -103,7 +104,7 @@
             # check for NOT_SUPPORTED because some schemas (e.g. file://) validly don't
             if e.code != Gio.IOErrorEnum.ALREADY_MOUNTED and e.code != Gio.IOErrorEnum.NOT_SUPPORTED:
                 log.FatalError(_("Connection failed, please check your password: %s")
-                               % str(e), log.ErrorCode.connection_failed)
+                               % util.uexc(e), log.ErrorCode.connection_failed)
         loop.quit()
 
     def __copy_progress(self, *args, **kwargs):

=== modified file 'duplicity/backends/swiftbackend.py'
--- duplicity/backends/swiftbackend.py	2014-04-28 02:49:39 +0000
+++ duplicity/backends/swiftbackend.py	2014-04-29 23:53:13 +0000
@@ -22,6 +22,7 @@
 
 import duplicity.backend
 from duplicity import log
+from duplicity import util
 from duplicity.errors import BackendException
 
 
@@ -76,7 +77,7 @@
             self.conn.put_container(self.container)
         except Exception as e:
             log.FatalError("Connection failed: %s %s"
-                           % (e.__class__.__name__, str(e)),
+                           % (e.__class__.__name__, util.uexc(e)),
                            log.ErrorCode.connection_failed)
 
     def _error_code(self, operation, e):

=== modified file 'duplicity/backends/webdavbackend.py'
--- duplicity/backends/webdavbackend.py	2014-04-28 02:49:39 +0000
+++ duplicity/backends/webdavbackend.py	2014-04-29 23:53:13 +0000
@@ -32,6 +32,7 @@
 import duplicity.backend
 from duplicity import globals
 from duplicity import log
+from duplicity import util
 from duplicity.errors import BackendException, FatalBackendException
 
 class CustomMethodRequest(urllib2.Request):
@@ -97,7 +98,7 @@
                 return httplib.HTTPSConnection.request(self, *args, **kwargs)
             except ssl.SSLError as e:
                 # encapsulate ssl errors
-                raise BackendException("SSL failed: %s" % str(e),log.ErrorCode.backend_error)
+                raise BackendException("SSL failed: %s" % util.uexc(e),log.ErrorCode.backend_error)
 
 
 class WebDAVBackend(duplicity.backend.Backend):

=== modified file 'duplicity/patchdir.py'
--- duplicity/patchdir.py	2014-04-25 23:53:46 +0000
+++ duplicity/patchdir.py	2014-04-29 23:53:13 +0000
@@ -508,7 +508,7 @@
         except Exception as e:
             filename = normalized[-1].get_ropath().get_relative_path()
             log.Warn(_("Error '%s' patching %s") % 
-                     (str(e), filename),
+                     (util.uexc(e), util.ufn(filename)),
                      log.WarningCode.cannot_process,
                      util.escape(filename))
 

=== modified file 'duplicity/util.py'
--- duplicity/util.py	2014-04-25 23:53:46 +0000
+++ duplicity/util.py	2014-04-29 23:53:13 +0000
@@ -48,11 +48,10 @@
     lines = traceback.format_tb(tb, limit)
     lines.extend(traceback.format_exception_only(type, value))
 
-    str = "Traceback (innermost last):\n"
-    str = str + "%-20s %s" % (string.join(lines[:-1], ""),
-                                lines[-1])
+    msg = "Traceback (innermost last):\n"
+    msg = msg + "%-20s %s" % (string.join(lines[:-1], ""), lines[-1])
 
-    return str
+    return uexc(msg)
 
 def escape(string):
     "Convert a (bytes) filename to a format suitable for logging (quoted utf8)"
@@ -71,6 +70,13 @@
     else:
         return u'.'
 
+def uexc(e):
+    # Exceptions in duplicity often have path names in them, which if they are
+    # non-ascii will cause a UnicodeDecodeError when implicitly decoding to
+    # unicode.  So we decode manually, using the filesystem encoding.
+    # 99.99% of the time, this will be a fine encoding to use.
+    return ufn(str(e))
+
 def maybe_ignore_errors(fn):
     """
     Execute fn. If the global configuration setting ignore_errors is
@@ -85,7 +91,7 @@
     except Exception as e:
         if globals.ignore_errors:
             log.Warn(_("IGNORED_ERROR: Warning: ignoring error as requested: %s: %s")
-                     % (e.__class__.__name__, str(e)))
+                     % (e.__class__.__name__, uexc(e)))
             return None
         else:
             raise

_______________________________________________
Mailing list: https://launchpad.net/~duplicity-team
Post to     : [email protected]
Unsubscribe : https://launchpad.net/~duplicity-team
More help   : https://help.launchpad.net/ListHelp

Reply via email to