Safe atomic write to filesystem by writing to temp file + atomic rename
(path: Union[Path, str], contents: Union[dict, str, bytes], overwrite: bool=True)
| 79 | |
| 80 | @enforce_types |
| 81 | def atomic_write(path: Union[Path, str], contents: Union[dict, str, bytes], overwrite: bool=True) -> None: |
| 82 | """Safe atomic write to filesystem by writing to temp file + atomic rename""" |
| 83 | |
| 84 | mode = 'wb+' if isinstance(contents, bytes) else 'w' |
| 85 | encoding = None if isinstance(contents, bytes) else 'utf-8' # enforce utf-8 on all text writes |
| 86 | |
| 87 | # print('\n> Atomic Write:', mode, path, len(contents), f'overwrite={overwrite}') |
| 88 | try: |
| 89 | with lib_atomic_write(path, mode=mode, overwrite=overwrite, encoding=encoding) as f: |
| 90 | if isinstance(contents, dict): |
| 91 | dump(contents, f, indent=4, sort_keys=True, cls=ExtendedEncoder) |
| 92 | elif isinstance(contents, (bytes, str)): |
| 93 | f.write(contents) |
| 94 | except OSError as e: |
| 95 | if ENFORCE_ATOMIC_WRITES: |
| 96 | print(f"[X] OSError: Failed to write {path} with fcntl.F_FULLFSYNC. ({e})") |
| 97 | print(" You can store the archive/ subfolder on a hard drive or network share that doesn't support support syncronous writes,") |
| 98 | print(" but the main folder containing the index.sqlite3 and ArchiveBox.conf files must be on a filesystem that supports FSYNC.") |
| 99 | raise SystemExit(1) |
| 100 | |
| 101 | # retry the write without forcing FSYNC (aka atomic mode) |
| 102 | with open(path, mode=mode, encoding=encoding) as f: |
| 103 | if isinstance(contents, dict): |
| 104 | dump(contents, f, indent=4, sort_keys=True, cls=ExtendedEncoder) |
| 105 | elif isinstance(contents, (bytes, str)): |
| 106 | f.write(contents) |
| 107 | |
| 108 | # set file permissions |
| 109 | os.chmod(path, int(OUTPUT_PERMISSIONS, base=8)) |
| 110 | |
| 111 | @enforce_types |
| 112 | def chmod_file(path: str, cwd: str='.') -> None: |
no outgoing calls
no test coverage detected