|
1 | 1 | import hashlib |
2 | 2 | import os |
3 | 3 | import shutil |
| 4 | +import glob |
4 | 5 | import sys |
5 | 6 | import re |
6 | 7 | import json |
7 | 8 | import getpass |
| 9 | +import tempfile |
| 10 | +import fcntl |
8 | 11 | from collections import OrderedDict |
9 | 12 | from cf_remote import log |
10 | 13 | from datetime import datetime |
@@ -226,15 +229,56 @@ def print_progress_dot(*args): |
226 | 229 | sys.stdout.flush() # STDOUT is line-buffered |
227 | 230 |
|
228 | 231 |
|
| 232 | +# atomic copy, see lock-free whack-a-mole algorithm |
| 233 | +# https://www.cl.cam.ac.uk/techreports/UCAM-CL-TR-697.pdf#page=66 |
229 | 234 | def copy_file(input_path, output_path): |
230 | | - filename = os.path.basename(input_path) |
231 | | - output_dir = os.path.dirname(output_path) |
| 235 | + assert not input_path.endswith("/") |
232 | 236 |
|
233 | | - tmp_filename = ".{}.tmp".format(filename) |
234 | | - tmp_output_path = os.path.join(output_dir, tmp_filename) |
| 237 | + output_filename = os.path.basename(output_path) |
| 238 | + if not output_filename: |
| 239 | + output_filename = os.path.basename(input_path) |
235 | 240 |
|
236 | | - shutil.copyfile(input_path, tmp_output_path) |
237 | | - os.rename(tmp_output_path, output_path) |
| 241 | + output_dirname = os.path.dirname(output_path) |
| 242 | + tmp_fd, tmp_path = tempfile.mkstemp( |
| 243 | + ".tmp", "{}-".format(output_filename), output_dirname |
| 244 | + ) |
| 245 | + |
| 246 | + # copy input content to tmp |
| 247 | + |
| 248 | + with open(input_path, "r") as input_file: |
| 249 | + input_fd = input_file.fileno() |
| 250 | + |
| 251 | + fcntl.flock(input_fd, fcntl.LOCK_SH) |
| 252 | + shutil.copy(input_path, tmp_path) |
| 253 | + fcntl.flock(input_fd, fcntl.LOCK_UN) |
| 254 | + |
| 255 | + # rename tmp to tmp.mole |
| 256 | + |
| 257 | + my_mole = "{}.mole".format(tmp_path) |
| 258 | + os.rename(tmp_path, my_mole) |
| 259 | + os.close(tmp_fd) |
| 260 | + |
| 261 | + glob_pattern = "{}-*.tmp.mole".format(output_filename) |
| 262 | + moles = glob.glob(os.path.join(output_dirname, glob_pattern)) |
| 263 | + for mole in moles: |
| 264 | + mole = os.path.join(output_dirname, mole) |
| 265 | + if mole == my_mole: |
| 266 | + continue |
| 267 | + |
| 268 | + mole_to_whack, my_mole = sorted((mole, my_mole)) |
| 269 | + try: |
| 270 | + os.remove(mole_to_whack) |
| 271 | + except OSError: |
| 272 | + pass |
| 273 | + try: |
| 274 | + with open(output_path, "a") as output_file: |
| 275 | + output_fd = output_file.fileno() |
| 276 | + |
| 277 | + fcntl.flock(output_fd, fcntl.LOCK_EX) |
| 278 | + os.rename(my_mole, output_path) |
| 279 | + fcntl.flock(output_fd, fcntl.LOCK_UN) |
| 280 | + except OSError: |
| 281 | + pass |
238 | 282 |
|
239 | 283 |
|
240 | 284 | def is_different_checksum(checksum, content): |
|
0 commit comments