/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-07 10:47:36 UTC
  • mto: (1092.3.1)
  • mto: This revision was merged to the branch mainline in revision 1397.
  • Revision ID: robertc@robertcollins.net-20050907104736-8e592b72108c577d
symlink support updated to work

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