~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/transport/local.py

Factor out another win32 special case and add platform independent tests for it.

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