Improve portalocker handle locking.

This commit is contained in:
voussoir 2025-11-13 23:05:20 -08:00
parent 67220ab09b
commit 88c78f1aa7

View file

@ -81,7 +81,7 @@ def copy_directory(
file_progressbar=None, file_progressbar=None,
files_per_second=None, files_per_second=None,
hash_class=None, hash_class=None,
lock_source_file=True, lock_files=True,
overwrite=OVERWRITE_OLD, overwrite=OVERWRITE_OLD,
precalcsize=False, precalcsize=False,
skip_symlinks=True, skip_symlinks=True,
@ -161,8 +161,8 @@ def copy_directory(
hash_class: hash_class:
Passed into each `copy_file` as `hash_class`. Passed into each `copy_file` as `hash_class`.
lock_source_file: lock_files:
Passed into each `copy_file` as `lock_source_file`. Passed into each `copy_file` as `lock_files`.
overwrite: overwrite:
Passed into each `copy_file` as `overwrite`. Passed into each `copy_file` as `overwrite`.
@ -308,7 +308,7 @@ def copy_directory(
chunk_size=chunk_size, chunk_size=chunk_size,
dry_run=dry_run, dry_run=dry_run,
hash_class=hash_class, hash_class=hash_class,
lock_source_file=lock_source_file, lock_files=lock_files,
overwrite=overwrite, overwrite=overwrite,
progressbar=file_progressbar, progressbar=file_progressbar,
verify_hash=verify_hash, verify_hash=verify_hash,
@ -350,7 +350,7 @@ def copy_file(
destination_new_root=None, destination_new_root=None,
dry_run=False, dry_run=False,
hash_class=None, hash_class=None,
lock_source_file=True, lock_files=True,
overwrite=OVERWRITE_OLD, overwrite=OVERWRITE_OLD,
progressbar=None, progressbar=None,
verify_hash=False, verify_hash=False,
@ -406,9 +406,10 @@ def copy_file(
needing overwrite, this won't be set, so be prepared to handle None. needing overwrite, this won't be set, so be prepared to handle None.
If None, the hash will not be calculated. If None, the hash will not be calculated.
lock_source_file: lock_files:
If True, attempt to lock the source file from being modified while we If True, attempt to lock the source file and destination file while we
are copying it, to prevent corruption. are copying it, to prevent corruption. Only works if portalocker is
installed.
overwrite: overwrite:
This option decides what to do when the destination file already exists. This option decides what to do when the destination file already exists.
@ -494,8 +495,22 @@ def copy_file(
def handlehelper(path, mode): def handlehelper(path, mode):
try: try:
handle = path.open(mode) if lock_files and portalocker is not None:
return handle log.loud(f'Locking {path.absolute_path}.')
lock = portalocker.Lock(
path.absolute_path,
mode=mode,
flags=portalocker.LockFlags.EXCLUSIVE | portalocker.LockFlags.NON_BLOCKING,
timeout=10,
)
handle = lock.acquire()
return (handle, lock)
else:
lock = None
handle = path.open(mode)
return (handle, lock)
except portalocker.exceptions.LockException:
raise
except PermissionError as exception: except PermissionError as exception:
if callback_permission_denied is not None: if callback_permission_denied is not None:
callback_permission_denied(exception) callback_permission_denied(exception)
@ -505,29 +520,20 @@ def copy_file(
log.loud('Copying file %s', source.absolute_path) log.loud('Copying file %s', source.absolute_path)
log.loud('Opening source handle.') log.loud('Opening source handle.')
source_handle = handlehelper(source, 'rb') (source_handle, source_lock) = handlehelper(source, 'rb')
if source_handle is None: if source_handle is None:
return results return results
source_file_lock = None
if lock_source_file and portalocker is not None:
log.loud('Locking source file.')
try:
source_file_lock = portalocker.Lock(
source.absolute_path,
mode='rb',
flags=portalocker.LockFlags.EXCLUSIVE | portalocker.LockFlags.NON_BLOCKING,
timeout=10,
)
source_file_lock.acquire()
except portalocker.exceptions.LockException:
pass
log.loud('Opening dest handle.') log.loud('Opening dest handle.')
destination_handle = handlehelper(destination, 'wb') if destination.is_file:
os.chmod(destination.absolute_path, 0o777)
(destination_handle, destination_lock) = handlehelper(destination, 'wb')
if destination_handle is None: if destination_handle is None:
if source_lock:
source_lock.release()
source_handle.close() source_handle.close()
return results return results
os.utime(destination.absolute_path, (315600000, 315600000))
if hash_class is not None: if hash_class is not None:
results.hash = hash_class() results.hash = hash_class()
@ -570,8 +576,11 @@ def copy_file(
chunk_time = time.perf_counter() - chunk_start chunk_time = time.perf_counter() - chunk_start
chunk_size = dynamic_chunk_sizer(chunk_size, chunk_time, IDEAL_CHUNK_TIME) chunk_size = dynamic_chunk_sizer(chunk_size, chunk_time, IDEAL_CHUNK_TIME)
if source_file_lock is not None: if source_lock is not None:
source_file_lock.release() source_lock.release()
if destination_lock is not None:
destination_lock.release()
progressbar.done() progressbar.done()