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

101 statements  

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# 

20 

21"""Safe access to git files.""" 

22 

23import os 

24import sys 

25import warnings 

26from typing import ClassVar, Set 

27 

28 

29def ensure_dir_exists(dirname): 

30 """Ensure a directory exists, creating if necessary.""" 

31 try: 

32 os.makedirs(dirname) 

33 except FileExistsError: 

34 pass 

35 

36 

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 

45 

46 # Defer the tempfile import since it pulls in a lot of other things. 

47 import tempfile 

48 

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) 

68 

69 

70def GitFile(filename, mode="rb", bufsize=-1, mask=0o644): 

71 """Create a file object that obeys the git file locking protocol. 

72 

73 Returns: a builtin file object or a _GitFile object 

74 

75 Note: See _GitFile for a description of the file locking protocol. 

76 

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. 

81 

82 The default file mask makes any created files user-writable and 

83 world-readable. 

84 

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) 

96 

97 

98class FileLocked(Exception): 

99 """File is already locked.""" 

100 

101 def __init__(self, filename, lockfilename) -> None: 

102 self.filename = filename 

103 self.lockfilename = lockfilename 

104 super().__init__(filename, lockfilename) 

105 

106 

107class _GitFile: 

108 """File that follows the git locking protocol for writes. 

109 

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. 

113 

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 """ 

117 

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 } 

141 

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 

158 

159 for method in self.PROXY_METHODS: 

160 setattr(self, method, getattr(self._file, method)) 

161 

162 def abort(self): 

163 """Close and discard the lockfile without overwriting the target. 

164 

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 

176 

177 def close(self): 

178 """Close this file, saving the lockfile over the original. 

179 

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. 

184 

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() 

207 

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() 

212 

213 def __enter__(self): 

214 return self 

215 

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() 

221 

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)