larmedix / utils /firebase_storage.py
larxius
feat(lab): photo-to-prediction via vision
1ca6e77
Raw History Blame Contribute Delete
5.8 kB
"""Firebase Storage uploads with local fallback.
Env:
FIREBASE_STORAGE_BUCKET e.g. myproj.appspot.com
FIREBASE_CREDENTIALS_JSON service-account JSON (raw), or
FIREBASE_CREDENTIALS_BASE64 base64 of that JSON (preferred for HF Secrets)
Behavior:
save_upload(file_storage, prefix) -> (stored_ref, filename)
- Always saves a local copy under UPLOAD_FOLDER (for immediate AI use).
- If Firebase is configured, also uploads to <prefix>/<uuid>_<filename>
and returns the public URL as stored_ref.
- If not configured (local dev), returns the local relative path.
resolve_to_local(ref, upload_folder, root_path) -> local path
- http(s) URL: downloads to temp, returns temp path.
- gs://... : downloads via bucket to temp.
- otherwise: legacy local relative path join.
"""
import base64
import json
import os
import tempfile
import uuid
_bucket = None
_init_tried = False
def is_configured() -> bool:
return bool(
os.environ.get("FIREBASE_STORAGE_BUCKET")
and (
os.environ.get("FIREBASE_CREDENTIALS_JSON")
or os.environ.get("FIREBASE_CREDENTIALS_BASE64")
)
)
def _bucket():
global _bucket, _init_tried
if _bucket is not None:
return _bucket
if _init_tried:
return None
_init_tried = True
try:
import firebase_admin
from firebase_admin import credentials, storage
except ImportError:
print("firebase-admin not installed; using local storage.")
return None
bucket_name = os.environ.get("FIREBASE_STORAGE_BUCKET", "")
cred_json = os.environ.get("FIREBASE_CREDENTIALS_JSON", "")
cred_b64 = os.environ.get("FIREBASE_CREDENTIALS_BASE64", "")
try:
if cred_b64 and not cred_json:
cred_json = base64.b64decode(cred_b64).decode("utf-8")
info = json.loads(cred_json)
cred = credentials.Certificate(info)
try:
firebase_admin.get_app()
except ValueError:
firebase_admin.initialize_app(cred, {"storageBucket": bucket_name})
_bucket = storage.bucket()
print(f"Firebase Storage ready (bucket={_bucket.name}).")
except Exception as e:
print(f"Firebase init failed, using local storage: {e}")
_bucket = None
return _bucket
def upload_path(local_path: str, dest: str, content_type: str = None) -> str:
"""Upload an existing local file; returns public URL or gs:// ref (or '' if skipped)."""
bucket = _bucket() if is_configured() else None
if bucket is None:
return ""
try:
blob = bucket.blob(dest.strip("/"))
blob.upload_from_filename(local_path, content_type=content_type)
try:
blob.make_public()
return blob.public_url
except Exception:
return f"gs://{bucket.name}/{dest.strip('/')}"
except Exception as e:
print(f"Firebase upload failed: {e}")
return ""
def save_upload(file_storage, prefix: str, upload_folder: str):
"""Save an uploaded FileStorage locally + to Firebase. Returns (ref, filename)."""
from werkzeug.utils import secure_filename
filename = secure_filename(file_storage.filename or "upload")
unique = f"{uuid.uuid4().hex}_{filename}"
local_path = os.path.join(upload_folder, unique)
os.makedirs(upload_folder, exist_ok=True)
file_storage.save(local_path)
rel = os.path.join(os.path.basename(upload_folder), unique).replace("\\", "/")
# rel like "uploads/<uuid>_name" — callers prepend static/ as before
stored = os.path.join("static", rel).replace("\\", "/")
bucket = _bucket() if is_configured() else None
if bucket is None:
try:
file_storage.seek(0)
except Exception:
pass
return stored, filename
dest = f"{prefix.strip('/')}/{unique}"
try:
try:
file_storage.seek(0)
except Exception:
pass
blob = bucket.blob(dest)
blob.upload_from_file(
file_storage.stream, content_type=file_storage.content_type
)
try:
blob.make_public()
return blob.public_url, filename
except Exception:
# Private bucket: fall back to gs:// reference (downloadable server-side)
return f"gs://{bucket.name}/{dest}", filename
except Exception as e:
print(f"Firebase upload failed, keeping local: {e}")
return stored, filename
def resolve_to_local(ref: str, upload_folder: str, root_path: str) -> str:
"""Turn a stored ref (URL, gs://, or legacy local path) into a local file path."""
ref = ref or ""
if ref.startswith("http://") or ref.startswith("https://"):
import requests
suffix = os.path.splitext(ref.split("?")[0])[1][:8] or ".bin"
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
r = requests.get(ref, timeout=60)
r.raise_for_status()
tmp.write(r.content)
tmp.close()
return tmp.name
if ref.startswith("gs://"):
bucket = _bucket()
if bucket is None:
raise FileNotFoundError("Firebase not configured for gs:// download.")
parts = ref[5:].split("/", 1)
blob = bucket.blob(parts[1] if len(parts) > 1 else "")
suffix = os.path.splitext(parts[-1])[1][:8] or ".bin"
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=suffix)
tmp.close()
blob.download_to_filename(tmp.name)
return tmp.name
# Legacy local path
if os.path.isabs(ref) and os.path.exists(ref):
return ref
cand = os.path.join(root_path, ref)
if os.path.exists(cand):
return cand
return os.path.join(root_path, "static", "uploads", os.path.basename(ref))