Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/dulwich/file.py: 57%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# file.py -- Safe access to git files
2# Copyright (C) 2010 Google, Inc.
3#
4# Dulwich is dual-licensed under the Apache License, Version 2.0 and the GNU
5# General Public License as public by the Free Software Foundation; version 2.0
6# or (at your option) any later version. You can redistribute it and/or
7# modify it under the terms of either of these two licenses.
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14#
15# You should have received a copy of the licenses; if not, see
16# <http://www.gnu.org/licenses/> for a copy of the GNU General Public License
17# and <http://www.apache.org/licenses/LICENSE-2.0> for a copy of the Apache
18# License, Version 2.0.
19#
21"""Safe access to git files."""
23import os
24import sys
25import warnings
26from typing import ClassVar, Set
29def ensure_dir_exists(dirname):
30 """Ensure a directory exists, creating if necessary."""
31 try:
32 os.makedirs(dirname)
33 except FileExistsError:
34 pass
37def _fancy_rename(oldname, newname):
38 """Rename file with temporary backup file to rollback if rename fails."""
39 if not os.path.exists(newname):
40 try:
41 os.rename(oldname, newname)
42 except OSError:
43 raise
44 return
46 # Defer the tempfile import since it pulls in a lot of other things.
47 import tempfile
49 # destination file exists
50 try:
51 (fd, tmpfile) = tempfile.mkstemp(".tmp", prefix=oldname, dir=".")
52 os.close(fd)
53 os.remove(tmpfile)
54 except OSError:
55 # either file could not be created (e.g. permission problem)
56 # or could not be deleted (e.g. rude virus scanner)
57 raise
58 try:
59 os.rename(newname, tmpfile)
60 except OSError:
61 raise # no rename occurred
62 try:
63 os.rename(oldname, newname)
64 except OSError:
65 os.rename(tmpfile, newname)
66 raise
67 os.remove(tmpfile)
70def GitFile(filename, mode="rb", bufsize=-1, mask=0o644):
71 """Create a file object that obeys the git file locking protocol.
73 Returns: a builtin file object or a _GitFile object
75 Note: See _GitFile for a description of the file locking protocol.
77 Only read-only and write-only (binary) modes are supported; r+, w+, and a
78 are not. To read and write from the same file, you can take advantage of
79 the fact that opening a file for write does not actually open the file you
80 request.
82 The default file mask makes any created files user-writable and
83 world-readable.
85 """
86 if "a" in mode:
87 raise OSError("append mode not supported for Git files")
88 if "+" in mode:
89 raise OSError("read/write mode not supported for Git files")
90 if "b" not in mode:
91 raise OSError("text mode not supported for Git files")
92 if "w" in mode:
93 return _GitFile(filename, mode, bufsize, mask)
94 else:
95 return open(filename, mode, bufsize)
98class FileLocked(Exception):
99 """File is already locked."""
101 def __init__(self, filename, lockfilename) -> None:
102 self.filename = filename
103 self.lockfilename = lockfilename
104 super().__init__(filename, lockfilename)
107class _GitFile:
108 """File that follows the git locking protocol for writes.
110 All writes to a file foo will be written into foo.lock in the same
111 directory, and the lockfile will be renamed to overwrite the original file
112 on close.
114 Note: You *must* call close() or abort() on a _GitFile for the lock to be
115 released. Typically this will happen in a finally block.
116 """
118 PROXY_PROPERTIES: ClassVar[Set[str]] = {
119 "closed",
120 "encoding",
121 "errors",
122 "mode",
123 "name",
124 "newlines",
125 "softspace",
126 }
127 PROXY_METHODS: ClassVar[Set[str]] = {
128 "__iter__",
129 "flush",
130 "fileno",
131 "isatty",
132 "read",
133 "readline",
134 "readlines",
135 "seek",
136 "tell",
137 "truncate",
138 "write",
139 "writelines",
140 }
142 def __init__(self, filename, mode, bufsize, mask) -> None:
143 self._filename = filename
144 if isinstance(self._filename, bytes):
145 self._lockfilename = self._filename + b".lock"
146 else:
147 self._lockfilename = self._filename + ".lock"
148 try:
149 fd = os.open(
150 self._lockfilename,
151 os.O_RDWR | os.O_CREAT | os.O_EXCL | getattr(os, "O_BINARY", 0),
152 mask,
153 )
154 except FileExistsError as exc:
155 raise FileLocked(filename, self._lockfilename) from exc
156 self._file = os.fdopen(fd, mode, bufsize)
157 self._closed = False
159 for method in self.PROXY_METHODS:
160 setattr(self, method, getattr(self._file, method))
162 def abort(self):
163 """Close and discard the lockfile without overwriting the target.
165 If the file is already closed, this is a no-op.
166 """
167 if self._closed:
168 return
169 self._file.close()
170 try:
171 os.remove(self._lockfilename)
172 self._closed = True
173 except FileNotFoundError:
174 # The file may have been removed already, which is ok.
175 self._closed = True
177 def close(self):
178 """Close this file, saving the lockfile over the original.
180 Note: If this method fails, it will attempt to delete the lockfile.
181 However, it is not guaranteed to do so (e.g. if a filesystem
182 becomes suddenly read-only), which will prevent future writes to
183 this file until the lockfile is removed manually.
185 Raises:
186 OSError: if the original file could not be overwritten. The
187 lock file is still closed, so further attempts to write to the same
188 file object will raise ValueError.
189 """
190 if self._closed:
191 return
192 self._file.flush()
193 os.fsync(self._file.fileno())
194 self._file.close()
195 try:
196 if getattr(os, "replace", None) is not None:
197 os.replace(self._lockfilename, self._filename)
198 else:
199 if sys.platform != "win32":
200 os.rename(self._lockfilename, self._filename)
201 else:
202 # Windows versions prior to Vista don't support atomic
203 # renames
204 _fancy_rename(self._lockfilename, self._filename)
205 finally:
206 self.abort()
208 def __del__(self) -> None:
209 if not getattr(self, "_closed", True):
210 warnings.warn(f"unclosed {self!r}", ResourceWarning, stacklevel=2)
211 self.abort()
213 def __enter__(self):
214 return self
216 def __exit__(self, exc_type, exc_val, exc_tb):
217 if exc_type is not None:
218 self.abort()
219 else:
220 self.close()
222 def __getattr__(self, name):
223 """Proxy property calls to the underlying file."""
224 if name in self.PROXY_PROPERTIES:
225 return getattr(self._file, name)
226 raise AttributeError(name)