cleanup
This commit is contained in:
@@ -48,6 +48,10 @@ class AuthService:
|
||||
self._hash: Optional[str] = settings.master_password_hash
|
||||
# In-memory failed attempts per identifier. Values are dicts with
|
||||
# keys: count, last, locked_until
|
||||
# WARNING: In-memory storage resets on process restart.
|
||||
# This is acceptable for development but PRODUCTION deployments
|
||||
# should use Redis or a database to persist failed login attempts
|
||||
# and prevent bypass via process restart.
|
||||
self._failed: Dict[str, Dict] = {}
|
||||
# Policy
|
||||
self.max_attempts = 5
|
||||
@@ -71,18 +75,42 @@ class AuthService:
|
||||
def setup_master_password(self, password: str) -> None:
|
||||
"""Set the master password (hash and store in memory/settings).
|
||||
|
||||
Enforces strong password requirements:
|
||||
- Minimum 8 characters
|
||||
- Mixed case (upper and lower)
|
||||
- At least one number
|
||||
- At least one special character
|
||||
|
||||
For now we update only the in-memory value and
|
||||
settings.master_password_hash. A future task should persist this
|
||||
to a config file.
|
||||
|
||||
Args:
|
||||
password: The password to set
|
||||
|
||||
Raises:
|
||||
ValueError: If password doesn't meet requirements
|
||||
"""
|
||||
# Length check
|
||||
if len(password) < 8:
|
||||
raise ValueError("Password must be at least 8 characters long")
|
||||
# Basic strength checks
|
||||
|
||||
# Mixed case check
|
||||
if password.islower() or password.isupper():
|
||||
raise ValueError("Password must include mixed case")
|
||||
raise ValueError(
|
||||
"Password must include both uppercase and lowercase letters"
|
||||
)
|
||||
|
||||
# Number check
|
||||
if not any(c.isdigit() for c in password):
|
||||
raise ValueError("Password must include at least one number")
|
||||
|
||||
# Special character check
|
||||
if password.isalnum():
|
||||
# encourage a special character
|
||||
raise ValueError("Password should include a symbol or punctuation")
|
||||
raise ValueError(
|
||||
"Password must include at least one special character "
|
||||
"(symbol or punctuation)"
|
||||
)
|
||||
|
||||
h = self._hash_password(password)
|
||||
self._hash = h
|
||||
|
||||
@@ -77,6 +77,8 @@ class DownloadService:
|
||||
|
||||
# Queue storage by status
|
||||
self._pending_queue: deque[DownloadItem] = deque()
|
||||
# Helper dict for O(1) lookup of pending items by ID
|
||||
self._pending_items_by_id: Dict[str, DownloadItem] = {}
|
||||
self._active_downloads: Dict[str, DownloadItem] = {}
|
||||
self._completed_items: deque[DownloadItem] = deque(maxlen=100)
|
||||
self._failed_items: deque[DownloadItem] = deque(maxlen=50)
|
||||
@@ -107,6 +109,46 @@ class DownloadService:
|
||||
max_retries=max_retries,
|
||||
)
|
||||
|
||||
def _add_to_pending_queue(
|
||||
self, item: DownloadItem, front: bool = False
|
||||
) -> None:
|
||||
"""Add item to pending queue and update helper dict.
|
||||
|
||||
Args:
|
||||
item: Download item to add
|
||||
front: If True, add to front of queue (higher priority)
|
||||
"""
|
||||
if front:
|
||||
self._pending_queue.appendleft(item)
|
||||
else:
|
||||
self._pending_queue.append(item)
|
||||
self._pending_items_by_id[item.id] = item
|
||||
|
||||
def _remove_from_pending_queue(self, item_or_id: str) -> Optional[DownloadItem]: # noqa: E501
|
||||
"""Remove item from pending queue and update helper dict.
|
||||
|
||||
Args:
|
||||
item_or_id: Item ID to remove
|
||||
|
||||
Returns:
|
||||
Removed item or None if not found
|
||||
"""
|
||||
if isinstance(item_or_id, str):
|
||||
item = self._pending_items_by_id.get(item_or_id)
|
||||
if not item:
|
||||
return None
|
||||
item_id = item_or_id
|
||||
else:
|
||||
item = item_or_id
|
||||
item_id = item.id
|
||||
|
||||
try:
|
||||
self._pending_queue.remove(item)
|
||||
del self._pending_items_by_id[item_id]
|
||||
return item
|
||||
except (ValueError, KeyError):
|
||||
return None
|
||||
|
||||
def set_broadcast_callback(self, callback: Callable) -> None:
|
||||
"""Set callback for broadcasting status updates via WebSocket."""
|
||||
self._broadcast_callback = callback
|
||||
@@ -146,14 +188,14 @@ class DownloadService:
|
||||
# Reset status if was downloading when saved
|
||||
if item.status == DownloadStatus.DOWNLOADING:
|
||||
item.status = DownloadStatus.PENDING
|
||||
self._pending_queue.append(item)
|
||||
self._add_to_pending_queue(item)
|
||||
|
||||
# Restore failed items that can be retried
|
||||
for item_dict in data.get("failed", []):
|
||||
item = DownloadItem(**item_dict)
|
||||
if item.retry_count < self._max_retries:
|
||||
item.status = DownloadStatus.PENDING
|
||||
self._pending_queue.append(item)
|
||||
self._add_to_pending_queue(item)
|
||||
else:
|
||||
self._failed_items.append(item)
|
||||
|
||||
@@ -231,10 +273,9 @@ class DownloadService:
|
||||
# Insert based on priority. High-priority downloads jump the
|
||||
# line via appendleft so they execute before existing work;
|
||||
# everything else is appended to preserve FIFO order.
|
||||
if priority == DownloadPriority.HIGH:
|
||||
self._pending_queue.appendleft(item)
|
||||
else:
|
||||
self._pending_queue.append(item)
|
||||
self._add_to_pending_queue(
|
||||
item, front=(priority == DownloadPriority.HIGH)
|
||||
)
|
||||
|
||||
created_ids.append(item.id)
|
||||
|
||||
@@ -293,15 +334,15 @@ class DownloadService:
|
||||
logger.info("Cancelled active download", item_id=item_id)
|
||||
continue
|
||||
|
||||
# Check pending queue
|
||||
for item in list(self._pending_queue):
|
||||
if item.id == item_id:
|
||||
self._pending_queue.remove(item)
|
||||
removed_ids.append(item_id)
|
||||
logger.info(
|
||||
"Removed from pending queue", item_id=item_id
|
||||
)
|
||||
break
|
||||
# Check pending queue - O(1) lookup using helper dict
|
||||
if item_id in self._pending_items_by_id:
|
||||
item = self._pending_items_by_id[item_id]
|
||||
self._pending_queue.remove(item)
|
||||
del self._pending_items_by_id[item_id]
|
||||
removed_ids.append(item_id)
|
||||
logger.info(
|
||||
"Removed from pending queue", item_id=item_id
|
||||
)
|
||||
|
||||
if removed_ids:
|
||||
self._save_queue()
|
||||
@@ -338,24 +379,25 @@ class DownloadService:
|
||||
DownloadServiceError: If reordering fails
|
||||
"""
|
||||
try:
|
||||
# Find and remove item
|
||||
item_to_move = None
|
||||
for item in list(self._pending_queue):
|
||||
if item.id == item_id:
|
||||
self._pending_queue.remove(item)
|
||||
item_to_move = item
|
||||
break
|
||||
# Find and remove item - O(1) lookup using helper dict
|
||||
item_to_move = self._pending_items_by_id.get(item_id)
|
||||
|
||||
if not item_to_move:
|
||||
raise DownloadServiceError(
|
||||
f"Item {item_id} not found in pending queue"
|
||||
)
|
||||
|
||||
# Remove from current position
|
||||
self._pending_queue.remove(item_to_move)
|
||||
del self._pending_items_by_id[item_id]
|
||||
|
||||
# Insert at new position
|
||||
queue_list = list(self._pending_queue)
|
||||
new_position = max(0, min(new_position, len(queue_list)))
|
||||
queue_list.insert(new_position, item_to_move)
|
||||
self._pending_queue = deque(queue_list)
|
||||
# Re-add to helper dict
|
||||
self._pending_items_by_id[item_id] = item_to_move
|
||||
|
||||
self._save_queue()
|
||||
|
||||
@@ -575,7 +617,7 @@ class DownloadService:
|
||||
item.retry_count += 1
|
||||
item.error = None
|
||||
item.progress = None
|
||||
self._pending_queue.append(item)
|
||||
self._add_to_pending_queue(item)
|
||||
retried_ids.append(item.id)
|
||||
|
||||
logger.info(
|
||||
|
||||
Reference in New Issue
Block a user