123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- # file.py -- Safe access to git files
- # Copyright (C) 2010 Google, Inc.
- #
- # Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
- # General Public License as public by the Free Software Foundation; version 2.0
- # or (at your option) any later version. You can redistribute it and/or
- # modify it under the terms of either of these two licenses.
- #
- # Unless required by applicable law or agreed to in writing, software
- # distributed under the License is distributed on an "AS IS" BASIS,
- # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- # See the License for the specific language governing permissions and
- # limitations under the License.
- #
- # You should have received a copy of the licenses; if not, see
- # <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
- # and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
- # License, Version 2.0.
- #
- """Safe access to git files."""
- import errno
- import io
- import os
- import sys
- import tempfile
- def ensure_dir_exists(dirname):
- """Ensure a directory exists, creating if necessary."""
- try:
- os.makedirs(dirname)
- except OSError as e:
- if e.errno != errno.EEXIST:
- raise
- def _fancy_rename(oldname, newname):
- """Rename file with temporary backup file to rollback if rename fails"""
- if not os.path.exists(newname):
- try:
- os.rename(oldname, newname)
- except OSError:
- raise
- return
- # destination file exists
- try:
- (fd, tmpfile) = tempfile.mkstemp(".tmp", prefix=oldname, dir=".")
- os.close(fd)
- os.remove(tmpfile)
- except OSError:
- # either file could not be created (e.g. permission problem)
- # or could not be deleted (e.g. rude virus scanner)
- raise
- try:
- os.rename(newname, tmpfile)
- except OSError:
- raise # no rename occurred
- try:
- os.rename(oldname, newname)
- except OSError:
- os.rename(tmpfile, newname)
- raise
- os.remove(tmpfile)
- def GitFile(filename, mode='rb', bufsize=-1):
- """Create a file object that obeys the git file locking protocol.
- :return: a builtin file object or a _GitFile object
- :note: See _GitFile for a description of the file locking protocol.
- Only read-only and write-only (binary) modes are supported; r+, w+, and a
- are not. To read and write from the same file, you can take advantage of
- the fact that opening a file for write does not actually open the file you
- request.
- """
- if 'a' in mode:
- raise IOError('append mode not supported for Git files')
- if '+' in mode:
- raise IOError('read/write mode not supported for Git files')
- if 'b' not in mode:
- raise IOError('text mode not supported for Git files')
- if 'w' in mode:
- return _GitFile(filename, mode, bufsize)
- else:
- return io.open(filename, mode, bufsize)
- class FileLocked(Exception):
- """File is already locked."""
- def __init__(self, filename, lockfilename):
- self.filename = filename
- self.lockfilename = lockfilename
- super(FileLocked, self).__init__(filename, lockfilename)
- class _GitFile(object):
- """File that follows the git locking protocol for writes.
- All writes to a file foo will be written into foo.lock in the same
- directory, and the lockfile will be renamed to overwrite the original file
- on close.
- :note: You *must* call close() or abort() on a _GitFile for the lock to be
- released. Typically this will happen in a finally block.
- """
- PROXY_PROPERTIES = set(['closed', 'encoding', 'errors', 'mode', 'name',
- 'newlines', 'softspace'])
- PROXY_METHODS = ('__iter__', 'flush', 'fileno', 'isatty', 'read',
- 'readline', 'readlines', 'seek', 'tell',
- 'truncate', 'write', 'writelines')
- def __init__(self, filename, mode, bufsize):
- self._filename = filename
- if isinstance(self._filename, bytes):
- self._lockfilename = self._filename + b'.lock'
- else:
- self._lockfilename = self._filename + '.lock'
- try:
- fd = os.open(
- self._lockfilename,
- os.O_RDWR | os.O_CREAT | os.O_EXCL |
- getattr(os, "O_BINARY", 0))
- except OSError as e:
- if e.errno == errno.EEXIST:
- raise FileLocked(filename, self._lockfilename)
- raise
- self._file = os.fdopen(fd, mode, bufsize)
- self._closed = False
- for method in self.PROXY_METHODS:
- setattr(self, method, getattr(self._file, method))
- def abort(self):
- """Close and discard the lockfile without overwriting the target.
- If the file is already closed, this is a no-op.
- """
- if self._closed:
- return
- self._file.close()
- try:
- os.remove(self._lockfilename)
- self._closed = True
- except OSError as e:
- # The file may have been removed already, which is ok.
- if e.errno != errno.ENOENT:
- raise
- self._closed = True
- def close(self):
- """Close this file, saving the lockfile over the original.
- :note: If this method fails, it will attempt to delete the lockfile.
- However, it is not guaranteed to do so (e.g. if a filesystem
- becomes suddenly read-only), which will prevent future writes to
- this file until the lockfile is removed manually.
- :raises OSError: if the original file could not be overwritten. The
- lock file is still closed, so further attempts to write to the same
- file object will raise ValueError.
- """
- if self._closed:
- return
- os.fsync(self._file.fileno())
- self._file.close()
- try:
- if getattr(os, 'replace', None) is not None:
- os.replace(self._lockfilename, self._filename)
- else:
- if sys.platform != 'win32':
- os.rename(self._lockfilename, self._filename)
- else:
- # Windows versions prior to Vista don't support atomic
- # renames
- _fancy_rename(self._lockfilename, self._filename)
- finally:
- self.abort()
- def __enter__(self):
- return self
- def __exit__(self, exc_type, exc_val, exc_tb):
- self.close()
- def __getattr__(self, name):
- """Proxy property calls to the underlying file."""
- if name in self.PROXY_PROPERTIES:
- return getattr(self._file, name)
- raise AttributeError(name)
|