~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/local.py

  • Committer: John Arbash Meinel
  • Date: 2006-04-29 15:30:26 UTC
  • mfrom: (1692 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1693.
  • Revision ID: john@arbash-meinel.com-20060429153026-cc7e756ff8a5bf50
[merge] bzr.dev 1692

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005, 2006 Canonical 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
"""Transport for the local filesystem.
 
18
 
 
19
This is a fairly thin wrapper on regular file IO."""
 
20
 
 
21
import os
 
22
import shutil
 
23
import sys
 
24
from stat import ST_MODE, S_ISDIR, ST_SIZE
 
25
import tempfile
 
26
import urllib
 
27
 
 
28
from bzrlib.trace import mutter
 
29
from bzrlib.transport import Transport, Server
 
30
from bzrlib.osutils import (abspath, realpath, normpath, pathjoin, rename, 
 
31
                            check_legal_path)
 
32
 
 
33
 
 
34
class LocalTransport(Transport):
 
35
    """This is the transport agent for local filesystem access."""
 
36
 
 
37
    def __init__(self, base):
 
38
        """Set the base path where files will be stored."""
 
39
        if base.startswith('file://'):
 
40
            base = base[len('file://'):]
 
41
        # realpath is incompatible with symlinks. When we traverse
 
42
        # up we might be able to normpath stuff. RBC 20051003
 
43
        base = normpath(abspath(base))
 
44
        if base[-1] != '/':
 
45
            base = base + '/'
 
46
        super(LocalTransport, self).__init__(base)
 
47
 
 
48
    def should_cache(self):
 
49
        return False
 
50
 
 
51
    def clone(self, offset=None):
 
52
        """Return a new LocalTransport with root at self.base + offset
 
53
        Because the local filesystem does not require a connection, 
 
54
        we can just return a new object.
 
55
        """
 
56
        if offset is None:
 
57
            return LocalTransport(self.base)
 
58
        else:
 
59
            return LocalTransport(self.abspath(offset))
 
60
 
 
61
    def abspath(self, relpath):
 
62
        """Return the full url to the given relative URL."""
 
63
        assert isinstance(relpath, basestring), (type(relpath), relpath)
 
64
        result = normpath(pathjoin(self.base, urllib.unquote(relpath)))
 
65
        #if result[-1] != '/':
 
66
        #    result += '/'
 
67
        return result
 
68
 
 
69
    def relpath(self, abspath):
 
70
        """Return the local path portion from a given absolute path.
 
71
        """
 
72
        from bzrlib.osutils import relpath, strip_trailing_slash
 
73
        if abspath is None:
 
74
            abspath = u'.'
 
75
 
 
76
        return relpath(strip_trailing_slash(self.base), 
 
77
                       strip_trailing_slash(abspath))
 
78
 
 
79
    def has(self, relpath):
 
80
        return os.access(self.abspath(relpath), os.F_OK)
 
81
 
 
82
    def get(self, relpath):
 
83
        """Get the file at the given relative path.
 
84
 
 
85
        :param relpath: The relative path to the file
 
86
        """
 
87
        try:
 
88
            path = self.abspath(relpath)
 
89
            return open(path, 'rb')
 
90
        except (IOError, OSError),e:
 
91
            self._translate_error(e, path)
 
92
 
 
93
    def put(self, relpath, f, mode=None):
 
94
        """Copy the file-like or string object into the location.
 
95
 
 
96
        :param relpath: Location to put the contents, relative to base.
 
97
        :param f:       File-like or string object.
 
98
        """
 
99
        from bzrlib.atomicfile import AtomicFile
 
100
 
 
101
        path = relpath
 
102
        try:
 
103
            path = self.abspath(relpath)
 
104
            check_legal_path(path)
 
105
            fp = AtomicFile(path, 'wb', new_mode=mode)
 
106
        except (IOError, OSError),e:
 
107
            self._translate_error(e, path)
 
108
        try:
 
109
            self._pump(f, fp)
 
110
            fp.commit()
 
111
        finally:
 
112
            fp.close()
 
113
 
 
114
    def iter_files_recursive(self):
 
115
        """Iter the relative paths of files in the transports sub-tree."""
 
116
        queue = list(self.list_dir(u'.'))
 
117
        while queue:
 
118
            relpath = queue.pop(0)
 
119
            st = self.stat(relpath)
 
120
            if S_ISDIR(st[ST_MODE]):
 
121
                for i, basename in enumerate(self.list_dir(relpath)):
 
122
                    queue.insert(i, relpath+'/'+basename)
 
123
            else:
 
124
                yield relpath
 
125
 
 
126
    def mkdir(self, relpath, mode=None):
 
127
        """Create a directory at the given path."""
 
128
        path = relpath
 
129
        try:
 
130
            path = self.abspath(relpath)
 
131
            os.mkdir(path)
 
132
            if mode is not None:
 
133
                os.chmod(path, mode)
 
134
        except (IOError, OSError),e:
 
135
            self._translate_error(e, path)
 
136
 
 
137
    def append(self, relpath, f, mode=None):
 
138
        """Append the text in the file-like object into the final
 
139
        location.
 
140
        """
 
141
        try:
 
142
            fp = open(self.abspath(relpath), 'ab')
 
143
            if mode is not None:
 
144
                os.chmod(self.abspath(relpath), mode)
 
145
        except (IOError, OSError),e:
 
146
            self._translate_error(e, relpath)
 
147
        # win32 workaround (tell on an unwritten file returns 0)
 
148
        fp.seek(0, 2)
 
149
        result = fp.tell()
 
150
        self._pump(f, fp)
 
151
        return result
 
152
 
 
153
    def copy(self, rel_from, rel_to):
 
154
        """Copy the item at rel_from to the location at rel_to"""
 
155
        import shutil
 
156
        path_from = self.abspath(rel_from)
 
157
        path_to = self.abspath(rel_to)
 
158
        try:
 
159
            shutil.copy(path_from, path_to)
 
160
        except (IOError, OSError),e:
 
161
            # TODO: What about path_to?
 
162
            self._translate_error(e, path_from)
 
163
 
 
164
    def rename(self, rel_from, rel_to):
 
165
        path_from = self.abspath(rel_from)
 
166
        try:
 
167
            # *don't* call bzrlib.osutils.rename, because we want to 
 
168
            # detect errors on rename
 
169
            os.rename(path_from, self.abspath(rel_to))
 
170
        except (IOError, OSError),e:
 
171
            # TODO: What about path_to?
 
172
            self._translate_error(e, path_from)
 
173
 
 
174
    def move(self, rel_from, rel_to):
 
175
        """Move the item at rel_from to the location at rel_to"""
 
176
        path_from = self.abspath(rel_from)
 
177
        path_to = self.abspath(rel_to)
 
178
 
 
179
        try:
 
180
            # this version will delete the destination if necessary
 
181
            rename(path_from, path_to)
 
182
        except (IOError, OSError),e:
 
183
            # TODO: What about path_to?
 
184
            self._translate_error(e, path_from)
 
185
 
 
186
    def delete(self, relpath):
 
187
        """Delete the item at relpath"""
 
188
        path = relpath
 
189
        try:
 
190
            path = self.abspath(relpath)
 
191
            os.remove(path)
 
192
        except (IOError, OSError),e:
 
193
            # TODO: What about path_to?
 
194
            self._translate_error(e, path)
 
195
 
 
196
    def copy_to(self, relpaths, other, mode=None, pb=None):
 
197
        """Copy a set of entries from self into another Transport.
 
198
 
 
199
        :param relpaths: A list/generator of entries to be copied.
 
200
        """
 
201
        if isinstance(other, LocalTransport):
 
202
            # Both from & to are on the local filesystem
 
203
            # Unfortunately, I can't think of anything faster than just
 
204
            # copying them across, one by one :(
 
205
            import shutil
 
206
 
 
207
            total = self._get_total(relpaths)
 
208
            count = 0
 
209
            for path in relpaths:
 
210
                self._update_pb(pb, 'copy-to', count, total)
 
211
                try:
 
212
                    mypath = self.abspath(path)
 
213
                    otherpath = other.abspath(path)
 
214
                    shutil.copy(mypath, otherpath)
 
215
                    if mode is not None:
 
216
                        os.chmod(otherpath, mode)
 
217
                except (IOError, OSError),e:
 
218
                    self._translate_error(e, path)
 
219
                count += 1
 
220
            return count
 
221
        else:
 
222
            return super(LocalTransport, self).copy_to(relpaths, other, mode=mode, pb=pb)
 
223
 
 
224
    def listable(self):
 
225
        """See Transport.listable."""
 
226
        return True
 
227
 
 
228
    def list_dir(self, relpath):
 
229
        """Return a list of all files at the given location.
 
230
        WARNING: many transports do not support this, so trying avoid using
 
231
        it if at all possible.
 
232
        """
 
233
        path = self.abspath(relpath)
 
234
        try:
 
235
            return [urllib.quote(entry) for entry in os.listdir(path)]
 
236
        except (IOError, OSError), e:
 
237
            self._translate_error(e, path)
 
238
 
 
239
    def stat(self, relpath):
 
240
        """Return the stat information for a file.
 
241
        """
 
242
        path = relpath
 
243
        try:
 
244
            path = self.abspath(relpath)
 
245
            return os.stat(path)
 
246
        except (IOError, OSError),e:
 
247
            self._translate_error(e, path)
 
248
 
 
249
    def lock_read(self, relpath):
 
250
        """Lock the given file for shared (read) access.
 
251
        :return: A lock object, which should be passed to Transport.unlock()
 
252
        """
 
253
        from bzrlib.lock import ReadLock
 
254
        path = relpath
 
255
        try:
 
256
            path = self.abspath(relpath)
 
257
            return ReadLock(path)
 
258
        except (IOError, OSError), e:
 
259
            self._translate_error(e, path)
 
260
 
 
261
    def lock_write(self, relpath):
 
262
        """Lock the given file for exclusive (write) access.
 
263
        WARNING: many transports do not support this, so trying avoid using it
 
264
 
 
265
        :return: A lock object, which should be passed to Transport.unlock()
 
266
        """
 
267
        from bzrlib.lock import WriteLock
 
268
        return WriteLock(self.abspath(relpath))
 
269
 
 
270
    def rmdir(self, relpath):
 
271
        """See Transport.rmdir."""
 
272
        path = relpath
 
273
        try:
 
274
            path = self.abspath(relpath)
 
275
            os.rmdir(path)
 
276
        except (IOError, OSError),e:
 
277
            self._translate_error(e, path)
 
278
 
 
279
    def _can_roundtrip_unix_modebits(self):
 
280
        if sys.platform == 'win32':
 
281
            # anyone else?
 
282
            return False
 
283
        else:
 
284
            return True
 
285
 
 
286
 
 
287
class ScratchTransport(LocalTransport):
 
288
    """A transport that works in a temporary dir and cleans up after itself.
 
289
    
 
290
    The dir only exists for the lifetime of the Python object.
 
291
    Obviously you should not put anything precious in it.
 
292
    """
 
293
 
 
294
    def __init__(self, base=None):
 
295
        if base is None:
 
296
            base = tempfile.mkdtemp()
 
297
        super(ScratchTransport, self).__init__(base)
 
298
 
 
299
    def __del__(self):
 
300
        shutil.rmtree(self.base, ignore_errors=True)
 
301
        mutter("%r destroyed" % self)
 
302
 
 
303
 
 
304
class LocalRelpathServer(Server):
 
305
    """A pretend server for local transports, using relpaths."""
 
306
 
 
307
    def get_url(self):
 
308
        """See Transport.Server.get_url."""
 
309
        return "."
 
310
 
 
311
 
 
312
class LocalAbspathServer(Server):
 
313
    """A pretend server for local transports, using absolute paths."""
 
314
 
 
315
    def get_url(self):
 
316
        """See Transport.Server.get_url."""
 
317
        return os.path.abspath("")
 
318
 
 
319
 
 
320
class LocalURLServer(Server):
 
321
    """A pretend server for local transports, using file:// urls."""
 
322
 
 
323
    def get_url(self):
 
324
        """See Transport.Server.get_url."""
 
325
        # FIXME: \ to / on windows
 
326
        return "file://%s" % os.path.abspath("")
 
327
 
 
328
 
 
329
def get_test_permutations():
 
330
    """Return the permutations to be used in testing."""
 
331
    return [(LocalTransport, LocalRelpathServer),
 
332
            (LocalTransport, LocalAbspathServer),
 
333
            (LocalTransport, LocalURLServer),
 
334
            ]