/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/store.py

  • Committer: Robert Collins
  • Date: 2005-09-29 02:55:34 UTC
  • mfrom: (1185.1.47)
  • mto: This revision was merged to the branch mainline in revision 1397.
  • Revision ID: robertc@robertcollins.net-20050929025534-1782933743abbfd5
update with integration

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 by Canonical Development Ltd
 
2
 
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
 
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
 
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
"""
 
18
Stores are the main data-storage mechanism for Bazaar-NG.
 
19
 
 
20
A store is a simple write-once container indexed by a universally
 
21
unique ID.
 
22
"""
 
23
 
 
24
import errno
 
25
import gzip
 
26
import os
 
27
import tempfile
 
28
import types
 
29
from stat import ST_SIZE
 
30
from StringIO import StringIO
 
31
 
 
32
from bzrlib.errors import BzrError, UnlistableStore
 
33
from bzrlib.trace import mutter
 
34
import bzrlib.ui
 
35
import bzrlib.osutils as osutils
 
36
#circular import
 
37
#from bzrlib.remotebranch import get_url
 
38
import urllib2
 
39
 
 
40
 
 
41
######################################################################
 
42
# stores
 
43
 
 
44
class StoreError(Exception):
 
45
    pass
 
46
 
 
47
 
 
48
class Store(object):
 
49
    """An abstract store that holds files indexed by unique names.
 
50
 
 
51
    Files can be added, but not modified once they are in.  Typically
 
52
    the hash is used as the name, or something else known to be unique,
 
53
    such as a UUID.
 
54
 
 
55
    >>> st = ImmutableScratchStore()
 
56
 
 
57
    >>> st.add(StringIO('hello'), 'aa')
 
58
    >>> 'aa' in st
 
59
    True
 
60
    >>> 'foo' in st
 
61
    False
 
62
 
 
63
    You are not allowed to add an id that is already present.
 
64
 
 
65
    Entries can be retrieved as files, which may then be read.
 
66
 
 
67
    >>> st.add(StringIO('goodbye'), '123123')
 
68
    >>> st['123123'].read()
 
69
    'goodbye'
 
70
    """
 
71
 
 
72
    def total_size(self):
 
73
        """Return (count, bytes)
 
74
 
 
75
        This is the (compressed) size stored on disk, not the size of
 
76
        the content."""
 
77
        total = 0
 
78
        count = 0
 
79
        for fid in self:
 
80
            count += 1
 
81
            total += self._item_size(fid)
 
82
        return count, total
 
83
 
 
84
 
 
85
class ImmutableStore(Store):
 
86
    """Store that stores files on disk.
 
87
 
 
88
    TODO: Atomic add by writing to a temporary file and renaming.
 
89
    TODO: Guard against the same thing being stored twice, compressed and
 
90
          uncompressed during copy_multi_immutable - the window is for a
 
91
          matching store with some crack code that lets it offer a 
 
92
          non gz FOO and then a fz FOO.
 
93
 
 
94
    In bzr 0.0.5 and earlier, files within the store were marked
 
95
    readonly on disk.  This is no longer done but existing stores need
 
96
    to be accomodated.
 
97
    """
 
98
 
 
99
    def __init__(self, basedir):
 
100
        super(ImmutableStore, self).__init__()
 
101
        self._basedir = basedir
 
102
 
 
103
    def _path(self, entry_id):
 
104
        if not isinstance(entry_id, basestring):
 
105
            raise TypeError(type(entry_id))
 
106
        if '\\' in entry_id or '/' in entry_id:
 
107
            raise ValueError("invalid store id %r" % entry_id)
 
108
        return os.path.join(self._basedir, entry_id)
 
109
 
 
110
    def __repr__(self):
 
111
        return "%s(%r)" % (self.__class__.__name__, self._basedir)
 
112
 
 
113
    def add(self, f, fileid, compressed=True):
 
114
        """Add contents of a file into the store.
 
115
 
 
116
        f -- An open file, or file-like object."""
 
117
        # FIXME: Only works on files that will fit in memory
 
118
        
 
119
        from bzrlib.atomicfile import AtomicFile
 
120
        
 
121
        mutter("add store entry %r" % (fileid))
 
122
        if isinstance(f, types.StringTypes):
 
123
            content = f
 
124
        else:
 
125
            content = f.read()
 
126
            
 
127
        p = self._path(fileid)
 
128
        if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK):
 
129
            raise BzrError("store %r already contains id %r" % (self._basedir, fileid))
 
130
 
 
131
        fn = p
 
132
        if compressed:
 
133
            fn = fn + '.gz'
 
134
            
 
135
        af = AtomicFile(fn, 'wb')
 
136
        try:
 
137
            if compressed:
 
138
                gf = gzip.GzipFile(mode='wb', fileobj=af)
 
139
                gf.write(content)
 
140
                gf.close()
 
141
            else:
 
142
                af.write(content)
 
143
            af.commit()
 
144
        finally:
 
145
            af.close()
 
146
 
 
147
 
 
148
    def copy_multi(self, other, ids, permit_failure=False):
 
149
        """Copy texts for ids from other into self.
 
150
 
 
151
        If an id is present in self, it is skipped.
 
152
 
 
153
        Returns (count_copied, failed), where failed is a collection of ids
 
154
        that could not be copied.
 
155
        """
 
156
        pb = bzrlib.ui.ui_factory.progress_bar()
 
157
        
 
158
        pb.update('preparing to copy')
 
159
        to_copy = [id for id in ids if id not in self]
 
160
        if isinstance(other, ImmutableStore):
 
161
            return self.copy_multi_immutable(other, to_copy, pb, 
 
162
                                             permit_failure=permit_failure)
 
163
        count = 0
 
164
        failed = set()
 
165
        for id in to_copy:
 
166
            count += 1
 
167
            pb.update('copy', count, len(to_copy))
 
168
            if not permit_failure:
 
169
                self.add(other[id], id)
 
170
            else:
 
171
                try:
 
172
                    entry = other[id]
 
173
                except KeyError:
 
174
                    failed.add(id)
 
175
                    continue
 
176
                self.add(entry, id)
 
177
                
 
178
        if not permit_failure:
 
179
            assert count == len(to_copy)
 
180
        pb.clear()
 
181
        return count, failed
 
182
 
 
183
    def copy_multi_immutable(self, other, to_copy, pb, permit_failure=False):
 
184
        count = 0
 
185
        failed = set()
 
186
        for id in to_copy:
 
187
            p = self._path(id)
 
188
            other_p = other._path(id)
 
189
            try:
 
190
                osutils.link_or_copy(other_p, p)
 
191
            except (IOError, OSError), e:
 
192
                if e.errno == errno.ENOENT:
 
193
                    if not permit_failure:
 
194
                        osutils.link_or_copy(other_p+".gz", p+".gz")
 
195
                    else:
 
196
                        try:
 
197
                            osutils.link_or_copy(other_p+".gz", p+".gz")
 
198
                        except IOError, e:
 
199
                            if e.errno == errno.ENOENT:
 
200
                                failed.add(id)
 
201
                            else:
 
202
                                raise
 
203
                else:
 
204
                    raise
 
205
            
 
206
            count += 1
 
207
            pb.update('copy', count, len(to_copy))
 
208
        assert count == len(to_copy)
 
209
        pb.clear()
 
210
        return count, failed
 
211
 
 
212
    def __contains__(self, fileid):
 
213
        """"""
 
214
        p = self._path(fileid)
 
215
        return (os.access(p, os.R_OK)
 
216
                or os.access(p + '.gz', os.R_OK))
 
217
 
 
218
    def _item_size(self, fid):
 
219
        p = self._path(fid)
 
220
        try:
 
221
            return os.stat(p)[ST_SIZE]
 
222
        except OSError:
 
223
            return os.stat(p + '.gz')[ST_SIZE]
 
224
 
 
225
    def __iter__(self):
 
226
        for f in os.listdir(self._basedir):
 
227
            if f[-3:] == '.gz':
 
228
                # TODO: case-insensitive?
 
229
                yield f[:-3]
 
230
            else:
 
231
                yield f
 
232
 
 
233
    def __len__(self):
 
234
        return len(os.listdir(self._basedir))
 
235
 
 
236
    def __getitem__(self, fileid):
 
237
        """Returns a file reading from a particular entry."""
 
238
        p = self._path(fileid)
 
239
        try:
 
240
            return gzip.GzipFile(p + '.gz', 'rb')
 
241
        except IOError, e:
 
242
            if e.errno != errno.ENOENT:
 
243
                raise
 
244
 
 
245
        try:
 
246
            return file(p, 'rb')
 
247
        except IOError, e:
 
248
            if e.errno != errno.ENOENT:
 
249
                raise
 
250
 
 
251
        raise KeyError(fileid)
 
252
 
 
253
 
 
254
class ImmutableScratchStore(ImmutableStore):
 
255
    """Self-destructing test subclass of ImmutableStore.
 
256
 
 
257
    The Store only exists for the lifetime of the Python object.
 
258
 Obviously you should not put anything precious in it.
 
259
    """
 
260
    def __init__(self):
 
261
        super(ImmutableScratchStore, self).__init__(tempfile.mkdtemp())
 
262
 
 
263
    def __del__(self):
 
264
        for f in os.listdir(self._basedir):
 
265
            fpath = os.path.join(self._basedir, f)
 
266
            # needed on windows, and maybe some other filesystems
 
267
            os.chmod(fpath, 0600)
 
268
            os.remove(fpath)
 
269
        os.rmdir(self._basedir)
 
270
        mutter("%r destroyed" % self)
 
271
 
 
272
 
 
273
class ImmutableMemoryStore(Store):
 
274
    """A memory only store."""
 
275
 
 
276
    def __init__(self):
 
277
        super(ImmutableMemoryStore, self).__init__()
 
278
        self._contents = {}
 
279
 
 
280
    def add(self, stream, fileid, compressed=True):
 
281
        if self._contents.has_key(fileid):
 
282
            raise StoreError("fileid %s already in the store" % fileid)
 
283
        self._contents[fileid] = stream.read()
 
284
 
 
285
    def __getitem__(self, fileid):
 
286
        """Returns a file reading from a particular entry."""
 
287
        if not self._contents.has_key(fileid):
 
288
            raise IndexError
 
289
        return StringIO(self._contents[fileid])
 
290
 
 
291
    def _item_size(self, fileid):
 
292
        return len(self._contents[fileid])
 
293
 
 
294
    def __iter__(self):
 
295
        return iter(self._contents.keys())
 
296
 
 
297
 
 
298
class RemoteStore(object):
 
299
 
 
300
    def __init__(self, baseurl):
 
301
        self._baseurl = baseurl
 
302
 
 
303
    def _path(self, name):
 
304
        if '/' in name:
 
305
            raise ValueError('invalid store id', name)
 
306
        return self._baseurl + '/' + name
 
307
        
 
308
    def __getitem__(self, fileid):
 
309
        # circular import.
 
310
        from bzrlib.remotebranch import get_url
 
311
        p = self._path(fileid)
 
312
        try:
 
313
            return get_url(p, compressed=True)
 
314
        except urllib2.URLError:
 
315
            pass
 
316
        try:
 
317
            return get_url(p, compressed=False)
 
318
        except urllib2.URLError:
 
319
            raise KeyError(fileid)
 
320
 
 
321
 
 
322
class CachedStore:
 
323
    """A store that caches data locally, to avoid repeated downloads.
 
324
    The precacache method should be used to avoid server round-trips for
 
325
    every piece of data.
 
326
    """
 
327
 
 
328
    def __init__(self, store, cache_dir):
 
329
        self.source_store = store
 
330
        self.cache_store = ImmutableStore(cache_dir)
 
331
 
 
332
    def __getitem__(self, id):
 
333
        mutter("Cache add %s" % id)
 
334
        if id not in self.cache_store:
 
335
            self.cache_store.add(self.source_store[id], id)
 
336
        return self.cache_store[id]
 
337
 
 
338
    def prefetch(self, ids):
 
339
        """Copy a series of ids into the cache, before they are used.
 
340
        For remote stores that support pipelining or async downloads, this can
 
341
        increase speed considerably.
 
342
        Failures while prefetching are ignored.
 
343
        """
 
344
        mutter("Prefetch of ids %s" % ",".join(ids))
 
345
        self.cache_store.copy_multi(self.source_store, ids,
 
346
                                    permit_failure=True)
 
347
 
 
348
 
 
349
def copy_all(store_from, store_to):
 
350
    """Copy all ids from one store to another."""
 
351
    if not hasattr(store_from, "__iter__"):
 
352
        raise UnlistableStore(store_from)
 
353
    ids = [f for f in store_from]
 
354
    store_to.copy_multi(store_from, ids)