Spaces:
Running on Zero
Running on Zero
| from __future__ import annotations | |
| import base64 | |
| import hashlib | |
| import hmac | |
| import json | |
| import mimetypes | |
| import os | |
| import re | |
| import secrets | |
| import shutil | |
| import struct | |
| import threading | |
| import uuid | |
| import zipfile | |
| from dataclasses import dataclass | |
| from datetime import datetime, timedelta, timezone | |
| from pathlib import Path | |
| from typing import Any, Iterable | |
| from cryptography.hazmat.primitives.ciphers.aead import AESGCM, ChaCha20Poly1305 | |
| from huggingface_hub import HfApi, hf_hub_download | |
| MB = 1024 * 1024 | |
| GB = 1024 * MB | |
| STATE_REMOTE_PATH = "state/state.json" | |
| MAGIC = b"CVLT3" | |
| CHUNK_SIZE = 4 * MB | |
| def utcnow() -> datetime: | |
| return datetime.now(timezone.utc) | |
| def iso(value: datetime | None = None) -> str: | |
| return (value or utcnow()).isoformat() | |
| def parse_dt(value: str | None) -> datetime | None: | |
| if not value: | |
| return None | |
| try: | |
| parsed = datetime.fromisoformat(value) | |
| return parsed if parsed.tzinfo else parsed.replace(tzinfo=timezone.utc) | |
| except (TypeError, ValueError): | |
| return None | |
| class Plan: | |
| key: str | |
| name: str | |
| quota_bytes: int | |
| reserved_bytes: int | |
| retention_days: int | |
| delete_after_age_even_if_downloaded: bool | |
| concurrent_downloads: int | |
| concurrent_uploads: int | |
| analytics_delay_hours: int | |
| analytics_history_days: int | |
| token_length: int | |
| folder_upload: bool | |
| encryption: str | |
| display_price: str | |
| old_price: str | None | |
| chat_access: bool | |
| priority: str | |
| PLANS: dict[str, Plan] = { | |
| "free": Plan( | |
| key="free", name="Free", quota_bytes=250 * MB, reserved_bytes=250 * MB, | |
| retention_days=2, delete_after_age_even_if_downloaded=True, | |
| concurrent_downloads=1, concurrent_uploads=1, | |
| analytics_delay_hours=24, analytics_history_days=30, | |
| token_length=24, folder_upload=True, encryption="none", | |
| display_price="$0", old_price=None, chat_access=False, priority="Low", | |
| ), | |
| "plus": Plan( | |
| key="plus", name="Plus", quota_bytes=int(3.5 * GB), reserved_bytes=int(3.5 * GB), | |
| retention_days=7, delete_after_age_even_if_downloaded=False, | |
| concurrent_downloads=3, concurrent_uploads=3, | |
| analytics_delay_hours=12, analytics_history_days=365, | |
| token_length=8, folder_upload=True, encryption="aes256-gcm", | |
| display_price="$2.49", old_price="$5.00", chat_access=True, priority="Medium", | |
| ), | |
| "pro": Plan( | |
| key="pro", name="Pro", quota_bytes=int(6.5 * GB), reserved_bytes=int(6.5 * GB), | |
| retention_days=30, delete_after_age_even_if_downloaded=False, | |
| concurrent_downloads=6, concurrent_uploads=6, | |
| analytics_delay_hours=6, analytics_history_days=365, | |
| token_length=12, folder_upload=True, encryption="chacha20-poly1305", | |
| display_price="$4.99", old_price="$10.00", chat_access=True, priority="High", | |
| ), | |
| "admin": Plan( | |
| key="admin", name="Developer", quota_bytes=10**18, reserved_bytes=0, | |
| retention_days=36500, delete_after_age_even_if_downloaded=False, | |
| concurrent_downloads=10**9, concurrent_uploads=10**9, | |
| analytics_delay_hours=0, analytics_history_days=3650, | |
| token_length=16, folder_upload=True, encryption="chacha20-poly1305", | |
| display_price="Developer", old_price=None, chat_access=True, priority="Developer", | |
| ), | |
| } | |
| class Settings: | |
| hf_token: str | None | |
| dataset_repo: str | None | |
| master_key: str | |
| admin_password: str | |
| public_base_url: str | |
| total_storage_capacity_bytes: int | |
| system_storage_reserve_bytes: int | |
| app_storage_limit_bytes: int | |
| purchase_safety_bytes: int | |
| temp_dir: Path | |
| local_root: Path | |
| local_mode: bool | |
| def get_settings() -> Settings: | |
| temp_dir = Path(os.getenv("TEMP_DIR", "/tmp/cloudvault-temp")) | |
| local_root = Path(os.getenv("LOCAL_STORAGE_DIR", "/tmp/cloudvault-local")) | |
| temp_dir.mkdir(parents=True, exist_ok=True) | |
| local_root.mkdir(parents=True, exist_ok=True) | |
| token = os.getenv("HF_TOKEN") or None | |
| repo = os.getenv("HF_DATASET_REPO") or None | |
| forced_local = os.getenv("LOCAL_MODE", "").lower() in {"1", "true", "yes"} | |
| space_host = os.getenv("SPACE_HOST", "").strip() | |
| public_url = os.getenv("PUBLIC_BASE_URL", "").strip().rstrip("/") | |
| if not public_url and space_host: | |
| public_url = f"https://{space_host}" | |
| total_storage_capacity_bytes = int(float(os.getenv("TOTAL_STORAGE_GB", "95")) * GB) | |
| system_storage_reserve_bytes = int(float(os.getenv("SYSTEM_STORAGE_RESERVE_GB", "5")) * GB) | |
| system_storage_reserve_bytes = min(max(0, system_storage_reserve_bytes), total_storage_capacity_bytes) | |
| package_pool_bytes = max(0, total_storage_capacity_bytes - system_storage_reserve_bytes) | |
| configured_package_pool = int(float(os.getenv("APP_STORAGE_LIMIT_GB", "90")) * GB) | |
| return Settings( | |
| hf_token=token, | |
| dataset_repo=repo, | |
| master_key=os.getenv("APP_MASTER_KEY", "CHANGE-ME-IN-SPACE-SECRETS"), | |
| admin_password="eses1428", | |
| public_base_url=public_url, | |
| total_storage_capacity_bytes=total_storage_capacity_bytes, | |
| system_storage_reserve_bytes=system_storage_reserve_bytes, | |
| app_storage_limit_bytes=min(package_pool_bytes, max(0, configured_package_pool)), | |
| purchase_safety_bytes=int(float(os.getenv("PURCHASE_SAFETY_GB", "0")) * GB), | |
| temp_dir=temp_dir, | |
| local_root=local_root, | |
| local_mode=forced_local or not (token and repo), | |
| ) | |
| SETTINGS = get_settings() | |
| def human_bytes(value: int) -> str: | |
| size = float(max(0, value)) | |
| for unit in ("B", "KB", "MB", "GB", "TB"): | |
| if size < 1024 or unit == "TB": | |
| return f"{size:.0f} {unit}" if unit in {"B", "KB"} else f"{size:.2f} {unit}" | |
| size /= 1024 | |
| return f"{size:.2f} TB" | |
| def safe_filename(name: str) -> str: | |
| clean = Path(name or "file.bin").name.replace("\x00", "") | |
| clean = re.sub(r"[\r\n]", "", clean).strip() | |
| return clean[:180] or "file.bin" | |
| def validate_registration(username: str, password: str) -> list[str]: | |
| username = (username or "").strip() | |
| password = password or "" | |
| errors: list[str] = [] | |
| if len(username) < 3: | |
| errors.append("Username must contain at least 3 characters.") | |
| if len(username) > 32: | |
| errors.append("Username may contain at most 32 characters.") | |
| if not re.fullmatch(r"[A-Za-z0-9_.-]+", username): | |
| errors.append("Username may contain letters, numbers, dot, underscore and dash only.") | |
| if username.casefold() == "admin": | |
| errors.append("This username is reserved.") | |
| if len(password) < 6: | |
| errors.append("Password must contain at least 6 characters.") | |
| if not re.search(r"[A-Za-z]", password): | |
| errors.append("Password must contain at least one letter.") | |
| if not re.search(r"\d", password): | |
| errors.append("Password must contain at least one number.") | |
| return errors | |
| def hash_password(password: str) -> str: | |
| salt = secrets.token_bytes(16) | |
| digest = hashlib.scrypt(password.encode("utf-8"), salt=salt, n=2**14, r=8, p=1, dklen=32) | |
| return "scrypt$" + base64.urlsafe_b64encode(salt).decode() + "$" + base64.urlsafe_b64encode(digest).decode() | |
| def verify_password(password: str, stored: str | None) -> bool: | |
| try: | |
| algorithm, salt_text, digest_text = (stored or "").split("$", 2) | |
| if algorithm != "scrypt": | |
| return False | |
| salt = base64.urlsafe_b64decode(salt_text.encode()) | |
| expected = base64.urlsafe_b64decode(digest_text.encode()) | |
| actual = hashlib.scrypt(password.encode("utf-8"), salt=salt, n=2**14, r=8, p=1, dklen=32) | |
| return hmac.compare_digest(actual, expected) | |
| except (ValueError, TypeError): | |
| return False | |
| def random_share_id(length: int) -> str: | |
| alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789" | |
| return "".join(secrets.choice(alphabet) for _ in range(length)) | |
| class StorageBackend: | |
| def __init__(self, settings: Settings) -> None: | |
| self.settings = settings | |
| self.api = HfApi(token=settings.hf_token) if not settings.local_mode else None | |
| if self.api and settings.dataset_repo: | |
| self.api.create_repo(repo_id=settings.dataset_repo, repo_type="dataset", private=True, exist_ok=True) | |
| def persistent(self) -> bool: | |
| return not self.settings.local_mode | |
| def upload(self, local_path: Path, remote_path: str, message: str) -> None: | |
| if self.settings.local_mode: | |
| target = self.settings.local_root / remote_path | |
| target.parent.mkdir(parents=True, exist_ok=True) | |
| shutil.copy2(local_path, target) | |
| return | |
| assert self.api and self.settings.dataset_repo | |
| self.api.upload_file( | |
| path_or_fileobj=str(local_path), path_in_repo=remote_path, | |
| repo_id=self.settings.dataset_repo, repo_type="dataset", commit_message=message, | |
| ) | |
| def download(self, remote_path: str, local_path: Path) -> bool: | |
| local_path.parent.mkdir(parents=True, exist_ok=True) | |
| if self.settings.local_mode: | |
| source = self.settings.local_root / remote_path | |
| if not source.exists(): | |
| return False | |
| shutil.copy2(source, local_path) | |
| return True | |
| assert self.settings.dataset_repo | |
| try: | |
| cached = hf_hub_download( | |
| repo_id=self.settings.dataset_repo, repo_type="dataset", | |
| filename=remote_path, token=self.settings.hf_token, | |
| ) | |
| shutil.copy2(cached, local_path) | |
| return True | |
| except Exception: | |
| return False | |
| def delete_many(self, remote_paths: Iterable[str], message: str = "Delete CloudVaultHub files") -> None: | |
| paths = [p for p in dict.fromkeys(remote_paths) if p] | |
| if not paths: | |
| return | |
| if self.settings.local_mode: | |
| for remote in paths: | |
| (self.settings.local_root / remote).unlink(missing_ok=True) | |
| return | |
| assert self.api and self.settings.dataset_repo | |
| from huggingface_hub import CommitOperationDelete | |
| operations = [CommitOperationDelete(path_in_repo=path) for path in paths] | |
| try: | |
| self.api.create_commit( | |
| repo_id=self.settings.dataset_repo, repo_type="dataset", | |
| operations=operations, commit_message=message, | |
| ) | |
| except Exception: | |
| pass | |
| BACKEND = StorageBackend(SETTINGS) | |
| class StateStore: | |
| def __init__(self, backend: StorageBackend, settings: Settings) -> None: | |
| self.backend = backend | |
| self.settings = settings | |
| self.lock = threading.RLock() | |
| self.state: dict[str, Any] = { | |
| "version": 3, | |
| "users": {}, | |
| "files": {}, | |
| "events": [], | |
| "chat": [], | |
| "chat_rooms": {}, | |
| "support_tickets": [], | |
| "config": {}, | |
| } | |
| self._load() | |
| def _load(self) -> None: | |
| changed = False | |
| loaded_existing = False | |
| with self.lock: | |
| temp = self.settings.temp_dir / f"state-load-{uuid.uuid4().hex}.json" | |
| if self.backend.download(STATE_REMOTE_PATH, temp): | |
| loaded_existing = True | |
| try: | |
| loaded = json.loads(temp.read_text(encoding="utf-8")) | |
| if isinstance(loaded, dict): | |
| self.state.update(loaded) | |
| except (OSError, json.JSONDecodeError): | |
| pass | |
| finally: | |
| temp.unlink(missing_ok=True) | |
| changed |= self._normalize_unlocked() | |
| changed |= self._ensure_admin_unlocked() | |
| if changed or not loaded_existing: | |
| self._save_unlocked("Initialize or upgrade CloudVaultHub state") | |
| def _normalize_unlocked(self) -> bool: | |
| changed = False | |
| defaults = { | |
| "system_enabled": True, | |
| "chat_enabled": True, | |
| "plus_chat_enabled": True, | |
| "pro_chat_enabled": False, | |
| "support_enabled": True, | |
| "support_force_open": False, | |
| "support_auto_paused": False, | |
| "purchases_enabled": True, | |
| "registrations_enabled": True, | |
| "delete_paid_on_expiry": False, | |
| "shutdown_mode": False, | |
| "chat_mode": "single", | |
| "chat_room_capacity": 0, | |
| } | |
| if not isinstance(self.state.get("users"), dict): | |
| self.state["users"] = {}; changed = True | |
| if not isinstance(self.state.get("files"), dict): | |
| self.state["files"] = {}; changed = True | |
| if not isinstance(self.state.get("events"), list): | |
| self.state["events"] = []; changed = True | |
| if not isinstance(self.state.get("notifications"), list): | |
| self.state["notifications"] = []; changed = True | |
| if not isinstance(self.state.get("notification_reads"), dict): | |
| self.state["notification_reads"] = {}; changed = True | |
| if not isinstance(self.state.get("plan_features"), dict): | |
| self.state["plan_features"] = {}; changed = True | |
| if not isinstance(self.state.get("websites"), dict): | |
| self.state["websites"] = {}; changed = True | |
| if not isinstance(self.state.get("chat"), list): | |
| self.state["chat"] = []; changed = True | |
| if not isinstance(self.state.get("chat_rooms"), dict): | |
| self.state["chat_rooms"] = {}; changed = True | |
| if not isinstance(self.state.get("support_tickets"), list): | |
| self.state["support_tickets"] = []; changed = True | |
| for ticket in self.state["support_tickets"]: | |
| ticket.setdefault("id", uuid.uuid4().hex) | |
| ticket.setdefault("user_id", "") | |
| ticket.setdefault("username", "User") | |
| ticket.setdefault("plan", "plus") | |
| ticket.setdefault("subject", "") | |
| ticket.setdefault("message", "") | |
| ticket.setdefault("created_at", iso()) | |
| ticket.setdefault("status", "open") | |
| ticket.setdefault("reply", "") | |
| ticket.setdefault("replied_at", None) | |
| ticket.setdefault("read_by_user", False) | |
| if not isinstance(self.state.get("config"), dict): | |
| self.state["config"] = {}; changed = True | |
| for key, value in defaults.items(): | |
| if key not in self.state["config"]: | |
| self.state["config"][key] = value; changed = True | |
| # CloudVaultHub uses exactly one unlimited Global Chat room. | |
| if self.state["config"].get("chat_mode") != "single": | |
| self.state["config"]["chat_mode"] = "single"; changed = True | |
| if self.state["config"].get("chat_room_capacity") != 0: | |
| self.state["config"]["chat_room_capacity"] = 0; changed = True | |
| if "global-1" not in self.state["chat_rooms"]: | |
| self.state["chat_rooms"]["global-1"] = { | |
| "id": "global-1", "name": "Global 1", "paused": False, "created_at": iso() | |
| } | |
| changed = True | |
| if set(self.state["chat_rooms"]) != {"global-1"}: | |
| room1 = self.state["chat_rooms"].get("global-1", { | |
| "id": "global-1", "name": "Global Chat", "paused": False, "created_at": iso() | |
| }) | |
| room1["name"] = "Global Chat" | |
| self.state["chat_rooms"] = {"global-1": room1} | |
| changed = True | |
| for message in self.state["chat"]: | |
| if message.get("room_id") != "global-1": | |
| message["room_id"] = "global-1"; changed = True | |
| for user in self.state["users"].values(): | |
| if "role" not in user: | |
| user["role"] = "user"; changed = True | |
| normalized_role = str(user.get("role") or "user").strip().lower() | |
| if normalized_role not in {"user", "admin"}: | |
| normalized_role = "user" | |
| if user.get("role") != normalized_role: | |
| user["role"] = normalized_role; changed = True | |
| normalized_plan = str(user.get("plan") or "free").strip().lower() | |
| if normalized_role == "admin": | |
| normalized_plan = "admin" | |
| elif normalized_plan not in {"free", "plus", "pro"}: | |
| normalized_plan = "free" | |
| if user.get("plan") != normalized_plan: | |
| user["plan"] = normalized_plan; changed = True | |
| if "is_banned" not in user: | |
| user["is_banned"] = False; changed = True | |
| if "ban_until" not in user: | |
| user["ban_until"] = None; changed = True | |
| if "delete_on_expiry" not in user: | |
| user["delete_on_expiry"] = False; changed = True | |
| if user.get("chat_room_id") != "global-1": | |
| user["chat_room_id"] = "global-1"; changed = True | |
| billing_defaults = { | |
| "pending_purchase_plan": None, | |
| "pending_purchase_expires_at": None, | |
| "billing_subscription_id": None, | |
| "billing_customer_id": None, | |
| "billing_plan": None, | |
| "billing_status": None, | |
| "billing_email": None, | |
| "billing_auto_renew": False, | |
| "billing_renews_at": None, | |
| "billing_ends_at": None, | |
| "billing_anchor": None, | |
| "billing_card_brand": None, | |
| "billing_card_last_four": None, | |
| "billing_payment_processor": None, | |
| "billing_urls": {}, | |
| "billing_updated_at": None, | |
| } | |
| for billing_key, billing_value in billing_defaults.items(): | |
| if billing_key not in user: | |
| user[billing_key] = billing_value | |
| changed = True | |
| # Keep only the newest 400 messages in the single Global Chat room. | |
| normalized_chat = sorted(self.state["chat"], key=lambda m: m.get("created_at", ""))[-400:] | |
| for message in normalized_chat: | |
| message["room_id"] = "global-1" | |
| if len(normalized_chat) != len(self.state["chat"]): | |
| changed = True | |
| self.state["chat"] = normalized_chat | |
| self.state["version"] = 4 | |
| return changed | |
| def _ensure_admin_unlocked(self) -> bool: | |
| """Guarantee exactly one canonical backend admin account.""" | |
| users = self.state["users"] | |
| exact = next( | |
| (u for u in users.values() if str(u.get("username", "")).strip().casefold() == "admin"), | |
| None, | |
| ) | |
| legacy_admins = [ | |
| u for u in users.values() | |
| if str(u.get("role", "")).strip().lower() == "admin" | |
| ] | |
| admin = exact or (legacy_admins[0] if legacy_admins else None) | |
| now = utcnow() | |
| if admin is None: | |
| uid = "cloudvault-admin" | |
| admin = { | |
| "id": uid, | |
| "username": "admin", | |
| "password_hash": hash_password(self.settings.admin_password), | |
| "plan": "admin", | |
| "role": "admin", | |
| "plan_started_at": iso(now), | |
| "plan_expires_at": None, | |
| "created_at": iso(now), | |
| "is_banned": False, | |
| "ban_until": None, | |
| "delete_on_expiry": False, | |
| "last_chat_at": None, | |
| "chat_room_id": "global-1", | |
| "pending_purchase_plan": None, | |
| "pending_purchase_expires_at": None, | |
| "billing_subscription_id": None, | |
| "billing_customer_id": None, | |
| "billing_plan": None, | |
| "billing_status": None, | |
| "billing_email": None, | |
| "billing_auto_renew": False, | |
| "billing_renews_at": None, | |
| "billing_ends_at": None, | |
| "billing_anchor": None, | |
| "billing_card_brand": None, | |
| "billing_card_last_four": None, | |
| "billing_payment_processor": None, | |
| "billing_urls": {}, | |
| "billing_updated_at": None, | |
| } | |
| users[uid] = admin | |
| return True | |
| changed = False | |
| # Migrate an old role=admin account to the canonical username instead of | |
| # creating a second hidden admin account. | |
| if str(admin.get("username", "")).strip().casefold() != "admin": | |
| admin["username"] = "admin" | |
| changed = True | |
| required = { | |
| "plan": "admin", "role": "admin", "is_banned": False, | |
| "ban_until": None, "plan_expires_at": None, "delete_on_expiry": False, | |
| "chat_room_id": admin.get("chat_room_id") or "global-1", | |
| } | |
| for key, value in required.items(): | |
| if admin.get(key) != value: | |
| admin[key] = value | |
| changed = True | |
| if not verify_password(self.settings.admin_password, admin.get("password_hash")): | |
| admin["password_hash"] = hash_password(self.settings.admin_password) | |
| changed = True | |
| # No other account may retain backend admin privileges. | |
| for other in users.values(): | |
| if other is admin: | |
| continue | |
| if str(other.get("role", "")).strip().lower() == "admin": | |
| other["role"] = "user" | |
| other["plan"] = "free" | |
| other["plan_expires_at"] = None | |
| other["delete_on_expiry"] = False | |
| changed = True | |
| return changed | |
| def _save_unlocked(self, message: str) -> None: | |
| temp = self.settings.temp_dir / f"state-save-{uuid.uuid4().hex}.json" | |
| temp.write_text(json.dumps(self.state, ensure_ascii=False, separators=(",", ":")), encoding="utf-8") | |
| self.backend.upload(temp, STATE_REMOTE_PATH, message) | |
| temp.unlink(missing_ok=True) | |
| def _username_exists_unlocked(self, username: str) -> bool: | |
| wanted = username.strip().casefold() | |
| return any(str(u.get("username", "")).casefold() == wanted for u in self.state["users"].values()) | |
| def _is_admin_unlocked(self, user_id: str | None) -> bool: | |
| user = self.state["users"].get(user_id or "") | |
| return bool(user and user.get("role") == "admin") | |
| def _assert_admin_unlocked(self, user_id: str | None) -> None: | |
| if not self._is_admin_unlocked(user_id): | |
| raise PermissionError("Developer access is required.") | |
| def _effective_reserved_for_user_unlocked( | |
| self, | |
| user: dict[str, Any], | |
| now: datetime | None = None, | |
| ) -> int: | |
| if user.get("role") == "admin": | |
| return 0 | |
| current_plan = PLANS.get( | |
| str(user.get("plan", "free")), | |
| PLANS["free"], | |
| ) | |
| reserved = current_plan.reserved_bytes | |
| check_time = now or utcnow() | |
| pending_plan_key = str( | |
| user.get("pending_purchase_plan") or "" | |
| ) | |
| pending_expires = parse_dt( | |
| user.get("pending_purchase_expires_at") | |
| ) | |
| if ( | |
| pending_plan_key in {"plus", "pro"} | |
| and pending_expires | |
| and pending_expires > check_time | |
| ): | |
| reserved = max( | |
| reserved, | |
| PLANS[pending_plan_key].reserved_bytes, | |
| ) | |
| return reserved | |
| def _reserved_unlocked( | |
| self, | |
| exclude_user_id: str | None = None, | |
| ) -> int: | |
| now = utcnow() | |
| total = 0 | |
| for uid, user in self.state["users"].items(): | |
| if uid == exclude_user_id or user.get("role") == "admin": | |
| continue | |
| total += self._effective_reserved_for_user_unlocked( | |
| user, | |
| now, | |
| ) | |
| return total | |
| def _used_unlocked(self, user_id: str | None = None) -> int: | |
| return sum( | |
| int(f.get("original_size", 0)) for f in self.state["files"].values() | |
| if user_id is None or f.get("owner_id") == user_id | |
| ) | |
| def _admin_used_unlocked(self) -> int: | |
| admin_ids = {uid for uid, u in self.state["users"].items() if u.get("role") == "admin"} | |
| return sum(int(f.get("original_size", 0)) for f in self.state["files"].values() if f.get("owner_id") in admin_ids) | |
| def _package_used_unlocked(self) -> int: | |
| admin_ids = {uid for uid, u in self.state["users"].items() if u.get("role") == "admin"} | |
| return sum( | |
| int(f.get("original_size", 0)) | |
| for f in self.state["files"].values() | |
| if f.get("owner_id") not in admin_ids | |
| ) | |
| def _capacity_for_new_reservation_unlocked( | |
| self, | |
| bytes_needed: int, | |
| exclude_user_id: str | None = None, | |
| safety_bytes: int = 0, | |
| ) -> bool: | |
| projected = ( | |
| self._reserved_unlocked( | |
| exclude_user_id=exclude_user_id | |
| ) | |
| + max(0, int(bytes_needed)) | |
| + max(0, int(safety_bytes)) | |
| ) | |
| return projected <= self.settings.app_storage_limit_bytes | |
| def _capacity_for_paid_plan_unlocked( | |
| self, | |
| user_id: str, | |
| plan_key: str, | |
| ) -> bool: | |
| return self._capacity_for_new_reservation_unlocked( | |
| PLANS[plan_key].reserved_bytes, | |
| exclude_user_id=user_id, | |
| safety_bytes=self.settings.purchase_safety_bytes, | |
| ) | |
| def purchase_capacity( | |
| self, | |
| user_id: str, | |
| plan_key: str, | |
| ) -> dict[str, Any]: | |
| if plan_key not in {"plus", "pro"}: | |
| raise ValueError("Invalid paid package.") | |
| with self.lock: | |
| if user_id not in self.state["users"]: | |
| raise ValueError("Account not found.") | |
| projected_without_safety = ( | |
| self._reserved_unlocked( | |
| exclude_user_id=user_id | |
| ) | |
| + PLANS[plan_key].reserved_bytes | |
| ) | |
| remaining = max( | |
| 0, | |
| self.settings.app_storage_limit_bytes | |
| - projected_without_safety, | |
| ) | |
| return { | |
| "allowed": remaining >= self.settings.purchase_safety_bytes, | |
| "remaining_after_reservation": remaining, | |
| "required_safety": self.settings.purchase_safety_bytes, | |
| "target_reserved": PLANS[plan_key].reserved_bytes, | |
| } | |
| def config(self) -> dict[str, Any]: | |
| with self.lock: | |
| return dict(self.state["config"]) | |
| def username_exists(self, username: str) -> bool: | |
| with self.lock: | |
| return self._username_exists_unlocked(username) | |
| def is_admin(self, user_id: str | None) -> bool: | |
| with self.lock: | |
| return self._is_admin_unlocked(user_id) | |
| def create_user(self, username: str, password_hash: str, plan_key: str) -> dict[str, Any]: | |
| if plan_key not in {"free", "plus", "pro"}: | |
| raise ValueError("Invalid package.") | |
| plan = PLANS[plan_key] | |
| with self.lock: | |
| cfg = self.state["config"] | |
| if not cfg.get("system_enabled", True): | |
| raise ValueError("The system is temporarily paused.") | |
| if not cfg.get("registrations_enabled", True): | |
| raise ValueError("New account registration is currently closed.") | |
| if plan_key in {"plus", "pro"} and not cfg.get("purchases_enabled", True): | |
| raise ValueError("Package activation is currently paused.") | |
| if self._username_exists_unlocked(username): | |
| raise ValueError("This username is already in use.") | |
| if not self._capacity_for_new_reservation_unlocked(plan.reserved_bytes): | |
| raise ValueError("The server does not have enough reserved storage to create this account.") | |
| user_id = uuid.uuid4().hex | |
| now = utcnow() | |
| user = { | |
| "id": user_id, | |
| "username": username.strip(), | |
| "password_hash": password_hash, | |
| "plan": plan_key, | |
| "role": "user", | |
| "plan_started_at": iso(now), | |
| "plan_expires_at": iso(now + timedelta(days=28)) if plan_key != "free" else None, | |
| "created_at": iso(now), | |
| "is_banned": False, | |
| "ban_until": None, | |
| "delete_on_expiry": bool(cfg.get("delete_paid_on_expiry", False) and plan_key != "free"), | |
| "last_chat_at": None, | |
| "chat_room_id": None, | |
| "pending_purchase_plan": None, | |
| "pending_purchase_expires_at": None, | |
| "billing_subscription_id": None, | |
| "billing_customer_id": None, | |
| "billing_plan": None, | |
| "billing_status": None, | |
| "billing_email": None, | |
| "billing_auto_renew": False, | |
| "billing_renews_at": None, | |
| "billing_ends_at": None, | |
| "billing_anchor": None, | |
| "billing_card_brand": None, | |
| "billing_card_last_four": None, | |
| "billing_payment_processor": None, | |
| "billing_urls": {}, | |
| "billing_updated_at": None, | |
| } | |
| self.state["users"][user_id] = user | |
| self._save_unlocked("Create CloudVaultHub account") | |
| return dict(user) | |
| def authenticate(self, username: str, password: str) -> dict[str, Any] | None: | |
| with self.lock: | |
| wanted = (username or "").strip().casefold() | |
| user = next((u for u in self.state["users"].values() if str(u.get("username", "")).casefold() == wanted), None) | |
| if not user or not verify_password(password, user.get("password_hash")): | |
| return None | |
| now = utcnow() | |
| ban_until = parse_dt(user.get("ban_until")) | |
| if user.get("is_banned"): | |
| if ban_until and ban_until <= now: | |
| user["is_banned"] = False | |
| user["ban_until"] = None | |
| self._save_unlocked("Automatically remove expired ban") | |
| else: | |
| when = "permanently" if not ban_until else f"until {ban_until.strftime('%d %b %Y %H:%M UTC')}" | |
| raise PermissionError(f"This account is banned {when}.") | |
| if user.get("role") != "admin" and not self.state["config"].get("system_enabled", True): | |
| raise PermissionError("CloudVaultHub is temporarily paused by the developer.") | |
| return dict(user) | |
| def get_user(self, user_id: str | None) -> dict[str, Any] | None: | |
| if not user_id: | |
| return None | |
| self.cleanup(user_id) | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| return dict(user) if user else None | |
| def list_users(self, admin_id: str) -> list[dict[str, Any]]: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| users = [dict(u) for u in self.state["users"].values() if u.get("role") != "admin"] | |
| return sorted(users, key=lambda u: u.get("created_at", ""), reverse=True) | |
| def change_plan(self, user_id: str, plan_key: str) -> dict[str, Any]: | |
| if plan_key not in {"free", "plus", "pro"}: | |
| raise ValueError("Invalid package.") | |
| plan = PLANS[plan_key] | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not user: | |
| raise ValueError("Account not found.") | |
| if user.get("role") == "admin": | |
| raise ValueError("The developer account does not use packages.") | |
| cfg = self.state["config"] | |
| if not cfg.get("system_enabled", True): | |
| raise ValueError("The system is temporarily paused.") | |
| if plan_key in {"plus", "pro"} and not cfg.get("purchases_enabled", True): | |
| raise ValueError("Package activation is currently paused.") | |
| if ( | |
| plan_key in {"plus", "pro"} | |
| and not self._capacity_for_paid_plan_unlocked( | |
| user_id, | |
| plan_key, | |
| ) | |
| ): | |
| raise ValueError( | |
| "The server cannot reserve this package while keeping " | |
| f"{human_bytes(self.settings.purchase_safety_bytes)} free." | |
| ) | |
| now = utcnow() | |
| previous_plan = user.get("plan", "free") | |
| user["plan"] = plan_key | |
| if previous_plan == "free" and plan_key in {"plus", "pro"}: | |
| user["chat_room_id"] = None | |
| user["plan_started_at"] = iso(now) | |
| user["plan_expires_at"] = iso(now + timedelta(days=28)) if plan_key != "free" else None | |
| user["delete_on_expiry"] = bool(cfg.get("delete_paid_on_expiry", False) and plan_key != "free") | |
| removed = self._trim_user_unlocked(user_id, plan.quota_bytes) | |
| self._save_unlocked("Change CloudVaultHub package") | |
| self.backend.delete_many(removed, "Trim files after package change") | |
| return self.get_user(user_id) or {} | |
| def admin_change_user_plan(self, admin_id: str, user_id: str, plan_key: str, duration_days: int = 28) -> dict[str, Any]: | |
| """Developer-only package assignment that does not require a checkout.""" | |
| if plan_key not in {"free", "plus", "pro"}: | |
| raise ValueError("Invalid package.") | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| user = self.state["users"].get(user_id) | |
| if not user: | |
| raise ValueError("Account not found.") | |
| if user.get("role") == "admin": | |
| raise ValueError("The developer account cannot be assigned a customer package.") | |
| plan = PLANS[plan_key] | |
| if plan_key in {"plus", "pro"} and not self._capacity_for_paid_plan_unlocked(user_id, plan_key): | |
| raise ValueError("There is not enough reservable server storage for this package.") | |
| now = utcnow() | |
| user["plan"] = plan_key | |
| user["plan_started_at"] = iso(now) | |
| user["plan_expires_at"] = iso(now + timedelta(days=max(1, int(duration_days)))) if plan_key != "free" else None | |
| user["delete_on_expiry"] = False | |
| user["pending_purchase_plan"] = None | |
| user["pending_purchase_expires_at"] = None | |
| removed = self._trim_user_unlocked(user_id, plan.quota_bytes) | |
| self._save_unlocked("Developer changed CloudVaultHub user package") | |
| self.backend.delete_many(removed, "Trim files after developer package change") | |
| return self.get_user(user_id) or {} | |
| def begin_purchase( | |
| self, | |
| user_id: str, | |
| plan_key: str, | |
| hold_hours: int = 24, | |
| ) -> dict[str, Any]: | |
| if plan_key not in {"plus", "pro"}: | |
| raise ValueError("Invalid paid package.") | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not user: | |
| raise ValueError("Account not found.") | |
| if user.get("role") == "admin": | |
| raise ValueError( | |
| "The developer account does not use paid packages." | |
| ) | |
| cfg = self.state["config"] | |
| if not cfg.get("system_enabled", True): | |
| raise ValueError("The system is temporarily paused.") | |
| if not cfg.get("purchases_enabled", True): | |
| raise ValueError( | |
| "Package purchases are currently paused." | |
| ) | |
| subscription_id = str( | |
| user.get("billing_subscription_id") or "" | |
| ) | |
| billing_status = str( | |
| user.get("billing_status") or "" | |
| ) | |
| if ( | |
| subscription_id | |
| and billing_status in { | |
| "active", | |
| "on_trial", | |
| "past_due", | |
| "paused", | |
| "cancelled", | |
| } | |
| ): | |
| raise ValueError( | |
| "This account already has a subscription. " | |
| "Use Billing Settings or the Customer Portal." | |
| ) | |
| if not self._capacity_for_paid_plan_unlocked( | |
| user_id, | |
| plan_key, | |
| ): | |
| raise ValueError( | |
| "The server cannot reserve this package while keeping " | |
| f"{human_bytes(self.settings.purchase_safety_bytes)} free." | |
| ) | |
| expires_at = utcnow() + timedelta( | |
| hours=max(1, min(int(hold_hours), 72)) | |
| ) | |
| user["pending_purchase_plan"] = plan_key | |
| user["pending_purchase_expires_at"] = iso(expires_at) | |
| self._save_unlocked( | |
| f"Reserve CloudVaultHub {plan_key} checkout capacity" | |
| ) | |
| return dict(user) | |
| def billing_info( | |
| self, | |
| user_id: str | None, | |
| ) -> dict[str, Any]: | |
| if not user_id: | |
| return {} | |
| self.cleanup(user_id) | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not user: | |
| return {} | |
| keys = [ | |
| "id", "username", "plan", "plan_expires_at", | |
| "pending_purchase_plan", "pending_purchase_expires_at", | |
| "billing_subscription_id", "billing_customer_id", | |
| "billing_plan", "billing_status", "billing_email", | |
| "billing_auto_renew", "billing_renews_at", | |
| "billing_ends_at", "billing_anchor", | |
| "billing_card_brand", "billing_card_last_four", | |
| "billing_payment_processor", "billing_urls", | |
| "billing_updated_at", | |
| ] | |
| return {key: user.get(key) for key in keys} | |
| def find_user_by_billing_email( | |
| self, | |
| email: str | None, | |
| ) -> dict[str, Any] | None: | |
| wanted = str(email or "").strip().casefold() | |
| if not wanted: | |
| return None | |
| with self.lock: | |
| user = next( | |
| ( | |
| account | |
| for account in self.state["users"].values() | |
| if str( | |
| account.get("billing_email") or "" | |
| ).strip().casefold() == wanted | |
| ), | |
| None, | |
| ) | |
| return dict(user) if user else None | |
| def apply_subscription_snapshot( | |
| self, | |
| user_id: str, | |
| plan_key: str, | |
| subscription_id: str, | |
| attributes: dict[str, Any], | |
| event_name: str = "subscription_updated", | |
| ) -> dict[str, Any]: | |
| if plan_key not in {"plus", "pro"}: | |
| raise ValueError("Unsupported subscription package.") | |
| removed: list[str] = [] | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not user: | |
| raise ValueError("Account not found.") | |
| if user.get("role") == "admin": | |
| raise ValueError( | |
| "The developer account cannot receive a subscription." | |
| ) | |
| status = str( | |
| attributes.get("status") or "" | |
| ).strip().lower() | |
| cancelled = bool( | |
| attributes.get("cancelled") | |
| or status == "cancelled" | |
| ) | |
| renews_at = attributes.get("renews_at") | |
| ends_at = attributes.get("ends_at") | |
| urls = attributes.get("urls") | |
| if not isinstance(urls, dict): | |
| urls = {} | |
| user["billing_subscription_id"] = ( | |
| str(subscription_id or "") | |
| or user.get("billing_subscription_id") | |
| ) | |
| user["billing_customer_id"] = attributes.get( | |
| "customer_id", | |
| user.get("billing_customer_id"), | |
| ) | |
| user["billing_plan"] = plan_key | |
| user["billing_status"] = status or event_name | |
| user["billing_email"] = attributes.get( | |
| "user_email", | |
| user.get("billing_email"), | |
| ) | |
| user["billing_auto_renew"] = ( | |
| not cancelled | |
| and status not in {"expired", "unpaid"} | |
| ) | |
| user["billing_renews_at"] = renews_at | |
| user["billing_ends_at"] = ends_at | |
| user["billing_anchor"] = attributes.get("billing_anchor") | |
| user["billing_card_brand"] = attributes.get("card_brand") | |
| user["billing_card_last_four"] = attributes.get("card_last_four") | |
| user["billing_payment_processor"] = attributes.get( | |
| "payment_processor" | |
| ) | |
| if urls: | |
| user["billing_urls"] = urls | |
| user["billing_updated_at"] = iso() | |
| user["pending_purchase_plan"] = None | |
| user["pending_purchase_expires_at"] = None | |
| now = utcnow() | |
| if status == "expired": | |
| user["plan"] = "free" | |
| user["plan_started_at"] = iso(now) | |
| user["plan_expires_at"] = None | |
| user["billing_auto_renew"] = False | |
| removed.extend( | |
| self._trim_user_unlocked( | |
| user_id, | |
| PLANS["free"].quota_bytes, | |
| ) | |
| ) | |
| else: | |
| user["plan"] = plan_key | |
| user["plan_started_at"] = ( | |
| user.get("plan_started_at") or iso(now) | |
| ) | |
| expiry_value = ( | |
| ends_at | |
| if cancelled and ends_at | |
| else renews_at | |
| ) | |
| expiry = parse_dt(expiry_value) | |
| if expiry is None: | |
| expiry = now + timedelta(days=28) | |
| user["plan_expires_at"] = iso(expiry) | |
| user["delete_on_expiry"] = False | |
| self._save_unlocked( | |
| f"Apply Lemon Squeezy event {event_name}" | |
| ) | |
| self.backend.delete_many( | |
| removed, | |
| "Trim files after subscription expiry", | |
| ) | |
| return self.get_user(user_id) or {} | |
| def used_bytes(self, user_id: str) -> int: | |
| with self.lock: | |
| return self._used_unlocked(user_id) | |
| def total_used_bytes(self) -> int: | |
| with self.lock: | |
| return self._used_unlocked() | |
| def total_reserved_bytes(self) -> int: | |
| with self.lock: | |
| return self._reserved_unlocked() | |
| def available_for_admin(self) -> int: | |
| with self.lock: | |
| return max(0, self.settings.app_storage_limit_bytes - self._used_unlocked()) | |
| def list_files(self, user_id: str) -> list[dict[str, Any]]: | |
| self.cleanup(user_id) | |
| with self.lock: | |
| files = [dict(f) for f in self.state["files"].values() if f.get("owner_id") == user_id] | |
| return sorted(files, key=lambda item: item.get("created_at", ""), reverse=True) | |
| def _unique_token_unlocked(self, length: int) -> str: | |
| existing = {f.get("share_token") for f in self.state["files"].values()} | |
| for _ in range(300): | |
| token = random_share_id(length) | |
| if token not in existing: | |
| return token | |
| raise RuntimeError("Could not create a unique sharing ID.") | |
| def add_file(self, user_id: str, metadata: dict[str, Any]) -> dict[str, Any]: | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not user: | |
| raise ValueError("Account not found.") | |
| cfg = self.state["config"] | |
| if user.get("role") != "admin" and not cfg.get("system_enabled", True): | |
| raise ValueError("The system is temporarily paused.") | |
| size = int(metadata["original_size"]) | |
| is_admin = str(user.get("role", "")).lower() == "admin" | |
| plan_key = str(user.get("plan", "free")).lower() | |
| plan = PLANS["admin"] if is_admin else PLANS.get(plan_key, PLANS["free"]) | |
| if is_admin: | |
| remaining = max(0, self.settings.app_storage_limit_bytes - self._used_unlocked()) | |
| if size > remaining: | |
| raise ValueError( | |
| f"Upload failed: developer storage is full. Available {human_bytes(remaining)}, file size {human_bytes(size)}." | |
| ) | |
| else: | |
| used_by_user = self._used_unlocked(user_id) | |
| remaining = max(0, plan.quota_bytes - used_by_user) | |
| if size > remaining: | |
| raise ValueError( | |
| f"Upload failed: your {plan.name} storage is full. Available {human_bytes(remaining)}, file size {human_bytes(size)}." | |
| ) | |
| server_remaining = max(0, self.settings.app_storage_limit_bytes - self._used_unlocked()) | |
| if size > server_remaining: | |
| raise ValueError( | |
| f"Upload failed: CloudVaultHub server storage is full. Available {human_bytes(server_remaining)}, file size {human_bytes(size)}." | |
| ) | |
| metadata = dict(metadata) | |
| metadata["share_token"] = self._unique_token_unlocked(plan.token_length) | |
| metadata["share_password_hash"] = None | |
| self.state["files"][metadata["id"]] = metadata | |
| self._save_unlocked("Save CloudVaultHub file metadata") | |
| return dict(metadata) | |
| def get_file_by_token(self, token: str) -> dict[str, Any] | None: | |
| self.cleanup() | |
| with self.lock: | |
| for item in self.state["files"].values(): | |
| if item.get("share_token") == token: | |
| return dict(item) | |
| return None | |
| def delete_file(self, user_id: str, file_id: str) -> bool: | |
| remote = None | |
| with self.lock: | |
| item = self.state["files"].get(file_id) | |
| if not item or item.get("owner_id") != user_id: | |
| return False | |
| remote = item.get("remote_path") | |
| del self.state["files"][file_id] | |
| self.state["events"] = [e for e in self.state["events"] if e.get("file_id") != file_id] | |
| self._save_unlocked("Delete CloudVaultHub file") | |
| self.backend.delete_many([remote], "Delete CloudVaultHub file object") | |
| return True | |
| def _delete_user_unlocked(self, user_id: str) -> list[str]: | |
| user = self.state["users"].get(user_id) | |
| if not user or user.get("role") == "admin": | |
| return [] | |
| remotes: list[str] = [] | |
| for file_id, item in list(self.state["files"].items()): | |
| if item.get("owner_id") == user_id: | |
| remotes.append(item.get("remote_path", "")) | |
| del self.state["files"][file_id] | |
| self.state["events"] = [e for e in self.state["events"] if e.get("owner_id") != user_id] | |
| self.state["chat"] = [m for m in self.state["chat"] if m.get("sender_id") != user_id] | |
| self.state["support_tickets"] = [t for t in self.state["support_tickets"] if t.get("user_id") != user_id] | |
| del self.state["users"][user_id] | |
| return remotes | |
| def delete_account(self, user_id: str) -> bool: | |
| with self.lock: | |
| if user_id not in self.state["users"] or self._is_admin_unlocked(user_id): | |
| return False | |
| remotes = self._delete_user_unlocked(user_id) | |
| self._save_unlocked("Delete CloudVaultHub account") | |
| self.backend.delete_many(remotes, "Delete account objects") | |
| return True | |
| def record_download(self, file_id: str, country: str, device: str) -> None: | |
| with self.lock: | |
| item = self.state["files"].get(file_id) | |
| if not item: | |
| return | |
| now = iso() | |
| item["download_count"] = int(item.get("download_count", 0)) + 1 | |
| item["last_download_at"] = now | |
| self.state["events"].append({ | |
| "id": uuid.uuid4().hex, | |
| "file_id": file_id, | |
| "owner_id": item.get("owner_id"), | |
| "created_at": now, | |
| "country": (country or "Unknown")[:100], | |
| "device": (device or "Unknown")[:60], | |
| }) | |
| cutoff = utcnow() - timedelta(days=400) | |
| self.state["events"] = [ | |
| e for e in self.state["events"] | |
| if (parse_dt(e.get("created_at")) or utcnow()) >= cutoff | |
| ][-50000:] | |
| self._save_unlocked("Record CloudVaultHub download") | |
| def analytics(self, user_id: str, requested_days: int) -> dict[str, Any]: | |
| user = self.get_user(user_id) | |
| if not user: | |
| return {"events": [], "total": 0, "unique_files": 0, "countries": {}, "devices": {}, "days": 0, "delay": 0} | |
| is_admin = str(user.get("role", "")).lower() == "admin" | |
| plan_key = str(user.get("plan", "free")).lower() | |
| plan = PLANS["admin"] if is_admin else PLANS.get(plan_key, PLANS["free"]) | |
| days = max(1, min(int(requested_days), plan.analytics_history_days)) | |
| now = utcnow() | |
| start = now - timedelta(days=days) | |
| visible_until = now - timedelta(hours=plan.analytics_delay_hours) | |
| with self.lock: | |
| events = [] | |
| for event in self.state["events"]: | |
| if event.get("owner_id") != user_id: | |
| continue | |
| timestamp = parse_dt(event.get("created_at")) | |
| if timestamp and start <= timestamp <= visible_until: | |
| events.append(dict(event)) | |
| countries: dict[str, int] = {} | |
| devices: dict[str, int] = {} | |
| for event in events: | |
| country = event.get("country", "Unknown") | |
| device = event.get("device", "Unknown") | |
| countries[country] = countries.get(country, 0) + 1 | |
| devices[device] = devices.get(device, 0) + 1 | |
| return { | |
| "events": sorted(events, key=lambda e: e.get("created_at", ""), reverse=True), | |
| "total": len(events), | |
| "unique_files": len({e.get("file_id") for e in events}), | |
| "countries": countries, | |
| "devices": devices, | |
| "days": days, | |
| "delay": plan.analytics_delay_hours, | |
| } | |
| def _trim_user_unlocked(self, user_id: str, max_bytes: int) -> list[str]: | |
| owned = [f for f in self.state["files"].values() if f.get("owner_id") == user_id] | |
| owned.sort(key=lambda f: f.get("created_at", "")) | |
| used = sum(int(f.get("original_size", 0)) for f in owned) | |
| remotes: list[str] = [] | |
| for item in owned: | |
| if used <= max_bytes: | |
| break | |
| used -= int(item.get("original_size", 0)) | |
| remotes.append(item.get("remote_path", "")) | |
| self.state["files"].pop(item["id"], None) | |
| self.state["events"] = [e for e in self.state["events"] if e.get("file_id") != item["id"]] | |
| return remotes | |
| def _notify_user_unlocked( | |
| self, | |
| user_id: str, | |
| title: str, | |
| message: str, | |
| kind: str = "system", | |
| ) -> None: | |
| """Append a targeted notification while the store lock is already held.""" | |
| if user_id not in self.state.get("users", {}): | |
| return | |
| notifications = self.state.setdefault("notifications", []) | |
| notifications.append({ | |
| "id": uuid.uuid4().hex, | |
| "title": str(title or "CloudVaultHub update")[:120], | |
| "message": str(message or "")[:3000], | |
| "audience": [], | |
| "kind": str(kind or "system"), | |
| "user_id": user_id, | |
| "recipient_ids": [user_id], | |
| "created_at": iso(), | |
| }) | |
| if len(notifications) > 2000: | |
| del notifications[:-2000] | |
| def cleanup(self, only_user_id: str | None = None) -> None: | |
| remotes: list[str] = [] | |
| changed = False | |
| now = utcnow() | |
| with self.lock: | |
| user_ids = [only_user_id] if only_user_id else list(self.state["users"].keys()) | |
| for uid in user_ids: | |
| user = self.state["users"].get(uid) | |
| if not user or user.get("role") == "admin": | |
| continue | |
| ban_until = parse_dt(user.get("ban_until")) | |
| if user.get("is_banned") and ban_until and ban_until <= now: | |
| user["is_banned"] = False | |
| user["ban_until"] = None | |
| changed = True | |
| pending_expires = parse_dt( | |
| user.get("pending_purchase_expires_at") | |
| ) | |
| if ( | |
| user.get("pending_purchase_plan") | |
| and pending_expires | |
| and pending_expires <= now | |
| ): | |
| user["pending_purchase_plan"] = None | |
| user["pending_purchase_expires_at"] = None | |
| changed = True | |
| expires = parse_dt(user.get("plan_expires_at")) | |
| if user.get("plan") != "free" and expires and expires <= now: | |
| should_delete = bool(user.get("delete_on_expiry") or self.state["config"].get("delete_paid_on_expiry", False)) | |
| if should_delete: | |
| remotes.extend(self._delete_user_unlocked(uid)) | |
| else: | |
| user["plan"] = "free" | |
| user["plan_started_at"] = iso(now) | |
| user["plan_expires_at"] = None | |
| user["delete_on_expiry"] = False | |
| user["billing_status"] = "expired" | |
| user["billing_auto_renew"] = False | |
| user["billing_ends_at"] = iso(now) | |
| user["pending_purchase_plan"] = None | |
| user["pending_purchase_expires_at"] = None | |
| remotes.extend(self._trim_user_unlocked(uid, PLANS["free"].quota_bytes)) | |
| self._notify_user_unlocked( | |
| uid, | |
| "Your package expired", | |
| "Your paid package ended and the account returned to Free. Files above the Free storage limit may have been removed.", | |
| "account", | |
| ) | |
| changed = True | |
| for file_id, item in list(self.state["files"].items()): | |
| if only_user_id and item.get("owner_id") != only_user_id: | |
| continue | |
| user = self.state["users"].get(item.get("owner_id")) | |
| if not user: | |
| remotes.append(item.get("remote_path", "")) | |
| del self.state["files"][file_id] | |
| changed = True | |
| continue | |
| if user.get("role") == "admin": | |
| continue | |
| uploaded_plan = PLANS.get(item.get("uploaded_under_plan", user.get("plan", "free")), PLANS["free"]) | |
| created = parse_dt(item.get("created_at")) or now | |
| age = now - created | |
| delete = False | |
| if uploaded_plan.delete_after_age_even_if_downloaded and age >= timedelta(days=uploaded_plan.retention_days): | |
| delete = True | |
| elif not uploaded_plan.delete_after_age_even_if_downloaded and int(item.get("download_count", 0)) == 0 and age >= timedelta(days=uploaded_plan.retention_days): | |
| delete = True | |
| if delete: | |
| display_name = str(item.get("display_name") or "A file") | |
| retention_days = max(0, int(uploaded_plan.retention_days)) | |
| remotes.append(item.get("remote_path", "")) | |
| del self.state["files"][file_id] | |
| self.state["events"] = [e for e in self.state["events"] if e.get("file_id") != file_id] | |
| self._notify_user_unlocked( | |
| str(item.get("owner_id") or ""), | |
| "File removed automatically", | |
| f"{display_name} was deleted automatically after {retention_days} day(s) under the {uploaded_plan.name} retention rule.", | |
| "file_deleted", | |
| ) | |
| changed = True | |
| if changed: | |
| self._save_unlocked("Automatic CloudVaultHub cleanup") | |
| self.backend.delete_many(remotes, "Delete expired CloudVaultHub objects") | |
| # ---------------- Developer administration ---------------- | |
| def admin_overview(self, admin_id: str) -> dict[str, Any]: | |
| self.cleanup() | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| counts = {"free": 0, "plus": 0, "pro": 0} | |
| used_by_plan = {"free": 0, "plus": 0, "pro": 0, "admin": 0} | |
| for uid, user in self.state["users"].items(): | |
| is_admin = str(user.get("role", "")).strip().lower() == "admin" | |
| plan_key = str(user.get("plan") or "free").strip().lower() | |
| if not is_admin and plan_key not in counts: | |
| plan_key = "free" | |
| if not is_admin: | |
| counts[plan_key] += 1 | |
| bucket = "admin" if is_admin else plan_key | |
| used_by_plan[bucket] = used_by_plan.get(bucket, 0) + self._used_unlocked(uid) | |
| total_used = self._used_unlocked() | |
| package_used = self._package_used_unlocked() | |
| system_used = self._admin_used_unlocked() | |
| return { | |
| "counts": counts, | |
| "registered": sum(counts.values()), | |
| "used_by_plan": used_by_plan, | |
| "total_used": total_used, | |
| "remaining": max(0, self.settings.total_storage_capacity_bytes - total_used), | |
| "limit": self.settings.app_storage_limit_bytes, | |
| "total_capacity": self.settings.total_storage_capacity_bytes, | |
| "system_reserve": self.settings.system_storage_reserve_bytes, | |
| "system_used": system_used, | |
| "system_remaining": max(0, self.settings.system_storage_reserve_bytes - system_used), | |
| "package_capacity": self.settings.app_storage_limit_bytes, | |
| "package_used": package_used, | |
| "package_remaining": max(0, self.settings.app_storage_limit_bytes - package_used), | |
| "storage_pool_remaining": max(0, self.settings.app_storage_limit_bytes - total_used), | |
| "reserved": self._reserved_unlocked(), | |
| "config": dict(self.state["config"]), | |
| "chat_messages": len(self.state["chat"]), | |
| "support_tickets": len(self.state["support_tickets"]), | |
| "support_open": sum(1 for t in self.state["support_tickets"] if t.get("status") != "answered"), | |
| } | |
| def ban_user(self, admin_id: str, target_id: str, hours: float | None = None) -> None: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| target = self.state["users"].get(target_id) | |
| if not target or target.get("role") == "admin": | |
| raise ValueError("The selected account cannot be banned.") | |
| target["is_banned"] = True | |
| target["ban_until"] = iso(utcnow() + timedelta(hours=float(hours))) if hours and float(hours) > 0 else None | |
| self._save_unlocked("Ban CloudVaultHub account") | |
| def unban_user(self, admin_id: str, target_id: str) -> None: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| target = self.state["users"].get(target_id) | |
| if not target or target.get("role") == "admin": | |
| raise ValueError("Account not found.") | |
| target["is_banned"] = False | |
| target["ban_until"] = None | |
| self._save_unlocked("Unban CloudVaultHub account") | |
| def admin_delete_user(self, admin_id: str, target_id: str) -> None: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| remotes = self._delete_user_unlocked(target_id) | |
| if not remotes and target_id not in self.state["users"]: | |
| pass | |
| self._save_unlocked("Developer deleted account") | |
| self.backend.delete_many(remotes, "Developer delete account objects") | |
| def factory_reset(self, admin_id: str, confirmation: str) -> None: | |
| """Delete all user data and restore the service to first-install state.""" | |
| if str(confirmation or "").strip() != "RESET CLOUDVAULTHUB": | |
| raise ValueError("Type RESET CLOUDVAULTHUB exactly.") | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| # Delete every persisted object except the state file itself. | |
| remote_paths: list[str] = [] | |
| if self.settings.local_mode: | |
| root = self.settings.local_root | |
| if root.exists(): | |
| for item in root.rglob("*"): | |
| if item.is_file() and item.relative_to(root).as_posix() != STATE_REMOTE_PATH: | |
| remote_paths.append(item.relative_to(root).as_posix()) | |
| else: | |
| try: | |
| assert self.backend.api and self.settings.dataset_repo | |
| remote_paths = [ | |
| name for name in self.backend.api.list_repo_files( | |
| repo_id=self.settings.dataset_repo, repo_type="dataset" | |
| ) if name != STATE_REMOTE_PATH and name != ".gitattributes" | |
| ] | |
| except Exception: | |
| # Fall back to paths referenced by the current state. | |
| for item in self.state.get("files", {}).values(): | |
| if isinstance(item, dict) and item.get("remote_path"): | |
| remote_paths.append(str(item["remote_path"])) | |
| for project in self.state.get("websites", {}).values(): | |
| for item in (project.get("entries", {}) if isinstance(project, dict) else {}).values(): | |
| if isinstance(item, dict) and item.get("remote_path"): | |
| remote_paths.append(str(item["remote_path"])) | |
| self.backend.delete_many(remote_paths, "Factory reset CloudVaultHub storage") | |
| self.state = { | |
| "version": 4, "users": {}, "files": {}, "events": [], "chat": [], | |
| "chat_rooms": {}, "support_tickets": [], "config": {}, "websites": {}, | |
| "notifications": [], "notification_reads": {}, "plan_features": {}, | |
| } | |
| self._normalize_unlocked() | |
| self._ensure_admin_unlocked() | |
| self._save_unlocked("Factory reset CloudVaultHub") | |
| def delete_all_free(self, admin_id: str) -> int: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| targets = [uid for uid, u in self.state["users"].items() if u.get("role") != "admin" and u.get("plan") == "free"] | |
| remotes: list[str] = [] | |
| for uid in targets: | |
| remotes.extend(self._delete_user_unlocked(uid)) | |
| self._save_unlocked("Delete all Free accounts") | |
| self.backend.delete_many(remotes, "Delete all Free account objects") | |
| return len(targets) | |
| def delete_all_users(self, admin_id: str) -> int: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| targets = [uid for uid, u in self.state["users"].items() if u.get("role") != "admin"] | |
| remotes: list[str] = [] | |
| for uid in targets: | |
| remotes.extend(self._delete_user_unlocked(uid)) | |
| self._save_unlocked("Delete all CloudVaultHub user accounts") | |
| self.backend.delete_many(remotes, "Delete all user objects") | |
| return len(targets) | |
| def set_config(self, admin_id: str, **updates: Any) -> dict[str, Any]: | |
| allowed = { | |
| "system_enabled", "chat_enabled", "plus_chat_enabled", "pro_chat_enabled", | |
| "support_enabled", "support_force_open", "support_auto_paused", "purchases_enabled", | |
| "registrations_enabled", "delete_paid_on_expiry", "shutdown_mode", | |
| } | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| for key, value in updates.items(): | |
| if key in allowed: | |
| self.state["config"][key] = bool(value) | |
| self._save_unlocked("Update CloudVaultHub developer controls") | |
| return dict(self.state["config"]) | |
| def begin_shutdown(self, admin_id: str) -> int: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| self.state["config"].update({ | |
| "system_enabled": False, | |
| "chat_enabled": False, | |
| "plus_chat_enabled": False, | |
| "pro_chat_enabled": False, | |
| "purchases_enabled": False, | |
| "registrations_enabled": False, | |
| "delete_paid_on_expiry": True, | |
| "shutdown_mode": True, | |
| }) | |
| free_ids = [uid for uid, u in self.state["users"].items() if u.get("role") != "admin" and u.get("plan") == "free"] | |
| remotes: list[str] = [] | |
| for uid in free_ids: | |
| remotes.extend(self._delete_user_unlocked(uid)) | |
| for user in self.state["users"].values(): | |
| if user.get("role") != "admin" and user.get("plan") in {"plus", "pro"}: | |
| user["delete_on_expiry"] = True | |
| self._save_unlocked("Begin CloudVaultHub shutdown mode") | |
| self.backend.delete_many(remotes, "Shutdown mode delete Free objects") | |
| return len(free_ids) | |
| def cancel_shutdown(self, admin_id: str) -> None: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| self.state["config"].update({ | |
| "system_enabled": True, | |
| "chat_enabled": True, | |
| "plus_chat_enabled": True, | |
| "pro_chat_enabled": True, | |
| "purchases_enabled": True, | |
| "registrations_enabled": True, | |
| "delete_paid_on_expiry": False, | |
| "shutdown_mode": False, | |
| }) | |
| for user in self.state["users"].values(): | |
| if user.get("role") != "admin": | |
| user["delete_on_expiry"] = False | |
| self._save_unlocked("Cancel CloudVaultHub shutdown mode") | |
| # ---------------- Admin support tickets ---------------- | |
| def _require_support_user_unlocked(self, user_id: str) -> dict[str, Any]: | |
| user = self.state["users"].get(user_id) | |
| if not user: | |
| raise PermissionError("Login required.") | |
| if user.get("role") == "admin": | |
| return user | |
| if user.get("plan") not in {"plus", "pro"}: | |
| raise PermissionError("Admin Support is available only for Plus and Pro accounts.") | |
| return user | |
| def support_runtime(self) -> dict[str, Any]: | |
| with self.lock: | |
| cfg = self.state["config"] | |
| enabled = bool(cfg.get("support_enabled", True)) | |
| forced = bool(cfg.get("support_force_open", False)) | |
| auto_paused = bool(cfg.get("support_auto_paused", False)) | |
| return {"enabled": bool(enabled and (forced or not auto_paused)), "manual_enabled": enabled, "force_open": forced, "auto_paused": auto_paused} | |
| def set_support_memory_state(self, paused: bool) -> None: | |
| with self.lock: | |
| paused = bool(paused) | |
| if self.state["config"].get("support_auto_paused", False) != paused: | |
| self.state["config"]["support_auto_paused"] = paused | |
| self._save_unlocked("Update CloudVaultHub support memory protection") | |
| def submit_support_ticket(self, user_id: str, subject: str, message: str) -> dict[str, Any]: | |
| subject = (subject or "").strip(); message = (message or "").strip() | |
| if not subject: raise ValueError("Subject is required.") | |
| if len(subject) > 200: raise ValueError("Subject may contain at most 200 characters.") | |
| if not message: raise ValueError("Message is required.") | |
| if len(message) > 5000: raise ValueError("Message may contain at most 5000 characters.") | |
| with self.lock: | |
| user = self._require_support_user_unlocked(user_id) | |
| cfg = self.state["config"] | |
| if not cfg.get("support_enabled", True): raise PermissionError("Admin Support is currently paused.") | |
| if cfg.get("support_auto_paused", False) and not cfg.get("support_force_open", False): raise PermissionError("Admin Support is temporarily paused by server protection.") | |
| today = utcnow().date() | |
| for ticket in self.state["support_tickets"]: | |
| created = parse_dt(ticket.get("created_at")) | |
| if ticket.get("user_id") == user_id and created and created.date() == today: | |
| raise ValueError("You can send only one Admin Support message per day.") | |
| ticket = {"id": uuid.uuid4().hex, "user_id": user_id, "username": user.get("username", "User"), "plan": user.get("plan", "plus"), "subject": subject, "message": message, "created_at": iso(), "status": "open", "reply": "", "replied_at": None, "read_by_user": False} | |
| self.state["support_tickets"].append(ticket) | |
| self._save_unlocked("Create CloudVaultHub support ticket") | |
| return dict(ticket) | |
| def user_support_tickets(self, user_id: str) -> list[dict[str, Any]]: | |
| with self.lock: | |
| self._require_support_user_unlocked(user_id) | |
| return sorted([dict(t) for t in self.state["support_tickets"] if t.get("user_id") == user_id], key=lambda t: t.get("created_at", ""), reverse=True) | |
| def admin_support_tickets(self, admin_id: str) -> list[dict[str, Any]]: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| priority = {"pro": 0, "plus": 1} | |
| tickets = [dict(t) for t in self.state["support_tickets"]] | |
| tickets.sort(key=lambda t: (priority.get(str(t.get("plan", "plus")), 2), -(parse_dt(t.get("created_at")).timestamp() if parse_dt(t.get("created_at")) else 0))) | |
| return tickets | |
| def get_support_ticket(self, actor_id: str, ticket_id: str) -> dict[str, Any] | None: | |
| with self.lock: | |
| actor = self.state["users"].get(actor_id) | |
| if not actor: return None | |
| for ticket in self.state["support_tickets"]: | |
| if ticket.get("id") == ticket_id and (actor.get("role") == "admin" or ticket.get("user_id") == actor_id): return dict(ticket) | |
| return None | |
| def reply_support_ticket(self, admin_id: str, ticket_id: str, reply: str) -> dict[str, Any]: | |
| reply = (reply or "").strip() | |
| if not reply: raise ValueError("Reply is required.") | |
| if len(reply) > 5000: raise ValueError("Reply may contain at most 5000 characters.") | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| for ticket in self.state["support_tickets"]: | |
| if ticket.get("id") == ticket_id: | |
| ticket.update({"reply": reply, "status": "answered", "replied_at": iso(), "read_by_user": False}) | |
| self._save_unlocked("Reply to CloudVaultHub support ticket") | |
| return dict(ticket) | |
| raise ValueError("Support message not found.") | |
| def mark_support_read(self, user_id: str, ticket_id: str) -> dict[str, Any]: | |
| with self.lock: | |
| for ticket in self.state["support_tickets"]: | |
| if ticket.get("id") == ticket_id and ticket.get("user_id") == user_id: | |
| ticket["read_by_user"] = True | |
| self._save_unlocked("Mark CloudVaultHub support reply as read") | |
| return dict(ticket) | |
| raise ValueError("Support message not found.") | |
| # ---------------- Legacy global chat (disabled) ---------------- | |
| def _chat_eligible_unlocked(self, user: dict[str, Any] | None) -> bool: | |
| if not user: | |
| return False | |
| if user.get("role") == "admin": | |
| return True | |
| return PLANS.get(user.get("plan", "free"), PLANS["free"]).chat_access | |
| def _room_sort_key_unlocked(self, room_id: str) -> tuple[int, str]: | |
| try: | |
| return int(room_id.rsplit("-", 1)[-1]), room_id | |
| except (TypeError, ValueError): | |
| return 10**9, room_id | |
| def _ensure_room_unlocked(self, room_id: str) -> dict[str, Any]: | |
| room = self.state["chat_rooms"].get(room_id) | |
| if room: | |
| return room | |
| number, _ = self._room_sort_key_unlocked(room_id) | |
| if number >= 10**9: | |
| number = max([self._room_sort_key_unlocked(rid)[0] for rid in self.state["chat_rooms"]] or [0]) + 1 | |
| room_id = f"global-{number}" | |
| room = {"id": room_id, "name": f"Global {number}", "paused": False, "created_at": iso()} | |
| self.state["chat_rooms"][room_id] = room | |
| return room | |
| def _room_member_count_unlocked(self, room_id: str, exclude_user_id: str | None = None) -> int: | |
| return sum( | |
| 1 for uid, user in self.state["users"].items() | |
| if uid != exclude_user_id and self._chat_eligible_unlocked(user) and user.get("chat_room_id", "global-1") == room_id | |
| ) | |
| def _new_room_unlocked(self) -> dict[str, Any]: | |
| numbers = [self._room_sort_key_unlocked(room_id)[0] for room_id in self.state["chat_rooms"]] | |
| number = max(numbers or [0]) + 1 | |
| room_id = f"global-{number}" | |
| room = {"id": room_id, "name": f"Global {number}", "paused": False, "created_at": iso()} | |
| self.state["chat_rooms"][room_id] = room | |
| return room | |
| def _assign_room_unlocked(self, user_id: str, force: bool = False) -> str: | |
| user = self.state["users"].get(user_id) | |
| if not self._chat_eligible_unlocked(user): | |
| return "" | |
| cfg = self.state["config"] | |
| if cfg.get("chat_mode", "single") == "single": | |
| user["chat_room_id"] = "global-1" | |
| return "global-1" | |
| current = user.get("chat_room_id") | |
| if not force and current in self.state["chat_rooms"]: | |
| return str(current) | |
| capacity = int(cfg.get("chat_room_capacity", 200)) | |
| available = [ | |
| rid for rid, room in self.state["chat_rooms"].items() | |
| if not room.get("paused") and self._room_member_count_unlocked(rid, exclude_user_id=user_id) < capacity | |
| ] | |
| if not available: | |
| available = [self._new_room_unlocked()["id"]] | |
| chosen = secrets.choice(sorted(available, key=self._room_sort_key_unlocked)) | |
| user["chat_room_id"] = chosen | |
| return chosen | |
| def chat_room_state(self, user_id: str) -> dict[str, Any]: | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not self._chat_eligible_unlocked(user): | |
| return {"mode": "single", "capacity": 0, "current": None, "can_choose": False, "rooms": []} | |
| user["chat_room_id"] = "global-1" | |
| room = self.state["chat_rooms"].setdefault( | |
| "global-1", | |
| {"id": "global-1", "name": "Global Chat", "paused": False, "created_at": iso()}, | |
| ) | |
| room["name"] = "Global Chat" | |
| return { | |
| "mode": "single", | |
| "capacity": 0, | |
| "current": "global-1", | |
| "can_choose": False, | |
| "rooms": [{ | |
| "id": "global-1", | |
| "name": "Global Chat", | |
| "paused": bool(room.get("paused", False)), | |
| "members": self._room_member_count_unlocked("global-1"), | |
| "messages": len(self.state["chat"]), | |
| }], | |
| } | |
| def change_chat_room(self, user_id: str, room_id: str) -> str: | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not self._chat_eligible_unlocked(user): | |
| raise PermissionError("Global Chat is available only to Plus and Pro accounts.") | |
| if self.state["config"].get("chat_mode", "single") != "multi": | |
| user["chat_room_id"] = "global-1" | |
| return "global-1" | |
| if user.get("role") != "admin" and user.get("plan") != "pro": | |
| raise PermissionError("Plus accounts are assigned to a room automatically.") | |
| room = self.state["chat_rooms"].get(room_id) | |
| if not room: | |
| raise ValueError("The selected chat room does not exist.") | |
| if room.get("paused") and user.get("role") != "admin": | |
| raise ValueError("The selected chat room is paused.") | |
| current = user.get("chat_room_id") | |
| capacity = int(self.state["config"].get("chat_room_capacity", 200)) | |
| if current != room_id and user.get("role") != "admin" and self._room_member_count_unlocked(room_id) >= capacity: | |
| raise ValueError("The selected chat room is full.") | |
| user["chat_room_id"] = room_id | |
| self._save_unlocked("Change CloudVaultHub chat room") | |
| return room_id | |
| def set_chat_configuration(self, admin_id: str, mode: str, capacity: int) -> dict[str, Any]: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| self.state["config"]["chat_mode"] = "single" | |
| self.state["config"]["chat_room_capacity"] = 0 | |
| room = self.state["chat_rooms"].setdefault( | |
| "global-1", | |
| {"id": "global-1", "name": "Global Chat", "paused": False, "created_at": iso()}, | |
| ) | |
| room["name"] = "Global Chat" | |
| self.state["chat_rooms"] = {"global-1": room} | |
| for user in self.state["users"].values(): | |
| if self._chat_eligible_unlocked(user): | |
| user["chat_room_id"] = "global-1" | |
| for message in self.state["chat"]: | |
| message["room_id"] = "global-1" | |
| self.state["chat"] = sorted(self.state["chat"], key=lambda m: m.get("created_at", ""))[-400:] | |
| self._save_unlocked("Use one unlimited Global Chat room") | |
| return dict(self.state["config"]) | |
| def toggle_chat_room(self, admin_id: str, room_id: str) -> bool: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| room = self.state["chat_rooms"].get(room_id) | |
| if not room: | |
| raise ValueError("Chat room not found.") | |
| room["paused"] = not bool(room.get("paused")) | |
| self._save_unlocked("Toggle CloudVaultHub chat room") | |
| return bool(room["paused"]) | |
| def clear_chat_room(self, admin_id: str, room_id: str) -> int: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| if room_id not in self.state["chat_rooms"]: | |
| raise ValueError("Chat room not found.") | |
| before = len(self.state["chat"]) | |
| self.state["chat"] = [m for m in self.state["chat"] if m.get("room_id", "global-1") != room_id] | |
| removed = before - len(self.state["chat"]) | |
| self._save_unlocked("Clear CloudVaultHub chat room") | |
| return removed | |
| def send_chat(self, user_id: str, text: str, room_id: str | None = None) -> dict[str, Any]: | |
| message = (text or "").strip() | |
| if not message: | |
| raise ValueError("Write a message first.") | |
| if len(message) > 1000: | |
| raise ValueError("Messages may contain at most 1000 characters.") | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not user: | |
| raise ValueError("Please log in again.") | |
| plan = PLANS.get(user.get("plan", "free"), PLANS["free"]) | |
| if not self._chat_eligible_unlocked(user): | |
| raise PermissionError("Global Chat is available only to Plus and Pro accounts.") | |
| if user.get("is_banned"): | |
| raise PermissionError("This account is banned.") | |
| cfg = self.state["config"] | |
| if not cfg.get("system_enabled", True): | |
| raise PermissionError("The system is paused.") | |
| if not cfg.get("chat_enabled", True): | |
| raise PermissionError("Global Chat is paused by the developer.") | |
| if user.get("role") != "admin" and user.get("plan") == "plus" and not cfg.get("plus_chat_enabled", True): | |
| raise PermissionError("Global Chat is paused for Plus accounts.") | |
| if user.get("role") != "admin" and user.get("plan") == "pro" and not cfg.get("pro_chat_enabled", True): | |
| raise PermissionError("Global Chat is paused for Pro accounts.") | |
| user["chat_room_id"] = "global-1" | |
| assigned_room = "global-1" | |
| room = self.state["chat_rooms"].get(assigned_room) | |
| if room and room.get("paused") and user.get("role") != "admin": | |
| raise PermissionError("This chat room is paused by the developer.") | |
| now = utcnow() | |
| last = parse_dt(user.get("last_chat_at")) | |
| if last and (now - last).total_seconds() < 3: | |
| wait = max(1, 3 - int((now - last).total_seconds())) | |
| raise ValueError(f"Wait {wait} second(s) before sending another message.") | |
| record = { | |
| "id": uuid.uuid4().hex, | |
| "room_id": assigned_room, | |
| "sender_id": user_id, | |
| "sender_name": "admin" if user.get("role") == "admin" else user.get("username", "User"), | |
| "sender_plan": "admin" if user.get("role") == "admin" else user.get("plan", "free"), | |
| "text": message, | |
| "created_at": iso(now), | |
| "visible_at": iso(now + timedelta(seconds=2)), | |
| } | |
| user["last_chat_at"] = iso(now) | |
| self.state["chat"].append(record) | |
| self.state["chat"] = sorted(self.state["chat"], key=lambda m: m.get("created_at", ""))[-400:] | |
| self._save_unlocked("Send CloudVaultHub Global Chat message") | |
| return dict(record) | |
| def chat_messages(self, user_id: str, room_id: str | None = None) -> list[dict[str, Any]]: | |
| with self.lock: | |
| user = self.state["users"].get(user_id) | |
| if not self._chat_eligible_unlocked(user): | |
| return [] | |
| assigned = self._assign_room_unlocked(user_id) | |
| now = utcnow() | |
| limit = 400 if user.get("role") == "admin" or user.get("plan") == "pro" else 200 | |
| return [ | |
| dict(m) for m in self.state["chat"] | |
| if m.get("room_id", "global-1") == assigned and (parse_dt(m.get("visible_at")) or now) <= now | |
| ][-limit:] | |
| def clear_chat(self, admin_id: str) -> None: | |
| with self.lock: | |
| self._assert_admin_unlocked(admin_id) | |
| self.state["chat"] = [] | |
| self._save_unlocked("Clear CloudVaultHub Global Chat") | |
| STORE = StateStore(BACKEND, SETTINGS) | |
| def derive_file_key(user_id: str, file_id: str, algorithm: str) -> bytes: | |
| key_material = hashlib.sha256(SETTINGS.master_key.encode("utf-8")).digest() | |
| return hmac.new(key_material, f"{user_id}:{file_id}:{algorithm}".encode("utf-8"), hashlib.sha256).digest() | |
| def encrypt_file(source: Path, destination: Path, user_id: str, file_id: str, algorithm: str) -> int: | |
| if algorithm == "none": | |
| shutil.copy2(source, destination) | |
| return destination.stat().st_size | |
| algorithm_id = 1 if algorithm == "aes256-gcm" else 2 | |
| key = derive_file_key(user_id, file_id, algorithm) | |
| cipher = AESGCM(key) if algorithm_id == 1 else ChaCha20Poly1305(key) | |
| with source.open("rb") as fin, destination.open("wb") as fout: | |
| fout.write(MAGIC + bytes([algorithm_id]) + struct.pack(">I", CHUNK_SIZE)) | |
| index = 0 | |
| while True: | |
| chunk = fin.read(CHUNK_SIZE) | |
| if not chunk: | |
| break | |
| nonce = os.urandom(12) | |
| encrypted = cipher.encrypt(nonce, chunk, struct.pack(">Q", index)) | |
| fout.write(nonce) | |
| fout.write(struct.pack(">I", len(encrypted))) | |
| fout.write(encrypted) | |
| index += 1 | |
| return destination.stat().st_size | |
| def decrypt_file(source: Path, destination: Path, user_id: str, file_id: str, algorithm: str) -> None: | |
| if algorithm == "none": | |
| shutil.copy2(source, destination) | |
| return | |
| with source.open("rb") as fin, destination.open("wb") as fout: | |
| magic = fin.read(len(MAGIC)) | |
| if magic not in {MAGIC, b"CVLT2"}: | |
| raise ValueError("The encrypted file header is invalid.") | |
| algorithm_id_raw = fin.read(1) | |
| if not algorithm_id_raw: | |
| raise ValueError("The encrypted file is incomplete.") | |
| algorithm_id = algorithm_id_raw[0] | |
| chunk_raw = fin.read(4) | |
| if len(chunk_raw) != 4: | |
| raise ValueError("The encrypted file is incomplete.") | |
| _chunk_size = struct.unpack(">I", chunk_raw)[0] | |
| expected = 1 if algorithm == "aes256-gcm" else 2 | |
| if algorithm_id != expected: | |
| raise ValueError("The encryption mode does not match the file metadata.") | |
| key = derive_file_key(user_id, file_id, algorithm) | |
| cipher = AESGCM(key) if algorithm_id == 1 else ChaCha20Poly1305(key) | |
| index = 0 | |
| while True: | |
| nonce = fin.read(12) | |
| if not nonce: | |
| break | |
| if len(nonce) != 12: | |
| raise ValueError("The encrypted file is incomplete.") | |
| length_raw = fin.read(4) | |
| if len(length_raw) != 4: | |
| raise ValueError("The encrypted file is incomplete.") | |
| length = struct.unpack(">I", length_raw)[0] | |
| encrypted = fin.read(length) | |
| if len(encrypted) != length: | |
| raise ValueError("The encrypted file is incomplete.") | |
| fout.write(cipher.decrypt(nonce, encrypted, struct.pack(">Q", index))) | |
| index += 1 | |
| def zip_directory_files(paths: list[str], destination: Path) -> tuple[int, str]: | |
| valid = [Path(p) for p in paths if p and Path(p).is_file()] | |
| if not valid: | |
| raise ValueError("Choose a folder first.") | |
| total = 0 | |
| with zipfile.ZipFile(destination, "w", compression=zipfile.ZIP_DEFLATED, allowZip64=True) as archive: | |
| used_names: set[str] = set() | |
| for index, path in enumerate(valid): | |
| name = safe_filename(path.name) | |
| if name in used_names: | |
| name = f"{index + 1}-{name}" | |
| used_names.add(name) | |
| archive.write(path, arcname=name) | |
| total += path.stat().st_size | |
| return total, "uploaded-folder.zip" | |
| def estimate_upload_seconds(size_bytes: int, plan_key: str) -> int: | |
| # Approximate end-to-end speed. Real time depends on network and Hugging Face load. | |
| mb_per_second = {"free": 2.0, "plus": 5.0, "pro": 8.0, "admin": 12.0}.get(plan_key, 2.0) | |
| processing_overhead = {"free": 3, "plus": 5, "pro": 6, "admin": 3}.get(plan_key, 4) | |
| return max(2, int(size_bytes / MB / mb_per_second) + processing_overhead) | |
| def format_eta(seconds: int) -> str: | |
| if seconds < 60: | |
| return f"about {seconds} seconds" | |
| minutes = max(1, round(seconds / 60)) | |
| return f"about {minutes} minute{'s' if minutes != 1 else ''}" | |
| def prepare_upload(source_path: str, folder_paths: list[str] | None, user_id: str, progress=None) -> dict[str, Any]: | |
| user = STORE.get_user(user_id) | |
| if not user: | |
| raise ValueError("Please log in again.") | |
| cfg = STORE.config() | |
| if user.get("role") != "admin" and not cfg.get("system_enabled", True): | |
| raise ValueError("The system is temporarily paused.") | |
| is_admin = str(user.get("role", "")).lower() == "admin" | |
| plan_key = str(user.get("plan", "free")).lower() | |
| plan = PLANS["admin"] if is_admin else PLANS.get(plan_key, PLANS["free"]) | |
| file_id = uuid.uuid4().hex | |
| raw_temp: Path | None = None | |
| encrypted_temp: Path | None = None | |
| try: | |
| if folder_paths: | |
| if not plan.folder_upload: | |
| raise ValueError("Folder upload is not available on the Free package.") | |
| raw_temp = SETTINGS.temp_dir / f"folder-{file_id}.zip" | |
| original_size, display_name = zip_directory_files(folder_paths, raw_temp) | |
| content_type = "application/zip" | |
| is_folder = True | |
| else: | |
| if not source_path: | |
| raise ValueError("Choose a file or folder first.") | |
| source = Path(source_path) | |
| if not source.is_file(): | |
| raise ValueError("The selected file could not be read.") | |
| raw_temp = SETTINGS.temp_dir / f"raw-{file_id}-{safe_filename(source.name)}" | |
| shutil.copy2(source, raw_temp) | |
| original_size = raw_temp.stat().st_size | |
| display_name = safe_filename(source.name) | |
| content_type = mimetypes.guess_type(display_name)[0] or "application/octet-stream" | |
| is_folder = False | |
| if original_size <= 0: | |
| raise ValueError("Empty files cannot be uploaded.") | |
| if is_admin: | |
| remaining = STORE.available_for_admin() | |
| if original_size > remaining: | |
| raise ValueError( | |
| f"Upload failed: developer storage is full. Available {human_bytes(remaining)}, file size {human_bytes(original_size)}." | |
| ) | |
| else: | |
| remaining = max(0, plan.quota_bytes - STORE.used_bytes(user_id)) | |
| if original_size > remaining: | |
| raise ValueError( | |
| f"Upload failed: your {plan.name} storage is full. Available {human_bytes(remaining)}, file size {human_bytes(original_size)}." | |
| ) | |
| estimated_seconds = estimate_upload_seconds(original_size, plan.key) | |
| estimated_text = format_eta(estimated_seconds) | |
| if progress: | |
| progress(0.12, desc=f"Estimated completion: {estimated_text}") | |
| progress(0.25, desc=f"Preparing file · {estimated_text}") | |
| upload_path = raw_temp | |
| stored_size = original_size | |
| if plan.encryption != "none": | |
| encrypted_temp = SETTINGS.temp_dir / f"encrypted-{file_id}.bin" | |
| stored_size = encrypt_file(raw_temp, encrypted_temp, user_id, file_id, plan.encryption) | |
| upload_path = encrypted_temp | |
| if progress: | |
| progress(0.60, desc="Encrypting file") | |
| remote_path = f"objects/{user_id}/{file_id}.blob" | |
| if progress: | |
| progress(0.78, desc="Saving to persistent storage") | |
| BACKEND.upload(upload_path, remote_path, "Upload CloudVaultHub user file") | |
| metadata = { | |
| "id": file_id, | |
| "owner_id": user_id, | |
| "display_name": display_name, | |
| "remote_path": remote_path, | |
| "original_size": original_size, | |
| "stored_size": stored_size, | |
| "is_folder": is_folder, | |
| "content_type": content_type, | |
| "encryption": plan.encryption, | |
| "share_password_hash": None, | |
| "download_count": 0, | |
| "last_download_at": None, | |
| "created_at": iso(), | |
| "uploaded_under_plan": plan.key, | |
| "estimated_upload_seconds": estimated_seconds, | |
| } | |
| try: | |
| saved = STORE.add_file(user_id, metadata) | |
| except Exception: | |
| BACKEND.delete_many([remote_path], "Rollback incomplete CloudVaultHub upload") | |
| raise | |
| if progress: | |
| progress(1.0, desc="Upload completed") | |
| return saved | |
| finally: | |
| if raw_temp: | |
| raw_temp.unlink(missing_ok=True) | |
| if encrypted_temp: | |
| encrypted_temp.unlink(missing_ok=True) | |
| def prepare_generated_folder_archive( | |
| archive_path: str | Path, | |
| display_name: str, | |
| user_id: str, | |
| progress=None, | |
| ) -> dict[str, Any]: | |
| user = STORE.get_user(user_id) | |
| if not user: | |
| raise ValueError("Please log in again.") | |
| cfg = STORE.config() | |
| if user.get("role") != "admin" and not cfg.get("system_enabled", True): | |
| raise ValueError("The system is temporarily paused.") | |
| if user.get("role") != "admin" and user.get("plan") != "pro": | |
| raise PermissionError("Bu özellik yalnızca Pro hesaplarda kullanılabilir.") | |
| source = Path(archive_path) | |
| if not source.is_file() or source.suffix.lower() != ".zip": | |
| raise ValueError("Hazırlanan web sitesi klasörü bulunamadı.") | |
| original_size = source.stat().st_size | |
| if original_size <= 0: | |
| raise ValueError("Boş klasör buluta eklenemez.") | |
| is_admin = str(user.get("role", "")).lower() == "admin" | |
| plan_key = str(user.get("plan", "free")).lower() | |
| plan = PLANS["admin"] if is_admin else PLANS.get(plan_key, PLANS["free"]) | |
| if user.get("role") == "admin": | |
| if original_size > STORE.available_for_admin(): | |
| raise ValueError("The remaining unreserved server storage is not enough for this upload.") | |
| elif STORE.used_bytes(user_id) + original_size > plan.quota_bytes: | |
| raise ValueError("There is not enough cloud storage space for this folder.") | |
| file_id = uuid.uuid4().hex | |
| encrypted_temp: Path | None = None | |
| try: | |
| upload_path = source | |
| stored_size = original_size | |
| if progress: | |
| progress(0.20, desc="Web sitesi klasörü hazırlanıyor") | |
| if plan.encryption != "none": | |
| encrypted_temp = SETTINGS.temp_dir / f"generated-folder-{file_id}.bin" | |
| stored_size = encrypt_file(source, encrypted_temp, user_id, file_id, plan.encryption) | |
| upload_path = encrypted_temp | |
| if progress: | |
| progress(0.60, desc="Klasör şifreleniyor") | |
| remote_path = f"objects/{user_id}/{file_id}.blob" | |
| BACKEND.upload(upload_path, remote_path, "Upload generated website folder") | |
| metadata = { | |
| "id": file_id, | |
| "owner_id": user_id, | |
| "display_name": safe_filename(display_name), | |
| "remote_path": remote_path, | |
| "original_size": original_size, | |
| "stored_size": stored_size, | |
| "is_folder": True, | |
| "content_type": "application/zip", | |
| "encryption": plan.encryption, | |
| "share_password_hash": None, | |
| "download_count": 0, | |
| "last_download_at": None, | |
| "created_at": iso(), | |
| "uploaded_under_plan": plan.key, | |
| "estimated_upload_seconds": estimate_upload_seconds(original_size, plan.key), | |
| } | |
| saved = STORE.add_file(user_id, metadata) | |
| if progress: | |
| progress(1.0, desc="Klasör bulut depolamaya eklendi") | |
| return saved | |
| finally: | |
| if encrypted_temp: | |
| encrypted_temp.unlink(missing_ok=True) | |
| def materialize_download(item: dict[str, Any]) -> Path: | |
| file_id = item["id"] | |
| remote_temp = SETTINGS.temp_dir / f"download-remote-{file_id}-{uuid.uuid4().hex}.bin" | |
| output = SETTINGS.temp_dir / f"download-{uuid.uuid4().hex}-{safe_filename(item.get('display_name', 'file.bin'))}" | |
| if not BACKEND.download(item["remote_path"], remote_temp): | |
| raise FileNotFoundError("The stored object could not be downloaded.") | |
| try: | |
| decrypt_file(remote_temp, output, item["owner_id"], file_id, item.get("encryption", "none")) | |
| finally: | |
| remote_temp.unlink(missing_ok=True) | |
| return output | |
| def guess_device(user_agent: str) -> str: | |
| value = (user_agent or "").lower() | |
| if "ipad" in value or "tablet" in value: | |
| return "Tablet" | |
| if "android" in value or "iphone" in value or "mobile" in value: | |
| return "Mobile" | |
| return "Desktop" | |