~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/atomicfile.py

  • Committer: John Arbash Meinel
  • Date: 2006-08-09 00:50:42 UTC
  • mto: This revision was merged to the branch mainline in revision 1912.
  • Revision ID: john@arbash-meinel.com-20060809005042-818f9535c3fd5e7e
Add a test suite for Atomic File, and clean it up so that it really does set the mode properly.

Show diffs side-by-side

added added

removed removed

Lines of Context:
18
18
import codecs
19
19
import errno
20
20
import os
 
21
import stat
21
22
import socket
22
23
import sys
23
24
from warnings import warn
24
25
 
 
26
from bzrlib import (
 
27
    errors,
 
28
    symbol_versioning,
 
29
    )
25
30
from bzrlib.osutils import rename
26
31
 
27
32
# not forksafe - but we dont fork.
28
33
_pid = os.getpid()
 
34
_hostname = socket.gethostname()
 
35
 
 
36
# On win32 O_BINARY will set binary versus text mode
 
37
# but the constant doesn't exist on platforms where it isn't
 
38
# needed
 
39
_binary = getattr(os, 'O_BINARY', 0)
 
40
 
29
41
 
30
42
class AtomicFile(object):
31
43
    """A file that does an atomic-rename to move into place.
34
46
 
35
47
    Open this as for a regular file, then use commit() to move into
36
48
    place or abort() to cancel.
37
 
 
38
 
    An encoding can be specified; otherwise the default is ascii.
39
49
    """
40
50
 
41
 
    __slots__ = ['closed', 'f', 'tmpfilename', 'realfilename', 'write']
 
51
    __slots__ = ['tmpfilename', 'realfilename', '_fd', '_new_mode']
42
52
 
43
53
    def __init__(self, filename, mode='wb', new_mode=0666):
44
 
        self.f = None
 
54
        self._fd = None
45
55
        assert mode in ('wb', 'wt'), \
46
56
            "invalid AtomicFile mode %r" % mode
47
57
 
48
 
        # old version:
49
 
        #self.tmpfilename = '%s.%d.%s.tmp' % (filename, os.getpid(),
50
 
        #                                     socket.gethostname())
51
 
        # new version:
52
 
        # This is 'broken' on NFS: it wmay collide with another NFS client.
53
 
        # however, we use this to write files within a directory that we have
54
 
        # locked, so it being racy on NFS is not a concern. The only other
55
 
        # files we use this for are .bzr.ignore, which can race anyhow.
56
 
        self.tmpfilename = '%s.%d.tmp' % (filename, _pid)
 
58
        self.tmpfilename = '%s.%d.%s.tmp' % (filename, _pid, _hostname)
57
59
 
58
60
        self.realfilename = filename
59
61
        
 
62
        flags = os.O_EXCL | os.O_CREAT | os.O_WRONLY
 
63
        if mode == 'wb':
 
64
            flags |= _binary
 
65
        
 
66
        self._new_mode = new_mode
60
67
        # Use a low level fd operation to avoid chmodding later.
61
 
        fd = os.open(self.tmpfilename, os.O_EXCL | os.O_CREAT | os.O_WRONLY,
62
 
            new_mode)
63
 
        # open a normal python file to get the text vs binary support needed
64
 
        # for windows.
65
 
        self.closed = False
66
 
        try:
67
 
            self.f = os.fdopen(fd, mode)
68
 
        except:
69
 
            os.close(fd)
70
 
            self.closed = True
71
 
            raise
72
 
        self.write = self.f.write
 
68
        # This may not succeed, but it should help most of the time
 
69
        self._fd = os.open(self.tmpfilename, flags, new_mode)
 
70
        st = os.fstat(self._fd)
 
71
        if stat.S_IMODE(st.st_mode) != new_mode:
 
72
            os.chmod(self.tmpfilename, new_mode)
 
73
 
 
74
    def _get_closed(self):
 
75
        symbol_versioning.warn('AtomicFile.closed deprecated in bzr 0.10',
 
76
                               DeprecationWarning, stacklevel=2)
 
77
        return self.f is None
 
78
 
 
79
    closed = property(_get_closed)
73
80
 
74
81
    def __repr__(self):
75
82
        return '%s(%r)' % (self.__class__.__name__,
76
83
                           self.realfilename)
77
84
 
 
85
    def write(self, data):
 
86
        """Write some data to the file. Like file.write()"""
 
87
        os.write(self._fd, data)
 
88
 
 
89
    def _close_tmpfile(self, func_name):
 
90
        """Close the local temp file in preparation for commit or abort"""
 
91
        if self._fd is None:
 
92
            raise errors.AtomicFileAlreadyClosed(path=self.realfilename,
 
93
                                                 function=func_name)
 
94
        fd = self._fd
 
95
        self._fd = None
 
96
        os.close(fd)
 
97
 
78
98
    def commit(self):
79
99
        """Close the file and move to final name."""
80
 
        if self.closed:
81
 
            raise Exception('%r is already closed' % self)
82
 
 
83
 
        f = self.f
84
 
        self.f = None
85
 
        f.close()
 
100
        self._close_tmpfile('commit')
86
101
        rename(self.tmpfilename, self.realfilename)
87
102
 
88
103
    def abort(self):
89
104
        """Discard temporary file without committing changes."""
90
 
 
91
 
        if self.f is None:
92
 
            raise Exception('%r is already closed' % self)
93
 
 
94
 
        f = self.f
95
 
        self.f = None
96
 
        f.close()
 
105
        self._close_tmpfile('abort')
97
106
        os.remove(self.tmpfilename)
98
107
 
99
108
    def close(self):
100
109
        """Discard the file unless already committed."""
101
 
        if self.f is not None:
 
110
        if self._fd is not None:
102
111
            self.abort()
103
112
 
104
113
    def __del__(self):
105
 
        if self.f is not None:
 
114
        if self._fd is not None:
106
115
            warn("%r leaked" % self)