duplicity-team team mailing list archive
-
duplicity-team team
-
Mailing list archive
-
Message #03811
[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