← Back to team overview

duplicity-team team mailing list archive

[Merge] lp:~mterry/duplicity/encode-exceptions into lp:duplicity

 

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


Follow ups