summaryrefslogtreecommitdiff
path: root/obnam/store.py
blob: 3ca6ba55d25b5abdaa4ba37c572f354a255c4117 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# Copyright (C) 2008  Lars Wirzenius <liw@iki.fi>
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.


"""Abstraction for storing backup data, for Obnam."""


import logging
import os

import obnam


class ObjectNotFoundInStore(obnam.exception.ObnamException):

    def __init__(self, id):
        self._msg = "Object %s not found in store" % id


class Store:

    def __init__(self, context):
        self._context = context
        self._host = None

    def close(self):
        """Close connection to the store.
        
        You must not use this store instance for anything after
        closing it.
        
        """
        
        self._context.be.close()

    def get_host_block(self):
        """Return current host block, or None if one is not known.
        
        You must call fetch_host_block to fetch the host block first.
        
        """

        return self._host

    def fetch_host_block(self):
        """Fetch host block from store, if one exists.
        
        If a host block does not exist, it is not an error. A new
        host block is then created.
        
        """
        
        if not self._host:
            host_block = obnam.io.get_host_block(self._context)
            if host_block:
                self._host = obnam.obj.create_host_from_block(host_block)
            else:
                id = self._context.config.get("backup", "host-id")
                self._host = obnam.obj.HostBlockObject(host_id=id)
        return self._host


    def load_maps(self):
        """Load non-content map blocks."""
        ids = self._host.get_map_block_ids()
        logging.info("Decoding %d mapping blocks" % len(ids))
        obnam.io.load_maps(self._context, self._context.map, ids)

    def load_content_maps(self):
        """Load content map blocks."""
        ids = self._host.get_contmap_block_ids()
        logging.info("Decoding %d content mapping blocks" % len(ids))
        obnam.io.load_maps(self._context, self._context.contmap, ids)

    def _update_map_helper(self, map):
        """Create new mapping blocks of a given kind, and upload them.
        
        Return list of block ids for the new blocks.

        """

        if obnam.map.get_new(map):
            id = self._context.be.generate_block_id()
            logging.debug("Creating mapping block %s" % id)
            block = obnam.map.encode_new_to_block(map, id)
            self._context.be.upload_block(id, block, True)
            return [id]
        else:
            logging.debug("No new mappings, no new mapping block")
            return []

    def update_maps(self):
        """Create new object mapping blocks and upload them."""
        logging.debug("Creating new mapping block for normal mappings")
        return self._update_map_helper(self._context.map)

    def update_content_maps(self):
        """Create new content object mapping blocks and upload them."""
        logging.debug("Creating new mapping block for content mappings")
        return self._update_map_helper(self._context.contmap)

    def commit_host_block(self, new_generations):
        """Commit the current host block to the store.
        
        If no host block exists, create one. If one already exists,
        update it with new info.
        
        NOTE that after this operation the host block has changed,
        and you need to call get_host_block again.
        
        """

        obnam.io.flush_all_object_queues(self._context)
    
        logging.info("Creating new mapping blocks")
        host = self.get_host_block()
        map_ids = host.get_map_block_ids() + self.update_maps()
        contmap_ids = (host.get_contmap_block_ids() + 
                       self.update_content_maps())
        
        logging.info("Creating new host block")
        gen_ids = (host.get_generation_ids() + 
                   [gen.get_id() for gen in new_generations])
        host2 = obnam.obj.HostBlockObject(host_id=host.get_id(), 
                                          gen_ids=gen_ids, 
                                          map_block_ids=map_ids,
                                          contmap_block_ids=contmap_ids)
        obnam.io.upload_host_block(self._context, host2.encode())

        self._host = host2

    def queue_object(self, object):
        """Queue an object for upload to the store.
        
        It won't necessarily be committed (i.e., uploaded, etc) until
        you call commit_host_block. Until it is committed, you may not
        call get_object on it.
        
        """
        
        obnam.io.enqueue_object(self._context, self._context.oq,
                                self._context.map, object.get_id(), 
                                object.encode(), True)

    def queue_objects(self, objects):
        """Queue a list of objects for upload to the store.
        
        See queue_object for information about what queuing means.
        
        """
        
        for object in objects:
            self.queue_object(object)

    def get_object(self, id):
        """Get an object from the store.
        
        If the object cannot be found, raise an exception.
        
        """

        object = obnam.io.get_object(self._context, id)
        if object:
            return object
        raise ObjectNotFoundInStore(id)

    def parse_pathname(self, pathname):
        """Return list of components in pathname."""

        list = []
        while pathname:
            dirname = os.path.dirname(pathname)
            basename = os.path.basename(pathname)
            if basename:
                list.insert(0, basename)
            elif dirname == os.sep:
                list.insert(0, "/")
                dirname = ""
            pathname = dirname

        return list

    def _lookup_dir_from_refs(self, dirrefs, parts):
        for ref in dirrefs:
            dir = self.get_object(ref)
            if dir.get_name() == parts[0]:
                parts = parts[1:]
                if parts:
                    dirrefs = dir.get_dirrefs()
                    return self._lookup_dir_from_refs(dirrefs, parts)
                else:
                    return dir
        return None

    def lookup_dir(self, generation, pathname):
        """Return a DirObject that corresponds to pathname in a generation.
        
        Look up the directory in the generation. If it does not exist,
        return None.
        
        """

        dirrefs = generation.get_dirrefs()
        parts = self.parse_pathname(pathname)
        
        for dirref in dirrefs:
            dir = self.get_object(dirref)
            name = dir.get_name()
            if name == pathname:
                return dir
            else:
                if not name.endswith(os.sep):
                    name += os.sep
                if pathname.startswith(name):
                    subpath = pathname[len(name):]
                    subparts = self.parse_pathname(subpath)
                    return self._lookup_dir_from_refs(dir.get_dirrefs(),
                                                      subparts)

        return self._lookup_dir_from_refs(dirrefs, parts)

    def lookup_file(self, generation, pathname):
        """Find a non-directory thingy in a generation.
        
        Return a FILE component that corresponds to the filesystem entity
        in question. If not found, return None.
        
        """

        dirname = os.path.dirname(pathname)
        if dirname:
            dir = self.lookup_dir(generation, dirname)
            if dir:
                basename = os.path.basename(pathname)
                for id in dir.get_filegrouprefs():
                    fg = self.get_object(id)
                    file = fg.get_file(basename)
                    if file:
                        return file
        
        return None