Update file contents atomically. Must be used as a context manager (defining the scope of the transaction). On a journaling filesystem the file contents are always updated atomically and won't become corrupted, even on power failures or crashes (for caveats see SyncFile).
| 220 | |
| 221 | |
| 222 | class SaveFile: |
| 223 | """ |
| 224 | Update file contents atomically. |
| 225 | |
| 226 | Must be used as a context manager (defining the scope of the transaction). |
| 227 | |
| 228 | On a journaling filesystem the file contents are always updated |
| 229 | atomically and won't become corrupted, even on power failures or |
| 230 | crashes (for caveats see SyncFile). |
| 231 | |
| 232 | SaveFile can safely be used in parallel (e.g., by multiple processes) to write |
| 233 | to the same target path. Whatever writer finishes last (executes the os.replace |
| 234 | last) "wins" and has successfully written its content to the target path. |
| 235 | Internally used temporary files are created in the target directory and are |
| 236 | named <BASENAME>-<RANDOMCHARS>.tmp and cleaned up in normal and error conditions. |
| 237 | """ |
| 238 | |
| 239 | def __init__(self, path, binary=False): |
| 240 | self.binary = binary |
| 241 | self.path = path |
| 242 | path_obj = Path(path) |
| 243 | self.dir = str(path_obj.parent) |
| 244 | self.tmp_prefix = path_obj.name + "-" |
| 245 | self.tmp_fd = None # OS-level fd |
| 246 | self.tmp_fname = None # full path/filename corresponding to self.tmp_fd |
| 247 | self.f = None # Python file-like SyncFile |
| 248 | |
| 249 | def __enter__(self): |
| 250 | from .. import platform |
| 251 | from ..helpers.fs import mkstemp_mode |
| 252 | |
| 253 | self.tmp_fd, self.tmp_fname = mkstemp_mode(prefix=self.tmp_prefix, suffix=".tmp", dir=self.dir, mode=0o666) |
| 254 | self.f = platform.SyncFile(self.tmp_fname, fd=self.tmp_fd, binary=self.binary) |
| 255 | return self.f |
| 256 | |
| 257 | def __exit__(self, exc_type, exc_val, exc_tb): |
| 258 | from .. import platform |
| 259 | |
| 260 | self.f.close() # this indirectly also closes self.tmp_fd |
| 261 | self.tmp_fd = None |
| 262 | if exc_type is not None: |
| 263 | safe_unlink(self.tmp_fname) # with-body has failed, clean up tmp file |
| 264 | return # continue processing the exception normally |
| 265 | |
| 266 | try: |
| 267 | os.replace(self.tmp_fname, self.path) # POSIX: atomic rename |
| 268 | except OSError: |
| 269 | safe_unlink(self.tmp_fname) # rename has failed, clean up tmp file |
| 270 | raise |
| 271 | finally: |
| 272 | platform.sync_dir(self.dir) |
| 273 | |
| 274 | |
| 275 | def swidth(s): |
no outgoing calls
no test coverage detected