|
|
|
__version__ = "0.1.0" |
|
import os |
|
import urllib.parse |
|
import tempfile |
|
import shutil |
|
import json |
|
import base64 |
|
from huggingface_hub import login, upload_folder, hf_hub_download, HfApi |
|
from huggingface_hub.utils import RepositoryNotFoundError, EntryNotFoundError |
|
from modules.constants import HF_API_TOKEN, upload_file_types, model_extensions, image_extensions, HF_REPO_ID, SHORTENER_JSON_FILE |
|
from typing import Any, Dict, List, Tuple, Union |
|
|
|
|
|
|
|
def generate_permalink(valid_files, base_url_external, permalink_viewer_url="surn-3d-viewer.hf.space"): |
|
""" |
|
Given a list of valid files, checks if they contain exactly 1 model file and 2 image files. |
|
Constructs and returns a permalink URL with query parameters if the criteria is met. |
|
Otherwise, returns None. |
|
""" |
|
model_link = None |
|
images_links = [] |
|
for f in valid_files: |
|
filename = os.path.basename(f) |
|
ext = os.path.splitext(filename)[1].lower() |
|
if ext in model_extensions: |
|
if model_link is None: |
|
model_link = f"{base_url_external}/{filename}" |
|
elif ext in image_extensions: |
|
images_links.append(f"{base_url_external}/{filename}") |
|
if model_link and len(images_links) == 2: |
|
|
|
permalink_viewer_url = f"https://{permalink_viewer_url}/" |
|
params = {"3d": model_link, "hm": images_links[0], "image": images_links[1]} |
|
query_str = urllib.parse.urlencode(params) |
|
return f"{permalink_viewer_url}?{query_str}" |
|
return None |
|
|
|
def generate_permalink_from_urls(model_url, hm_url, img_url, permalink_viewer_url="surn-3d-viewer.hf.space"): |
|
""" |
|
Constructs and returns a permalink URL with query string parameters for the viewer. |
|
Each parameter is passed separately so that the image positions remain consistent. |
|
|
|
Parameters: |
|
model_url (str): Processed URL for the 3D model. |
|
hm_url (str): Processed URL for the height map image. |
|
img_url (str): Processed URL for the main image. |
|
permalink_viewer_url (str): The base viewer URL. |
|
|
|
Returns: |
|
str: The generated permalink URL. |
|
""" |
|
import urllib.parse |
|
params = {"3d": model_url, "hm": hm_url, "image": img_url} |
|
query_str = urllib.parse.urlencode(params) |
|
return f"https://{permalink_viewer_url}/?{query_str}" |
|
|
|
def upload_files_to_repo( |
|
files: List[Any], |
|
repo_id: str, |
|
folder_name: str, |
|
create_permalink: bool = False, |
|
repo_type: str = "dataset", |
|
permalink_viewer_url: str = "surn-3d-viewer.hf.space" |
|
) -> Union[Dict[str, Any], List[Tuple[Any, str]]]: |
|
""" |
|
Uploads multiple files to a Hugging Face repository using a batch upload approach via upload_folder. |
|
|
|
Parameters: |
|
files (list): A list of file paths (str) to upload. |
|
repo_id (str): The repository ID on Hugging Face for storage, e.g. "Surn/Storage". |
|
folder_name (str): The subfolder within the repository where files will be saved. |
|
create_permalink (bool): If True and if exactly three files are uploaded (1 model and 2 images), |
|
returns a single permalink to the project with query parameters. |
|
Otherwise, returns individual permalinks for each file. |
|
repo_type (str): Repository type ("space", "dataset", etc.). Default is "dataset". |
|
permalink_viewer_url (str): The base viewer URL. |
|
|
|
Returns: |
|
Union[Dict[str, Any], List[Tuple[Any, str]]]: |
|
If create_permalink is True and files match the criteria: |
|
dict: { |
|
"response": <upload response>, |
|
"permalink": <full_permalink URL>, |
|
"short_permalink": <shortened permalink URL> |
|
} |
|
Otherwise: |
|
list: A list of tuples (response, permalink) for each file. |
|
""" |
|
|
|
login(token=HF_API_TOKEN) |
|
|
|
valid_files = [] |
|
permalink_short = None |
|
|
|
|
|
folder_name = folder_name.rstrip("/") |
|
|
|
|
|
for f in files: |
|
file_name = f if isinstance(f, str) else f.name if hasattr(f, "name") else None |
|
if file_name is None: |
|
continue |
|
ext = os.path.splitext(file_name)[1].lower() |
|
if ext in upload_file_types: |
|
valid_files.append(f) |
|
|
|
if not valid_files: |
|
|
|
if create_permalink: |
|
return { |
|
"response": "No valid files to upload.", |
|
"permalink": None, |
|
"short_permalink": None |
|
} |
|
return [] |
|
|
|
|
|
with tempfile.TemporaryDirectory(dir=os.getenv("TMPDIR", "/tmp")) as temp_dir: |
|
for file_path in valid_files: |
|
filename = os.path.basename(file_path) |
|
dest_path = os.path.join(temp_dir, filename) |
|
shutil.copy(file_path, dest_path) |
|
|
|
|
|
|
|
response = upload_folder( |
|
folder_path=temp_dir, |
|
repo_id=repo_id, |
|
repo_type=repo_type, |
|
path_in_repo=folder_name, |
|
commit_message="Batch upload files" |
|
) |
|
|
|
|
|
base_url_external = f"https://huggingface.co/datasets/{repo_id}/resolve/main/{folder_name}" |
|
individual_links = [] |
|
for file_path in valid_files: |
|
filename = os.path.basename(file_path) |
|
link = f"{base_url_external}/{filename}" |
|
individual_links.append(link) |
|
|
|
|
|
|
|
if create_permalink: |
|
permalink = generate_permalink(valid_files, base_url_external, permalink_viewer_url) |
|
if permalink: |
|
status, short_id = gen_full_url( |
|
full_url=permalink, |
|
repo_id=HF_REPO_ID, |
|
json_file=SHORTENER_JSON_FILE |
|
) |
|
if status in ["created_short", "success_retrieved_short", "exists_match"]: |
|
permalink_short = f"https://{permalink_viewer_url}/?sid={short_id}" |
|
else: |
|
permalink_short = None |
|
print(f"URL shortening status: {status} for {permalink}") |
|
|
|
return { |
|
"response": response, |
|
"permalink": permalink, |
|
"short_permalink": permalink_short |
|
} |
|
else: |
|
return { |
|
"response": response, |
|
"permalink": None, |
|
"short_permalink": None |
|
} |
|
|
|
|
|
return [(response, link) for link in individual_links] |
|
|
|
def _generate_short_id(length=8): |
|
"""Generates a random base64 URL-safe string.""" |
|
return base64.urlsafe_b64encode(os.urandom(length * 2))[:length].decode('utf-8') |
|
|
|
def _get_json_from_repo(repo_id, json_file_name, repo_type="dataset"): |
|
"""Downloads and loads the JSON file from the repo. Returns empty list if not found or error.""" |
|
try: |
|
login(token=HF_API_TOKEN) |
|
json_path = hf_hub_download( |
|
repo_id=repo_id, |
|
filename=json_file_name, |
|
repo_type=repo_type, |
|
token=HF_API_TOKEN |
|
) |
|
with open(json_path, 'r') as f: |
|
data = json.load(f) |
|
os.remove(json_path) |
|
return data |
|
except RepositoryNotFoundError: |
|
print(f"Repository {repo_id} not found.") |
|
return [] |
|
except EntryNotFoundError: |
|
print(f"JSON file {json_file_name} not found in {repo_id}. Initializing with empty list.") |
|
return [] |
|
except json.JSONDecodeError: |
|
print(f"Error decoding JSON from {json_file_name}. Returning empty list.") |
|
return [] |
|
except Exception as e: |
|
print(f"An unexpected error occurred while fetching {json_file_name}: {e}") |
|
return [] |
|
|
|
def _upload_json_to_repo(data, repo_id, json_file_name, repo_type="dataset"): |
|
"""Uploads the JSON data to the specified file in the repo.""" |
|
try: |
|
login(token=HF_API_TOKEN) |
|
api = HfApi() |
|
|
|
temp_dir_for_json = os.getenv("TMPDIR", tempfile.gettempdir()) |
|
os.makedirs(temp_dir_for_json, exist_ok=True) |
|
|
|
with tempfile.NamedTemporaryFile(mode="w+", delete=False, suffix=".json", dir=temp_dir_for_json) as tmp_file: |
|
json.dump(data, tmp_file, indent=2) |
|
tmp_file_path = tmp_file.name |
|
|
|
api.upload_file( |
|
path_or_fileobj=tmp_file_path, |
|
path_in_repo=json_file_name, |
|
repo_id=repo_id, |
|
repo_type=repo_type, |
|
commit_message=f"Update {json_file_name}" |
|
) |
|
os.remove(tmp_file_path) |
|
return True |
|
except Exception as e: |
|
print(f"Failed to upload {json_file_name} to {repo_id}: {e}") |
|
if 'tmp_file_path' in locals() and os.path.exists(tmp_file_path): |
|
os.remove(tmp_file_path) |
|
return False |
|
|
|
def _find_url_in_json(data, short_url=None, full_url=None): |
|
""" |
|
Searches the JSON data. |
|
If short_url is provided, returns the corresponding full_url or None. |
|
If full_url is provided, returns the corresponding short_url or None. |
|
""" |
|
if not data: |
|
return None |
|
if short_url: |
|
for item in data: |
|
if item.get("short_url") == short_url: |
|
return item.get("full_url") |
|
if full_url: |
|
for item in data: |
|
if item.get("full_url") == full_url: |
|
return item.get("short_url") |
|
return None |
|
|
|
def _add_url_to_json(data, short_url, full_url): |
|
"""Adds a new short_url/full_url pair to the data. Returns updated data.""" |
|
if data is None: |
|
data = [] |
|
data.append({"short_url": short_url, "full_url": full_url}) |
|
return data |
|
|
|
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"): |
|
""" |
|
Manages short URLs and their corresponding full URLs in a JSON file stored in a Hugging Face repository. |
|
|
|
- If short_url is provided, attempts to retrieve and return the full_url. |
|
- If full_url is provided, attempts to retrieve an existing short_url or creates a new one, stores it, and returns the short_url. |
|
- If both are provided, checks for consistency or creates a new entry. |
|
- If neither is provided, or repo_id is missing, returns an error status. |
|
|
|
Returns: |
|
tuple: (status_message, result_url) |
|
status_message can be "success", "created", "exists", "error", "not_found". |
|
result_url is the relevant URL (short or full) or None if an error occurs or not found. |
|
""" |
|
if not repo_id: |
|
return "error_repo_id_missing", None |
|
if not short_url and not full_url: |
|
return "error_no_input", None |
|
|
|
login(token=HF_API_TOKEN) |
|
url_data = _get_json_from_repo(repo_id, json_file, repo_type) |
|
|
|
|
|
if short_url and not full_url: |
|
found_full_url = _find_url_in_json(url_data, short_url=short_url) |
|
return ("success_retrieved_full", found_full_url) if found_full_url else ("not_found_short", None) |
|
|
|
|
|
if full_url and not short_url: |
|
existing_short_url = _find_url_in_json(url_data, full_url=full_url) |
|
if existing_short_url: |
|
return "success_retrieved_short", existing_short_url |
|
else: |
|
|
|
new_short_id = _generate_short_id() |
|
url_data = _add_url_to_json(url_data, new_short_id, full_url) |
|
if _upload_json_to_repo(url_data, repo_id, json_file, repo_type): |
|
return "created_short", new_short_id |
|
else: |
|
return "error_upload", None |
|
|
|
|
|
if short_url and full_url: |
|
found_full_for_short = _find_url_in_json(url_data, short_url=short_url) |
|
found_short_for_full = _find_url_in_json(url_data, full_url=full_url) |
|
|
|
if found_full_for_short == full_url: |
|
return "exists_match", short_url |
|
if found_full_for_short is not None and found_full_for_short != full_url: |
|
return "error_conflict_short_exists_different_full", short_url |
|
if found_short_for_full is not None and found_short_for_full != short_url: |
|
return "error_conflict_full_exists_different_short", found_short_for_full |
|
|
|
|
|
|
|
|
|
url_data = _add_url_to_json(url_data, short_url, full_url) |
|
if _upload_json_to_repo(url_data, repo_id, json_file, repo_type): |
|
return "created_specific_pair", short_url |
|
else: |
|
return "error_upload", None |
|
|
|
return "error_unhandled_case", None |
|
|