Add shortened URL and Open Graph
Browse files- README.md +1 -1
- app.py +124 -38
- modules/constants.py +12 -1
- modules/storage.py +170 -1
README.md
CHANGED
@@ -4,7 +4,7 @@ emoji: 👀
|
|
4 |
colorFrom: yellow
|
5 |
colorTo: blue
|
6 |
sdk: gradio
|
7 |
-
sdk_version: 5.
|
8 |
python_version: 3.12.8
|
9 |
license: apache-2.0
|
10 |
app_file: app.py
|
|
|
4 |
colorFrom: yellow
|
5 |
colorTo: blue
|
6 |
sdk: gradio
|
7 |
+
sdk_version: 5.31.0
|
8 |
python_version: 3.12.8
|
9 |
license: apache-2.0
|
10 |
app_file: app.py
|
app.py
CHANGED
@@ -5,10 +5,45 @@ import modules.constants as constants
|
|
5 |
import modules.version_info as version_info
|
6 |
import modules.storage as storage
|
7 |
|
|
|
|
|
8 |
|
9 |
user_dir = constants.TMPDIR
|
10 |
default_folder = "saved_models/3d_model_" + format(random.randint(1, 999999), "06d")
|
11 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
12 |
def getVersions():
|
13 |
#return html_versions
|
14 |
return version_info.versions_html()
|
@@ -30,8 +65,6 @@ def process_url(url, default_ext=".png"):
|
|
30 |
|
31 |
# Parse URL to get components
|
32 |
try:
|
33 |
-
import urllib.request
|
34 |
-
from urllib.parse import urlparse
|
35 |
|
36 |
# Create filename from URL.
|
37 |
parsed_url = urlparse(url)
|
@@ -98,7 +131,6 @@ def process_url(url, default_ext=".png"):
|
|
98 |
print(f"Error downloading file {url}: {e}")
|
99 |
return url # Return original URL if download fails
|
100 |
|
101 |
-
|
102 |
def load_data(request: gr.Request, model_3d, image_slider):
|
103 |
"""
|
104 |
Load data from query parameters, download files if needed,
|
@@ -121,15 +153,19 @@ def load_data(request: gr.Request, model_3d, image_slider):
|
|
121 |
# Parse query parameters.
|
122 |
query_params = dict(request.query_params) if request is not None else {}
|
123 |
|
124 |
-
#
|
|
|
|
|
|
|
125 |
model_url = query_params.get("3d", None)
|
126 |
hm_url = query_params.get("hm", None)
|
127 |
img_url = query_params.get("image", None)
|
128 |
|
129 |
-
if
|
130 |
-
|
131 |
-
|
132 |
-
|
|
|
133 |
# Process the model URL if provided.
|
134 |
if model_url:
|
135 |
model_url = process_url(model_url, default_ext=".glb")
|
@@ -143,25 +179,33 @@ def load_data(request: gr.Request, model_3d, image_slider):
|
|
143 |
if hm_url:
|
144 |
local_hm = process_url(hm_url, default_ext=".png")
|
145 |
if local_hm:
|
146 |
-
slider_images
|
|
|
|
|
|
|
|
|
|
|
147 |
|
148 |
|
149 |
-
# Set default values if no URLs provided:
|
150 |
-
|
151 |
-
|
152 |
|
153 |
if not slider_images:
|
154 |
-
slider_images =
|
155 |
|
156 |
if not model_url:
|
157 |
-
model_url =
|
158 |
|
159 |
-
# If any query parameters were
|
160 |
permalink = ""
|
161 |
-
if
|
162 |
try:
|
163 |
-
|
164 |
-
|
|
|
|
|
|
|
165 |
except Exception as e:
|
166 |
print(f"Error generating permalink: {e}")
|
167 |
|
@@ -212,21 +256,70 @@ def process_upload(files, current_model, current_images):
|
|
212 |
|
213 |
# For the image slider, we expect a list of exactly 2 images.
|
214 |
# Start with current images (or use defaults if None).
|
215 |
-
if current_images is None or not isinstance(current_images, list):
|
216 |
new_images = [None, None]
|
217 |
else:
|
218 |
-
new_images = current_images
|
219 |
-
new_images
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
|
|
|
|
226 |
return updated_model, new_images
|
227 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
228 |
gr.set_static_paths(paths=["images/", "models/", "assets/"])
|
229 |
-
with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/beeuty',delete_cache=(21600,86400), fill_width=True) as viewer3d:
|
230 |
gr.Markdown("# 3D Model Viewer")
|
231 |
|
232 |
with gr.Row():
|
@@ -257,7 +350,6 @@ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/be
|
|
257 |
)
|
258 |
|
259 |
with gr.Row():
|
260 |
-
# New textbox for folder name.
|
261 |
folder_name_box = gr.Textbox(
|
262 |
label="Folder Name",
|
263 |
value=default_folder,
|
@@ -282,15 +374,12 @@ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/be
|
|
282 |
with gr.Row():
|
283 |
gr.HTML(value=getVersions(), visible=True, elem_id="versions")
|
284 |
|
285 |
-
# Use JavaScript to pass the query parameters to your callback.
|
286 |
viewer3d.load(
|
287 |
load_data,
|
288 |
inputs=[model_3d, image_slider],
|
289 |
outputs=[model_3d, image_slider, permalink],
|
290 |
scroll_to_output=True
|
291 |
).then(
|
292 |
-
# If the returned permalink (link) is non-empty then make the permalink row visible
|
293 |
-
# and disable the permalink button; otherwise, hide the row and enable the button.
|
294 |
lambda link: (gr.update(visible=True), gr.update(interactive=False))
|
295 |
if link and len(link) > 0
|
296 |
else (gr.update(visible=False), gr.update(interactive=True)),
|
@@ -298,7 +387,6 @@ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/be
|
|
298 |
outputs=[permalink_row, permalink_button]
|
299 |
)
|
300 |
|
301 |
-
# Process uploaded files to update the Model3D or ImageSlider component.
|
302 |
upload_btn.upload(
|
303 |
process_upload,
|
304 |
inputs=[upload_btn, model_3d, image_slider],
|
@@ -307,20 +395,18 @@ with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/be
|
|
307 |
api_name="process_upload",
|
308 |
show_progress=True
|
309 |
).then(
|
310 |
-
# After a successful upload, enable the permalink button.
|
311 |
lambda m, i: gr.update(interactive=True),
|
312 |
inputs=[model_3d, image_slider],
|
313 |
outputs=[permalink_button]
|
314 |
)
|
315 |
-
# Generate a permalink based on the current model, images, and folder name.
|
316 |
permalink_button.click(
|
317 |
lambda model, images, folder: storage.upload_files_to_repo(
|
318 |
-
files=[model] + list(images),
|
319 |
-
repo_id=
|
320 |
folder_name=folder,
|
321 |
create_permalink=True,
|
322 |
repo_type="dataset"
|
323 |
-
)[1],
|
324 |
inputs=[model_3d, image_slider, folder_name_box],
|
325 |
outputs=[permalink],
|
326 |
scroll_to_output=True
|
|
|
5 |
import modules.version_info as version_info
|
6 |
import modules.storage as storage
|
7 |
|
8 |
+
import urllib.request
|
9 |
+
from urllib.parse import urlparse, parse_qs, urlencode
|
10 |
|
11 |
user_dir = constants.TMPDIR
|
12 |
default_folder = "saved_models/3d_model_" + format(random.randint(1, 999999), "06d")
|
13 |
|
14 |
+
def _resolve_short_id_to_query_params(query_params: dict) -> dict:
|
15 |
+
"""
|
16 |
+
Checks for a 'sid' (short ID) in query_params. If found, attempts to resolve
|
17 |
+
it to a full URL and updates query_params with the parameters ('3d', 'hm', 'image')
|
18 |
+
from the resolved URL.
|
19 |
+
"""
|
20 |
+
short_id = query_params.get("sid")
|
21 |
+
if short_id:
|
22 |
+
status, full_url_from_shortener = storage.gen_full_url(
|
23 |
+
short_url=short_id,
|
24 |
+
repo_id=constants.HF_REPO_ID,
|
25 |
+
json_file=constants.SHORTENER_JSON_FILE
|
26 |
+
)
|
27 |
+
if status == "success_retrieved_full" and full_url_from_shortener:
|
28 |
+
print(f"Retrieved full URL from short ID '{short_id}': {full_url_from_shortener}")
|
29 |
+
try:
|
30 |
+
parsed_full_url = urlparse(full_url_from_shortener)
|
31 |
+
retrieved_params = parse_qs(parsed_full_url.query)
|
32 |
+
|
33 |
+
# Update query_params with those from the full_url_from_shortener
|
34 |
+
# The parse_qs function returns lists for values, so get the first element.
|
35 |
+
# If a param is not in the resolved URL, it will be set to None.
|
36 |
+
query_params["3d"] = retrieved_params.get("3d", [None])[0]
|
37 |
+
query_params["hm"] = retrieved_params.get("hm", [None])[0]
|
38 |
+
query_params["image"] = retrieved_params.get("image", [None])[0]
|
39 |
+
except Exception as e:
|
40 |
+
print(f"Error parsing full URL from shortener: {e}")
|
41 |
+
# Proceed with original query_params if parsing fails (i.e., don't overwrite them with Nones here)
|
42 |
+
else:
|
43 |
+
print(f"Failed to retrieve full URL for short ID '{short_id}': {status}")
|
44 |
+
# If sid resolution fails, original query_params (including potentially 3d, hm, image if passed alongside sid) remain.
|
45 |
+
return query_params
|
46 |
+
|
47 |
def getVersions():
|
48 |
#return html_versions
|
49 |
return version_info.versions_html()
|
|
|
65 |
|
66 |
# Parse URL to get components
|
67 |
try:
|
|
|
|
|
68 |
|
69 |
# Create filename from URL.
|
70 |
parsed_url = urlparse(url)
|
|
|
131 |
print(f"Error downloading file {url}: {e}")
|
132 |
return url # Return original URL if download fails
|
133 |
|
|
|
134 |
def load_data(request: gr.Request, model_3d, image_slider):
|
135 |
"""
|
136 |
Load data from query parameters, download files if needed,
|
|
|
153 |
# Parse query parameters.
|
154 |
query_params = dict(request.query_params) if request is not None else {}
|
155 |
|
156 |
+
# Resolve short ID if present
|
157 |
+
query_params = _resolve_short_id_to_query_params(query_params)
|
158 |
+
|
159 |
+
# Extract URLs from query parameters (which may have been updated)
|
160 |
model_url = query_params.get("3d", None)
|
161 |
hm_url = query_params.get("hm", None)
|
162 |
img_url = query_params.get("image", None)
|
163 |
|
164 |
+
# Check if any relevant parameters are set to decide if we should clear query_params for permalink generation
|
165 |
+
# This logic ensures that if only 'sid' was passed and it resolved, query_params will now contain 3d, hm, image.
|
166 |
+
# If 'sid' was passed but didn't resolve, or no params were passed, then model_url, hm_url, img_url will be None.
|
167 |
+
has_loadable_params = bool(model_url or hm_url or img_url)
|
168 |
+
|
169 |
# Process the model URL if provided.
|
170 |
if model_url:
|
171 |
model_url = process_url(model_url, default_ext=".glb")
|
|
|
179 |
if hm_url:
|
180 |
local_hm = process_url(hm_url, default_ext=".png")
|
181 |
if local_hm:
|
182 |
+
if not slider_images or local_hm != slider_images[0]:
|
183 |
+
if len(slider_images) == 1 and img_url:
|
184 |
+
slider_images.append(local_hm)
|
185 |
+
else:
|
186 |
+
slider_images = [local_hm] + slider_images
|
187 |
+
slider_images = slider_images[:2]
|
188 |
|
189 |
|
190 |
+
# Set default values if no URLs provided or processed:
|
191 |
+
default_model_val = getattr(model_3d, "value", model_3d)
|
192 |
+
default_images_val = getattr(image_slider, "value", image_slider)
|
193 |
|
194 |
if not slider_images:
|
195 |
+
slider_images = default_images_val if default_images_val and default_images_val != (None, None) else constants.default_slider_images
|
196 |
|
197 |
if not model_url:
|
198 |
+
model_url = default_model_val if default_model_val else constants.default_model_3d
|
199 |
|
200 |
+
# If any loadable query parameters were effectively present (either directly or via sid), generate a permalink.
|
201 |
permalink = ""
|
202 |
+
if has_loadable_params:
|
203 |
try:
|
204 |
+
permalink_model_url = query_params.get("3d", model_url)
|
205 |
+
permalink_hm_url = query_params.get("hm", hm_url if len(slider_images) > 1 and hm_url else (slider_images[1] if len(slider_images) > 1 else None) )
|
206 |
+
permalink_img_url = query_params.get("image", img_url if slider_images and img_url else (slider_images[0] if slider_images else None) )
|
207 |
+
|
208 |
+
permalink = storage.generate_permalink_from_urls(permalink_model_url, permalink_hm_url, permalink_img_url)
|
209 |
except Exception as e:
|
210 |
print(f"Error generating permalink: {e}")
|
211 |
|
|
|
256 |
|
257 |
# For the image slider, we expect a list of exactly 2 images.
|
258 |
# Start with current images (or use defaults if None).
|
259 |
+
if current_images is None or not isinstance(current_images, list) or len(current_images) == 0:
|
260 |
new_images = [None, None]
|
261 |
else:
|
262 |
+
new_images = current_images[:2]
|
263 |
+
if len(new_images) < 2:
|
264 |
+
new_images.append(None)
|
265 |
+
|
266 |
+
if len(extracted_images) == 1:
|
267 |
+
new_images[0] = extracted_images[0]
|
268 |
+
elif len(extracted_images) == 2:
|
269 |
+
new_images[0] = extracted_images[0]
|
270 |
+
new_images[1] = extracted_images[1]
|
271 |
+
|
272 |
return updated_model, new_images
|
273 |
|
274 |
+
def get_open_graph_meta_tags(query_params):
|
275 |
+
"""Generates Open Graph meta tags based on query parameters."""
|
276 |
+
og_title = constants.DEFAULT_OG_TITLE
|
277 |
+
og_description = constants.DEFAULT_OG_DESCRIPTION
|
278 |
+
og_image = constants.DEFAULT_OG_IMAGE_URL
|
279 |
+
og_type = constants.DEFAULT_OG_TYPE
|
280 |
+
og_url = constants.APP_BASE_URL
|
281 |
+
|
282 |
+
img_url_from_query = query_params.get("image")
|
283 |
+
if img_url_from_query:
|
284 |
+
og_image = img_url_from_query
|
285 |
+
|
286 |
+
model_url_from_query = query_params.get("3d")
|
287 |
+
if model_url_from_query:
|
288 |
+
try:
|
289 |
+
model_filename = os.path.basename(urlparse(model_url_from_query).path)
|
290 |
+
if model_filename:
|
291 |
+
og_title = f"3D Viewer: {model_filename}"
|
292 |
+
else:
|
293 |
+
og_title = f"Shared 3D Model"
|
294 |
+
except Exception:
|
295 |
+
og_title = "Shared 3D Model"
|
296 |
+
else:
|
297 |
+
og_title = "Shared Image"
|
298 |
+
|
299 |
+
if query_params:
|
300 |
+
filtered_query_params = {k: v for k, v in query_params.items() if v is not None}
|
301 |
+
if filtered_query_params:
|
302 |
+
og_url += "?" + urlencode(filtered_query_params)
|
303 |
+
|
304 |
+
meta_tags = f'''
|
305 |
+
<meta property="og:title" content="{og_title}" />
|
306 |
+
<meta property="og:description" content="{og_description}" />
|
307 |
+
<meta property="og:image" content="{og_image}" />
|
308 |
+
<meta property="og:url" content="{og_url}" />
|
309 |
+
<meta property="og:type" content="{og_type}" />
|
310 |
+
<meta name="twitter:card" content="summary_large_image">
|
311 |
+
<meta name="twitter:title" content="{og_title}">
|
312 |
+
<meta name="twitter:description" content="{og_description}">
|
313 |
+
<meta name="twitter:image" content="{og_image}">
|
314 |
+
'''
|
315 |
+
return meta_tags
|
316 |
+
|
317 |
+
placeholder_initial_query_params = {}
|
318 |
+
processed_placeholder_params = _resolve_short_id_to_query_params(placeholder_initial_query_params)
|
319 |
+
initial_og_tags = get_open_graph_meta_tags(processed_placeholder_params)
|
320 |
+
|
321 |
gr.set_static_paths(paths=["images/", "models/", "assets/"])
|
322 |
+
with gr.Blocks(css_paths="style_20250503.css", title="3D viewer", theme='Surn/beeuty',delete_cache=(21600,86400), fill_width=True, head=initial_og_tags) as viewer3d:
|
323 |
gr.Markdown("# 3D Model Viewer")
|
324 |
|
325 |
with gr.Row():
|
|
|
350 |
)
|
351 |
|
352 |
with gr.Row():
|
|
|
353 |
folder_name_box = gr.Textbox(
|
354 |
label="Folder Name",
|
355 |
value=default_folder,
|
|
|
374 |
with gr.Row():
|
375 |
gr.HTML(value=getVersions(), visible=True, elem_id="versions")
|
376 |
|
|
|
377 |
viewer3d.load(
|
378 |
load_data,
|
379 |
inputs=[model_3d, image_slider],
|
380 |
outputs=[model_3d, image_slider, permalink],
|
381 |
scroll_to_output=True
|
382 |
).then(
|
|
|
|
|
383 |
lambda link: (gr.update(visible=True), gr.update(interactive=False))
|
384 |
if link and len(link) > 0
|
385 |
else (gr.update(visible=False), gr.update(interactive=True)),
|
|
|
387 |
outputs=[permalink_row, permalink_button]
|
388 |
)
|
389 |
|
|
|
390 |
upload_btn.upload(
|
391 |
process_upload,
|
392 |
inputs=[upload_btn, model_3d, image_slider],
|
|
|
395 |
api_name="process_upload",
|
396 |
show_progress=True
|
397 |
).then(
|
|
|
398 |
lambda m, i: gr.update(interactive=True),
|
399 |
inputs=[model_3d, image_slider],
|
400 |
outputs=[permalink_button]
|
401 |
)
|
|
|
402 |
permalink_button.click(
|
403 |
lambda model, images, folder: storage.upload_files_to_repo(
|
404 |
+
files=[model] + list(images if images else []),
|
405 |
+
repo_id=constants.HF_REPO_ID,
|
406 |
folder_name=folder,
|
407 |
create_permalink=True,
|
408 |
repo_type="dataset"
|
409 |
+
)[1],
|
410 |
inputs=[model_3d, image_slider, folder_name_box],
|
411 |
outputs=[permalink],
|
412 |
scroll_to_output=True
|
modules/constants.py
CHANGED
@@ -34,4 +34,15 @@ default_slider_images = [
|
|
34 |
"images/slider/beeuty_545jlbh1_v12_alpha96_300dpi_depth.png"
|
35 |
]
|
36 |
|
37 |
-
default_model_3d = "models/beeuty_545jlbh1_300dpi.glb"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
"images/slider/beeuty_545jlbh1_v12_alpha96_300dpi_depth.png"
|
35 |
]
|
36 |
|
37 |
+
default_model_3d = "models/beeuty_545jlbh1_300dpi.glb"
|
38 |
+
|
39 |
+
# Constants for URL shortener
|
40 |
+
HF_REPO_ID = "Surn/Storage" # Replace with your Hugging Face repository ID
|
41 |
+
SHORTENER_JSON_FILE = "shortener.json" # The name of your JSON file in the repo
|
42 |
+
|
43 |
+
# Open Graph Defaults
|
44 |
+
APP_BASE_URL = "https://surn-3d-viewer.hf.space" # Replace if your app URL is different
|
45 |
+
DEFAULT_OG_TITLE = "3D Viewer"
|
46 |
+
DEFAULT_OG_DESCRIPTION = "View and share 3D models and images interactively."
|
47 |
+
DEFAULT_OG_IMAGE_URL = APP_BASE_URL + "/gradio_api/file=" + default_slider_images[0]
|
48 |
+
DEFAULT_OG_TYPE = "website"
|
modules/storage.py
CHANGED
@@ -3,7 +3,10 @@ import os
|
|
3 |
import urllib.parse
|
4 |
import tempfile
|
5 |
import shutil
|
6 |
-
|
|
|
|
|
|
|
7 |
from modules.constants import HF_API_TOKEN, upload_file_types, model_extensions, image_extensions
|
8 |
|
9 |
def generate_permalink(valid_files, base_url_external, permalink_viewer_url="surn-3d-viewer.hf.space"):
|
@@ -125,3 +128,169 @@ def upload_files_to_repo(files, repo_id, folder_name, create_permalink=False, re
|
|
125 |
|
126 |
# Otherwise, return individual tuples for each file.
|
127 |
return [(response, link) for link in individual_links]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
3 |
import urllib.parse
|
4 |
import tempfile
|
5 |
import shutil
|
6 |
+
import json
|
7 |
+
import base64
|
8 |
+
from huggingface_hub import login, upload_folder, hf_hub_download, HfApi
|
9 |
+
from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError
|
10 |
from modules.constants import HF_API_TOKEN, upload_file_types, model_extensions, image_extensions
|
11 |
|
12 |
def generate_permalink(valid_files, base_url_external, permalink_viewer_url="surn-3d-viewer.hf.space"):
|
|
|
128 |
|
129 |
# Otherwise, return individual tuples for each file.
|
130 |
return [(response, link) for link in individual_links]
|
131 |
+
|
132 |
+
def _generate_short_id(length=8):
|
133 |
+
"""Generates a random base64 URL-safe string."""
|
134 |
+
return base64.urlsafe_b64encode(os.urandom(length * 2))[:length].decode('utf-8')
|
135 |
+
|
136 |
+
def _get_json_from_repo(repo_id, json_file_name, repo_type="dataset"):
|
137 |
+
"""Downloads and loads the JSON file from the repo. Returns empty list if not found or error."""
|
138 |
+
try:
|
139 |
+
login(token=HF_API_TOKEN)
|
140 |
+
json_path = hf_hub_download(
|
141 |
+
repo_id=repo_id,
|
142 |
+
filename=json_file_name,
|
143 |
+
repo_type=repo_type,
|
144 |
+
token=HF_API_TOKEN # Added token for consistency, though login might suffice
|
145 |
+
)
|
146 |
+
with open(json_path, 'r') as f:
|
147 |
+
data = json.load(f)
|
148 |
+
os.remove(json_path) # Clean up downloaded file
|
149 |
+
return data
|
150 |
+
except RepositoryNotFoundError:
|
151 |
+
print(f"Repository {repo_id} not found.")
|
152 |
+
return []
|
153 |
+
except EntryNotFoundError:
|
154 |
+
print(f"JSON file {json_file_name} not found in {repo_id}. Initializing with empty list.")
|
155 |
+
return []
|
156 |
+
except json.JSONDecodeError:
|
157 |
+
print(f"Error decoding JSON from {json_file_name}. Returning empty list.")
|
158 |
+
return []
|
159 |
+
except Exception as e:
|
160 |
+
print(f"An unexpected error occurred while fetching {json_file_name}: {e}")
|
161 |
+
return []
|
162 |
+
|
163 |
+
def _upload_json_to_repo(data, repo_id, json_file_name, repo_type="dataset"):
|
164 |
+
"""Uploads the JSON data to the specified file in the repo."""
|
165 |
+
try:
|
166 |
+
login(token=HF_API_TOKEN)
|
167 |
+
api = HfApi()
|
168 |
+
with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".json") as tmp_file:
|
169 |
+
json.dump(data, tmp_file, indent=2)
|
170 |
+
tmp_file_path = tmp_file.name
|
171 |
+
|
172 |
+
api.upload_file(
|
173 |
+
path_or_fileobj=tmp_file_path,
|
174 |
+
path_in_repo=json_file_name,
|
175 |
+
repo_id=repo_id,
|
176 |
+
repo_type=repo_type,
|
177 |
+
commit_message=f"Update {json_file_name}"
|
178 |
+
)
|
179 |
+
os.remove(tmp_file_path) # Clean up temporary file
|
180 |
+
return True
|
181 |
+
except Exception as e:
|
182 |
+
print(f"Failed to upload {json_file_name} to {repo_id}: {e}")
|
183 |
+
if 'tmp_file_path' in locals() and os.path.exists(tmp_file_path):
|
184 |
+
os.remove(tmp_file_path) # Ensure cleanup on error too
|
185 |
+
return False
|
186 |
+
|
187 |
+
def _find_url_in_json(data, short_url=None, full_url=None):
|
188 |
+
"""
|
189 |
+
Searches the JSON data.
|
190 |
+
If short_url is provided, returns the corresponding full_url or None.
|
191 |
+
If full_url is provided, returns the corresponding short_url or None.
|
192 |
+
"""
|
193 |
+
if not data: # Handles cases where data might be None or empty
|
194 |
+
return None
|
195 |
+
if short_url:
|
196 |
+
for item in data:
|
197 |
+
if item.get("short_url") == short_url:
|
198 |
+
return item.get("full_url")
|
199 |
+
if full_url:
|
200 |
+
for item in data:
|
201 |
+
if item.get("full_url") == full_url:
|
202 |
+
return item.get("short_url")
|
203 |
+
return None
|
204 |
+
|
205 |
+
def _add_url_to_json(data, short_url, full_url):
|
206 |
+
"""Adds a new short_url/full_url pair to the data. Returns updated data."""
|
207 |
+
if data is None: # Initialize if data is None
|
208 |
+
data = []
|
209 |
+
data.append({"short_url": short_url, "full_url": full_url})
|
210 |
+
return data
|
211 |
+
|
212 |
+
def gen_full_url(short_url=None, full_url=None, repo_id=None, repo_type="dataset", permalink_viewer_url="surn-3d-viewer.hf.space", json_file="shortener.json"):
|
213 |
+
"""
|
214 |
+
Manages short URLs and their corresponding full URLs in a JSON file stored in a Hugging Face repository.
|
215 |
+
|
216 |
+
- If short_url is provided, attempts to retrieve and return the full_url.
|
217 |
+
- If full_url is provided, attempts to retrieve an existing short_url or creates a new one, stores it, and returns the short_url.
|
218 |
+
- If both are provided, checks for consistency or creates a new entry.
|
219 |
+
- If neither is provided, or repo_id is missing, returns an error status.
|
220 |
+
|
221 |
+
Returns:
|
222 |
+
tuple: (status_message, result_url)
|
223 |
+
status_message can be "success", "created", "exists", "error", "not_found".
|
224 |
+
result_url is the relevant URL (short or full) or None if an error occurs or not found.
|
225 |
+
"""
|
226 |
+
if not repo_id:
|
227 |
+
return "error_repo_id_missing", None
|
228 |
+
if not short_url and not full_url:
|
229 |
+
return "error_no_input", None
|
230 |
+
|
231 |
+
login(token=HF_API_TOKEN) # Ensure login at the beginning
|
232 |
+
url_data = _get_json_from_repo(repo_id, json_file, repo_type)
|
233 |
+
|
234 |
+
# Case 1: Only short_url provided (lookup full_url)
|
235 |
+
if short_url and not full_url:
|
236 |
+
found_full_url = _find_url_in_json(url_data, short_url=short_url)
|
237 |
+
if found_full_url:
|
238 |
+
return "success_retrieved_full", found_full_url
|
239 |
+
else:
|
240 |
+
return "not_found_short", None
|
241 |
+
|
242 |
+
# Case 2: Only full_url provided (lookup or create short_url)
|
243 |
+
if full_url and not short_url:
|
244 |
+
existing_short_url = _find_url_in_json(url_data, full_url=full_url)
|
245 |
+
if existing_short_url:
|
246 |
+
return "success_retrieved_short", existing_short_url
|
247 |
+
else:
|
248 |
+
# Create new short_url
|
249 |
+
new_short_id = _generate_short_id()
|
250 |
+
# Construct the short URL using the permalink_viewer_url and the new_short_id as a query parameter or path
|
251 |
+
# For this example, let's assume short URLs are like: https://permalink_viewer_url/?id=new_short_id
|
252 |
+
# This part might need adjustment based on how you want to structure your short URLs.
|
253 |
+
# A common pattern is permalink_viewer_url/new_short_id if the viewer can handle path-based routing.
|
254 |
+
# Or, if the viewer expects a query param like `?short=new_short_id`
|
255 |
+
# For now, let's assume the short_url itself is just the ID, and the full viewer URL is prepended elsewhere if needed.
|
256 |
+
# Or, more directly, the `short_url` parameter to this function *is* the ID.
|
257 |
+
# The request implies `short_url` is the *key* in the JSON.
|
258 |
+
|
259 |
+
# Let's refine: the `short_url` stored in JSON is the ID. The "shortened URL" returned to user might be different.
|
260 |
+
# The function is `gen_full_url`, implying it can also *generate* a short URL if one doesn't exist for a full_url.
|
261 |
+
|
262 |
+
url_data = _add_url_to_json(url_data, new_short_id, full_url)
|
263 |
+
if _upload_json_to_repo(url_data, repo_id, json_file, repo_type):
|
264 |
+
# The value returned as "shortened_url" should be the ID itself, or a URL constructed with it.
|
265 |
+
# Let's return the ID for now, as the prompt asks for "shortened_url" as output.
|
266 |
+
return "created_short", new_short_id
|
267 |
+
else:
|
268 |
+
return "error_upload", None
|
269 |
+
|
270 |
+
# Case 3: Both short_url and full_url provided
|
271 |
+
if short_url and full_url:
|
272 |
+
found_full_for_short = _find_url_in_json(url_data, short_url=short_url)
|
273 |
+
found_short_for_full = _find_url_in_json(url_data, full_url=full_url)
|
274 |
+
|
275 |
+
if found_full_for_short and found_full_for_short == full_url: # Both exist and match
|
276 |
+
return "exists_match", short_url
|
277 |
+
elif found_short_for_full and found_short_for_full == short_url: # Both exist and match (redundant check, but safe)
|
278 |
+
return "exists_match", short_url
|
279 |
+
elif found_full_for_short and found_full_for_short != full_url: # short_url exists but maps to a different full_url
|
280 |
+
return "error_conflict_short_exists", short_url # Or perhaps update, depending on desired behavior
|
281 |
+
elif found_short_for_full and found_short_for_full != short_url: # full_url exists but maps to a different short_url
|
282 |
+
# This implies the user provided a short_url that is *not* the one already associated with this full_url.
|
283 |
+
# We should probably return the *existing* short_url for that full_url.
|
284 |
+
return "exists_full_maps_to_different_short", found_short_for_full
|
285 |
+
else: # Neither exists, or one exists but not the pair. Create new entry.
|
286 |
+
# Check if the provided short_url is already in use by another full_url
|
287 |
+
if _find_url_in_json(url_data, short_url=short_url) is not None:
|
288 |
+
return "error_conflict_short_id_taken", short_url
|
289 |
+
|
290 |
+
url_data = _add_url_to_json(url_data, short_url, full_url)
|
291 |
+
if _upload_json_to_repo(url_data, repo_id, json_file, repo_type):
|
292 |
+
return "created_specific_pair", short_url
|
293 |
+
else:
|
294 |
+
return "error_upload", None
|
295 |
+
|
296 |
+
return "error_unhandled_case", None # Should not be reached
|