File size: 10,603 Bytes
74bf532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8bebe44
 
 
 
 
 
 
 
 
 
 
 
74bf532
 
 
 
 
 
 
8bebe44
 
 
 
 
74bf532
 
8bebe44
74bf532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8f14c1d
74bf532
 
 
8f14c1d
74bf532
8f14c1d
 
74bf532
8f14c1d
74bf532
 
2e39262
 
 
 
 
 
 
 
74bf532
 
 
 
 
2e39262
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74bf532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8f14c1d
 
 
74bf532
 
 
 
 
 
 
b56a933
 
 
 
74bf532
b56a933
74bf532
 
 
 
 
 
 
 
 
8f14c1d
74bf532
 
 
 
 
8f14c1d
74bf532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8f14c1d
74bf532
 
 
 
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
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
"""
Push URAAS to Hugging Face Spaces β€” Lordkiki/APA-URAAS

Usage:
    python scripts/push_to_hf.py

What it does:
  1. Logs you into HF (paste your write token when prompted)
  2. Stages a clean copy of the project:
       Dockerfile.hf  β†’ Dockerfile   (HF Spaces Dockerfile, not the prod one)
       README.hf.md   β†’ README.md    (has the HF Space frontmatter)
  3. Uploads everything to the Space via huggingface_hub.upload_folder
  4. HF triggers an auto-build β€” app is live in ~5 minutes
"""

import os
import shutil
import sys
import tempfile

# ── Config ────────────────────────────────────────────────────────────────────
REPO_ID   = "Lordkiki/APA-URAAS"
REPO_TYPE = "space"
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# Directories/files to never push
IGNORE_DIRS = {
    ".git", ".claude", "__pycache__", ".pytest_cache", ".mypy_cache",
    "storage", "logs", "data", "backups", "node_modules",
    ".venv", "venv", "env",
}
IGNORE_FILES = {
    ".env", ".env.prod", ".env.prod.example",
    "uraas.db",                  # DB lives on /data in HF, not in image
    "Dockerfile",                # replaced by Dockerfile.hf
    "README.md",                 # replaced by README.hf.md (has HF frontmatter)
    "docker-compose.yml",
    "docker-compose.prod.yml",
    "docker-compose.replica.yml",
    "docker-compose.demo.yml",
}
IGNORE_EXTS = {
    ".pyc", ".pyo", ".pyd",
    # Database files/backups β€” belt-and-suspenders beyond the exact
    # "uraas.db" name check below and the startswith("uraas.db") check,
    # since a missed pattern here means real crawled data (author names,
    # DOIs, institutional affiliations, emails) goes to a PUBLIC repo.
    # Confirmed this actually happened (2026-07-19): uraas.db.bak, a
    # 1819-item/14MB snapshot, was uploaded because only "uraas.db" itself
    # was excluded, not the ".bak" variant β€” deleted from the live Space
    # after the fact, but should never have gone up in the first place.
    ".bak", ".backup", ".old", ".orig", ".db", ".sqlite", ".sqlite3",
}


def should_skip(rel_path: str, is_dir: bool) -> bool:
    parts = rel_path.replace("\\", "/").split("/")
    name  = parts[-1]
    if is_dir:
        return name in IGNORE_DIRS
    # Catches any suffix variant regardless of extension β€” uraas.db-wal,
    # uraas.db-shm, uraas.db-journal, timestamped backups like
    # uraas.db.20260629, etc. β€” not just the exact names/extensions above.
    if name.startswith("uraas.db"):
        return True
    return (
        name in IGNORE_FILES
        or os.path.splitext(name)[1].lower() in IGNORE_EXTS
    )


def stage_project(src: str, dst: str) -> int:
    """Copy src β†’ dst with HF-specific renames and exclusions."""
    count = 0
    for root, dirs, files in os.walk(src):
        rel_root = os.path.relpath(root, src)

        # Prune excluded directories in-place so os.walk doesn't recurse
        dirs[:] = [
            d for d in dirs
            if not should_skip(
                os.path.join(rel_root, d) if rel_root != "." else d,
                is_dir=True,
            )
        ]

        for fname in files:
            rel = os.path.join(rel_root, fname) if rel_root != "." else fname
            if should_skip(rel, is_dir=False):
                continue

            src_file = os.path.join(root, fname)

            # HF-specific renames
            if fname == "Dockerfile.hf":
                dest_rel = os.path.join(os.path.dirname(rel), "Dockerfile") if os.path.dirname(rel) else "Dockerfile"
            elif fname == "README.hf.md":
                dest_rel = "README.md"
            else:
                dest_rel = rel

            dst_file = os.path.join(dst, dest_rel)
            os.makedirs(os.path.dirname(dst_file), exist_ok=True)
            shutil.copy2(src_file, dst_file)
            count += 1
    return count


def main():
    # ── Ensure huggingface_hub is available ────────────────────────────────
    try:
        from huggingface_hub import HfApi, login
    except ImportError:
        print("Installing huggingface_hub...")
        os.system(f"{sys.executable} -m pip install huggingface_hub -q")
        from huggingface_hub import HfApi, login  # type: ignore

    _SEP = "=" * 55
    print()
    print(_SEP)
    print("  URAAS -> Hugging Face Spaces")
    print(f"  Space: {REPO_ID}")
    print(_SEP)

    # ── Auth ───────────────────────────────────────────────────────────────
    # Live-verified 2026-08-04: with no HF_TOKEN env var, this used to always
    # call login() with no token, which prompts interactively for one β€” in
    # any non-interactive shell (CI, a background task, this script run
    # without a TTY) that blocks forever waiting for input that can never
    # arrive, even when a valid token is already cached from a previous
    # `huggingface-cli login` on the same machine. Try the cached credential
    # first; only fall back to an interactive prompt when actually attached
    # to a terminal.
    token = os.getenv("HF_TOKEN")
    if token:
        login(token=token, add_to_git_credential=True)
        print("  Logged in via HF_TOKEN env var.")
    else:
        from huggingface_hub import HfFolder
        cached = HfFolder.get_token()
        if cached:
            try:
                HfApi().whoami(token=cached)
                print("  Logged in via cached HF credential.")
            except Exception:
                cached = None
        if not cached:
            if not sys.stdin.isatty():
                print()
                print("  No HF_TOKEN env var, no cached credential, and no terminal to prompt on.")
                print("  Set HF_TOKEN=hf_... and re-run, e.g.:")
                print(f"    HF_TOKEN=hf_... python {os.path.basename(__file__)}")
                sys.exit(1)
            print()
            print("  Paste your HF write token below.")
            print("  (Get one at: https://huggingface.co/settings/tokens)")
            print()
            login(add_to_git_credential=True)

    api = HfApi()

    # ── Stage files ────────────────────────────────────────────────────────
    print()
    print("Staging project files…")
    with tempfile.TemporaryDirectory() as staging:
        n = stage_project(REPO_ROOT, staging)
        staged_names = os.listdir(staging)
        print(f"  {n} files staged across {len(staged_names)} top-level items")

        # Sanity checks
        has_dockerfile = "Dockerfile" in staged_names
        has_readme     = "README.md" in staged_names
        has_start_sh   = os.path.exists(os.path.join(staging, "scripts", "start_hf.sh"))

        print(f"  Dockerfile   : {'OK' if has_dockerfile else 'MISSING -- check Dockerfile.hf exists'}")
        print(f"  README.md    : {'OK' if has_readme else 'MISSING -- check README.hf.md exists'}")
        print(f"  start_hf.sh  : {'OK' if has_start_sh else 'MISSING -- check scripts/start_hf.sh'}")

        if not has_dockerfile:
            print()
            print("ERROR: Dockerfile missing from staging. Aborting.")
            sys.exit(1)

        # ── Upload ─────────────────────────────────────────────────────────
        # Skip HF's remote YAML validation β€” it times out on some networks.
        # README.hf.md is already valid so this is safe to skip.
        api._validate_yaml = lambda *a, **kw: None

        print()
        print(f"Uploading to {REPO_ID}...")
        api.upload_folder(
            folder_path=staging,
            repo_id=REPO_ID,
            repo_type=REPO_TYPE,
            commit_message="Deploy URAAS β€” African Research Archival & Analytics System",
        )

    # ── Done ───────────────────────────────────────────────────────────────
    print()
    print(_SEP)
    print("  Upload complete! Build starting on HF (~5 min).")
    print()
    print("  Watch build: https://huggingface.co/spaces/Lordkiki/APA-URAAS")
    print("  App URL    : https://lordkiki-apa-uraas.hf.space")
    print()
    print("  --- Secrets to set in Space Settings -> Variables & Secrets ---")
    secrets = [
        ("URAAS_ENV",              "production"),
        ("DASHBOARD_SECRET_KEY",   "307790fc5aff3fe1e766303f6b94e2fc28c831582bfba5b34802e2c9cbbac0ce"),
        ("ADMIN_USERNAME",         "admin"),
        ("ADMIN_PASSWORD_HASH",    "scrypt:32768:8:1$r2KZFX32rJ2twbfV$16f394a253c2b505a215ff2747f7dafbb098eb0eb8b4e6bb9fb521f0ea38af8ce71f33d2354245d68cc383678257367bf410aa45f835b5e848bff95a746878c8"),
        ("VIEWER_USERNAME",        "viewer"),
        ("VIEWER_PASSWORD_HASH",   "scrypt:32768:8:1$M8OjWxX64B38akos$2803ad5b29508c4d69df115579630b2a4cbdf2c8406157598c088d5511a7b3d79f91399035040660705413c63fdc41aa0d020cbd7b45038c8ed51d20092ea609"),
        ("SMTP_HOST",              "smtp.gmail.com"),
        ("SMTP_PORT",              "587"),
        ("SMTP_USE_TLS",           "true"),
        ("SMTP_USER",              "lawalgiyath200716@gmail.com"),
        ("SMTP_PASSWORD",          "ufwqbdrecpfrzppn"),
        ("SMTP_FROM",              "URAAS UNILAG <lawalgiyath200716@gmail.com>"),
        ("DASHBOARD_BASE_URL",     "https://lordkiki-apa-uraas.hf.space"),
        ("DASHBOARD_CORS_ORIGINS", "https://lordkiki-apa-uraas.hf.space"),
        ("ARK_NAAN",               "99999"),
        ("ARK_SHOULDER",           "z1"),
        ("OPENALEX_MAILTO",        "lawalgiyath200716@gmail.com"),
        ("DSPACE_API_URL",         "https://api-ir.unilag.edu.ng/server"),
        ("DSPACE_USERNAME",        "<professor email β€” set as Secret, not Variable>"),
        ("DSPACE_PASSWORD",        "<professor password β€” set as Secret, not Variable>"),
    ]
    max_k = max(len(k) for k, _ in secrets)
    for k, v in secrets:
        print(f"  {k:<{max_k}} = {v}")
    print()
    print("  Admin login : admin / URAAS2024demo")
    print("  Viewer login: viewer / view2024")
    print(_SEP)


if __name__ == "__main__":
    main()