summaryrefslogtreecommitdiff
path: root/python
diff options
context:
space:
mode:
authorJohn Estabrook <jestabro@vyos.io>2025-09-24 18:07:59 -0500
committerJohn Estabrook <jestabro@vyos.io>2025-09-24 19:02:27 -0500
commit629ff86edb9e5f68dd5849c6300a2ad97aaed4a0 (patch)
treeda221ee400125e62e9348617c826e34d2e262bbc /python
parentd222f1accd6d6a5bda92dd570c2761ad5a343f45 (diff)
downloadvyos-1x-629ff86edb9e5f68dd5849c6300a2ad97aaed4a0.tar.gz
vyos-1x-629ff86edb9e5f68dd5849c6300a2ad97aaed4a0.zip
T7709: add utils write_file_sync and write_file_atomic
write_file_sync does an explicit fsync of file and directory on write. write_file_atomic calls write_file_sync on a temp file and uses os.rename to provide an 'atomic' write.
Diffstat (limited to 'python')
-rw-r--r--python/vyos/utils/file.py83
1 files changed, 83 insertions, 0 deletions
diff --git a/python/vyos/utils/file.py b/python/vyos/utils/file.py
index 1e2de2b39..491cc6547 100644
--- a/python/vyos/utils/file.py
+++ b/python/vyos/utils/file.py
@@ -14,6 +14,8 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import tempfile
+
from vyos.utils.permission import chown
def makedir(path, user=None, group=None):
@@ -185,3 +187,84 @@ def wait_for_file_write_complete(file_path, pre_hook=None, timeout=None, sleep_i
""" Waits for a process to close a file after opening it in write mode. """
wait_for_inotify(file_path,
event_type='IN_CLOSE_WRITE', pre_hook=pre_hook, timeout=timeout, sleep_interval=sleep_interval)
+
+
+def copy_chown(source, target):
+ # pylint: disable=import-outside-toplevel
+ import shutil
+ import stat
+
+ shutil.copy2(source, target)
+ st = os.stat(source)
+ os.chown(target, st[stat.ST_UID], st[stat.ST_GID])
+
+
+def write_file_sync(file_path, data: str, mode='w'):
+ """Write file with explicit sync of file and directory"""
+ # pylint: disable=consider-using-with
+ file_dir = os.path.dirname(file_path)
+
+ # write and sync file
+ try:
+ file = open(file_path, mode)
+ file.write(data)
+ file.flush()
+ os.fsync(file.fileno())
+ file.close()
+ except OSError as e:
+ try:
+ file.close()
+ except OSError:
+ pass
+ raise e
+
+ # sync directory entry
+ try:
+ fd = os.open(file_dir, os.O_DIRECTORY | os.O_RDONLY)
+ os.fsync(fd)
+ os.close(fd)
+ except OSError as e:
+ try:
+ os.close(fd)
+ except OSError:
+ pass
+ raise e
+
+
+def write_file_atomic(file_path, data: str, mode='w'):
+ """Use os.rename for 'atomic' write.
+
+ Note that this requires an euid/egid of that of the source file for the
+ chown operation.
+
+ Note that this calls write_file_sync, above.
+ """
+ # pylint: disable=consider-using-with,raise-missing-from
+ file_dir = os.path.dirname(file_path)
+ temp_file = tempfile.NamedTemporaryFile(delete=False, dir=file_dir).name
+
+ def cleanup():
+ if os.path.exists(temp_file):
+ try:
+ os.unlink(temp_file)
+ except OSError:
+ pass
+
+ if os.path.exists(file_path):
+ try:
+ copy_chown(file_path, temp_file)
+ except OSError as e:
+ cleanup()
+ raise OSError(f'copy_chown {e}')
+
+ try:
+ write_file_sync(temp_file, data, mode=mode)
+ except OSError as e:
+ cleanup()
+ raise OSError(f'write_file_sync {e}')
+
+ try:
+ os.rename(temp_file, file_path)
+ except OSError as e:
+ cleanup()
+ raise OSError(f'rename {e}')