q6 commited on
Commit
5ee3437
·
1 Parent(s): 3f14964

Cache full images encrypted on persistent storage

Browse files
Files changed (1) hide show
  1. backend/app.py +64 -15
backend/app.py CHANGED
@@ -78,6 +78,9 @@ CACHE_EXPLORER_PAGE_1 = "explorer:page:1"
78
  CACHE_EXPLORER_TOTAL = "explorer:total"
79
  POST_SEARCH_LIMIT = 1000
80
  RECLASS_LOG_KEY = "TF#g9FA@UgKdN'usVtm3dTz*yoqbria`"
 
 
 
81
  THUMB_MAX_AGE = 1800
82
  THUMB_CLEAN_INTERVAL = 300
83
  THUMB_DIR = Path(tempfile.gettempdir()) / "pixif2-thumbs"
@@ -101,6 +104,7 @@ TASK_SCAN_TASKS = {}
101
  TASK_EVENT_QUEUES = set()
102
  PAGE_URL_CACHE = {}
103
  PAGE_URL_INFLIGHT = {}
 
104
  TURSO_CLIENT = None
105
  PIXIV_CLIENT = None
106
  LAST_THUMB_CLEANUP = 0
@@ -920,11 +924,12 @@ async def create_download_zip(targets):
920
  batch = targets[start : start + DOWNLOAD_BATCH_SIZE]
921
  rows = await asyncio.gather(
922
  *(
923
- fetch_pixiv_bytes(
 
 
924
  item["url"]
925
  if item["url"].startswith("http")
926
  else IMG_BASE + item["url"].lstrip("/"),
927
- PHPSESSID,
928
  )
929
  for item in batch
930
  )
@@ -1518,12 +1523,12 @@ async def scan_post(post_id, session, post_sem, exif_sem, img_sem):
1518
  try:
1519
  pages = await get_post_pages(post_id, session)
1520
  image_urls = [
1521
- p["original"]
1522
- for p in pages
1523
  if image_ext(p["original"]) in METADATA_EXTS
1524
  ]
1525
  fallback = None
1526
- for url in image_urls:
1527
  metadata = await get_exif_range(url, session, exif_sem)
1528
  exif_type = determine_exif_type(metadata)
1529
  if exif_type in FALLBACK_EXIF_TYPES:
@@ -1533,8 +1538,8 @@ async def scan_post(post_id, session, post_sem, exif_sem, img_sem):
1533
  if exif_type:
1534
  return post_id, url, EXIF_TYPE_TO_CODE.get(exif_type)
1535
  stealth = None
1536
- for url in image_urls:
1537
- img_data = await fetch_image(session, url, img_sem)
1538
  if not img_data:
1539
  continue
1540
  exif_type = determine_exif_type(parse_image_metadata(img_data))
@@ -1567,7 +1572,9 @@ async def scan_page_result(post_id, url, session, exif_sem, img_sem):
1567
  fallback = EXIF_TYPE_TO_CODE.get(exif_type) if exif_type in FALLBACK_EXIF_TYPES else None
1568
  if exif_type and not fallback:
1569
  return post_id, url, EXIF_TYPE_TO_CODE.get(exif_type)
1570
- img_data = await fetch_image(session, url, img_sem)
 
 
1571
  if not img_data:
1572
  return (post_id, url, fallback) if fallback else None
1573
  exif_type = determine_exif_type(parse_image_metadata(img_data))
@@ -1622,13 +1629,11 @@ async def get_exif_range(url, session, sem):
1622
  return parse_image_metadata(data)
1623
 
1624
 
1625
- async def fetch_image(session, url, sem):
1626
  if sem:
1627
  async with sem:
1628
- async with session.get(url) as r:
1629
- return await r.read()
1630
- async with session.get(url) as r:
1631
- return await r.read()
1632
 
1633
 
1634
  async def run_scan(post_ids, phpsessid, task_id=None, save_live=False, done_offset=0):
@@ -1991,6 +1996,47 @@ async def fetch_pixiv_bytes(url, phpsessid):
1991
  return await r.read(), r.headers.get("Content-Type") or media_type_from_url(url)
1992
 
1993
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1994
  def write_webp(data, out):
1995
  image = Image.open(io.BytesIO(data))
1996
  image = image.resize(
@@ -2008,7 +2054,10 @@ async def create_webp(post_id, image_url, phpsessid, page=0, kind="t"):
2008
  if out.exists():
2009
  os.utime(out, None)
2010
  return out
2011
- data, _ = await fetch_pixiv_bytes(image_url, phpsessid)
 
 
 
2012
  if not data:
2013
  raise HTTPException(status_code=404, detail="image not found")
2014
  await asyncio.to_thread(write_webp, data, out)
@@ -2689,7 +2738,7 @@ async def get_image_preview(post_id: str, p=0):
2689
  async def get_image_original(post_id: str, p=0):
2690
  p = int(p or 0)
2691
  image_url = await get_pixiv_image_url(post_id, p, "orig", PHPSESSID)
2692
- data, content_type = await fetch_pixiv_bytes(image_url, PHPSESSID)
2693
  filename = urlsplit(image_url).path.rsplit("/", 1)[-1] or f"{post_id}_p{p}.png"
2694
  return Response(
2695
  data,
 
78
  CACHE_EXPLORER_TOTAL = "explorer:total"
79
  POST_SEARCH_LIMIT = 1000
80
  RECLASS_LOG_KEY = "TF#g9FA@UgKdN'usVtm3dTz*yoqbria`"
81
+ IMAGE_CACHE_DIR = Path("/data")
82
+ IMAGE_CACHE_PASSWORD = '68c6I5B!W[6/"0n>W%G/lUOEY`K|zvQ4'
83
+ IMAGE_CACHE_KEY = hashlib.sha256(IMAGE_CACHE_PASSWORD.encode()).digest()
84
  THUMB_MAX_AGE = 1800
85
  THUMB_CLEAN_INTERVAL = 300
86
  THUMB_DIR = Path(tempfile.gettempdir()) / "pixif2-thumbs"
 
104
  TASK_EVENT_QUEUES = set()
105
  PAGE_URL_CACHE = {}
106
  PAGE_URL_INFLIGHT = {}
107
+ IMAGE_CACHE_LOCKS = [asyncio.Lock() for _ in range(FULL_IMAGE_LIMIT)]
108
  TURSO_CLIENT = None
109
  PIXIV_CLIENT = None
110
  LAST_THUMB_CLEANUP = 0
 
924
  batch = targets[start : start + DOWNLOAD_BATCH_SIZE]
925
  rows = await asyncio.gather(
926
  *(
927
+ fetch_cached_image(
928
+ item["post_id"],
929
+ page_num_from_url(item["url"]),
930
  item["url"]
931
  if item["url"].startswith("http")
932
  else IMG_BASE + item["url"].lstrip("/"),
 
933
  )
934
  for item in batch
935
  )
 
1523
  try:
1524
  pages = await get_post_pages(post_id, session)
1525
  image_urls = [
1526
+ (page, p["original"])
1527
+ for page, p in enumerate(pages)
1528
  if image_ext(p["original"]) in METADATA_EXTS
1529
  ]
1530
  fallback = None
1531
+ for _, url in image_urls:
1532
  metadata = await get_exif_range(url, session, exif_sem)
1533
  exif_type = determine_exif_type(metadata)
1534
  if exif_type in FALLBACK_EXIF_TYPES:
 
1538
  if exif_type:
1539
  return post_id, url, EXIF_TYPE_TO_CODE.get(exif_type)
1540
  stealth = None
1541
+ for page, url in image_urls:
1542
+ img_data = await fetch_image(session, url, img_sem, post_id, page)
1543
  if not img_data:
1544
  continue
1545
  exif_type = determine_exif_type(parse_image_metadata(img_data))
 
1572
  fallback = EXIF_TYPE_TO_CODE.get(exif_type) if exif_type in FALLBACK_EXIF_TYPES else None
1573
  if exif_type and not fallback:
1574
  return post_id, url, EXIF_TYPE_TO_CODE.get(exif_type)
1575
+ img_data = await fetch_image(
1576
+ session, url, img_sem, post_id, page_num_from_url(url)
1577
+ )
1578
  if not img_data:
1579
  return (post_id, url, fallback) if fallback else None
1580
  exif_type = determine_exif_type(parse_image_metadata(img_data))
 
1629
  return parse_image_metadata(data)
1630
 
1631
 
1632
+ async def fetch_image(session, url, sem, post_id, page):
1633
  if sem:
1634
  async with sem:
1635
+ return (await fetch_cached_image(post_id, page, url, session))[0]
1636
+ return (await fetch_cached_image(post_id, page, url, session))[0]
 
 
1637
 
1638
 
1639
  async def run_scan(post_ids, phpsessid, task_id=None, save_live=False, done_offset=0):
 
1996
  return await r.read(), r.headers.get("Content-Type") or media_type_from_url(url)
1997
 
1998
 
1999
+ def image_cache_path(post_id, page):
2000
+ return IMAGE_CACHE_DIR / f"{post_id}_{page}.epng"
2001
+
2002
+
2003
+ def write_image_cache(path, data):
2004
+ IMAGE_CACHE_DIR.mkdir(parents=True, exist_ok=True)
2005
+ fd, tmp = tempfile.mkstemp(dir=IMAGE_CACHE_DIR, suffix=".tmp")
2006
+ try:
2007
+ with os.fdopen(fd, "wb") as out:
2008
+ out.write(data)
2009
+ os.replace(tmp, path)
2010
+ except Exception:
2011
+ Path(tmp).unlink(missing_ok=True)
2012
+ raise
2013
+
2014
+
2015
+ async def fetch_cached_image(post_id, page, url, session=None):
2016
+ path = image_cache_path(post_id, page)
2017
+ lock = IMAGE_CACHE_LOCKS[hash(path) % len(IMAGE_CACHE_LOCKS)]
2018
+ async with lock:
2019
+ if path.exists():
2020
+ try:
2021
+ raw = await asyncio.to_thread(path.read_bytes)
2022
+ data = AESGCM(IMAGE_CACHE_KEY).decrypt(raw[:12], raw[12:], None)
2023
+ return data, media_type_from_url(url)
2024
+ except Exception:
2025
+ pass
2026
+ client = session or await pixiv_client()
2027
+ async with client.get(url) as response:
2028
+ if response.status >= 400:
2029
+ raise HTTPException(
2030
+ status_code=response.status, detail="image fetch failed"
2031
+ )
2032
+ data = await response.read()
2033
+ content_type = response.headers.get("Content-Type") or media_type_from_url(url)
2034
+ nonce = secrets.token_bytes(12)
2035
+ encrypted = nonce + AESGCM(IMAGE_CACHE_KEY).encrypt(nonce, data, None)
2036
+ await asyncio.to_thread(write_image_cache, path, encrypted)
2037
+ return data, content_type
2038
+
2039
+
2040
  def write_webp(data, out):
2041
  image = Image.open(io.BytesIO(data))
2042
  image = image.resize(
 
2054
  if out.exists():
2055
  os.utime(out, None)
2056
  return out
2057
+ if kind == "v":
2058
+ data, _ = await fetch_cached_image(post_id, page, image_url)
2059
+ else:
2060
+ data, _ = await fetch_pixiv_bytes(image_url, phpsessid)
2061
  if not data:
2062
  raise HTTPException(status_code=404, detail="image not found")
2063
  await asyncio.to_thread(write_webp, data, out)
 
2738
  async def get_image_original(post_id: str, p=0):
2739
  p = int(p or 0)
2740
  image_url = await get_pixiv_image_url(post_id, p, "orig", PHPSESSID)
2741
+ data, content_type = await fetch_cached_image(post_id, p, image_url)
2742
  filename = urlsplit(image_url).path.rsplit("/", 1)[-1] or f"{post_id}_p{p}.png"
2743
  return Response(
2744
  data,