~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/store.py

  • Committer: Martin Pool
  • Date: 2005-07-16 00:07:40 UTC
  • mfrom: (909.1.5)
  • Revision ID: mbp@sourcefrog.net-20050716000740-f2dcb8894a23fd2d
- merge aaron's bugfix branch
  up to abentley@panoramicfeedback.com-20050715134354-78f2bca607acb415

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
"""
 
18
Stores are the main data-storage mechanism for Bazaar-NG.
19
19
 
20
20
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
 
21
unique ID.
 
22
"""
 
23
 
 
24
import os, tempfile, types, osutils, gzip, errno
27
25
from stat import ST_SIZE
28
26
from StringIO import StringIO
29
27
from trace import mutter
30
28
 
31
 
 
32
29
######################################################################
33
30
# stores
34
31
 
36
33
    pass
37
34
 
38
35
 
39
 
class ImmutableStore:
 
36
class ImmutableStore(object):
40
37
    """Store that holds files indexed by unique names.
41
38
 
42
39
    Files can be added, but not modified once they are in.  Typically
59
56
    >>> st['123123'].read()
60
57
    'goodbye'
61
58
 
62
 
    :todo: Atomic add by writing to a temporary file and renaming.
63
 
 
64
 
    :todo: Perhaps automatically transform to/from XML in a method?
65
 
           Would just need to tell the constructor what class to
66
 
           use...
67
 
 
68
 
    :todo: Even within a simple disk store like this, we could
69
 
           gzip the files.  But since many are less than one disk
70
 
           block, that might not help a lot.
71
 
 
 
59
    TODO: Atomic add by writing to a temporary file and renaming.
 
60
 
 
61
    In bzr 0.0.5 and earlier, files within the store were marked
 
62
    readonly on disk.  This is no longer done but existing stores need
 
63
    to be accomodated.
72
64
    """
73
65
 
74
66
    def __init__(self, basedir):
75
 
        """ImmutableStore constructor."""
76
67
        self._basedir = basedir
77
68
 
78
69
    def _path(self, id):
 
70
        if '\\' in id or '/' in id:
 
71
            raise ValueError("invalid store id %r" % id)
79
72
        return os.path.join(self._basedir, id)
80
73
 
81
74
    def __repr__(self):
82
75
        return "%s(%r)" % (self.__class__.__name__, self._basedir)
83
76
 
84
 
    def add(self, f, fileid):
 
77
    def add(self, f, fileid, compressed=True):
85
78
        """Add contents of a file into the store.
86
79
 
87
 
        :param f: An open file, or file-like object."""
88
 
        # FIXME: Only works on smallish files
89
 
        # TODO: Can be optimized by copying at the same time as
90
 
        # computing the sum.
 
80
        f -- An open file, or file-like object."""
 
81
        # FIXME: Only works on files that will fit in memory
 
82
        
 
83
        from bzrlib.atomicfile import AtomicFile
 
84
        
91
85
        mutter("add store entry %r" % (fileid))
92
86
        if isinstance(f, types.StringTypes):
93
87
            content = f
94
88
        else:
95
89
            content = f.read()
96
 
        if fileid not in self:
97
 
            filename = self._path(fileid)
98
 
            f = file(filename, 'wb')
99
 
            f.write(content)
100
 
            f.flush()
101
 
            os.fsync(f.fileno())
102
 
            f.close()
103
 
            osutils.make_readonly(filename)
104
 
 
 
90
            
 
91
        p = self._path(fileid)
 
92
        if os.access(p, os.F_OK) or os.access(p + '.gz', os.F_OK):
 
93
            from bzrlib.errors import bailout
 
94
            raise BzrError("store %r already contains id %r" % (self._basedir, fileid))
 
95
 
 
96
        fn = p
 
97
        if compressed:
 
98
            fn = fn + '.gz'
 
99
            
 
100
        af = AtomicFile(fn, 'wb')
 
101
        try:
 
102
            if compressed:
 
103
                gf = gzip.GzipFile(mode='wb', fileobj=af)
 
104
                gf.write(content)
 
105
                gf.close()
 
106
            else:
 
107
                af.write(content)
 
108
            af.commit()
 
109
        finally:
 
110
            af.close()
 
111
 
 
112
 
 
113
    def copy_multi(self, other, ids):
 
114
        """Copy texts for ids from other into self.
 
115
 
 
116
        If an id is present in self, it is skipped.  A count of copied
 
117
        ids is returned, which may be less than len(ids).
 
118
        """
 
119
        from bzrlib.progress import ProgressBar
 
120
        pb = ProgressBar()
 
121
        pb.update('preparing to copy')
 
122
        to_copy = [id for id in ids if id not in self]
 
123
        if isinstance(other, ImmutableStore):
 
124
            return self.copy_multi_immutable(other, to_copy, pb)
 
125
        count = 0
 
126
        for id in to_copy:
 
127
            count += 1
 
128
            pb.update('copy', count, len(to_copy))
 
129
            self.add(other[id], id)
 
130
        assert count == len(to_copy)
 
131
        pb.clear()
 
132
        return count
 
133
 
 
134
 
 
135
    def copy_multi_immutable(self, other, to_copy, pb):
 
136
        from shutil import copyfile
 
137
        count = 0
 
138
        for id in to_copy:
 
139
            p = self._path(id)
 
140
            other_p = other._path(id)
 
141
            try:
 
142
                copyfile(other_p, p)
 
143
            except IOError, e:
 
144
                if e.errno == errno.ENOENT:
 
145
                    copyfile(other_p+".gz", p+".gz")
 
146
                else:
 
147
                    raise
 
148
            
 
149
            count += 1
 
150
            pb.update('copy', count, len(to_copy))
 
151
        assert count == len(to_copy)
 
152
        pb.clear()
 
153
        return count
 
154
    
105
155
 
106
156
    def __contains__(self, fileid):
107
157
        """"""
108
 
        return os.access(self._path(fileid), os.R_OK)
 
158
        p = self._path(fileid)
 
159
        return (os.access(p, os.R_OK)
 
160
                or os.access(p + '.gz', os.R_OK))
109
161
 
 
162
    # TODO: Guard against the same thing being stored twice, compressed and uncompresse
110
163
 
111
164
    def __iter__(self):
112
 
        return iter(os.listdir(self._basedir))
 
165
        for f in os.listdir(self._basedir):
 
166
            if f[-3:] == '.gz':
 
167
                # TODO: case-insensitive?
 
168
                yield f[:-3]
 
169
            else:
 
170
                yield f
113
171
 
114
172
    def __len__(self):
115
173
        return len(os.listdir(self._basedir))
116
174
 
117
175
    def __getitem__(self, fileid):
118
176
        """Returns a file reading from a particular entry."""
119
 
        return file(self._path(fileid), 'rb')
 
177
        p = self._path(fileid)
 
178
        try:
 
179
            return gzip.GzipFile(p + '.gz', 'rb')
 
180
        except IOError, e:
 
181
            if e.errno == errno.ENOENT:
 
182
                return file(p, 'rb')
 
183
            else:
 
184
                raise e
120
185
 
121
186
    def total_size(self):
122
 
        """Return (count, bytes)"""
 
187
        """Return (count, bytes)
 
188
 
 
189
        This is the (compressed) size stored on disk, not the size of
 
190
        the content."""
123
191
        total = 0
124
192
        count = 0
125
193
        for fid in self:
126
194
            count += 1
127
 
            total += os.stat(self._path(fid))[ST_SIZE]
 
195
            p = self._path(fid)
 
196
            try:
 
197
                total += os.stat(p)[ST_SIZE]
 
198
            except OSError:
 
199
                total += os.stat(p + '.gz')[ST_SIZE]
 
200
                
128
201
        return count, total
129
202
 
130
 
    def delete_all(self):
131
 
        for fileid in self:
132
 
            self.delete(fileid)
133
 
 
134
 
    def delete(self, fileid):
135
 
        """Remove nominated store entry.
136
 
 
137
 
        Most stores will be add-only."""
138
 
        filename = self._path(fileid)
139
 
        ## osutils.make_writable(filename)
140
 
        os.remove(filename)
141
 
 
142
 
    def destroy(self):
143
 
        """Remove store; only allowed if it is empty."""
144
 
        os.rmdir(self._basedir)
145
 
        mutter("%r destroyed" % self)
146
203
 
147
204
 
148
205
 
150
207
    """Self-destructing test subclass of ImmutableStore.
151
208
 
152
209
    The Store only exists for the lifetime of the Python object.
153
 
    Obviously you should not put anything precious in it.
 
210
 Obviously you should not put anything precious in it.
154
211
    """
155
212
    def __init__(self):
156
213
        ImmutableStore.__init__(self, tempfile.mkdtemp())
157
214
 
158
215
    def __del__(self):
159
 
        self.delete_all()
160
 
        self.destroy()
 
216
        for f in os.listdir(self._basedir):
 
217
            fpath = os.path.join(self._basedir, f)
 
218
            # needed on windows, and maybe some other filesystems
 
219
            os.chmod(fpath, 0600)
 
220
            os.remove(fpath)
 
221
        os.rmdir(self._basedir)
 
222
        mutter("%r destroyed" % self)