Upload 20 files
Browse files- Dockerfile +5 -3
- routers/backup.py +86 -18
Dockerfile
CHANGED
|
@@ -1,9 +1,11 @@
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
-
# Cài system dependencies
|
| 4 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 5 |
-
curl wget git build-essential procps htop nano vim \
|
| 6 |
-
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
|
|
|
| 7 |
|
| 8 |
# Cài Node.js 20
|
| 9 |
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
|
|
| 1 |
FROM python:3.11-slim
|
| 2 |
|
| 3 |
+
# Cài system dependencies + thêm DNS fallback
|
| 4 |
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 5 |
+
curl wget git build-essential procps htop nano vim dnsutils \
|
| 6 |
+
&& rm -rf /var/lib/apt/lists/* \
|
| 7 |
+
&& echo "nameserver 1.1.1.1" >> /etc/resolv.conf \
|
| 8 |
+
&& echo "nameserver 8.8.8.8" >> /etc/resolv.conf
|
| 9 |
|
| 10 |
# Cài Node.js 20
|
| 11 |
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
routers/backup.py
CHANGED
|
@@ -10,10 +10,12 @@ Requires env var:
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import os
|
|
|
|
| 13 |
import shutil
|
| 14 |
import tarfile
|
| 15 |
from datetime import datetime
|
| 16 |
from pathlib import Path
|
|
|
|
| 17 |
|
| 18 |
import httpx
|
| 19 |
from fastapi import APIRouter, HTTPException, BackgroundTasks, Request
|
|
@@ -23,6 +25,77 @@ from storage import load_meta, save_meta, validate_zone_name
|
|
| 23 |
|
| 24 |
router = APIRouter(prefix="/api/backup", tags=["backup"])
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
def _get_token(request: Request) -> str:
|
| 28 |
"""Extract JWT token from request Authorization header."""
|
|
@@ -32,11 +105,6 @@ def _get_token(request: Request) -> str:
|
|
| 32 |
return ""
|
| 33 |
|
| 34 |
|
| 35 |
-
def _worker_headers(token: str) -> dict:
|
| 36 |
-
"""Build headers for Worker API calls."""
|
| 37 |
-
return {"Authorization": f"Bearer {token}"}
|
| 38 |
-
|
| 39 |
-
|
| 40 |
def _create_zone_archive(zone_name: str) -> Path:
|
| 41 |
"""Create a tar.gz archive of a zone directory."""
|
| 42 |
zone_path = DATA_DIR / zone_name
|
|
@@ -72,8 +140,8 @@ async def list_backups(request: Request):
|
|
| 72 |
if not token:
|
| 73 |
raise HTTPException(401, "Chua dang nhap")
|
| 74 |
try:
|
| 75 |
-
async with
|
| 76 |
-
resp = await client.get(
|
| 77 |
if resp.status_code != 200:
|
| 78 |
data = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {"error": resp.text}
|
| 79 |
raise HTTPException(resp.status_code, data.get("error", "Worker error"))
|
|
@@ -105,10 +173,10 @@ async def backup_zone(zone_name: str, request: Request, background_tasks: Backgr
|
|
| 105 |
try:
|
| 106 |
archive_path = _create_zone_archive(zone_name)
|
| 107 |
try:
|
| 108 |
-
with
|
| 109 |
with open(archive_path, "rb") as f:
|
| 110 |
resp = client.post(
|
| 111 |
-
f"
|
| 112 |
headers={**_worker_headers(token), "Content-Type": "application/octet-stream"},
|
| 113 |
content=f.read(),
|
| 114 |
)
|
|
@@ -153,10 +221,10 @@ async def backup_all(request: Request, background_tasks: BackgroundTasks):
|
|
| 153 |
_backup_status["progress"] = f"Dang backup zone {zone_name} ({done + 1}/{total})..."
|
| 154 |
archive_path = _create_zone_archive(zone_name)
|
| 155 |
try:
|
| 156 |
-
with
|
| 157 |
with open(archive_path, "rb") as f:
|
| 158 |
resp = client.post(
|
| 159 |
-
f"
|
| 160 |
headers={**_worker_headers(token), "Content-Type": "application/octet-stream"},
|
| 161 |
content=f.read(),
|
| 162 |
)
|
|
@@ -196,8 +264,8 @@ async def restore_zone(zone_name: str, request: Request, background_tasks: Backg
|
|
| 196 |
_backup_status["error"] = None
|
| 197 |
_backup_status["progress"] = f"Dang restore zone: {zone_name}..."
|
| 198 |
try:
|
| 199 |
-
with
|
| 200 |
-
resp = client.get(f"
|
| 201 |
if resp.status_code == 404:
|
| 202 |
raise ValueError(f"Backup zone '{zone_name}' khong ton tai")
|
| 203 |
if resp.status_code != 200:
|
|
@@ -251,8 +319,8 @@ async def restore_all(request: Request, background_tasks: BackgroundTasks):
|
|
| 251 |
_backup_status["error"] = None
|
| 252 |
_backup_status["progress"] = "Dang restore tat ca zones..."
|
| 253 |
try:
|
| 254 |
-
with
|
| 255 |
-
resp = client.get(
|
| 256 |
if resp.status_code != 200:
|
| 257 |
raise ValueError(f"Khong the lay danh sach backup: {resp.text}")
|
| 258 |
backup_list = resp.json()
|
|
@@ -262,8 +330,8 @@ async def restore_all(request: Request, background_tasks: BackgroundTasks):
|
|
| 262 |
for b in backup_list:
|
| 263 |
zone_name = b["zone_name"]
|
| 264 |
_backup_status["progress"] = f"Dang restore zone {zone_name} ({done + 1}/{total})..."
|
| 265 |
-
with
|
| 266 |
-
resp = client.get(f"
|
| 267 |
if resp.status_code != 200:
|
| 268 |
continue
|
| 269 |
archive_path = BACKUP_DIR / f"{zone_name}.tar.gz"
|
|
@@ -298,4 +366,4 @@ async def restore_all(request: Request, background_tasks: BackgroundTasks):
|
|
| 298 |
_backup_status["running"] = False
|
| 299 |
|
| 300 |
background_tasks.add_task(_run)
|
| 301 |
-
return {"ok": True, "message": "Dang restore tat ca zones trong nen..."}
|
|
|
|
| 10 |
"""
|
| 11 |
|
| 12 |
import os
|
| 13 |
+
import socket
|
| 14 |
import shutil
|
| 15 |
import tarfile
|
| 16 |
from datetime import datetime
|
| 17 |
from pathlib import Path
|
| 18 |
+
from urllib.parse import urlparse
|
| 19 |
|
| 20 |
import httpx
|
| 21 |
from fastapi import APIRouter, HTTPException, BackgroundTasks, Request
|
|
|
|
| 25 |
|
| 26 |
router = APIRouter(prefix="/api/backup", tags=["backup"])
|
| 27 |
|
| 28 |
+
# ── Resolve Worker IP at startup to avoid DNS issues in HF Spaces ──
|
| 29 |
+
_worker_ip: str | None = None
|
| 30 |
+
|
| 31 |
+
def _resolve_worker_ip() -> str | None:
|
| 32 |
+
"""Resolve the Worker hostname to an IP address using system + fallback DNS."""
|
| 33 |
+
global _worker_ip
|
| 34 |
+
if _worker_ip:
|
| 35 |
+
return _worker_ip
|
| 36 |
+
if not ADMIN_API_URL:
|
| 37 |
+
return None
|
| 38 |
+
hostname = urlparse(ADMIN_API_URL).hostname
|
| 39 |
+
if not hostname:
|
| 40 |
+
return None
|
| 41 |
+
# Try system DNS first
|
| 42 |
+
try:
|
| 43 |
+
_worker_ip = socket.getaddrinfo(hostname, 443, socket.AF_INET)[0][4][0]
|
| 44 |
+
return _worker_ip
|
| 45 |
+
except socket.gaierror:
|
| 46 |
+
pass
|
| 47 |
+
# Fallback: query Cloudflare DNS over HTTPS
|
| 48 |
+
try:
|
| 49 |
+
import json
|
| 50 |
+
from urllib.request import urlopen, Request as UrlRequest
|
| 51 |
+
req = UrlRequest(
|
| 52 |
+
f"https://1.1.1.1/dns-query?name={hostname}&type=A",
|
| 53 |
+
headers={"Accept": "application/dns-json"},
|
| 54 |
+
)
|
| 55 |
+
with urlopen(req, timeout=5) as resp:
|
| 56 |
+
data = json.loads(resp.read())
|
| 57 |
+
for ans in data.get("Answer", []):
|
| 58 |
+
if ans.get("type") == 1: # A record
|
| 59 |
+
_worker_ip = ans["data"]
|
| 60 |
+
return _worker_ip
|
| 61 |
+
except Exception:
|
| 62 |
+
pass
|
| 63 |
+
return None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def _make_client(timeout: int = 30) -> httpx.AsyncClient:
|
| 67 |
+
"""Create an httpx AsyncClient."""
|
| 68 |
+
return httpx.AsyncClient(timeout=timeout)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def _make_sync_client(timeout: int = 300) -> httpx.Client:
|
| 72 |
+
"""Create a sync httpx Client."""
|
| 73 |
+
return httpx.Client(timeout=timeout)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def _url(path: str) -> str:
|
| 77 |
+
"""Build the full Worker URL for a given path, using resolved IP if needed."""
|
| 78 |
+
full = f"{ADMIN_API_URL}{path}"
|
| 79 |
+
ip = _resolve_worker_ip()
|
| 80 |
+
if ip:
|
| 81 |
+
hostname = urlparse(ADMIN_API_URL).hostname or ""
|
| 82 |
+
return full.replace(hostname, ip)
|
| 83 |
+
return full
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def _host_header() -> dict:
|
| 87 |
+
"""Return Host header if using IP-resolved URL."""
|
| 88 |
+
ip = _resolve_worker_ip()
|
| 89 |
+
if ip:
|
| 90 |
+
hostname = urlparse(ADMIN_API_URL).hostname or ""
|
| 91 |
+
return {"Host": hostname}
|
| 92 |
+
return {}
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _worker_headers(token: str) -> dict:
|
| 96 |
+
"""Build headers for Worker API calls."""
|
| 97 |
+
return {"Authorization": f"Bearer {token}", **_host_header()}
|
| 98 |
+
|
| 99 |
|
| 100 |
def _get_token(request: Request) -> str:
|
| 101 |
"""Extract JWT token from request Authorization header."""
|
|
|
|
| 105 |
return ""
|
| 106 |
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
def _create_zone_archive(zone_name: str) -> Path:
|
| 109 |
"""Create a tar.gz archive of a zone directory."""
|
| 110 |
zone_path = DATA_DIR / zone_name
|
|
|
|
| 140 |
if not token:
|
| 141 |
raise HTTPException(401, "Chua dang nhap")
|
| 142 |
try:
|
| 143 |
+
async with _make_client(timeout=30) as client:
|
| 144 |
+
resp = await client.get(_url("/backup/list"), headers=_worker_headers(token))
|
| 145 |
if resp.status_code != 200:
|
| 146 |
data = resp.json() if resp.headers.get("content-type", "").startswith("application/json") else {"error": resp.text}
|
| 147 |
raise HTTPException(resp.status_code, data.get("error", "Worker error"))
|
|
|
|
| 173 |
try:
|
| 174 |
archive_path = _create_zone_archive(zone_name)
|
| 175 |
try:
|
| 176 |
+
with _make_sync_client(timeout=300) as client:
|
| 177 |
with open(archive_path, "rb") as f:
|
| 178 |
resp = client.post(
|
| 179 |
+
_url(f"/backup/upload/{zone_name}"),
|
| 180 |
headers={**_worker_headers(token), "Content-Type": "application/octet-stream"},
|
| 181 |
content=f.read(),
|
| 182 |
)
|
|
|
|
| 221 |
_backup_status["progress"] = f"Dang backup zone {zone_name} ({done + 1}/{total})..."
|
| 222 |
archive_path = _create_zone_archive(zone_name)
|
| 223 |
try:
|
| 224 |
+
with _make_sync_client(timeout=300) as client:
|
| 225 |
with open(archive_path, "rb") as f:
|
| 226 |
resp = client.post(
|
| 227 |
+
_url(f"/backup/upload/{zone_name}"),
|
| 228 |
headers={**_worker_headers(token), "Content-Type": "application/octet-stream"},
|
| 229 |
content=f.read(),
|
| 230 |
)
|
|
|
|
| 264 |
_backup_status["error"] = None
|
| 265 |
_backup_status["progress"] = f"Dang restore zone: {zone_name}..."
|
| 266 |
try:
|
| 267 |
+
with _make_sync_client(timeout=300) as client:
|
| 268 |
+
resp = client.get(_url(f"/backup/download/{zone_name}"), headers=_worker_headers(token))
|
| 269 |
if resp.status_code == 404:
|
| 270 |
raise ValueError(f"Backup zone '{zone_name}' khong ton tai")
|
| 271 |
if resp.status_code != 200:
|
|
|
|
| 319 |
_backup_status["error"] = None
|
| 320 |
_backup_status["progress"] = "Dang restore tat ca zones..."
|
| 321 |
try:
|
| 322 |
+
with _make_sync_client(timeout=30) as client:
|
| 323 |
+
resp = client.get(_url("/backup/list"), headers=_worker_headers(token))
|
| 324 |
if resp.status_code != 200:
|
| 325 |
raise ValueError(f"Khong the lay danh sach backup: {resp.text}")
|
| 326 |
backup_list = resp.json()
|
|
|
|
| 330 |
for b in backup_list:
|
| 331 |
zone_name = b["zone_name"]
|
| 332 |
_backup_status["progress"] = f"Dang restore zone {zone_name} ({done + 1}/{total})..."
|
| 333 |
+
with _make_sync_client(timeout=300) as client:
|
| 334 |
+
resp = client.get(_url(f"/backup/download/{zone_name}"), headers=_worker_headers(token))
|
| 335 |
if resp.status_code != 200:
|
| 336 |
continue
|
| 337 |
archive_path = BACKUP_DIR / f"{zone_name}.tar.gz"
|
|
|
|
| 366 |
_backup_status["running"] = False
|
| 367 |
|
| 368 |
background_tasks.add_task(_run)
|
| 369 |
+
return {"ok": True, "message": "Dang restore tat ca zones trong nen..."}
|