Sweep of defensive hardening across the core layers plus a related popout overlay regression that surfaced during verification. Database integrity (core/db.py) - Wrap delete_site, add_search_history, remove_folder, rename_folder, and _migrate in `with self.conn:` so partial commits can't leave orphan rows on a crash mid-method. - add_bookmark re-SELECTs the existing id when INSERT OR IGNORE collides on (site_id, post_id). Was returning Bookmark(id=0) silently, which then no-op'd update_bookmark_cache_path the next time the post was bookmarked. - get_bookmarks LIKE clauses now ESCAPE '%', '_', '\\' so user search literals stop acting as SQL wildcards (cat_ear no longer matches catear). Path traversal (core/db.py + core/config.py) - Validate folder names at write time via _validate_folder_name — rejects '..', os.sep, leading '.' / '~'. Permits Unicode/spaces/ parens so existing folders keep working. - saved_folder_dir() resolves the candidate path and refuses anything that doesn't relative_to the saved-images base. Defense in depth against folder strings that bypass the write-time validator. - gui/bookmarks.py and gui/app.py wrap add_folder calls in try/except ValueError and surface a QMessageBox.warning instead of crashing. Download safety (core/cache.py) - New _do_download(): payloads >=50MB stream to a tempfile in the destination dir and atomically os.replace into place; smaller payloads keep the existing buffer-then-write fast path. Both enforce a 500MB hard cap against the advertised Content-Length AND the running total inside the chunk loop (servers can lie). - Per-URL asyncio.Lock coalesces concurrent downloads of the same URL so two callers don't race write_bytes on the same path. - Image.MAX_IMAGE_PIXELS = 256M with DecompressionBombError handling in both converters. - _convert_ugoira_to_gif checks frame count + cumulative uncompressed size against UGOIRA_MAX_FRAMES / UGOIRA_MAX_UNCOMPRESSED_BYTES from ZipInfo headers BEFORE decompressing — defends against zip bombs. - _convert_animated_to_gif writes a .convfailed sentinel sibling on failure to break the re-decode-on-every-paint loop for malformed animated PNGs/WebPs. - _is_valid_media returns True (don't delete) on OSError so a transient EBUSY/permissions hiccup no longer triggers a delete + re-download loop on every access. - _referer_for() uses proper hostname suffix matching, not substring `in` (imgblahgelbooru.attacker.com no longer maps to gelbooru.com). - PIL handles wrapped in `with` blocks for deterministic cleanup. API client retry + visibility (core/api/*) - base.py: _request retries on httpx.NetworkError + ConnectError in addition to TimeoutException. test_connection no longer echoes the HTTP response body in the error string (it was an SSRF body-leak gadget when used via detect_site_type's redirect-following client). - detect.py + danbooru.py + e621.py + gelbooru.py + moebooru.py: every previously-swallowed exception in search/autocomplete/probe paths now logs at WARNING with type, message, and (where relevant) the response body prefix. Debugging "the site isn't working" used to be a total blackout. main_gui.py - file_dialog_platform DB probe failure prints to stderr instead of vanishing. Popout overlay (gui/preview.py + gui/app.py) - preview.py:79,141 — setAttribute(WA_StyledBackground, True) on _slideshow_toolbar and _slideshow_controls. Plain QWidget parents silently ignore QSS `background:` declarations without this attribute, which is why the popout overlay strip was rendering fully transparent (buttons styled, bar behind them showing the letterbox color). - app.py: bake _BASE_POPOUT_OVERLAY_QSS as a fallback prepended before the user's custom.qss in the loader. Custom themes that don't define overlay rules now still get a translucent black bar with white text + hairline borders. Bundled themes win on tie because their identical-specificity rules come last in the prepended string.
44 lines
1.3 KiB
Python
44 lines
1.3 KiB
Python
"""GUI entry point."""
|
|
|
|
import os
|
|
import sys
|
|
|
|
|
|
def main() -> None:
|
|
# Windows: set App User Model ID so taskbar pinning works
|
|
if sys.platform == "win32":
|
|
try:
|
|
import ctypes
|
|
ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID(
|
|
u"pax.booru-viewer.gui.1"
|
|
)
|
|
except Exception:
|
|
pass
|
|
|
|
# Apply file dialog platform setting before Qt initializes
|
|
if sys.platform != "win32":
|
|
try:
|
|
from booru_viewer.core.db import Database
|
|
db = Database()
|
|
platform = db.get_setting("file_dialog_platform")
|
|
db.close()
|
|
if platform == "gtk":
|
|
# Use xdg-desktop-portal which routes to GTK portal (Thunar)
|
|
os.environ.setdefault("QT_QPA_PLATFORMTHEME", "xdgdesktopportal")
|
|
except Exception as e:
|
|
# Surface DB-init failures to stderr — silently swallowing meant
|
|
# users debugging "why is my file picker the wrong one" had no
|
|
# signal at all when the DB was missing or corrupt.
|
|
print(
|
|
f"booru-viewer: file_dialog_platform DB probe failed: "
|
|
f"{type(e).__name__}: {e}",
|
|
file=sys.stderr,
|
|
)
|
|
|
|
from booru_viewer.gui.app import run
|
|
run()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|