/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-28 05:25:54 UTC
  • mfrom: (1185.1.42)
  • mto: (1092.2.18)
  • mto: This revision was merged to the branch mainline in revision 1397.
  • Revision ID: robertc@robertcollins.net-20050928052554-beb985505f77ea6a
update symlink branch to 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
 
33
from bzrlib.trace import mutter
 
34
import bzrlib.ui
 
35
import bzrlib.osutils as osutils
 
36
from bzrlib.remotebranch import get_url
 
37
import urllib2
 
38
 
 
39
 
 
40
######################################################################
 
41
# stores
 
42
 
 
43
class StoreError(Exception):
 
44
    pass
 
45
 
 
46
 
 
47
class Store(object):
 
48
    """An abstract store that holds files indexed by unique names.
 
49
 
 
50
    Files can be added, but not modified once they are in.  Typically
 
51
    the hash is used as the name, or something else known to be unique,
 
52
    such as a UUID.
 
53
 
 
54
    >>> st = ImmutableScratchStore()
 
55
 
 
56
    >>> st.add(StringIO('hello'), 'aa')
 
57
    >>> 'aa' in st
 
58
    True
 
59
    >>> 'foo' in st
 
60
    False
 
61
 
 
62
    You are not allowed to add an id that is already present.
 
63
 
 
64
    Entries can be retrieved as files, which may then be read.
 
65
 
 
66
    >>> st.add(StringIO('goodbye'), '123123')
 
67
    >>> st['123123'].read()
 
68
    'goodbye'
 
69
    """
 
70
 
 
71
    def total_size(self):
 
72
        """Return (count, bytes)
 
73
 
 
74
        This is the (compressed) size stored on disk, not the size of
 
75
        the content."""
 
76
        total = 0
 
77
        count = 0
 
78
        for fid in self:
 
79
            count += 1
 
80
            total += self._item_size(fid)
 
81
        return count, total
 
82
 
 
83
 
 
84
class ImmutableStore(Store):
 
85
    """Store that stores files on disk.
 
86
 
 
87
    TODO: Atomic add by writing to a temporary file and renaming.
 
88
    TODO: Guard against the same thing being stored twice, compressed and
 
89
          uncompressed during copy_multi_immutable - the window is for a
 
90
          matching store with some crack code that lets it offer a 
 
91
          non gz FOO and then a fz FOO.
 
92
 
 
93
    In bzr 0.0.5 and earlier, files within the store were marked
 
94
    readonly on disk.  This is no longer done but existing stores need
 
95
    to be accomodated.
 
96
    """
 
97
 
 
98
    def __init__(self, basedir):
 
99
        super(ImmutableStore, self).__init__()
 
100
        self._basedir = basedir
 
101
 
 
102
    def _path(self, entry_id):
 
103
        if not isinstance(entry_id, basestring):
 
104
            raise TypeError(type(entry_id))
 
105
        if '\\' in entry_id or '/' in entry_id:
 
106
            raise ValueError("invalid store id %r" % entry_id)
 
107
        return os.path.join(self._basedir, entry_id)
 
108
 
 
109
    def __repr__(self):
 
110
        return "%s(%r)" % (self.__class__.__name__, self._basedir)
 
111
 
 
112
    def add(self, f, fileid, compressed=True):
 
113
        """Add contents of a file into the store.
 
114
 
 
115
        f -- An open file, or file-like object."""
 
116
        # FIXME: Only works on files that will fit in memory
 
117
        
 
118
        from bzrlib.atomicfile import AtomicFile
 
119
        
 
120
        mutter("add store entry %r" % (fileid))
 
121
        if isinstance(f, types.StringTypes):
 
122
            content = f
 
123
        else:
 
124
            content = f.read()
 
125
            
 
126
        p = self._path(fileid)
 
127
        if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK):
 
128
            raise BzrError("store %r already contains id %r" % (self._basedir, fileid))
 
129
 
 
130
        fn = p
 
131
        if compressed:
 
132
            fn = fn + '.gz'
 
133
            
 
134
        af = AtomicFile(fn, 'wb')
 
135
        try:
 
136
            if compressed:
 
137
                gf = gzip.GzipFile(mode='wb', fileobj=af)
 
138
                gf.write(content)
 
139
                gf.close()
 
140
            else:
 
141
                af.write(content)
 
142
            af.commit()
 
143
        finally:
 
144
            af.close()
 
145
 
 
146
 
 
147
    def copy_multi(self, other, ids, permit_failure=False):
 
148
        """Copy texts for ids from other into self.
 
149
 
 
150
        If an id is present in self, it is skipped.
 
151
 
 
152
        Returns (count_copied, failed), where failed is a collection of ids
 
153
        that could not be copied.
 
154
        """
 
155
        pb = bzrlib.ui.ui_factory.progress_bar()
 
156
        
 
157
        pb.update('preparing to copy')
 
158
        to_copy = [id for id in ids if id not in self]
 
159
        if isinstance(other, ImmutableStore):
 
160
            return self.copy_multi_immutable(other, to_copy, pb, 
 
161
                                             permit_failure=permit_failure)
 
162
        count = 0
 
163
        failed = set()
 
164
        for id in to_copy:
 
165
            count += 1
 
166
            pb.update('copy', count, len(to_copy))
 
167
            if not permit_failure:
 
168
                self.add(other[id], id)
 
169
            else:
 
170
                try:
 
171
                    entry = other[id]
 
172
                except KeyError:
 
173
                    failed.add(id)
 
174
                    continue
 
175
                self.add(entry, id)
 
176
                
 
177
        if not permit_failure:
 
178
            assert count == len(to_copy)
 
179
        pb.clear()
 
180
        return count, failed
 
181
 
 
182
    def copy_multi_immutable(self, other, to_copy, pb, permit_failure=False):
 
183
        from shutil import copyfile
 
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
                copyfile(other_p, p)
 
191
            except IOError, e:
 
192
                if e.errno == errno.ENOENT:
 
193
                    if not permit_failure:
 
194
                        copyfile(other_p+".gz", p+".gz")
 
195
                    else:
 
196
                        try:
 
197
                            copyfile(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
        p = self._path(fileid)
 
310
        try:
 
311
            return get_url(p, compressed=True)
 
312
        except urllib2.URLError:
 
313
            pass
 
314
        try:
 
315
            return get_url(p, compressed=False)
 
316
        except urllib2.URLError:
 
317
            raise KeyError(fileid)
 
318
 
 
319
class CachedStore:
 
320
    """A store that caches data locally, to avoid repeated downloads.
 
321
    The precacache method should be used to avoid server round-trips for
 
322
    every piece of data.
 
323
    """
 
324
 
 
325
    def __init__(self, store, cache_dir):
 
326
        self.source_store = store
 
327
        self.cache_store = ImmutableStore(cache_dir)
 
328
 
 
329
    def __getitem__(self, id):
 
330
        mutter("Cache add %s" % id)
 
331
        if id not in self.cache_store:
 
332
            self.cache_store.add(self.source_store[id], id)
 
333
        return self.cache_store[id]
 
334
 
 
335
    def prefetch(self, ids):
 
336
        """Copy a series of ids into the cache, before they are used.
 
337
        For remote stores that support pipelining or async downloads, this can
 
338
        increase speed considerably.
 
339
        Failures while prefetching are ignored.
 
340
        """
 
341
        mutter("Prefetch of ids %s" % ",".join(ids))
 
342
        self.cache_store.copy_multi(self.source_store, ids,
 
343
                                    permit_failure=True)