← Back to team overview

duplicity-team team mailing list archive

[Merge] lp:~havard/duplicity/jottacloudbackend into lp:duplicity

 

Håvard Gulldahl has proposed merging lp:~havard/duplicity/jottacloudbackend into lp:duplicity.

Commit message:
Add JottaCloudBackend

Requested reviews:
  duplicity-team (duplicity-team)

For more details, see:
https://code.launchpad.net/~havard/duplicity/jottacloudbackend/+merge/304955

Hello everyone. 

This adds support for a new backend, jottacloud.com, using the scheme `jottacloud:/<folder>`.

Support is added through a reverse-engineered library, `jottalib`: http://github.com/havardgulldahl/jottalib

Please consider for inclusion. Any comments or review would be greatly appreciated.

Thanks! 
-- 
Your team duplicity-team is requested to review the proposed merge of lp:~havard/duplicity/jottacloudbackend into lp:duplicity.
=== added file 'duplicity/backends/jottacloudbackend.py'
--- duplicity/backends/jottacloudbackend.py	1970-01-01 00:00:00 +0000
+++ duplicity/backends/jottacloudbackend.py	2016-09-05 22:35:21 +0000
@@ -0,0 +1,162 @@
+# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; encoding:utf-8 -*-
+#
+# Copyright 2014 Håvard Gulldahl
+#
+# in part based on dpbxbackend.py:
+# Copyright 2013 jno <jno@xxxxxxxxx>
+#
+# This file is part of duplicity.
+#
+# Duplicity is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the
+# Free Software Foundation; either version 2 of the License, or (at your
+# option) any later version.
+#
+# Duplicity is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with duplicity; if not, write to the Free Software Foundation,
+# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+
+# stdlib
+import posixpath
+import locale
+import logging
+
+# import duplicity stuff # version 0.6
+import duplicity.backend
+from duplicity import log
+from duplicity.errors import BackendException
+
+def get_jotta_device(jfs):
+    jottadev = None
+    for j in jfs.devices: # find Jotta/Shared folder
+        if j.name == 'Jotta':
+            jottadev = j
+    return jottadev
+
+
+def get_root_dir(jfs):
+    jottadev = get_jotta_device(jfs)
+    root_dir = jottadev.mountPoints['Archive']
+    return root_dir
+
+
+def set_jottalib_logging_level(log_level):
+    logger = logging.getLogger('jottalib')
+    logger.setLevel(getattr(logging, log_level))
+
+
+def set_jottalib_log_handlers(handlers):
+    logger = logging.getLogger('jottalib')
+    for handler in handlers:
+        logger.addHandler(handler)
+
+
+def get_duplicity_log_level():
+    """ Get the current duplicity log level as a stdlib-compatible logging level"""
+    duplicity_log_level = log.LevelName(log.getverbosity())
+
+    # notice is a duplicity-specific logging level not supported by stdlib
+    if duplicity_log_level == 'NOTICE':
+        duplicity_log_level = 'INFO'
+
+    return duplicity_log_level
+
+
+class JottaCloudBackend(duplicity.backend.Backend):
+    """Connect to remote store using JottaCloud API"""
+
+    def __init__(self, parsed_url):
+        duplicity.backend.Backend.__init__(self, parsed_url)
+
+        # Import JottaCloud libraries.
+        try:
+            from jottalib import JFS
+        except ImportError:
+            raise BackendException('JottaCloud backend requires jottalib'
+                                   ' (see https://pypi.python.org/pypi/jottalib).')
+
+        # Set jottalib loggers to the same verbosity as duplicity
+        duplicity_log_level = get_duplicity_log_level()
+        set_jottalib_logging_level(duplicity_log_level)
+
+        # Ensure jottalib and duplicity log to the same handlers
+        set_jottalib_log_handlers(log._logger.handlers)
+
+        # Will fetch jottacloud auth from environment or .netrc
+        self.client = JFS.JFS()
+
+        self.folder = self.get_or_create_directory(parsed_url.path.lstrip('/'))
+        log.Debug("Jottacloud folder for duplicity: %r" % self.folder.path)
+
+
+    def get_or_create_directory(self, directory_name):
+        from jottalib.JFS import JFSNotFoundError
+        root_directory = get_root_dir(self.client)
+        full_path = posixpath.join(root_directory.path, directory_name)
+        try:
+            return self.client.getObject(full_path)
+        except JFSNotFoundError:
+            return root_directory.mkdir(directory_name)
+
+
+    def _put(self, source_path, remote_filename):
+        # - Upload one file
+        # - Retried if an exception is thrown
+        resp = self.folder.up(source_path.open(), remote_filename)
+        log.Debug('jottacloud.put(%s,%s): %s' % (source_path.name, remote_filename, resp))
+
+
+    def _get(self, remote_filename, local_path):
+        # - Get one file
+        # - Retried if an exception is thrown
+        remote_file = self.client.getObject(posixpath.join(self.folder.path, remote_filename))
+        log.Debug('jottacloud.get(%s,%s): %s' % (remote_filename, local_path.name, remote_file))
+        with open(local_path.name, 'wb') as to_file:
+            for chunk in remote_file.stream():
+                to_file.write(chunk)
+
+
+    def _list(self):
+        # - List all files in the backend
+        # - Return a list of filenames
+        # - Retried if an exception is thrown
+        return list([f.name for f in self.folder.files()
+                     if not f.is_deleted() and f.state != 'INCOMPLETE'])
+
+
+    def _delete(self, filename):
+        # - Delete one file
+        # - Retried if an exception is thrown
+        remote_path = posixpath.join(self.folder.path, filename)
+        remote_file = self.client.getObject(remote_path)
+        log.Debug('jottacloud.delete deleting: %s (%s)' % (remote_file, type(remote_file)))
+        remote_file.delete()
+
+
+    def _query(self, filename):
+        """Get size of filename"""
+        #  - Query metadata of one file
+        #  - Return a dict with a 'size' key, and a file size value (-1 for not found)
+        #  - Retried if an exception is thrown
+        log.Info('Querying size of %s' % filename)
+        from jottalib.JFS import JFSNotFoundError, JFSIncompleteFile
+        remote_path = posixpath.join(self.folder.path, filename)
+        try:
+            remote_file = self.client.getObject(remote_path)
+        except JFSNotFoundError:
+            return {'size': -1}
+        return {
+            'size': remote_file.size,
+        }
+
+    def _close(self):
+        # - If your backend needs to clean up after itself, do that here.
+        pass
+
+duplicity.backend.register_backend("jottacloud", JottaCloudBackend)
+""" jottacloud is a Norwegian backup company """


Follow ups