1# -*- Mode:Python; indent-tabs-mode:nil; tab-width:4; -*- 2# 3# Copyright 2014 Havard Gulldahl 4# 5# in part based on dpbxbackend.py: 6# Copyright 2013 jno <jno@pisem.net> 7# 8# This file is part of duplicity. 9# 10# Duplicity is free software; you can redistribute it and/or modify it 11# under the terms of the GNU General Public License as published by the 12# Free Software Foundation; either version 2 of the License, or (at your 13# option) any later version. 14# 15# Duplicity is distributed in the hope that it will be useful, but 16# WITHOUT ANY WARRANTY; without even the implied warranty of 17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 18# General Public License for more details. 19# 20# You should have received a copy of the GNU General Public License 21# along with duplicity; if not, write to the Free Software Foundation, 22# Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 23 24# stdlib 25import logging 26import posixpath 27 28# import duplicity stuff 29from duplicity import log 30from duplicity.errors import BackendException 31import duplicity.backend 32 33 34def get_jotta_device(jfs): 35 jottadev = None 36 for j in jfs.devices: # find Jotta/Shared folder 37 if j.name == u'Jotta': 38 jottadev = j 39 return jottadev 40 41 42def get_root_dir(jfs): 43 jottadev = get_jotta_device(jfs) 44 root_dir = jottadev.mountPoints[u'Archive'] 45 return root_dir 46 47 48def set_jottalib_logging_level(log_level): 49 logger = logging.getLogger(u'jottalib') 50 logger.setLevel(getattr(logging, log_level)) 51 52 53def set_jottalib_log_handlers(handlers): 54 logger = logging.getLogger(u'jottalib') 55 for handler in handlers: 56 logger.addHandler(handler) 57 58 59def get_duplicity_log_level(): 60 u""" Get the current duplicity log level as a stdlib-compatible logging level""" 61 duplicity_log_level = log.LevelName(log.getverbosity()) 62 63 # notice is a duplicity-specific logging level not supported by stdlib 64 if duplicity_log_level == u'NOTICE': 65 duplicity_log_level = u'INFO' 66 67 return duplicity_log_level 68 69 70class JottaCloudBackend(duplicity.backend.Backend): 71 u"""Connect to remote store using JottaCloud API""" 72 73 def __init__(self, parsed_url): 74 duplicity.backend.Backend.__init__(self, parsed_url) 75 76 # Import JottaCloud libraries. 77 try: 78 from jottalib import JFS 79 from jottalib.JFS import JFSNotFoundError, JFSIncompleteFile 80 except ImportError: 81 raise BackendException(u'JottaCloud backend requires jottalib' 82 u' (see https://pypi.python.org/pypi/jottalib).') 83 84 # Set jottalib loggers to the same verbosity as duplicity 85 duplicity_log_level = get_duplicity_log_level() 86 set_jottalib_logging_level(duplicity_log_level) 87 88 # Ensure jottalib and duplicity log to the same handlers 89 set_jottalib_log_handlers(log._logger.handlers) 90 91 # Will fetch jottacloud auth from environment or .netrc 92 self.client = JFS.JFS() 93 94 self.folder = self.get_or_create_directory(parsed_url.path.lstrip(u'/')) 95 log.Debug(u"Jottacloud folder for duplicity: %r" % self.folder.path) 96 97 def get_or_create_directory(self, directory_name): 98 root_directory = get_root_dir(self.client) 99 full_path = posixpath.join(root_directory.path, directory_name) 100 try: 101 return self.client.getObject(full_path) 102 except JFSNotFoundError: 103 return root_directory.mkdir(directory_name) 104 105 def _put(self, source_path, remote_filename): 106 # - Upload one file 107 # - Retried if an exception is thrown 108 resp = self.folder.up(source_path.open(), remote_filename) 109 log.Debug(u'jottacloud.put(%s,%s): %s' % (source_path.name, remote_filename, resp)) 110 111 def _get(self, remote_filename, local_path): 112 # - Get one file 113 # - Retried if an exception is thrown 114 remote_file = self.client.getObject(posixpath.join(self.folder.path, remote_filename)) 115 log.Debug(u'jottacloud.get(%s,%s): %s' % (remote_filename, local_path.name, remote_file)) 116 with open(local_path.name, u'wb') as to_file: 117 for chunk in remote_file.stream(): 118 to_file.write(chunk) 119 120 def _list(self): 121 # - List all files in the backend 122 # - Return a list of filenames 123 # - Retried if an exception is thrown 124 return list([f.name for f in self.folder.files() 125 if not f.is_deleted() and f.state != u'INCOMPLETE']) 126 127 def _delete(self, filename): 128 # - Delete one file 129 # - Retried if an exception is thrown 130 remote_path = posixpath.join(self.folder.path, filename) 131 remote_file = self.client.getObject(remote_path) 132 log.Debug(u'jottacloud.delete deleting: %s (%s)' % (remote_file, type(remote_file))) 133 remote_file.delete() 134 135 def _query(self, filename): 136 u"""Get size of filename""" 137 # - Query metadata of one file 138 # - Return a dict with a 'size' key, and a file size value (-1 for not found) 139 # - Retried if an exception is thrown 140 log.Info(u'Querying size of %s' % filename) 141 remote_path = posixpath.join(self.folder.path, filename) 142 try: 143 remote_file = self.client.getObject(remote_path) 144 except JFSNotFoundError: 145 return {u'size': -1} 146 return { 147 u'size': remote_file.size, 148 } 149 150 def _close(self): 151 # - If your backend needs to clean up after itself, do that here. 152 pass 153 154 155duplicity.backend.register_backend(u"jottacloud", JottaCloudBackend) 156u""" jottacloud is a Norwegian backup company """ 157