Skip to content

Commit 8577f43

Browse files
committed
Atomicized data.json.write()
1 parent 44b04e3 commit 8577f43

4 files changed

Lines changed: 65 additions & 30 deletions

File tree

remove-json-keys/src/remove_json_keys/lib/data/file.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
def atomic_write(file_path, data, encoding='utf-8'): # to prevent TOCTOU
2+
import os
3+
from pathlib import Path
4+
5+
file_path = Path(file_path)
6+
file_path.parent.mkdir(parents=True, exist_ok=True)
7+
tmp_path = file_path.parent / f'.{file_path.name}.tmp'
8+
9+
try:
10+
with open(tmp_path, 'w', encoding=encoding) as file:
11+
file.write(data) ; file.flush() ; os.fsync(file.fileno())
12+
os.replace(tmp_path, file_path) # atomic rename
13+
except Exception:
14+
if tmp_path.exists() : tmp_path.unlink()
15+
raise
16+
117
def read(file_path, encoding='utf-8'):
218
with open(file_path, 'r', encoding=encoding) as file:
319
return file.read()

remove-json-keys/src/remove_json_keys/lib/data/json.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -43,19 +43,20 @@ def remove_keys(json_path, keys):
4343
files_processed_cnt += 1
4444
return keys_removed, keys_skipped, files_processed_cnt
4545

46-
def write(file_path, data, encoding='utf-8', ensure_ascii=False, style='pretty'):
46+
def write(file_path, data, encoding='utf-8', ensure_ascii=False, style='pretty', atomic=True):
4747
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
48-
with open(file_path, 'w', encoding=encoding) as file:
49-
if style == 'pretty': # single key/val spans multi-lines
50-
json.dump(data, file, indent=2, ensure_ascii=ensure_ascii)
51-
elif style == 'compact': # single key/val per line
52-
file.write('{\n')
53-
items = list(data.items())
54-
for idx, (key, val) in enumerate(items):
55-
line_end = ',' if idx < len(items) -1 else ''
56-
inner = f'{{ {json.dumps(val, ensure_ascii=ensure_ascii)[1:-1]} }}'
57-
file.write(f' "{key}": {inner}{line_end}\n')
58-
file.write('}')
59-
else: # minified to single line
60-
json.dump(data, file, separators=(',', ':'), ensure_ascii=ensure_ascii)
61-
file.write('\n') # trailing newline
48+
if style == 'pretty': # single key/val spans multi-lines
49+
json_str = json.dumps(data, indent=2, ensure_ascii=ensure_ascii)
50+
elif style == 'compact': # single key/val per line
51+
lines = ['{']
52+
items = list(data.items())
53+
for idx, (key, val) in enumerate(items):
54+
line_end = ',' if idx < len(items) -1 else ''
55+
inner = f'{{ {json.dumps(val, ensure_ascii=ensure_ascii)[1:-1]} }}'
56+
lines.append(f' "{key}": {inner}{line_end}')
57+
lines.append('}')
58+
json_str = '\n'.join(lines)
59+
else: # minified
60+
json_str = json.dumps(data, separators=(',', ':'), ensure_ascii=ensure_ascii)
61+
json_str += '\n'
62+
getattr(file, 'atomic_write' if atomic else 'write')(file_path, json_str, encoding=encoding)

translate-messages/src/translate_messages/lib/data/file.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,19 @@
1+
def atomic_write(file_path, data, encoding='utf-8'): # to prevent TOCTOU
2+
import os
3+
from pathlib import Path
4+
5+
file_path = Path(file_path)
6+
file_path.parent.mkdir(parents=True, exist_ok=True)
7+
tmp_path = file_path.parent / f'.{file_path.name}.tmp'
8+
9+
try:
10+
with open(tmp_path, 'w', encoding=encoding) as file:
11+
file.write(data) ; file.flush() ; os.fsync(file.fileno())
12+
os.replace(tmp_path, file_path) # atomic rename
13+
except Exception:
14+
if tmp_path.exists() : tmp_path.unlink()
15+
raise
16+
117
def read(file_path, encoding='utf-8'):
218
with open(file_path, 'r', encoding=encoding) as file:
319
return file.read()

translate-messages/src/translate_messages/lib/data/json.py

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,19 +23,21 @@ def read(file_path, encoding='utf-8'):
2323
with open(file_path, 'r', encoding=encoding) as file:
2424
return json5.load(file)
2525

26-
def write(file_path, data, encoding='utf-8', ensure_ascii=False, style='pretty'):
26+
def write(file_path, data, encoding='utf-8', ensure_ascii=False, style='pretty', atomic=True):
27+
from . import file
2728
Path(file_path).parent.mkdir(parents=True, exist_ok=True)
28-
with open(file_path, 'w', encoding=encoding) as file:
29-
if style == 'pretty': # single key/val spans multi-lines
30-
json.dump(data, file, indent=2, ensure_ascii=ensure_ascii)
31-
elif style == 'compact': # single key/val per line
32-
file.write('{\n')
33-
items = list(data.items())
34-
for idx, (key, val) in enumerate(items):
35-
line_end = ',' if idx < len(items) -1 else ''
36-
inner = f'{{ {json.dumps(val, ensure_ascii=ensure_ascii)[1:-1]} }}'
37-
file.write(f' "{key}": {inner}{line_end}\n')
38-
file.write('}')
39-
else: # minified to single line
40-
json.dump(data, file, separators=(',', ':'), ensure_ascii=ensure_ascii)
41-
file.write('\n') # trailing newline
29+
if style == 'pretty': # single key/val spans multi-lines
30+
json_str = json.dumps(data, indent=2, ensure_ascii=ensure_ascii)
31+
elif style == 'compact': # single key/val per line
32+
lines = ['{']
33+
items = list(data.items())
34+
for idx, (key, val) in enumerate(items):
35+
line_end = ',' if idx < len(items) -1 else ''
36+
inner = f'{{ {json.dumps(val, ensure_ascii=ensure_ascii)[1:-1]} }}'
37+
lines.append(f' "{key}": {inner}{line_end}')
38+
lines.append('}')
39+
json_str = '\n'.join(lines)
40+
else: # minified
41+
json_str = json.dumps(data, separators=(',', ':'), ensure_ascii=ensure_ascii)
42+
json_str += '\n'
43+
getattr(file, 'atomic_write' if atomic else 'write')(file_path, json_str, encoding=encoding)

0 commit comments

Comments
 (0)