feat: Add installation scripts for Windows and Unix-based systems
- 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.
This commit is contained in:
309
core/credentials.py
Normal file
309
core/credentials.py
Normal file
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user