Pavle-17's picture
InfoShield backend (Docker)
1cf82f0
Raw
History Blame Contribute Delete
4.82 kB
"""JSON-file + in-memory caching. No database.
- `cache.json` holds per-post classification + analysis so the demo is instant
and works fully offline after one build.
- `activation.json` holds the landing-page-selected target URL and on/off state.
- An in-memory dict caches ad-hoc /classify and /analyze results by content hash.
"""
from __future__ import annotations
import hashlib
import json
import logging
import threading
from datetime import datetime, timezone
from typing import Optional
from config import ACTIVATION_FILE, CACHE_FILE, CACHE_VERSION, DATA_DIR, POSTS_FILE
log = logging.getLogger("infoshield.cache")
_lock = threading.Lock()
def _now() -> str:
return datetime.now(timezone.utc).isoformat(timespec="seconds")
def text_hash(text: str) -> str:
return hashlib.sha256(text.strip().encode("utf-8")).hexdigest()[:16]
# --- posts ----------------------------------------------------------------
def load_posts() -> list[dict]:
with open(POSTS_FILE, "r", encoding="utf-8") as f:
return json.load(f)
# --- post cache -----------------------------------------------------------
class PostCache:
def __init__(self) -> None:
self.meta: dict = {}
self.posts: dict[str, dict] = {}
self.adhoc: dict[str, dict] = {} # content-hash -> {classification, analysis}
def load(self) -> bool:
if not CACHE_FILE.exists():
log.warning("Cache file %s not found -- starting empty.", CACHE_FILE)
return False
try:
with open(CACHE_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
self.meta = data.get("meta", {})
self.posts = data.get("posts", {})
if self.meta.get("version") != CACHE_VERSION:
log.info("Cache version mismatch (have %s, want %s) -- discarding stale cache.",
self.meta.get("version"), CACHE_VERSION)
self.posts = {}
self.meta = {}
return False
log.info("Loaded cache: %d posts (built_at=%s).",
len(self.posts), self.meta.get("built_at"))
return True
except Exception as exc: # noqa: BLE001
log.error("Failed to load cache: %s", exc)
return False
def save(self) -> None:
DATA_DIR.mkdir(parents=True, exist_ok=True)
self.meta["version"] = CACHE_VERSION
with _lock:
payload = {"meta": self.meta, "posts": self.posts}
with open(CACHE_FILE, "w", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, indent=2)
log.info("Cache saved (%d posts).", len(self.posts))
def get(self, post_id: str) -> Optional[dict]:
return self.posts.get(post_id)
def set(self, post_id: str, classification: dict, analysis: Optional[dict]) -> None:
self.posts[post_id] = {"classification": classification, "analysis": analysis}
def set_meta(self, **kw) -> None:
self.meta.update(kw)
self.meta["built_at"] = _now()
self.meta["version"] = CACHE_VERSION
@property
def mode(self) -> str:
return "cache-first" if self.posts else "empty"
_cache: Optional[PostCache] = None
def get_cache() -> PostCache:
global _cache
if _cache is None:
_cache = PostCache()
_cache.load()
return _cache
# --- activation config ----------------------------------------------------
DEFAULT_ACTIVATION = {
"target_url": "",
"enabled": False,
"updated_at": None,
}
def load_activation() -> dict:
if not ACTIVATION_FILE.exists():
return dict(DEFAULT_ACTIVATION)
try:
with open(ACTIVATION_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
return {**DEFAULT_ACTIVATION, **data}
except Exception: # noqa: BLE001
return dict(DEFAULT_ACTIVATION)
def save_activation(target_url: str, enabled: bool = True) -> dict:
DATA_DIR.mkdir(parents=True, exist_ok=True)
cfg = {
"target_url": target_url,
"enabled": enabled,
"updated_at": _now(),
}
with _lock:
with open(ACTIVATION_FILE, "w", encoding="utf-8") as f:
json.dump(cfg, f, ensure_ascii=False, indent=2)
log.info("Activation target saved: %s (enabled=%s)", target_url, enabled)
return cfg
def reset_activation() -> dict:
"""Reset activation to default (disabled) so each fresh run starts at the
landing page's ACTIVATE button."""
DATA_DIR.mkdir(parents=True, exist_ok=True)
with _lock:
with open(ACTIVATION_FILE, "w", encoding="utf-8") as f:
json.dump(dict(DEFAULT_ACTIVATION), f, ensure_ascii=False, indent=2)
log.info("Activation reset to default (fresh demo).")
return dict(DEFAULT_ACTIVATION)