143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
# services/profile_service.py
|
|
import os
|
|
import re
|
|
import shutil
|
|
import logging
|
|
from typing import Optional, List
|
|
|
|
logger = logging.getLogger(__name__)
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
|
def _sanitize_name(name: str) -> str:
|
|
"""
|
|
Chuyển tên (email/username) thành dạng an toàn cho filesystem.
|
|
Giữ chữ, số, dấu gạch ngang và gạch dưới.
|
|
Ví dụ: "user+test@example.com" -> "user-test_example-com"
|
|
"""
|
|
if not isinstance(name, str):
|
|
raise ValueError("Profile name must be a string")
|
|
name = name.strip().lower()
|
|
# Thay các kí tự '@' và '+' thành '-'
|
|
name = name.replace("@", "-at-").replace("+", "-plus-")
|
|
# Thay các kí tự không an toàn thành '-'
|
|
name = re.sub(r"[^a-z0-9._-]", "-", name)
|
|
# Compact nhiều dấu '-' liên tiếp
|
|
name = re.sub(r"-{2,}", "-", name)
|
|
# Trim dấu '-' đầu cuối
|
|
name = name.strip("-")
|
|
return name or "profile"
|
|
|
|
|
|
class ProfileService:
|
|
"""
|
|
Service để quản lý thư mục profiles.
|
|
Mặc định root folder là ./profiles (tương đối với working dir).
|
|
"""
|
|
|
|
def __init__(self, profiles_root: Optional[str] = None):
|
|
self.profiles_root = os.path.abspath(profiles_root or "profiles")
|
|
os.makedirs(self.profiles_root, exist_ok=True)
|
|
logger.info("Profiles root: %s", self.profiles_root)
|
|
|
|
def get_profile_dirname(self, name: str) -> str:
|
|
"""Tên folder đã sanitize (chỉ tên folder, không có path)"""
|
|
return _sanitize_name(name)
|
|
|
|
def get_profile_path(self, name: str) -> str:
|
|
"""Trả về path tuyệt đối tới folder profile"""
|
|
return os.path.join(self.profiles_root, self.get_profile_dirname(name))
|
|
|
|
def exists(self, name: str) -> bool:
|
|
"""Check folder có tồn tại không"""
|
|
return os.path.isdir(self.get_profile_path(name))
|
|
|
|
def create(self, name: str, copy_from: Optional[str] = None, exist_ok: bool = True) -> str:
|
|
"""
|
|
Tạo folder profile.
|
|
- name: email/username
|
|
- copy_from: nếu truyền path tới folder mẫu, sẽ copy nội dung từ đó
|
|
- exist_ok: nếu True và folder đã tồn tại thì không raise
|
|
Trả về path tới folder profile.
|
|
"""
|
|
path = self.get_profile_path(name)
|
|
if os.path.isdir(path):
|
|
if exist_ok:
|
|
logger.debug("Profile already exists: %s", path)
|
|
return path
|
|
raise FileExistsError(f"Profile already exists: {path}")
|
|
|
|
os.makedirs(path, exist_ok=True)
|
|
logger.info("Created profile dir: %s", path)
|
|
|
|
if copy_from:
|
|
copy_from = os.path.abspath(copy_from)
|
|
if os.path.isdir(copy_from):
|
|
# copy nội dung bên trong copy_from vào path
|
|
for item in os.listdir(copy_from):
|
|
s = os.path.join(copy_from, item)
|
|
d = os.path.join(path, item)
|
|
if os.path.isdir(s):
|
|
shutil.copytree(s, d, dirs_exist_ok=True)
|
|
else:
|
|
shutil.copy2(s, d)
|
|
logger.info("Copied profile template from %s to %s", copy_from, path)
|
|
else:
|
|
logger.warning("copy_from path not found or not a dir: %s", copy_from)
|
|
|
|
return path
|
|
|
|
def delete(self, name: str, ignore_errors: bool = False) -> None:
|
|
"""Xóa folder profile (recursive)."""
|
|
path = self.get_profile_path(name)
|
|
if not os.path.isdir(path):
|
|
raise FileNotFoundError(f"Profile not found: {path}")
|
|
shutil.rmtree(path, ignore_errors=ignore_errors)
|
|
logger.info("Deleted profile dir: %s", path)
|
|
|
|
def list_profiles(self) -> List[str]:
|
|
"""Trả về danh sách tên folder (dirnames) trong profiles_root."""
|
|
try:
|
|
return sorted(
|
|
[
|
|
d
|
|
for d in os.listdir(self.profiles_root)
|
|
if os.path.isdir(os.path.join(self.profiles_root, d))
|
|
]
|
|
)
|
|
except FileNotFoundError:
|
|
return []
|
|
|
|
# ----------------- Optional: QWebEngineProfile creator -----------------
|
|
def create_qwebengine_profile(self, name: str, parent=None, profile_id: Optional[str] = None):
|
|
"""
|
|
Tạo và trả về QWebEngineProfile đã cấu hình persistent storage (cookies, local storage, cache).
|
|
Yêu cầu PyQt5.QtWebEngineWidgets được cài.
|
|
- name: email/username để đặt thư mục lưu
|
|
- parent: parent QObject cho QWebEngineProfile (thường là self)
|
|
- profile_id: tên id cho profile (tùy chọn)
|
|
"""
|
|
try:
|
|
from PyQt5.QtWebEngineWidgets import QWebEngineProfile
|
|
except Exception as e:
|
|
raise RuntimeError(
|
|
"PyQt5.QtWebEngineWidgets không khả dụng. "
|
|
"Không thể tạo QWebEngineProfile."
|
|
) from e
|
|
|
|
profile_path = self.get_profile_path(name)
|
|
os.makedirs(profile_path, exist_ok=True)
|
|
|
|
profile_name = profile_id or self.get_profile_dirname(name)
|
|
profile = QWebEngineProfile(profile_name, parent)
|
|
profile.setPersistentStoragePath(profile_path)
|
|
profile.setCachePath(profile_path)
|
|
# Force lưu cookie persist
|
|
try:
|
|
profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies)
|
|
except Exception:
|
|
# Một vài phiên bản PyQt có thể khác tên hằng, bọc try/except để an toàn
|
|
pass
|
|
logger.info("Created QWebEngineProfile for %s -> %s", name, profile_path)
|
|
return profile
|