~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/store/__init__.py

  • Committer: John Arbash Meinel
  • Date: 2005-09-29 20:34:25 UTC
  • mfrom: (1185.11.24)
  • mto: (1393.1.12)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: john@arbash-meinel.com-20050929203425-7fc2ea87f449dfe8
Merged in split-storage-2 branch. Need to cleanup a little bit more still.

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
unique ID.
25
25
"""
26
26
 
27
 
import errno
28
 
import gzip
29
 
import os
30
 
import tempfile
31
 
import types
32
 
from stat import ST_SIZE
33
 
from StringIO import StringIO
34
 
 
35
27
from bzrlib.errors import BzrError, UnlistableStore
36
28
from bzrlib.trace import mutter
37
 
import bzrlib.ui
38
 
import bzrlib.osutils as osutils
39
 
 
 
29
import bzrlib.transport
40
30
 
41
31
######################################################################
42
32
# stores
44
34
class StoreError(Exception):
45
35
    pass
46
36
 
47
 
 
48
 
class ImmutableStore(object):
49
 
    """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
 
    TODO: Atomic add by writing to a temporary file and renaming.
72
 
 
73
 
    In bzr 0.0.5 and earlier, files within the store were marked
74
 
    readonly on disk.  This is no longer done but existing stores need
75
 
    to be accomodated.
 
37
class Store(object):
 
38
    """This class represents the abstract storage layout for saving information.
76
39
    """
77
 
 
78
 
    def __init__(self, basedir):
79
 
        self._basedir = basedir
80
 
 
81
 
    def _path(self, entry_id):
82
 
        if not isinstance(entry_id, basestring):
83
 
            raise TypeError(type(entry_id))
84
 
        if '\\' in entry_id or '/' in entry_id:
85
 
            raise ValueError("invalid store id %r" % entry_id)
86
 
        return os.path.join(self._basedir, entry_id)
 
40
    _transport = None
 
41
    _max_buffered_requests = 10
 
42
 
 
43
    def __init__(self, transport):
 
44
        assert isinstance(transport, bzrlib.transport.Transport)
 
45
        self._transport = transport
87
46
 
88
47
    def __repr__(self):
89
 
        return "%s(%r)" % (self.__class__.__name__, self._basedir)
90
 
 
91
 
    def add(self, f, fileid, compressed=True):
92
 
        """Add contents of a file into the store.
93
 
 
94
 
        f -- An open file, or file-like object."""
95
 
        # FIXME: Only works on files that will fit in memory
96
 
        
97
 
        from bzrlib.atomicfile import AtomicFile
98
 
        
99
 
        mutter("add store entry %r" % (fileid))
100
 
        if isinstance(f, types.StringTypes):
101
 
            content = f
 
48
        if self._transport is None:
 
49
            return "%s(None)" % (self.__class__.__name__)
102
50
        else:
103
 
            content = f.read()
104
 
            
105
 
        p = self._path(fileid)
106
 
        if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK):
107
 
            raise BzrError("store %r already contains id %r" % (self._basedir, fileid))
108
 
 
109
 
        fn = p
110
 
        if compressed:
111
 
            fn = fn + '.gz'
112
 
            
113
 
        af = AtomicFile(fn, 'wb')
114
 
        try:
115
 
            if compressed:
116
 
                gf = gzip.GzipFile(mode='wb', fileobj=af)
117
 
                gf.write(content)
118
 
                gf.close()
 
51
            return "%s(%r)" % (self.__class__.__name__, self._transport.base)
 
52
 
 
53
    __str__ = __repr__
 
54
 
 
55
    def __len__(self):
 
56
        raise NotImplementedError('Children should define their length')
 
57
 
 
58
    def __getitem__(self, fileid):
 
59
        """Returns a file reading from a particular entry."""
 
60
        raise NotImplementedError
 
61
 
 
62
    def __contains__(self, fileid):
 
63
        """"""
 
64
        raise NotImplementedError
 
65
 
 
66
    def __iter__(self):
 
67
        raise NotImplementedError
 
68
 
 
69
    def __del__(self):
 
70
        """Delete an entry from the store."""
 
71
        raise NotImplementedError('Children need to define delete rights.')
 
72
 
 
73
    def add(self, f, fileid):
 
74
        """Add a file object f to the store accessible from the given fileid"""
 
75
        raise NotImplementedError('Children of Store must define their method of adding entries.')
 
76
 
 
77
    def add_multi(self, entries):
 
78
        """Add a series of file-like or string objects to the store with the given
 
79
        identities.
 
80
        
 
81
        :param entries: A list of tuples of file,id pairs [(file1, id1), (file2, id2), ...]
 
82
                        This could also be a generator yielding (file,id) pairs.
 
83
        """
 
84
        for f, fileid in entries:
 
85
            self.add(f, fileid)
 
86
 
 
87
    def has(self, fileids):
 
88
        """Return True/False for each entry in fileids.
 
89
 
 
90
        :param fileids: A List or generator yielding file ids.
 
91
        :return: A generator or list returning True/False for each entry.
 
92
        """
 
93
        for fileid in fileids:
 
94
            if fileid in self:
 
95
                yield True
119
96
            else:
120
 
                af.write(content)
121
 
            af.commit()
122
 
        finally:
123
 
            af.close()
124
 
 
125
 
 
126
 
    def copy_multi(self, other, ids, permit_failure=False):
 
97
                yield False
 
98
 
 
99
    def get(self, fileids, permit_failure=False, pb=None):
 
100
        """Return a set of files, one for each requested entry.
 
101
        
 
102
        :param permit_failure: If true, return None for entries which do not 
 
103
                               exist.
 
104
        :return: A list or generator of file-like objects, one for each id.
 
105
        """
 
106
        for fileid in fileids:
 
107
            try:
 
108
                yield self[fileid]
 
109
            except KeyError:
 
110
                if permit_failure:
 
111
                    yield None
 
112
                else:
 
113
                    raise
 
114
 
 
115
    def copy_multi(self, other, ids, pb=None, permit_failure=False):
127
116
        """Copy texts for ids from other into self.
128
117
 
129
 
        If an id is present in self, it is skipped.
 
118
        If an id is present in self, it is skipped.  A count of copied
 
119
        ids is returned, which may be less than len(ids).
130
120
 
131
 
        Returns (count_copied, failed), where failed is a collection of ids
132
 
        that could not be copied.
 
121
        :param other: Another Store object
 
122
        :param ids: A list of entry ids to be copied
 
123
        :param pb: A ProgressBar object, if none is given, the default will be created.
 
124
        :param permit_failure: Allow missing entries to be ignored
 
125
        :return: (n_copied, [failed]) The number of entries copied successfully,
 
126
            followed by a list of entries which could not be copied (because they
 
127
            were missing)
133
128
        """
134
 
        pb = bzrlib.ui.ui_factory.progress_bar()
135
 
        
 
129
        if pb is None:
 
130
            pb = bzrlib.ui.ui_factory.progress_bar()
 
131
 
 
132
        ids = list(ids) # Make sure we don't have a generator, since we iterate 2 times
136
133
        pb.update('preparing to copy')
137
 
        to_copy = [id for id in ids if id not in self]
138
 
        if isinstance(other, ImmutableStore):
139
 
            return self.copy_multi_immutable(other, to_copy, pb, 
140
 
                                             permit_failure=permit_failure)
141
 
        count = 0
 
134
        to_copy = []
 
135
        for file_id, has in zip(ids, self.has(ids)):
 
136
            if not has:
 
137
                to_copy.append(file_id)
 
138
        return self._do_copy(other, to_copy, pb, permit_failure=permit_failure)
 
139
 
 
140
    def _do_copy(self, other, to_copy, pb, permit_failure=False):
 
141
        """This is the standard copying mechanism, just get them one at
 
142
        a time from remote, and store them locally.
 
143
 
 
144
        :param other: Another Store object
 
145
        :param to_copy: A list of entry ids to copy
 
146
        :param pb: A ProgressBar object to display completion status.
 
147
        :param permit_failure: Allow missing entries to be ignored
 
148
        :return: (n_copied, [failed])
 
149
            The number of entries copied, and a list of failed entries.
 
150
        """
 
151
        # This should be updated to use add_multi() rather than
 
152
        # the current methods of buffering requests.
 
153
        # One question, is it faster to queue up 1-10 and then copy 1-10
 
154
        # then queue up 11-20, copy 11-20
 
155
        # or to queue up 1-10, copy 1, queue 11, copy 2, etc?
 
156
        # sort of pipeline versus batch.
 
157
 
 
158
        # We can't use self._transport.copy_to because we don't know
 
159
        # whether the local tree is in the same format as other
142
160
        failed = set()
143
 
        for id in to_copy:
144
 
            count += 1
145
 
            pb.update('copy', count, len(to_copy))
146
 
            if not permit_failure:
147
 
                self.add(other[id], id)
148
 
            else:
 
161
        def buffer_requests():
 
162
            count = 0
 
163
            buffered_requests = []
 
164
            for fileid in to_copy:
149
165
                try:
150
 
                    entry = other[id]
 
166
                    f = other[fileid]
151
167
                except KeyError:
152
 
                    failed.add(id)
153
 
                    continue
154
 
                self.add(entry, id)
155
 
                
156
 
        if not permit_failure:
 
168
                    if permit_failure:
 
169
                        failed.add(fileid)
 
170
                        continue
 
171
                    else:
 
172
                        raise
 
173
 
 
174
                buffered_requests.append((f, fileid))
 
175
                if len(buffered_requests) > self._max_buffered_requests:
 
176
                    yield buffered_requests.pop(0)
 
177
                    count += 1
 
178
                    pb.update('copy', count, len(to_copy))
 
179
 
 
180
            for req in buffered_requests:
 
181
                yield req
 
182
                count += 1
 
183
                pb.update('copy', count, len(to_copy))
 
184
 
157
185
            assert count == len(to_copy)
158
 
        pb.clear()
159
 
        return count, failed
160
 
 
161
 
    def copy_multi_immutable(self, other, to_copy, pb, permit_failure=False):
162
 
        count = 0
163
 
        failed = set()
164
 
        for id in to_copy:
165
 
            p = self._path(id)
166
 
            other_p = other._path(id)
167
 
            try:
168
 
                osutils.link_or_copy(other_p, p)
169
 
            except (IOError, OSError), e:
170
 
                if e.errno == errno.ENOENT:
171
 
                    if not permit_failure:
172
 
                        osutils.link_or_copy(other_p+".gz", p+".gz")
173
 
                    else:
174
 
                        try:
175
 
                            osutils.link_or_copy(other_p+".gz", p+".gz")
176
 
                        except IOError, e:
177
 
                            if e.errno == errno.ENOENT:
178
 
                                failed.add(id)
179
 
                            else:
180
 
                                raise
181
 
                else:
182
 
                    raise
183
 
            
184
 
            count += 1
185
 
            pb.update('copy', count, len(to_copy))
186
 
        assert count == len(to_copy)
187
 
        pb.clear()
188
 
        return count, failed
189
 
    
190
 
 
191
 
    def __contains__(self, fileid):
192
 
        """"""
193
 
        p = self._path(fileid)
194
 
        return (os.access(p, os.R_OK)
195
 
                or os.access(p + '.gz', os.R_OK))
196
 
 
197
 
    # TODO: Guard against the same thing being stored twice,
198
 
    # compressed and uncompressed
199
 
 
200
 
    def __iter__(self):
201
 
        for f in os.listdir(self._basedir):
202
 
            if f[-3:] == '.gz':
203
 
                # TODO: case-insensitive?
204
 
                yield f[:-3]
205
 
            else:
206
 
                yield f
207
 
 
208
 
    def __len__(self):
209
 
        return len(os.listdir(self._basedir))
210
 
 
211
 
 
212
 
    def __getitem__(self, fileid):
213
 
        """Returns a file reading from a particular entry."""
214
 
        p = self._path(fileid)
215
 
        try:
216
 
            return gzip.GzipFile(p + '.gz', 'rb')
217
 
        except IOError, e:
218
 
            if e.errno != errno.ENOENT:
219
 
                raise
220
 
 
221
 
        try:
222
 
            return file(p, 'rb')
223
 
        except IOError, e:
224
 
            if e.errno != errno.ENOENT:
225
 
                raise
226
 
 
227
 
        raise KeyError(fileid)
228
 
 
229
 
 
230
 
    def total_size(self):
231
 
        """Return (count, bytes)
232
 
 
233
 
        This is the (compressed) size stored on disk, not the size of
234
 
        the content."""
235
 
        total = 0
236
 
        count = 0
237
 
        for fid in self:
238
 
            count += 1
239
 
            p = self._path(fid)
240
 
            try:
241
 
                total += os.stat(p)[ST_SIZE]
242
 
            except OSError:
243
 
                total += os.stat(p + '.gz')[ST_SIZE]
244
 
                
245
 
        return count, total
246
 
 
247
 
 
248
 
 
249
 
 
250
 
class ImmutableScratchStore(ImmutableStore):
251
 
    """Self-destructing test subclass of ImmutableStore.
252
 
 
253
 
    The Store only exists for the lifetime of the Python object.
254
 
 Obviously you should not put anything precious in it.
255
 
    """
256
 
    def __init__(self):
257
 
        ImmutableStore.__init__(self, tempfile.mkdtemp())
258
 
 
259
 
    def __del__(self):
260
 
        for f in os.listdir(self._basedir):
261
 
            fpath = os.path.join(self._basedir, f)
262
 
            # needed on windows, and maybe some other filesystems
263
 
            os.chmod(fpath, 0600)
264
 
            os.remove(fpath)
265
 
        os.rmdir(self._basedir)
266
 
        mutter("%r destroyed" % self)
 
186
 
 
187
        self.add_multi(buffer_requests())
 
188
 
 
189
        pb.clear()
 
190
        return len(to_copy), failed
267
191
 
268
192
def copy_all(store_from, store_to):
269
193
    """Copy all ids from one store to another."""
270
194
    if not hasattr(store_from, "__iter__"):
271
195
        raise UnlistableStore(store_from)
272
 
    ids = [f for f in store_from]
 
196
    try:
 
197
        ids = [f for f in store_from]
 
198
    except NotImplementedError:
 
199
        raise UnlistableStore(store_from)
273
200
    store_to.copy_multi(store_from, ids)
 
201