← Back to team overview

duplicity-team team mailing list archive

[Merge] lp:~mterry/duplicity/backend-log-codes into lp:duplicity

 

Michael Terry has proposed merging lp:~mterry/duplicity/backend-log-codes into lp:duplicity.

Requested reviews:
  duplicity-team (duplicity-team)
Related bugs:
  #578663 Use log codes for common backend errors
  https://bugs.launchpad.net/bugs/578663


I've added some new error codes (and took the liberty of reserving a chunk of numbers for future backend codes):

backend_error = 50
backend_permission_denied = 51
backend_not_found = 52
backend_no_space = 53

I've converted two of the backends to use the new scheme, rather than throwing up BackendExceptions: localbackend and giobackend.  Converting all backends would be nice, but I didn't consider it necessary.

Using a proper error message for these errors is better for the user because a simple error message is more friendly than an exception, and it's better for wrappers because they can display an intelligent message or change behavior based on the error.

The schema I came up with for the machine readable bit is:
ERROR <num> <operation> <file1> <file2>

Where num is the error code; operation is 'list', 'delete', 'put', or 'get'; and file2 is only provided for put and get.  I have working code that consumes all this in a Deja Dup branch.

I've tested all four error paths in both backends.  Seems to work fine.
-- 
https://code.launchpad.net/~mterry/duplicity/backend-log-codes/+merge/25373
Your team duplicity-team is requested to review the proposed merge of lp:~mterry/duplicity/backend-log-codes into lp:duplicity.
=== modified file 'duplicity/backends/giobackend.py'
--- duplicity/backends/giobackend.py	2010-02-09 01:00:37 +0000
+++ duplicity/backends/giobackend.py	2010-05-15 14:47:22 +0000
@@ -29,6 +29,7 @@
 import duplicity.backend
 from duplicity import log
 from duplicity import globals
+from duplicity import util
 from duplicity.errors import *
 from duplicity.util import exception_traceback 
 
@@ -88,10 +89,24 @@
                                % str(e), log.ErrorCode.connection_failed)
         loop.quit()
 
+    def handle_error(self, e, op, file1=None, file2=None):
+        code = log.ErrorCode.backend_error
+        if isinstance(e, gio.Error):
+            if e.code == gio.ERROR_PERMISSION_DENIED:
+                code = log.ErrorCode.backend_permission_denied
+            elif e.code == gio.ERROR_NOT_FOUND:
+                code = log.ErrorCode.backend_not_found
+            elif e.code == gio.ERROR_NO_SPACE:
+                code = log.ErrorCode.backend_no_space
+        extra = ' '.join([util.escape(x) for x in [file1, file2] if x])
+        extra = ' '.join([op, extra])
+        log.FatalError(str(e), code, extra)
+
     def copy_progress(self, *args, **kwargs):
         pass
 
-    def copy_file(self, source, target):
+    def copy_file(self, op, source, target):
+        exc = None
         for n in range(1, globals.num_retries+1):
             log.Info(_("Writing %s") % target.get_parse_name())
             try:
@@ -103,8 +118,8 @@
                         % (target.get_parse_name(), n, e.__class__.__name__, str(e)))
                 log.Debug("Backtrace of previous error: %s"
                           % exception_traceback())
-        raise BackendException(_("Could not copy %s to %s") % (source.get_parse_name(),
-                                                               target.get_parse_name()))
+                exc = e
+        self.handle_error(exc, op, source.get_parse_name(), target.get_parse_name())
 
     def put(self, source_path, remote_filename = None):
         """Copy file to remote"""
@@ -112,19 +127,22 @@
             remote_filename = source_path.get_filename()
         source_file = gio.File(path=source_path.name)
         target_file = self.remote_file.get_child_for_display_name(remote_filename)
-        self.copy_file(source_file, target_file)
+        self.copy_file('put', source_file, target_file)
 
     def get(self, filename, local_path):
         """Get file and put in local_path (Path object)"""
         source_file = self.remote_file.get_child_for_display_name(filename)
         target_file = gio.File(path=local_path.name)
-        self.copy_file(source_file, target_file)
+        self.copy_file('get', source_file, target_file)
         local_path.setdata()
 
     def list(self):
         """List files in that directory"""
-        enum = self.remote_file.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
-                                                   gio.FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
+        try:
+            enum = self.remote_file.enumerate_children(gio.FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
+                                                       gio.FILE_QUERY_INFO_NOFOLLOW_SYMLINKS)
+        except Exception, e:
+            self.handle_error(e, 'list', self.remote_file.get_parse_name())
         files = []
         try:
             info = enum.next_file()
@@ -133,7 +151,7 @@
                 info = enum.next_file()
             return files
         except Exception, e:
-            raise BackendException(str(e))
+            self.handle_error(e, 'list')
 
     def delete(self, filename_list):
         """Delete all files in filename list"""
@@ -142,4 +160,4 @@
                 for filename in filename_list:
                         self.remote_file.get_child_for_display_name(filename).delete()
         except Exception, e:
-            raise BackendException(str(e))
+            self.handle_error(e, 'delete', self.remote_file.get_child_for_display_name(filename).get_parse_name())

=== modified file 'duplicity/backends/localbackend.py'
--- duplicity/backends/localbackend.py	2010-02-09 01:00:37 +0000
+++ duplicity/backends/localbackend.py	2010-05-15 14:47:22 +0000
@@ -21,10 +21,12 @@
 
 import os
 import types
+import errno
 
 import duplicity.backend
 from duplicity import log
 from duplicity import path
+from duplicity import util
 from duplicity.errors import *
 
 class LocalBackend(duplicity.backend.Backend):
@@ -41,6 +43,19 @@
             raise BackendException( "Bad file:// path syntax." )
         self.remote_pathdir = path.Path(parsed_url.path[2:])
 
+    def handle_error(self, e, op, file1=None, file2=None):
+        code = log.ErrorCode.backend_error
+        if hasattr(e, 'errno'):
+            if e.errno == errno.EACCES:
+                code = log.ErrorCode.backend_permission_denied
+            elif e.errno == errno.ENOENT:
+                code = log.ErrorCode.backend_not_found
+            elif e.errno == errno.ENOSPC:
+                code = log.ErrorCode.backend_no_space
+        extra = ' '.join([util.escape(x) for x in [file1, file2] if x])
+        extra = ' '.join([op, extra])
+        log.FatalError(str(e), code, extra)
+
     def put(self, source_path, remote_filename = None, rename = None):
         """If rename is set, try that first, copying if doesn't work"""
         if not remote_filename:
@@ -52,14 +67,22 @@
                 source_path.rename(target_path)
             except OSError:
                 pass
+            except Exception, e:
+                handle_error(e, 'put', source_path.name, target_path.name)
             else:
                 return
-        target_path.writefileobj(source_path.open("rb"))
+        try:
+            target_path.writefileobj(source_path.open("rb"))
+        except Exception, e:
+            self.handle_error(e, 'put', source_path.name, target_path.name)
 
     def get(self, filename, local_path):
         """Get file and put in local_path (Path object)"""
         source_path = self.remote_pathdir.append(filename)
-        local_path.writefileobj(source_path.open("rb"))
+        try:
+            local_path.writefileobj(source_path.open("rb"))
+        except Exception, e:
+            self.handle_error(e, 'get', source_path.name, local_path.name)
 
     def list(self):
         """List files in that directory"""
@@ -67,7 +90,10 @@
                 os.makedirs(self.remote_pathdir.base)
         except:
                 pass
-        return self.remote_pathdir.listdir()
+        try:
+            return self.remote_pathdir.listdir()
+        except Exception, e:
+            self.handle_error(e, 'list', self.remote_pathdir.name)
 
     def delete(self, filename_list):
         """Delete all files in filename list"""
@@ -75,8 +101,8 @@
         for filename in filename_list:
             try:
                 self.remote_pathdir.append(filename).delete()
-            except OSError, e:
-                raise BackendException(str(e))
+            except Exception, e:
+                self.handle_error(e, 'delete', self.remote_pathdir.append(filename).name)
 
 
 duplicity.backend.register_backend("file", LocalBackend)

=== modified file 'duplicity/log.py'
--- duplicity/log.py	2010-02-09 01:00:37 +0000
+++ duplicity/log.py	2010-05-15 14:47:22 +0000
@@ -181,6 +181,13 @@
     gio_not_available = 40
     par2_missing = 41
     source_dir_mismatch = 42
+
+    # 50->69 reserved for backend errors
+    backend_error = 50
+    backend_permission_denied = 51
+    backend_not_found = 52
+    backend_no_space = 53
+
     # Reserve 255 because it is used as an error code for gksu
 
 def FatalError(s, code, extra=None):


Follow ups