- Created `install_and_run.bat` for Windows installation and setup. - Created `install_and_run.sh` for Unix-based systems installation and setup. - Removed `main.py` as it is no longer needed. - Updated `requirements.txt` to specify package versions and added PyQt5. - Deleted `start.bat` as it is redundant. - Added unit tests for core functionality and scraping modes. - Implemented input validation utilities in `utils/validators.py`. - Added support for dual scraping modes in the scraper.
309 lines
9.1 KiB
Python
309 lines
9.1 KiB
Python
"""
|
|
Simple JSON-based credential storage system for EBoek.info scraper.
|
|
"""
|
|
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import stat
|
|
|
|
|
|
class CredentialManager:
|
|
"""
|
|
Manages storage and retrieval of user credentials in a JSON config file.
|
|
|
|
Credentials are stored in the user's home directory in a hidden folder
|
|
with appropriate file permissions for basic security.
|
|
"""
|
|
|
|
def __init__(self, app_name="eboek_scraper"):
|
|
"""
|
|
Initialize the credential manager.
|
|
|
|
Args:
|
|
app_name (str): Application name for config directory
|
|
"""
|
|
self.app_name = app_name
|
|
self.config_dir = Path.home() / f".{app_name}"
|
|
self.config_file = self.config_dir / "config.json"
|
|
self._ensure_config_dir()
|
|
|
|
def _ensure_config_dir(self):
|
|
"""
|
|
Ensure the configuration directory exists with appropriate permissions.
|
|
"""
|
|
try:
|
|
if not self.config_dir.exists():
|
|
self.config_dir.mkdir(mode=0o700, exist_ok=True) # Only user can read/write/execute
|
|
|
|
# Ensure directory has correct permissions (user only)
|
|
if os.name != 'nt': # Unix-like systems (macOS, Linux)
|
|
os.chmod(self.config_dir, stat.S_IRWXU) # 700 permissions
|
|
|
|
except Exception as e:
|
|
# If we can't create the config directory, fall back to current directory
|
|
self.config_dir = Path(".")
|
|
self.config_file = self.config_dir / f".{self.app_name}_config.json"
|
|
|
|
def _load_config(self):
|
|
"""
|
|
Load the configuration file.
|
|
|
|
Returns:
|
|
dict: Configuration data, empty dict if file doesn't exist
|
|
"""
|
|
try:
|
|
if self.config_file.exists():
|
|
with open(self.config_file, 'r', encoding='utf-8') as f:
|
|
return json.load(f)
|
|
except (json.JSONDecodeError, IOError, PermissionError) as e:
|
|
# If there's any error reading the config, return empty dict
|
|
pass
|
|
|
|
return {}
|
|
|
|
def _save_config(self, config_data):
|
|
"""
|
|
Save configuration data to file.
|
|
|
|
Args:
|
|
config_data (dict): Configuration data to save
|
|
|
|
Returns:
|
|
bool: True if saved successfully, False otherwise
|
|
"""
|
|
try:
|
|
with open(self.config_file, 'w', encoding='utf-8') as f:
|
|
json.dump(config_data, f, indent=2, ensure_ascii=False)
|
|
|
|
# Set file permissions to be readable/writable by user only
|
|
if os.name != 'nt': # Unix-like systems
|
|
os.chmod(self.config_file, stat.S_IRUSR | stat.S_IWUSR) # 600 permissions
|
|
|
|
return True
|
|
|
|
except (IOError, PermissionError) as e:
|
|
return False
|
|
|
|
def save_credentials(self, username, password, remember=True):
|
|
"""
|
|
Save user credentials to the config file.
|
|
|
|
Args:
|
|
username (str): EBoek.info username
|
|
password (str): EBoek.info password
|
|
remember (bool): Whether to save credentials for future use
|
|
|
|
Returns:
|
|
bool: True if saved successfully, False otherwise
|
|
"""
|
|
if not remember:
|
|
# If remember is False, just clear any existing credentials
|
|
return self.clear_credentials()
|
|
|
|
try:
|
|
config = self._load_config()
|
|
|
|
config['credentials'] = {
|
|
'username': username,
|
|
'password': password,
|
|
'saved_at': str(Path.home()), # Just to know which user saved it
|
|
}
|
|
|
|
return self._save_config(config)
|
|
|
|
except Exception as e:
|
|
return False
|
|
|
|
def load_credentials(self):
|
|
"""
|
|
Load stored credentials.
|
|
|
|
Returns:
|
|
dict or None: Dictionary with 'username' and 'password' keys if found,
|
|
None if no credentials are stored
|
|
"""
|
|
try:
|
|
config = self._load_config()
|
|
credentials = config.get('credentials')
|
|
|
|
if credentials and 'username' in credentials and 'password' in credentials:
|
|
return {
|
|
'username': credentials['username'],
|
|
'password': credentials['password']
|
|
}
|
|
|
|
except Exception as e:
|
|
pass
|
|
|
|
return None
|
|
|
|
def has_saved_credentials(self):
|
|
"""
|
|
Check if there are saved credentials available.
|
|
|
|
Returns:
|
|
bool: True if credentials are available, False otherwise
|
|
"""
|
|
return self.load_credentials() is not None
|
|
|
|
def get_saved_username(self):
|
|
"""
|
|
Get the saved username without the password.
|
|
|
|
Returns:
|
|
str or None: Saved username if available, None otherwise
|
|
"""
|
|
credentials = self.load_credentials()
|
|
return credentials['username'] if credentials else None
|
|
|
|
def clear_credentials(self):
|
|
"""
|
|
Remove stored credentials from the config file.
|
|
|
|
Returns:
|
|
bool: True if cleared successfully, False otherwise
|
|
"""
|
|
try:
|
|
config = self._load_config()
|
|
|
|
if 'credentials' in config:
|
|
del config['credentials']
|
|
return self._save_config(config)
|
|
|
|
return True # No credentials to clear is success
|
|
|
|
except Exception as e:
|
|
return False
|
|
|
|
def validate_credentials(self, username, password):
|
|
"""
|
|
Basic validation of credential format.
|
|
|
|
Args:
|
|
username (str): Username to validate
|
|
password (str): Password to validate
|
|
|
|
Returns:
|
|
dict: Validation result with 'valid' bool and 'errors' list
|
|
"""
|
|
errors = []
|
|
|
|
if not username or not username.strip():
|
|
errors.append("Username cannot be empty")
|
|
elif len(username.strip()) < 2:
|
|
errors.append("Username must be at least 2 characters")
|
|
|
|
if not password or not password.strip():
|
|
errors.append("Password cannot be empty")
|
|
elif len(password) < 3:
|
|
errors.append("Password must be at least 3 characters")
|
|
|
|
return {
|
|
'valid': len(errors) == 0,
|
|
'errors': errors
|
|
}
|
|
|
|
def get_config_file_path(self):
|
|
"""
|
|
Get the path to the configuration file.
|
|
|
|
Returns:
|
|
Path: Path to the config file
|
|
"""
|
|
return self.config_file
|
|
|
|
def save_app_settings(self, settings):
|
|
"""
|
|
Save application settings (non-credential settings).
|
|
|
|
Args:
|
|
settings (dict): Application settings to save
|
|
|
|
Returns:
|
|
bool: True if saved successfully, False otherwise
|
|
"""
|
|
try:
|
|
config = self._load_config()
|
|
config['app_settings'] = settings
|
|
return self._save_config(config)
|
|
except Exception as e:
|
|
return False
|
|
|
|
def load_app_settings(self):
|
|
"""
|
|
Load application settings (non-credential settings).
|
|
|
|
Returns:
|
|
dict: Application settings, empty dict if none saved
|
|
"""
|
|
try:
|
|
config = self._load_config()
|
|
return config.get('app_settings', {})
|
|
except Exception as e:
|
|
return {}
|
|
|
|
def get_default_settings(self):
|
|
"""
|
|
Get default application settings.
|
|
|
|
Returns:
|
|
dict: Default settings
|
|
"""
|
|
return {
|
|
'headless_mode': True,
|
|
'verbose_logging': False,
|
|
'auto_save_credentials': True,
|
|
'download_path': str(Path.home() / "Downloads"),
|
|
'default_start_page': 1,
|
|
'default_end_page': 1,
|
|
'scraping_mode': 0 # 0=All Comics, 1=Latest Comics
|
|
}
|
|
|
|
def export_settings(self, export_path):
|
|
"""
|
|
Export settings (excluding credentials) to a file.
|
|
|
|
Args:
|
|
export_path (str or Path): Path to export settings to
|
|
|
|
Returns:
|
|
bool: True if exported successfully, False otherwise
|
|
"""
|
|
try:
|
|
config = self._load_config()
|
|
# Remove credentials from export
|
|
export_config = {k: v for k, v in config.items() if k != 'credentials'}
|
|
|
|
with open(export_path, 'w', encoding='utf-8') as f:
|
|
json.dump(export_config, f, indent=2, ensure_ascii=False)
|
|
|
|
return True
|
|
except Exception as e:
|
|
return False
|
|
|
|
def import_settings(self, import_path):
|
|
"""
|
|
Import settings (excluding credentials) from a file.
|
|
|
|
Args:
|
|
import_path (str or Path): Path to import settings from
|
|
|
|
Returns:
|
|
bool: True if imported successfully, False otherwise
|
|
"""
|
|
try:
|
|
with open(import_path, 'r', encoding='utf-8') as f:
|
|
imported_config = json.load(f)
|
|
|
|
# Don't import credentials for security
|
|
if 'credentials' in imported_config:
|
|
del imported_config['credentials']
|
|
|
|
# Merge with existing config
|
|
config = self._load_config()
|
|
config.update(imported_config)
|
|
|
|
return self._save_config(config)
|
|
except Exception as e:
|
|
return False |