~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/store.py

  • Committer: Martin Pool
  • Date: 2005-09-30 00:58:02 UTC
  • mto: (1185.14.2)
  • mto: This revision was merged to the branch mainline in revision 1396.
  • Revision ID: mbp@sourcefrog.net-20050930005802-721cfc318e393817
- copy_branch creates destination if it doesn't exist

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#! /usr/bin/env python
2
 
# -*- coding: UTF-8 -*-
 
1
# Copyright (C) 2005 by Canonical Development Ltd
3
2
 
4
3
# This program is free software; you can redistribute it and/or modify
5
4
# it under the terms of the GNU General Public License as published by
15
14
# along with this program; if not, write to the Free Software
16
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17
16
 
18
 
"""Stores are the main data-storage mechanism for Bazaar-NG.
 
17
# TODO: Could remember a bias towards whether a particular store is typically
 
18
# compressed or not.
 
19
 
 
20
"""
 
21
Stores are the main data-storage mechanism for Bazaar-NG.
19
22
 
20
23
A store is a simple write-once container indexed by a universally
21
 
unique ID, which is typically the SHA-1 of the content."""
22
 
 
23
 
__copyright__ = "Copyright (C) 2005 Canonical Ltd."
24
 
__author__ = "Martin Pool <mbp@canonical.com>"
25
 
 
26
 
import os, tempfile, types, osutils
 
24
unique ID.
 
25
"""
 
26
 
 
27
import errno
 
28
import gzip
 
29
import os
 
30
import tempfile
 
31
import types
 
32
from stat import ST_SIZE
27
33
from StringIO import StringIO
28
 
from trace import mutter
 
34
 
 
35
from bzrlib.errors import BzrError, UnlistableStore
 
36
from bzrlib.trace import mutter
 
37
import bzrlib.ui
 
38
import bzrlib.osutils as osutils
29
39
 
30
40
 
31
41
######################################################################
35
45
    pass
36
46
 
37
47
 
38
 
class ImmutableStore:
 
48
class ImmutableStore(object):
39
49
    """Store that holds files indexed by unique names.
40
50
 
41
51
    Files can be added, but not modified once they are in.  Typically
58
68
    >>> st['123123'].read()
59
69
    'goodbye'
60
70
 
61
 
    :todo: Atomic add by writing to a temporary file and renaming.
62
 
 
63
 
    :todo: Perhaps automatically transform to/from XML in a method?
64
 
           Would just need to tell the constructor what class to
65
 
           use...
66
 
 
67
 
    :todo: Even within a simple disk store like this, we could
68
 
           gzip the files.  But since many are less than one disk
69
 
           block, that might not help a lot.
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.
71
76
    """
72
77
 
73
78
    def __init__(self, basedir):
74
 
        """ImmutableStore constructor."""
75
79
        self._basedir = basedir
76
80
 
77
 
    def _path(self, id):
78
 
        return os.path.join(self._basedir, id)
 
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)
79
87
 
80
88
    def __repr__(self):
81
89
        return "%s(%r)" % (self.__class__.__name__, self._basedir)
82
90
 
83
 
    def add(self, f, fileid):
 
91
    def add(self, f, fileid, compressed=True):
84
92
        """Add contents of a file into the store.
85
93
 
86
 
        :param f: An open file, or file-like object."""
87
 
        # FIXME: Only works on smallish files
88
 
        # TODO: Can be optimized by copying at the same time as
89
 
        # computing the sum.
 
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
        
90
99
        mutter("add store entry %r" % (fileid))
91
100
        if isinstance(f, types.StringTypes):
92
101
            content = f
93
102
        else:
94
103
            content = f.read()
95
 
        if fileid not in self:
96
 
            filename = self._path(fileid)
97
 
            f = file(filename, 'wb')
98
 
            f.write(content)
99
 
            f.flush()
100
 
            os.fsync(f.fileno())
101
 
            f.close()
102
 
            osutils.make_readonly(filename)
103
 
 
 
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()
 
119
            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):
 
127
        """Copy texts for ids from other into self.
 
128
 
 
129
        If an id is present in self, it is skipped.
 
130
 
 
131
        Returns (count_copied, failed), where failed is a collection of ids
 
132
        that could not be copied.
 
133
        """
 
134
        pb = bzrlib.ui.ui_factory.progress_bar()
 
135
        
 
136
        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
 
142
        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:
 
149
                try:
 
150
                    entry = other[id]
 
151
                except KeyError:
 
152
                    failed.add(id)
 
153
                    continue
 
154
                self.add(entry, id)
 
155
                
 
156
        if not permit_failure:
 
157
            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
    
104
190
 
105
191
    def __contains__(self, fileid):
106
192
        """"""
107
 
        return os.access(self._path(fileid), os.R_OK)
 
193
        p = self._path(fileid)
 
194
        return (os.access(p, os.R_OK)
 
195
                or os.access(p + '.gz', os.R_OK))
108
196
 
 
197
    # TODO: Guard against the same thing being stored twice,
 
198
    # compressed and uncompressed
109
199
 
110
200
    def __iter__(self):
111
 
        return iter(os.listdir(self._basedir))
 
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
 
112
211
 
113
212
    def __getitem__(self, fileid):
114
213
        """Returns a file reading from a particular entry."""
115
 
        return file(self._path(fileid), 'rb')
116
 
 
117
 
    def delete_all(self):
118
 
        for fileid in self:
119
 
            self.delete(fileid)
120
 
 
121
 
    def delete(self, fileid):
122
 
        """Remove nominated store entry.
123
 
 
124
 
        Most stores will be add-only."""
125
 
        filename = self._path(fileid)
126
 
        ## osutils.make_writable(filename)
127
 
        os.remove(filename)
128
 
 
129
 
    def destroy(self):
130
 
        """Remove store; only allowed if it is empty."""
131
 
        os.rmdir(self._basedir)
132
 
        mutter("%r destroyed" % self)
 
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
 
133
247
 
134
248
 
135
249
 
137
251
    """Self-destructing test subclass of ImmutableStore.
138
252
 
139
253
    The Store only exists for the lifetime of the Python object.
140
 
    Obviously you should not put anything precious in it.
 
254
 Obviously you should not put anything precious in it.
141
255
    """
142
256
    def __init__(self):
143
257
        ImmutableStore.__init__(self, tempfile.mkdtemp())
144
258
 
145
259
    def __del__(self):
146
 
        self.delete_all()
147
 
        self.destroy()
 
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)
 
267
 
 
268
def copy_all(store_from, store_to):
 
269
    """Copy all ids from one store to another."""
 
270
    if not hasattr(store_from, "__iter__"):
 
271
        raise UnlistableStore(store_from)
 
272
    ids = [f for f in store_from]
 
273
    store_to.copy_multi(store_from, ids)