File size: 5,076 Bytes
d543fc1
 
 
 
 
 
 
cd1d605
d543fc1
 
 
 
 
cd1d605
 
d543fc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
cd1d605
 
 
 
 
 
 
 
 
d543fc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""
Firebase Cloud Storage helper for SentinelScan.

Uploads organization logos and scan report PDFs to Firebase Storage
and returns public/signed download URLs.

Env vars required:
    FIREBASE_CREDENTIALS  — path to serviceAccountKey.json OR base64-encoded JSON
    FIREBASE_STORAGE_BUCKET — e.g. your-project.appspot.com
"""

import os
import io
import base64
import json
import logging
from typing import Optional

logger = logging.getLogger(__name__)

# Firebase references — lazily initialized
_app = None
_bucket = None
_initialized = False


def _get_credentials_path() -> Optional[str]:
    return os.getenv("FIREBASE_CREDENTIALS")


def _get_bucket_name() -> Optional[str]:
    return os.getenv("FIREBASE_STORAGE_BUCKET")


def init_firebase() -> bool:
    """
    Initialize the Firebase Admin SDK with service-account credentials.
    Safe to call multiple times — only initializes once.
    Returns True if initialization succeeded, False otherwise.
    """
    global _app, _bucket, _initialized

    if _initialized:
        return _bucket is not None

    creds_path = _get_credentials_path()
    bucket_name = _get_bucket_name()

    if not creds_path or not bucket_name:
        logger.warning(
            "[Firebase] FIREBASE_CREDENTIALS or FIREBASE_STORAGE_BUCKET not set. "
            "File uploads will fall back to local disk."
        )
        _initialized = True
        return False

    try:
        import firebase_admin
        from firebase_admin import credentials, storage

        if not firebase_admin._apps:
            # Support both file path and base64-encoded JSON (for HF Spaces)
            if os.path.isfile(creds_path):
                cred = credentials.Certificate(creds_path)
            else:
                # Treat as base64-encoded JSON string
                creds_json = base64.b64decode(creds_path).decode("utf-8")
                creds_dict = json.loads(creds_json)
                cred = credentials.Certificate(creds_dict)

            _app = firebase_admin.initialize_app(cred, {"storageBucket": bucket_name})

        _bucket = storage.bucket()
        _initialized = True
        logger.info(f"[Firebase] Initialized. Bucket: {bucket_name}")
        return True

    except Exception as e:
        logger.error(f"[Firebase] Initialization failed: {e}")
        _initialized = True
        return False


def is_available() -> bool:
    """Check if Firebase Storage is ready to use."""
    if not _initialized:
        init_firebase()
    return _bucket is not None


def upload_bytes(
    data: bytes,
    content_type: str,
    destination_blob: str,
) -> Optional[str]:
    """
    Upload raw bytes to Firebase Storage.

    Args:
        data: File content as bytes.
        content_type: MIME type (e.g. "image/png", "application/pdf").
        destination_blob: Full blob path (e.g. "logos/org123/logo.png").

    Returns:
        Public download URL on success, None on failure.
    """
    if not is_available():
        logger.error("[Firebase] Storage not available. Upload skipped.")
        return None

    try:
        blob = _bucket.blob(destination_blob)
        blob.upload_from_string(data, content_type=content_type)
        blob.make_public()
        url = blob.public_url
        logger.info(f"[Firebase] Uploaded {destination_blob} ({len(data)} bytes)")
        return url

    except Exception as e:
        logger.error(f"[Firebase] Upload failed for {destination_blob}: {e}")
        return None


def upload_fileobj(
    fileobj: io.BytesIO,
    content_type: str,
    destination_blob: str,
) -> Optional[str]:
    """
    Upload a file-like object to Firebase Storage.

    Args:
        fileobj: A BytesIO (or similar) with the file data.
        content_type: MIME type.
        destination_blob: Full blob path.

    Returns:
        Public download URL on success, None on failure.
    """
    if not is_available():
        return None

    try:
        blob = _bucket.blob(destination_blob)
        fileobj.seek(0)
        blob.upload_from_file(fileobj, content_type=content_type)
        blob.make_public()
        url = blob.public_url
        logger.info(f"[Firebase] Uploaded {destination_blob}")
        return url

    except Exception as e:
        logger.error(f"[Firebase] Upload failed for {destination_blob}: {e}")
        return None


def delete_blob(destination_blob: str) -> bool:
    """Delete a file from Firebase Storage."""
    if not is_available():
        return False

    try:
        blob = _bucket.blob(destination_blob)
        blob.delete()
        logger.info(f"[Firebase] Deleted {destination_blob}")
        return True

    except Exception as e:
        logger.error(f"[Firebase] Delete failed for {destination_blob}: {e}")
        return False


def get_blob_url(blob_name: str) -> Optional[str]:
    """Return the public URL for an existing blob (without uploading)."""
    if not is_available():
        return None
    try:
        blob = _bucket.blob(blob_name)
        return blob.public_url
    except Exception:
        return None