Files
2026-07-13 13:08:41 +08:00

157 lines
6.0 KiB
Python

import os
from typing import Optional
from urllib.parse import urlparse, unquote
from utils.get_env import get_app_data_directory_env, get_fastapi_public_base_url
def absolute_fastapi_asset_url(path: str) -> str:
"""
Turn a FastAPI-served path (/app_data/..., /static/...) into a full URL when the public
base is configured (split Next + FastAPI, e.g. Electron); otherwise return the path.
"""
p = (path or "").strip()
if not p:
return p
if p.startswith(("http://", "https://")):
return p
if not p.startswith("/"):
p = f"/{p}"
base = get_fastapi_public_base_url()
if base:
return f"{base}{p}"
return p
def normalize_slide_asset_url(path_or_url: str) -> str:
"""Slide JSON media URLs: keep https/data/blob; make /app_data and /static absolute when FastAPI base is set."""
if not path_or_url or not isinstance(path_or_url, str):
return path_or_url
s = path_or_url.strip()
if s.startswith(("http://", "https://", "data:", "blob:")):
return s
if s.startswith(("/app_data/", "/static/")):
return absolute_fastapi_asset_url(s)
return filesystem_image_path_to_app_data_url(s)
def filesystem_image_path_to_app_data_url(path_or_url: str) -> str:
"""
Browser-facing URL for files saved under APP_DATA_DIRECTORY/images.
Raw absolute paths (Linux/macOS/Windows) are interpreted by the browser as paths on the
web origin (e.g. Next.js), so AI-generated images break while https stock URLs work.
Map known app-data image files to FastAPI's /app_data/images/... mount, as an absolute
URL when NEXT_PUBLIC_FAST_API is set (Electron).
"""
if not path_or_url or not isinstance(path_or_url, str):
return path_or_url
stripped = path_or_url.strip()
if stripped.startswith(("http://", "https://", "data:", "blob:")):
return stripped
if stripped.startswith(("/app_data/", "/static/")):
return absolute_fastapi_asset_url(stripped)
app_data = get_app_data_directory_env()
if not app_data:
return stripped
images_root = os.path.normpath(os.path.join(app_data, "images"))
try:
abs_image = os.path.normpath(os.path.abspath(stripped))
abs_root = os.path.normpath(os.path.abspath(images_root))
except (OSError, ValueError):
return stripped
abs_image_key = os.path.normcase(abs_image)
abs_root_key = os.path.normcase(abs_root)
try:
common = os.path.commonpath([abs_root, abs_image])
except ValueError:
return stripped
if os.path.normcase(common) != abs_root_key:
return stripped
rel = os.path.relpath(abs_image, abs_root)
if rel.startswith(".."):
return stripped
return absolute_fastapi_asset_url("/app_data/images/" + rel.replace(os.sep, "/"))
def resolve_app_path_to_filesystem(path_or_url: str) -> Optional[str]:
"""
Resolve an app-served path or URL to an actual filesystem path.
Handles:
- Path strings: /app_data/images/..., /static/..., absolute paths, relative
- file:// URLs returned by export runtimes
- HTTP URLs whose path component is an absolute filesystem path:
When img src is /Users/.../images/xxx.png, browser resolves to
http://origin/Users/.../images/xxx.png. Next.js returns 404 for these.
Returns the filesystem path if the file exists, else None.
"""
if not path_or_url:
return None
# Extract path from HTTP URL if needed
path = path_or_url
if path_or_url.startswith("http") or path_or_url.startswith("file:"):
try:
parsed = urlparse(path_or_url)
path = unquote(parsed.path)
if parsed.scheme == "file" and os.name == "nt" and path.startswith("/"):
path = path[1:]
except Exception:
return None
# Handle /app_data/images/
if path.startswith("/app_data/images/"):
relative = path[len("/app_data/images/"):]
app_data = get_app_data_directory_env()
if app_data:
actual = os.path.join(app_data, "images", relative)
if os.path.isfile(actual):
return actual
# Fallback: get_images_directory() + relative
actual = os.path.join(get_images_directory(), relative)
return actual if os.path.isfile(actual) else None
# Handle /app_data/ (other subdirs)
if path.startswith("/app_data/"):
relative = path[len("/app_data/"):]
app_data = get_app_data_directory_env()
if app_data:
actual = os.path.join(app_data, relative)
return actual if os.path.isfile(actual) else None
# Handle absolute filesystem path (e.g. from HTTP URL path on Mac)
if path.startswith("/Users/") or path.startswith("/home/") or path.startswith("/var/"):
return path if os.path.isfile(path) else None
if "Application Support" in path or ("Library" in path and "images" in path):
return path if os.path.isfile(path) else None
# Handle /static/
if path.startswith("/static/"):
relative = path[len("/static/"):]
actual = os.path.join("static", relative)
return actual if os.path.isfile(actual) else None
# Absolute path as-is
if os.path.isabs(path):
return path if os.path.isfile(path) else None
# Relative to images directory
actual = os.path.join(get_images_directory(), path)
return actual if os.path.isfile(actual) else None
def resolve_image_path_to_filesystem(path_or_url: str) -> Optional[str]:
return resolve_app_path_to_filesystem(path_or_url)
def get_images_directory():
images_directory = os.path.join(get_app_data_directory_env(), "images")
os.makedirs(images_directory, exist_ok=True)
return images_directory
def get_exports_directory():
export_directory = os.path.join(get_app_data_directory_env(), "exports")
os.makedirs(export_directory, exist_ok=True)
return export_directory
def get_uploads_directory():
uploads_directory = os.path.join(get_app_data_directory_env(), "uploads")
os.makedirs(uploads_directory, exist_ok=True)
return uploads_directory