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