/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-06 15:19:44 UTC
  • mto: (1092.3.1)
  • mto: This revision was merged to the branch mainline in revision 1397.
  • Revision ID: robertc@robertcollins.net-20050906151944-04320f095a2abc31
minor refactors to store, create an ImmutableMemoryStore for testing or other such operations

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