sushilideaclan01 commited on
Commit
80f0f30
·
1 Parent(s): 3ed8a18
api/routers/generate.py CHANGED
@@ -76,6 +76,30 @@ async def get_image(filename: str):
76
  return FileResponse(filepath)
77
 
78
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  @router.get("/api/download-image")
80
  async def download_image_proxy(
81
  image_url: Optional[str] = None,
 
76
  return FileResponse(filepath)
77
 
78
 
79
+ @router.get("/serve-image")
80
+ async def serve_image(filename: Optional[str] = None):
81
+ """
82
+ Proxy endpoint for generated images: serve from local disk or fetch from R2.
83
+ Use this when the client cannot reach R2 (e.g. net::ERR_TUNNEL_CONNECTION_FAILED).
84
+ No auth so gallery and public views can load images.
85
+ """
86
+ if not filename or ".." in filename or "/" in filename or "\\" in filename:
87
+ raise HTTPException(status_code=400, detail="Invalid filename")
88
+ filepath = os.path.join(settings.output_dir, filename)
89
+ if os.path.exists(filepath):
90
+ return FileResponse(filepath)
91
+ try:
92
+ from services.r2_storage import get_r2_storage
93
+ r2 = get_r2_storage()
94
+ if r2:
95
+ data = r2.get_object_bytes(filename)
96
+ if data:
97
+ return FastAPIResponse(content=data, media_type="image/png")
98
+ except Exception:
99
+ pass
100
+ raise HTTPException(status_code=404, detail="Image not found")
101
+
102
+
103
  @router.get("/api/download-image")
104
  async def download_image_proxy(
105
  image_url: Optional[str] = None,
frontend/lib/utils/formatters.ts CHANGED
@@ -80,7 +80,20 @@ export const truncateText = (text: string, maxLength: number): string => {
80
  return text.slice(0, maxLength) + "...";
81
  };
82
 
83
- const getApiBase = () => process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000";
 
 
 
 
 
 
 
 
 
 
 
 
 
84
 
85
  const isAbsoluteUrl = (url: string) =>
86
  typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"));
@@ -103,8 +116,8 @@ export const getImageUrl = (
103
  };
104
 
105
  /**
106
- * Primary = R2 URL or absolute image_url (for display). Fallback = local /images/{filename} or relative URL.
107
- * Use primary in <img src>, then onError switch to fallback so images show via R2 when available.
108
  */
109
  export const getImageUrlFallback = (
110
  imageUrl: string | null | undefined,
@@ -116,11 +129,14 @@ export const getImageUrlFallback = (
116
  (r2Url && isAbsoluteUrl(r2Url)) ? r2Url
117
  : (imageUrl && isAbsoluteUrl(imageUrl)) ? imageUrl
118
  : null;
 
 
 
119
  const fromFilename = filename ? `${apiBase}/images/${filename}` : null;
120
  const fromRelative =
121
  imageUrl && typeof imageUrl === "string" && imageUrl.startsWith("/")
122
  ? `${apiBase}${imageUrl}`
123
  : null;
124
- const fallback = fromFilename ?? fromRelative ?? null;
125
  return { primary, fallback };
126
  };
 
80
  return text.slice(0, maxLength) + "...";
81
  };
82
 
83
+ /**
84
+ * API base for building image/asset URLs. Avoids mixed content on HTTPS:
85
+ * - Empty NEXT_PUBLIC_API_URL → same origin (relative).
86
+ * - In browser on HTTPS with no API URL set → use current origin so we don't request http://localhost.
87
+ */
88
+ function getApiBase(): string {
89
+ const env = process.env.NEXT_PUBLIC_API_URL;
90
+ if (env === "") return "";
91
+ if (env && (env.startsWith("http://") || env.startsWith("https://"))) return env;
92
+ if (typeof window !== "undefined" && window.location?.protocol === "https:") {
93
+ return window.location.origin;
94
+ }
95
+ return "http://localhost:8000";
96
+ }
97
 
98
  const isAbsoluteUrl = (url: string) =>
99
  typeof url === "string" && (url.startsWith("http://") || url.startsWith("https://"));
 
116
  };
117
 
118
  /**
119
+ * Primary = R2 URL or absolute image_url (for display). Fallback = backend proxy (serve-image) or local /images.
120
+ * Use primary in <img src>, then onError switch to fallback. Proxy fetches from R2 server-side when the client cannot reach R2 (e.g. ERR_TUNNEL_CONNECTION_FAILED).
121
  */
122
  export const getImageUrlFallback = (
123
  imageUrl: string | null | undefined,
 
129
  (r2Url && isAbsoluteUrl(r2Url)) ? r2Url
130
  : (imageUrl && isAbsoluteUrl(imageUrl)) ? imageUrl
131
  : null;
132
+ const fromProxy = filename
133
+ ? `${apiBase}/serve-image?filename=${encodeURIComponent(filename)}`
134
+ : null;
135
  const fromFilename = filename ? `${apiBase}/images/${filename}` : null;
136
  const fromRelative =
137
  imageUrl && typeof imageUrl === "string" && imageUrl.startsWith("/")
138
  ? `${apiBase}${imageUrl}`
139
  : null;
140
+ const fallback = fromProxy ?? fromFilename ?? fromRelative ?? null;
141
  return { primary, fallback };
142
  };
services/r2_storage.py CHANGED
@@ -109,6 +109,18 @@ class R2StorageService:
109
  print(f"Error uploading to R2: {e}")
110
  raise Exception(f"Failed to upload image to R2: {str(e)}")
111
 
 
 
 
 
 
 
 
 
 
 
 
 
112
  def get_public_url(self, filename: str) -> str:
113
  """
114
  Get public URL for an image in R2.
 
109
  print(f"Error uploading to R2: {e}")
110
  raise Exception(f"Failed to upload image to R2: {str(e)}")
111
 
112
+ def get_object_bytes(self, filename: str) -> Optional[bytes]:
113
+ """
114
+ Download image bytes from R2 by filename (server-side).
115
+ Use this to proxy images when the client cannot reach R2 (e.g. ERR_TUNNEL_CONNECTION_FAILED).
116
+ """
117
+ r2_key = f"{self.folder}/{filename}"
118
+ try:
119
+ response = self.s3_client.get_object(Bucket=self.bucket_name, Key=r2_key)
120
+ return response["Body"].read()
121
+ except (ClientError, BotoCoreError):
122
+ return None
123
+
124
  def get_public_url(self, filename: str) -> str:
125
  """
126
  Get public URL for an image in R2.