mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2025-09-08 23:38:49 +00:00
Use portalocker for cross-platform locking
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
# main.py
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import logging
|
||||
import signal
|
||||
@@ -23,13 +24,13 @@ def configure_logging():
|
||||
logger.removeHandler(handler)
|
||||
|
||||
# Ensure the 'logs' directory exists
|
||||
log_directory = "logs"
|
||||
if not os.path.exists(log_directory):
|
||||
os.makedirs(log_directory)
|
||||
log_directory = Path("logs")
|
||||
if not log_directory.exists():
|
||||
log_directory.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Create handlers
|
||||
c_handler = logging.StreamHandler(sys.stdout)
|
||||
f_handler = logging.FileHandler(os.path.join(log_directory, "main.log"))
|
||||
f_handler = logging.FileHandler(log_directory / "main.log")
|
||||
|
||||
# Set levels: only errors and critical messages will be shown in the console
|
||||
c_handler.setLevel(logging.ERROR)
|
||||
|
@@ -17,13 +17,12 @@ from monstr.event.event import Event
|
||||
|
||||
import threading
|
||||
import uuid
|
||||
import fcntl
|
||||
|
||||
from .key_manager import KeyManager
|
||||
from .encryption_manager import EncryptionManager
|
||||
from .event_handler import EventHandler
|
||||
from constants import APP_DIR
|
||||
from utils.file_lock import lock_file
|
||||
from utils.file_lock import exclusive_lock
|
||||
|
||||
# Get the logger for this module
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -416,7 +415,7 @@ class NostrClient:
|
||||
json.dumps(data).encode("utf-8")
|
||||
)
|
||||
index_file_path = self.fingerprint_dir / "seedpass_passwords_db.json.enc"
|
||||
with lock_file(index_file_path, fcntl.LOCK_EX):
|
||||
with exclusive_lock(index_file_path):
|
||||
with open(index_file_path, "wb") as f:
|
||||
f.write(encrypted_data)
|
||||
logger.debug(f"Encrypted data saved to {index_file_path}.")
|
||||
@@ -442,7 +441,7 @@ class NostrClient:
|
||||
|
||||
checksum_file = self.fingerprint_dir / "seedpass_passwords_db_checksum.txt"
|
||||
|
||||
with lock_file(checksum_file, fcntl.LOCK_EX):
|
||||
with exclusive_lock(checksum_file):
|
||||
with open(checksum_file, "w") as f:
|
||||
f.write(checksum)
|
||||
|
||||
@@ -465,7 +464,7 @@ class NostrClient:
|
||||
:return: Decrypted data as bytes.
|
||||
"""
|
||||
try:
|
||||
with lock_file(file_path, fcntl.LOCK_SH):
|
||||
with exclusive_lock(file_path):
|
||||
with open(file_path, "rb") as f:
|
||||
encrypted_data = f.read()
|
||||
decrypted_data = self.encryption_manager.decrypt_data(encrypted_data)
|
||||
|
@@ -16,11 +16,10 @@ import os
|
||||
import shutil
|
||||
import time
|
||||
import traceback
|
||||
import fcntl
|
||||
from pathlib import Path
|
||||
from termcolor import colored
|
||||
|
||||
from utils.file_lock import lock_file
|
||||
from utils.file_lock import exclusive_lock
|
||||
from constants import APP_DIR
|
||||
|
||||
# Instantiate the logger
|
||||
@@ -144,7 +143,7 @@ class BackupManager:
|
||||
return
|
||||
|
||||
try:
|
||||
with lock_file(backup_file, lock_type=fcntl.LOCK_SH):
|
||||
with exclusive_lock(backup_file):
|
||||
shutil.copy2(backup_file, self.index_file)
|
||||
logger.info(f"Restored the index file from backup '{backup_file}'.")
|
||||
print(
|
||||
|
@@ -24,8 +24,9 @@ from typing import Optional
|
||||
|
||||
from cryptography.fernet import Fernet, InvalidToken
|
||||
from termcolor import colored
|
||||
from utils.file_lock import lock_file # Ensure this utility is correctly implemented
|
||||
import fcntl # For file locking
|
||||
from utils.file_lock import (
|
||||
exclusive_lock,
|
||||
) # Ensure this utility is correctly implemented
|
||||
|
||||
# Instantiate the logger
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -77,7 +78,7 @@ class EncryptionManager:
|
||||
encrypted_data = self.encrypt_data(data)
|
||||
|
||||
# Write the encrypted data to the file with locking
|
||||
with lock_file(self.parent_seed_file, fcntl.LOCK_EX):
|
||||
with exclusive_lock(self.parent_seed_file):
|
||||
with open(self.parent_seed_file, "wb") as f:
|
||||
f.write(encrypted_data)
|
||||
|
||||
@@ -107,7 +108,7 @@ class EncryptionManager:
|
||||
"""
|
||||
try:
|
||||
parent_seed_path = self.fingerprint_dir / "parent_seed.enc"
|
||||
with lock_file(parent_seed_path, fcntl.LOCK_SH):
|
||||
with exclusive_lock(parent_seed_path):
|
||||
with open(parent_seed_path, "rb") as f:
|
||||
encrypted_data = f.read()
|
||||
|
||||
@@ -188,7 +189,7 @@ class EncryptionManager:
|
||||
encrypted_data = self.encrypt_data(data)
|
||||
|
||||
# Write the encrypted data to the file with locking
|
||||
with lock_file(file_path, fcntl.LOCK_EX):
|
||||
with exclusive_lock(file_path):
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(encrypted_data)
|
||||
|
||||
@@ -220,7 +221,7 @@ class EncryptionManager:
|
||||
file_path = self.fingerprint_dir / relative_path
|
||||
|
||||
# Read the encrypted data with locking
|
||||
with lock_file(file_path, fcntl.LOCK_SH):
|
||||
with exclusive_lock(file_path):
|
||||
with open(file_path, "rb") as f:
|
||||
encrypted_data = f.read()
|
||||
|
||||
@@ -351,7 +352,7 @@ class EncryptionManager:
|
||||
checksum_file = file_path.parent / f"{file_path.stem}_checksum.txt"
|
||||
|
||||
# Write the checksum to the file with locking
|
||||
with lock_file(checksum_file, fcntl.LOCK_EX):
|
||||
with exclusive_lock(checksum_file):
|
||||
with open(checksum_file, "w") as f:
|
||||
f.write(checksum)
|
||||
|
||||
@@ -392,7 +393,7 @@ class EncryptionManager:
|
||||
)
|
||||
return None
|
||||
|
||||
with lock_file(self.fingerprint_dir / relative_path, fcntl.LOCK_SH):
|
||||
with exclusive_lock(self.fingerprint_dir / relative_path):
|
||||
with open(self.fingerprint_dir / relative_path, "rb") as file:
|
||||
encrypted_data = file.read()
|
||||
|
||||
|
@@ -29,9 +29,8 @@ from pathlib import Path
|
||||
from termcolor import colored
|
||||
|
||||
from password_manager.encryption import EncryptionManager
|
||||
from utils.file_lock import lock_file
|
||||
from utils.file_lock import exclusive_lock
|
||||
|
||||
import fcntl
|
||||
|
||||
# Instantiate the logger
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -407,7 +406,8 @@ class EntryManager:
|
||||
:param backup_path: The file path of the backup to restore from.
|
||||
"""
|
||||
try:
|
||||
if not os.path.exists(backup_path):
|
||||
backup_path = Path(backup_path)
|
||||
if not backup_path.exists():
|
||||
logger.error(f"Backup file '{backup_path}' does not exist.")
|
||||
print(
|
||||
colored(
|
||||
|
@@ -10,4 +10,5 @@ bcrypt
|
||||
bip85
|
||||
pytest>=7.0
|
||||
pytest-cov
|
||||
portalocker>=2.8
|
||||
|
||||
|
36
src/tests/test_file_lock.py
Normal file
36
src/tests/test_file_lock.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import multiprocessing as mp
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from utils.file_lock import exclusive_lock
|
||||
|
||||
|
||||
def _hold_lock(path: Path, hold_time: float, started: mp.Event):
|
||||
with exclusive_lock(path):
|
||||
started.set()
|
||||
time.sleep(hold_time)
|
||||
|
||||
|
||||
def _try_lock(path: Path, wait_time: mp.Value):
|
||||
t0 = time.perf_counter()
|
||||
with exclusive_lock(path):
|
||||
wait_time.value = time.perf_counter() - t0
|
||||
|
||||
|
||||
def test_exclusive_lock_blocks_until_released(tmp_path: Path):
|
||||
file_path = tmp_path / "locktest.txt"
|
||||
|
||||
started = mp.Event()
|
||||
wait_time = mp.Value("d", 0.0)
|
||||
|
||||
p1 = mp.Process(target=_hold_lock, args=(file_path, 1.0, started))
|
||||
p2 = mp.Process(target=_try_lock, args=(file_path, wait_time))
|
||||
|
||||
p1.start()
|
||||
started.wait()
|
||||
p2.start()
|
||||
|
||||
p1.join()
|
||||
p2.join()
|
||||
|
||||
assert wait_time.value >= 1.0
|
@@ -4,7 +4,7 @@ import logging
|
||||
import traceback
|
||||
|
||||
try:
|
||||
from .file_lock import lock_file
|
||||
from .file_lock import exclusive_lock
|
||||
from .key_derivation import derive_key_from_password, derive_key_from_parent_seed
|
||||
from .checksum import calculate_checksum, verify_checksum
|
||||
from .password_prompt import prompt_for_password
|
||||
@@ -19,6 +19,6 @@ __all__ = [
|
||||
"derive_key_from_parent_seed",
|
||||
"calculate_checksum",
|
||||
"verify_checksum",
|
||||
"lock_file",
|
||||
"exclusive_lock",
|
||||
"prompt_for_password",
|
||||
]
|
||||
|
@@ -1,142 +1,23 @@
|
||||
# utils/file_lock.py
|
||||
|
||||
"""
|
||||
File Lock Module
|
||||
|
||||
This module provides a single context manager, `lock_file`, for acquiring and releasing
|
||||
locks on files using the `fcntl` library. It ensures that critical files are accessed
|
||||
safely, preventing race conditions and maintaining data integrity when multiple processes
|
||||
or threads attempt to read from or write to the same file concurrently.
|
||||
|
||||
I need to change this to something that supports Windows in the future.
|
||||
|
||||
Ensure that all dependencies are installed and properly configured in your environment.
|
||||
"""
|
||||
|
||||
import os
|
||||
import fcntl
|
||||
import logging
|
||||
"""File-based locking utilities using portalocker for cross-platform support."""
|
||||
from contextlib import contextmanager
|
||||
from typing import Generator
|
||||
from typing import Generator, Optional
|
||||
from pathlib import Path
|
||||
from termcolor import colored
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
# Instantiate the logger
|
||||
logger = logging.getLogger(__name__)
|
||||
import portalocker
|
||||
|
||||
|
||||
@contextmanager
|
||||
def lock_file(file_path: Path, lock_type: int) -> Generator[None, None, None]:
|
||||
def exclusive_lock(
|
||||
path: Path, timeout: Optional[float] = None
|
||||
) -> Generator[None, None, None]:
|
||||
"""Context manager that locks *path* exclusively.
|
||||
|
||||
The function opens the file in binary append mode and obtains an
|
||||
exclusive lock using ``portalocker``. If ``timeout`` is provided,
|
||||
acquiring the lock will wait for at most that many seconds before
|
||||
raising ``portalocker.exceptions.LockException``.
|
||||
"""
|
||||
Context manager to acquire a lock on a file.
|
||||
|
||||
Parameters:
|
||||
file_path (Path): The path to the file to lock.
|
||||
lock_type (int): The type of lock to acquire (`fcntl.LOCK_EX` for exclusive,
|
||||
`fcntl.LOCK_SH` for shared).
|
||||
|
||||
Yields:
|
||||
None
|
||||
|
||||
Raises:
|
||||
ValueError: If an invalid lock type is provided.
|
||||
SystemExit: Exits the program if the lock cannot be acquired.
|
||||
"""
|
||||
if lock_type not in (fcntl.LOCK_EX, fcntl.LOCK_SH):
|
||||
logging.error(
|
||||
f"Invalid lock type: {lock_type}. Use fcntl.LOCK_EX or fcntl.LOCK_SH."
|
||||
)
|
||||
print(colored("Error: Invalid lock type provided.", "red"))
|
||||
sys.exit(1)
|
||||
|
||||
file = None
|
||||
try:
|
||||
# Determine the mode based on whether the file exists
|
||||
mode = "rb+" if file_path.exists() else "wb"
|
||||
|
||||
# Open the file
|
||||
file = open(file_path, mode)
|
||||
logging.debug(f"Opened file '{file_path}' in mode '{mode}' for locking.")
|
||||
|
||||
# Acquire the lock
|
||||
fcntl.flock(file, lock_type)
|
||||
lock_type_str = "Exclusive" if lock_type == fcntl.LOCK_EX else "Shared"
|
||||
logging.debug(f"{lock_type_str} lock acquired on '{file_path}'.")
|
||||
yield # Control is transferred to the block inside the `with` statement
|
||||
|
||||
except IOError as e:
|
||||
lock_type_str = "exclusive" if lock_type == fcntl.LOCK_EX else "shared"
|
||||
logging.error(f"Failed to acquire {lock_type_str} lock on '{file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(
|
||||
colored(
|
||||
f"Error: Failed to acquire {lock_type_str} lock on '{file_path}': {e}",
|
||||
"red",
|
||||
)
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
finally:
|
||||
if file:
|
||||
try:
|
||||
# Release the lock
|
||||
fcntl.flock(file, fcntl.LOCK_UN)
|
||||
logging.debug(f"Lock released on '{file_path}'.")
|
||||
except Exception as e:
|
||||
lock_type_str = "exclusive" if lock_type == fcntl.LOCK_EX else "shared"
|
||||
logging.warning(
|
||||
f"Failed to release {lock_type_str} lock on '{file_path}': {e}"
|
||||
)
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(
|
||||
colored(
|
||||
f"Warning: Failed to release {lock_type_str} lock on '{file_path}': {e}",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
finally:
|
||||
# Close the file
|
||||
try:
|
||||
file.close()
|
||||
logging.debug(f"File '{file_path}' closed successfully.")
|
||||
except Exception as e:
|
||||
logging.warning(f"Failed to close file '{file_path}': {e}")
|
||||
logging.error(traceback.format_exc()) # Log full traceback
|
||||
print(
|
||||
colored(
|
||||
f"Warning: Failed to close file '{file_path}': {e}",
|
||||
"yellow",
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def exclusive_lock(file_path: Path) -> Generator[None, None, None]:
|
||||
"""
|
||||
Convenience context manager to acquire an exclusive lock on a file.
|
||||
|
||||
Parameters:
|
||||
file_path (Path): The path to the file to lock.
|
||||
|
||||
Yields:
|
||||
None
|
||||
"""
|
||||
with lock_file(file_path, fcntl.LOCK_EX):
|
||||
yield
|
||||
|
||||
|
||||
@contextmanager
|
||||
def shared_lock(file_path: Path) -> Generator[None, None, None]:
|
||||
"""
|
||||
Convenience context manager to acquire a shared lock on a file.
|
||||
|
||||
Parameters:
|
||||
file_path (Path): The path to the file to lock.
|
||||
|
||||
Yields:
|
||||
None
|
||||
"""
|
||||
with lock_file(file_path, fcntl.LOCK_SH):
|
||||
yield
|
||||
path = Path(path)
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
lock = portalocker.Lock(str(path), mode="a+b", timeout=timeout)
|
||||
with lock as fh:
|
||||
yield fh
|
||||
|
Reference in New Issue
Block a user