Spaces:
Running
Running
Update README.md
Browse files
README.md
CHANGED
|
@@ -1,715 +1,701 @@
|
|
| 1 |
-
|
| 2 |
-
title: PostgreSQL General
|
| 3 |
-
emoji: 🏃
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: docker
|
| 7 |
-
pinned: false
|
| 8 |
-
---
|
| 9 |
|
| 10 |
-
|
| 11 |
|
| 12 |
-
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
-
```
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
Adminer Web 管理界面:通过 7860 端口访问
|
| 21 |
```
|
| 22 |
|
| 23 |
-
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
|
|
|
| 28 |
|
| 29 |
-
|
| 30 |
|
| 31 |
```text
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
|
|
|
|
|
|
|
|
|
| 35 |
```
|
| 36 |
|
| 37 |
-
|
| 38 |
|
| 39 |
-
|
| 40 |
|
| 41 |
-
|
| 42 |
-
浏览器
|
| 43 |
-
↓
|
| 44 |
-
Hugging Face Space Web 入口,端口 7860
|
| 45 |
-
↓
|
| 46 |
-
Adminer 管理界面
|
| 47 |
-
↓
|
| 48 |
-
PostgreSQL,容器内部 5432 端口
|
| 49 |
-
```
|
| 50 |
|
| 51 |
-
|
| 52 |
|
| 53 |
```text
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
/data
|
| 58 |
-
├── backups
|
| 59 |
-
│ ├── latest.sql
|
| 60 |
-
│ └── backup_appdb_YYYYMMDD_HHMMSS.sql
|
| 61 |
-
├── files
|
| 62 |
-
│ └── 用户上传文件
|
| 63 |
-
├── exports
|
| 64 |
-
│ └── 导出的 CSV / JSON / SQL 文件
|
| 65 |
-
└── generated
|
| 66 |
-
└── AI 生成文件、图片、视频等
|
| 67 |
```
|
| 68 |
|
| 69 |
-
数据库
|
| 70 |
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
-
|
| 74 |
|
| 75 |
-
```
|
| 76 |
-
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
|
| 81 |
-
|
| 82 |
-
|
| 83 |
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
ENV USER_FILE_DIR=/data/files
|
| 88 |
-
ENV EXPORT_DIR=/data/exports
|
| 89 |
-
ENV GENERATED_DIR=/data/generated
|
| 90 |
|
| 91 |
-
|
| 92 |
-
ENV BACKUP_INTERVAL_SECONDS=3600
|
| 93 |
|
| 94 |
-
|
| 95 |
-
|
|
|
|
|
|
|
| 96 |
|
| 97 |
-
|
| 98 |
-
curl \
|
| 99 |
-
php \
|
| 100 |
-
php-pgsql \
|
| 101 |
-
ca-certificates \
|
| 102 |
-
procps \
|
| 103 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 104 |
|
| 105 |
-
|
| 106 |
-
https://github.com/vrana/adminer/releases/download/v${ADMINER_VERSION}/adminer-${ADMINER_VERSION}.php
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 110 |
|
| 111 |
-
|
| 112 |
-
|
| 113 |
|
| 114 |
-
|
|
|
|
| 115 |
```
|
| 116 |
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
## 4. start.sh
|
| 120 |
|
| 121 |
-
```
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
echo "======================================"
|
| 126 |
-
echo "PostgreSQL + Adminer for Hugging Face"
|
| 127 |
-
echo "======================================"
|
| 128 |
-
|
| 129 |
-
if [ -z "$POSTGRES_PASSWORD" ]; then
|
| 130 |
-
echo "ERROR: POSTGRES_PASSWORD is not set."
|
| 131 |
-
echo "Please set POSTGRES_PASSWORD in Hugging Face Space Secrets."
|
| 132 |
-
exit 1
|
| 133 |
-
fi
|
| 134 |
-
|
| 135 |
-
export POSTGRES_USER="${POSTGRES_USER:-admin}"
|
| 136 |
-
export POSTGRES_DB="${POSTGRES_DB:-appdb}"
|
| 137 |
-
export PGDATA="${PGDATA:-/home/user/pgdata}"
|
| 138 |
-
|
| 139 |
-
export DATA_DIR="${DATA_DIR:-/data}"
|
| 140 |
-
export BACKUP_DIR="${BACKUP_DIR:-/data/backups}"
|
| 141 |
-
export USER_FILE_DIR="${USER_FILE_DIR:-/data/files}"
|
| 142 |
-
export EXPORT_DIR="${EXPORT_DIR:-/data/exports}"
|
| 143 |
-
export GENERATED_DIR="${GENERATED_DIR:-/data/generated}"
|
| 144 |
-
export BACKUP_INTERVAL_SECONDS="${BACKUP_INTERVAL_SECONDS:-3600}"
|
| 145 |
-
|
| 146 |
-
echo "POSTGRES_USER=$POSTGRES_USER"
|
| 147 |
-
echo "POSTGRES_DB=$POSTGRES_DB"
|
| 148 |
-
echo "PGDATA=$PGDATA"
|
| 149 |
-
echo "DATA_DIR=$DATA_DIR"
|
| 150 |
-
echo "BACKUP_DIR=$BACKUP_DIR"
|
| 151 |
-
echo "USER_FILE_DIR=$USER_FILE_DIR"
|
| 152 |
-
echo "EXPORT_DIR=$EXPORT_DIR"
|
| 153 |
-
echo "GENERATED_DIR=$GENERATED_DIR"
|
| 154 |
-
echo "BACKUP_INTERVAL_SECONDS=$BACKUP_INTERVAL_SECONDS"
|
| 155 |
-
|
| 156 |
-
echo "Creating directories..."
|
| 157 |
-
|
| 158 |
-
mkdir -p "$PGDATA"
|
| 159 |
-
mkdir -p "$BACKUP_DIR"
|
| 160 |
-
mkdir -p "$USER_FILE_DIR"
|
| 161 |
-
mkdir -p "$EXPORT_DIR"
|
| 162 |
-
mkdir -p "$GENERATED_DIR"
|
| 163 |
-
|
| 164 |
-
# PostgreSQL 数据目录需要 postgres 用户权限
|
| 165 |
-
chown -R postgres:postgres "$PGDATA" || true
|
| 166 |
-
chmod 700 "$PGDATA" || true
|
| 167 |
-
|
| 168 |
-
# /data 是挂载大磁盘,可能不支持 chown,所以这里允许失败
|
| 169 |
-
chmod -R 755 "$DATA_DIR" || true
|
| 170 |
-
|
| 171 |
-
echo "Directory status:"
|
| 172 |
-
ls -ld "$PGDATA" || true
|
| 173 |
-
ls -ld "$DATA_DIR" || true
|
| 174 |
-
ls -ld "$BACKUP_DIR" || true
|
| 175 |
-
ls -ld "$USER_FILE_DIR" || true
|
| 176 |
-
ls -ld "$EXPORT_DIR" || true
|
| 177 |
-
ls -ld "$GENERATED_DIR" || true
|
| 178 |
-
|
| 179 |
-
RESTORE_BACKUP=0
|
| 180 |
-
|
| 181 |
-
if [ ! -s "$PGDATA/PG_VERSION" ]; then
|
| 182 |
-
echo "No existing PostgreSQL data directory found."
|
| 183 |
-
|
| 184 |
-
if [ -f "$BACKUP_DIR/latest.sql" ]; then
|
| 185 |
-
echo "Backup file found: $BACKUP_DIR/latest.sql"
|
| 186 |
-
echo "Database will be restored after PostgreSQL initialization."
|
| 187 |
-
RESTORE_BACKUP=1
|
| 188 |
-
else
|
| 189 |
-
echo "No backup file found. A new database will be initialized."
|
| 190 |
-
fi
|
| 191 |
-
else
|
| 192 |
-
echo "Existing PostgreSQL data directory found."
|
| 193 |
-
fi
|
| 194 |
-
|
| 195 |
-
echo "Starting PostgreSQL..."
|
| 196 |
-
docker-entrypoint.sh postgres &
|
| 197 |
-
|
| 198 |
-
POSTGRES_PID=$!
|
| 199 |
-
|
| 200 |
-
echo "Waiting for PostgreSQL to become ready..."
|
| 201 |
-
|
| 202 |
-
until pg_isready -h 127.0.0.1 -p 5432 -U "$POSTGRES_USER"; do
|
| 203 |
-
sleep 2
|
| 204 |
-
done
|
| 205 |
-
|
| 206 |
-
echo "PostgreSQL is ready."
|
| 207 |
-
|
| 208 |
-
if [ "$RESTORE_BACKUP" = "1" ]; then
|
| 209 |
-
echo "Restoring database from $BACKUP_DIR/latest.sql ..."
|
| 210 |
-
|
| 211 |
-
psql \
|
| 212 |
-
-h 127.0.0.1 \
|
| 213 |
-
-U "$POSTGRES_USER" \
|
| 214 |
-
-d "$POSTGRES_DB" \
|
| 215 |
-
< "$BACKUP_DIR/latest.sql" || {
|
| 216 |
-
echo "WARNING: Restore failed. Please check backup file."
|
| 217 |
-
}
|
| 218 |
|
| 219 |
-
|
| 220 |
-
fi
|
| 221 |
|
| 222 |
-
|
| 223 |
-
TIMESTAMP="$(date +%Y%m%d_%H%M%S)"
|
| 224 |
-
BACKUP_FILE="$BACKUP_DIR/backup_${POSTGRES_DB}_${TIMESTAMP}.sql"
|
| 225 |
-
LATEST_FILE="$BACKUP_DIR/latest.sql"
|
| 226 |
|
| 227 |
-
|
| 228 |
|
| 229 |
-
|
| 230 |
-
-h 127.0.0.1 \
|
| 231 |
-
-U "$POSTGRES_USER" \
|
| 232 |
-
-d "$POSTGRES_DB" \
|
| 233 |
-
> "$BACKUP_FILE.tmp"
|
| 234 |
|
| 235 |
-
|
| 236 |
-
cp "$BACKUP_FILE" "$LATEST_FILE"
|
| 237 |
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
echo "- $LATEST_FILE"
|
| 241 |
-
}
|
| 242 |
|
| 243 |
-
|
| 244 |
-
|
| 245 |
|
| 246 |
-
|
| 247 |
-
sleep "$BACKUP_INTERVAL_SECONDS"
|
| 248 |
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
echo "WARNING: PostgreSQL is not ready. Backup skipped."
|
| 253 |
-
fi
|
| 254 |
-
done
|
| 255 |
-
}
|
| 256 |
|
| 257 |
-
|
| 258 |
-
if pg_isready -h 127.0.0.1 -p 5432 -U "$POSTGRES_USER"; then
|
| 259 |
-
create_backup || echo "WARNING: Initial backup failed."
|
| 260 |
-
fi
|
| 261 |
|
| 262 |
-
|
|
|
|
|
|
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
echo "Server: 127.0.0.1"
|
| 268 |
-
echo "Username: $POSTGRES_USER"
|
| 269 |
-
echo "Database: $POSTGRES_DB"
|
| 270 |
|
| 271 |
-
|
| 272 |
|
| 273 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 274 |
```
|
| 275 |
|
| 276 |
---
|
| 277 |
|
| 278 |
-
##
|
| 279 |
|
| 280 |
-
|
| 281 |
|
| 282 |
-
```
|
| 283 |
-
|
| 284 |
-
Visibility: Public 或 Private
|
| 285 |
-
Hardware: CPU Basic 即可
|
| 286 |
```
|
| 287 |
|
| 288 |
-
|
| 289 |
|
| 290 |
-
```yaml
|
| 291 |
-
---
|
| 292 |
-
title: PostgreSQL Adminer
|
| 293 |
-
emoji: 🐘
|
| 294 |
-
colorFrom: blue
|
| 295 |
-
colorTo: indigo
|
| 296 |
-
sdk: docker
|
| 297 |
-
app_port: 7860
|
| 298 |
-
pinned: false
|
| 299 |
---
|
| 300 |
-
```
|
| 301 |
|
| 302 |
-
|
| 303 |
|
| 304 |
-
|
| 305 |
-
| ---------------- | -------------------------------- |
|
| 306 |
-
| `sdk: docker` | 使用 Dockerfile 构建 Space |
|
| 307 |
-
| `app_port: 7860` | Hugging Face 对外暴露 Adminer Web 页面 |
|
| 308 |
-
| `title` | Space 显示名称 |
|
| 309 |
-
| `emoji` | Space 图标 |
|
| 310 |
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
## 6. 环境变量
|
| 314 |
-
|
| 315 |
-
在 Hugging Face Space 的:
|
| 316 |
|
| 317 |
-
```
|
| 318 |
-
|
| 319 |
```
|
| 320 |
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
### 必填 Secret
|
| 324 |
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
|
| 329 |
-
|
| 330 |
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
| `PGDATA` | `/home/user/pgdata` | PostgreSQL 运行数据目录 |
|
| 336 |
-
| `DATA_DIR` | `/data` | 挂载大磁盘根目录 |
|
| 337 |
-
| `BACKUP_DIR` | `/data/backups` | 数据库备份目录 |
|
| 338 |
-
| `USER_FILE_DIR` | `/data/files` | 用户上传文件目录 |
|
| 339 |
-
| `EXPORT_DIR` | `/data/exports` | 导出文件目录 |
|
| 340 |
-
| `GENERATED_DIR` | `/data/generated` | AI 生成文件目录 |
|
| 341 |
-
| `BACKUP_INTERVAL_SECONDS` | `3600` | 自动备份间隔,单位为秒 |
|
| 342 |
|
| 343 |
-
|
| 344 |
|
| 345 |
-
```
|
| 346 |
-
|
| 347 |
-
POSTGRES_DB=appdb
|
| 348 |
-
PGDATA=/home/user/pgdata
|
| 349 |
-
DATA_DIR=/data
|
| 350 |
-
BACKUP_DIR=/data/backups
|
| 351 |
-
USER_FILE_DIR=/data/files
|
| 352 |
-
EXPORT_DIR=/data/exports
|
| 353 |
-
GENERATED_DIR=/data/generated
|
| 354 |
-
BACKUP_INTERVAL_SECONDS=3600
|
| 355 |
```
|
| 356 |
|
| 357 |
-
|
| 358 |
|
| 359 |
---
|
| 360 |
|
| 361 |
-
##
|
| 362 |
|
| 363 |
-
|
| 364 |
|
| 365 |
```text
|
| 366 |
-
|
| 367 |
```
|
| 368 |
|
| 369 |
-
|
|
|
|
|
|
|
| 370 |
|
| 371 |
```text
|
| 372 |
-
|
| 373 |
-
Access mode: Read & Write
|
| 374 |
-
Bucket visibility: Private
|
| 375 |
```
|
| 376 |
|
| 377 |
-
|
| 378 |
|
| 379 |
```text
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
|
|
|
| 384 |
```
|
| 385 |
|
| 386 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 387 |
|
| 388 |
---
|
| 389 |
|
| 390 |
-
##
|
| 391 |
|
| 392 |
-
部
|
| 393 |
|
| 394 |
-
|
| 395 |
|
| 396 |
```text
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
```
|
| 403 |
|
| 404 |
-
|
|
|
|
|
|
|
| 405 |
|
| 406 |
```text
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
```
|
| 410 |
|
| 411 |
---
|
| 412 |
|
| 413 |
-
##
|
| 414 |
|
| 415 |
-
|
| 416 |
|
| 417 |
-
|
| 418 |
|
| 419 |
-
```
|
| 420 |
-
|
| 421 |
-
id SERIAL PRIMARY KEY,
|
| 422 |
-
content BYTEA
|
| 423 |
-
);
|
| 424 |
```
|
| 425 |
|
| 426 |
-
|
| 427 |
|
| 428 |
-
```
|
| 429 |
-
|
| 430 |
-
id SERIAL PRIMARY KEY,
|
| 431 |
-
user_id TEXT,
|
| 432 |
-
filename TEXT NOT NULL,
|
| 433 |
-
file_path TEXT NOT NULL,
|
| 434 |
-
file_size BIGINT,
|
| 435 |
-
mime_type TEXT,
|
| 436 |
-
created_at TIMESTAMP DEFAULT now()
|
| 437 |
-
);
|
| 438 |
```
|
| 439 |
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
```text
|
| 443 |
-
filename: image_001.png
|
| 444 |
-
file_path: /data/files/image_001.png
|
| 445 |
-
file_size: 245891
|
| 446 |
-
mime_type: image/png
|
| 447 |
-
```
|
| 448 |
|
| 449 |
---
|
| 450 |
|
| 451 |
-
##
|
| 452 |
|
| 453 |
-
|
| 454 |
|
| 455 |
-
|
| 456 |
|
| 457 |
-
```
|
| 458 |
-
|
| 459 |
-
|
|
|
|
|
|
|
| 460 |
|
| 461 |
-
|
| 462 |
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
|
|
|
|
|
|
| 466 |
|
| 467 |
-
备份文件保存到:
|
| 468 |
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
| 472 |
|
| 473 |
-
|
|
|
|
| 474 |
|
| 475 |
-
|
| 476 |
-
/data/backups/latest.sql
|
| 477 |
-
/data/backups/backup_appdb_20260529_180000.sql
|
| 478 |
-
```
|
| 479 |
-
|
| 480 |
-
其中:
|
| 481 |
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
```
|
| 485 |
|
| 486 |
-
始终指向最近一次备份内容。
|
| 487 |
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
|
|
|
|
|
|
|
|
|
| 491 |
|
| 492 |
-
如果 Space 重启或重建后,`/home/user/pgdata` 不存在,但 `/data/backups/latest.sql` 存在,启动脚本会尝试自动恢复:
|
| 493 |
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
PostgreSQL appdb
|
| 498 |
-
```
|
| 499 |
|
| 500 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
| 506 |
-
4. 重要数据库建议额外同步到 R2 / S3 / Hugging Face Dataset
|
| 507 |
```
|
| 508 |
|
| 509 |
---
|
| 510 |
|
| 511 |
-
##
|
| 512 |
|
| 513 |
-
|
| 514 |
|
| 515 |
-
|
| 516 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 517 |
```
|
| 518 |
|
| 519 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 520 |
|
| 521 |
-
```bash
|
| 522 |
-
docker run --rm -it \
|
| 523 |
-
-p 7860:7860 \
|
| 524 |
-
-p 5432:5432 \
|
| 525 |
-
-e POSTGRES_PASSWORD=your_strong_password \
|
| 526 |
-
-v $(pwd)/data:/data \
|
| 527 |
-
hf-postgres-adminer
|
| 528 |
-
```
|
| 529 |
|
| 530 |
-
|
|
|
|
|
|
|
| 531 |
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
```
|
| 535 |
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
|
| 538 |
-
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
```
|
| 541 |
|
| 542 |
---
|
| 543 |
|
| 544 |
-
##
|
| 545 |
-
|
| 546 |
-
进入数据库:
|
| 547 |
|
| 548 |
-
|
| 549 |
-
psql -h 127.0.0.1 -U admin -d appdb
|
| 550 |
-
```
|
| 551 |
|
| 552 |
-
|
| 553 |
|
| 554 |
```sql
|
| 555 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 556 |
```
|
| 557 |
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
```
|
| 561 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 562 |
```
|
| 563 |
|
| 564 |
-
|
| 565 |
|
| 566 |
-
|
| 567 |
-
CREATE DATABASE project_db;
|
| 568 |
-
```
|
| 569 |
|
| 570 |
-
|
| 571 |
|
| 572 |
-
```
|
| 573 |
-
|
| 574 |
```
|
| 575 |
|
| 576 |
-
|
| 577 |
|
| 578 |
-
```
|
| 579 |
-
|
|
|
|
|
|
|
| 580 |
```
|
| 581 |
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
## 14. 多项目隔离建议
|
| 585 |
|
| 586 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 587 |
|
| 588 |
-
|
| 589 |
|
| 590 |
```text
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
|
|
|
|
|
|
|
|
|
| 594 |
```
|
| 595 |
|
| 596 |
-
|
| 597 |
|
| 598 |
-
|
| 599 |
-
CREATE DATABASE rss_project;
|
| 600 |
-
CREATE USER rss_user WITH PASSWORD 'rss_project_password';
|
| 601 |
-
GRANT ALL PRIVILEGES ON DATABASE rss_project TO rss_user;
|
| 602 |
|
| 603 |
-
|
| 604 |
-
CREATE USER image_user WITH PASSWORD 'image_project_password';
|
| 605 |
-
GRANT ALL PRIVILEGES ON DATABASE image_project TO image_user;
|
| 606 |
-
```
|
| 607 |
|
| 608 |
-
|
| 609 |
|
| 610 |
-
```
|
| 611 |
-
|
| 612 |
-
|
|
|
|
|
|
|
| 613 |
|
| 614 |
-
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 615 |
|
| 616 |
-
|
| 617 |
|
| 618 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 619 |
|
| 620 |
-
|
| 621 |
|
| 622 |
-
```
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
3. Adminer 必须使用强密码
|
| 626 |
-
4. 不建议直接暴露 PostgreSQL 5432 给公网
|
| 627 |
-
5. 尽量在外层增加 Cloudflare Access / OAuth / API Gateway
|
| 628 |
-
6. 不要让业务项目使用 admin 超级用户
|
| 629 |
-
7. 每个项目创建独立 database 和 user
|
| 630 |
-
8. 定期备份数据库
|
| 631 |
-
9. 重要备份同步到外部对象存储
|
| 632 |
-
10. 不要在公开日志中输出数据库密码
|
| 633 |
```
|
| 634 |
|
| 635 |
---
|
| 636 |
|
| 637 |
-
##
|
|
|
|
|
|
|
| 638 |
|
| 639 |
-
|
| 640 |
|
| 641 |
```text
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
Hugging Face Space 内部应用数据库
|
| 648 |
-
AI 生成文件索引系统
|
| 649 |
-
用户文件索引系统
|
| 650 |
```
|
| 651 |
|
| 652 |
-
不
|
|
|
|
|
|
|
| 653 |
|
| 654 |
```text
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
需要专业数据库 SLA 的业务
|
| 659 |
-
需要大量公网 PostgreSQL 长连接的业务
|
| 660 |
```
|
| 661 |
|
| 662 |
---
|
| 663 |
|
| 664 |
-
##
|
| 665 |
|
| 666 |
-
|
| 667 |
|
| 668 |
-
```
|
| 669 |
-
https://
|
|
|
|
| 670 |
```
|
| 671 |
|
| 672 |
-
|
| 673 |
|
| 674 |
-
```
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
|
|
|
|
|
|
|
|
|
| 682 |
```
|
| 683 |
|
| 684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 685 |
|
| 686 |
---
|
| 687 |
|
| 688 |
-
##
|
| 689 |
|
| 690 |
-
|
| 691 |
|
| 692 |
```text
|
| 693 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 694 |
```
|
| 695 |
|
| 696 |
-
|
| 697 |
|
| 698 |
-
```
|
| 699 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 700 |
```
|
| 701 |
|
| 702 |
-
|
|
|
|
|
|
|
| 703 |
|
| 704 |
-
|
| 705 |
|
| 706 |
```text
|
| 707 |
-
|
| 708 |
-
挂载
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
## 14. 用户账号机制说明
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
+
本项目不提供公开注册功能。
|
| 4 |
|
| 5 |
+
PostgreSQL 用户账号不是网站账号,也不是任何人都可以自助注册的账号。所有数据库用户、数据库、schema 和权限都必须由管理员通过 Adminer 或 SQL 命令手动创建。
|
| 6 |
|
| 7 |
+
默认初始化管理员账号由以下环境变量决定:
|
| 8 |
|
| 9 |
+
```env
|
| 10 |
+
POSTGRES_USER=admin
|
| 11 |
+
POSTGRES_PASSWORD=your_strong_password
|
| 12 |
+
POSTGRES_DB=appdb
|
|
|
|
| 13 |
```
|
| 14 |
|
| 15 |
+
默认管理员:
|
| 16 |
|
| 17 |
+
```text
|
| 18 |
+
Username: admin
|
| 19 |
+
Database: appdb
|
| 20 |
+
Password: POSTGRES_PASSWORD
|
| 21 |
+
```
|
| 22 |
|
| 23 |
+
`admin` 是数据库管理员账号,拥有较高权限,主要用于:
|
| 24 |
|
| 25 |
```text
|
| 26 |
+
1. 登录 Adminer
|
| 27 |
+
2. 创建数据库
|
| 28 |
+
3. 创建项目用户
|
| 29 |
+
4. 分配数据库权限
|
| 30 |
+
5. 查看和维护数据库
|
| 31 |
+
6. 执行备份、恢复、迁移等管理操作
|
| 32 |
```
|
| 33 |
|
| 34 |
+
不建议把 `admin` 账号直接提供给外部项目或普通用户使用。
|
| 35 |
|
| 36 |
+
---
|
| 37 |
|
| 38 |
+
## 15. 管理员如何创建项目数据库和账号
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
推荐原则:
|
| 41 |
|
| 42 |
```text
|
| 43 |
+
一个项目一个 database
|
| 44 |
+
一个项目一个 user
|
| 45 |
+
一个项目一个 password
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
```
|
| 47 |
|
| 48 |
+
例如,管理员要为 RSS 项目创建独立数据库和账号,可以登录 Adminer 后执行以下 SQL:
|
| 49 |
|
| 50 |
+
```sql
|
| 51 |
+
CREATE DATABASE rss_project;
|
| 52 |
+
CREATE USER rss_user WITH PASSWORD 'replace_with_strong_password';
|
| 53 |
+
GRANT ALL PRIVILEGES ON DATABASE rss_project TO rss_user;
|
| 54 |
+
```
|
| 55 |
|
| 56 |
+
然后切换到 `rss_project` 数据库,继续执行 schema 权限配置:
|
| 57 |
|
| 58 |
+
```sql
|
| 59 |
+
GRANT ALL ON SCHEMA public TO rss_user;
|
| 60 |
|
| 61 |
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
| 62 |
+
GRANT ALL ON TABLES TO rss_user;
|
| 63 |
|
| 64 |
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
| 65 |
+
GRANT ALL ON SEQUENCES TO rss_user;
|
| 66 |
|
| 67 |
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
| 68 |
+
GRANT ALL ON FUNCTIONS TO rss_user;
|
| 69 |
+
```
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
+
完整示例:
|
|
|
|
| 72 |
|
| 73 |
+
```sql
|
| 74 |
+
CREATE DATABASE image_project;
|
| 75 |
+
CREATE USER image_user WITH PASSWORD 'image_project_strong_password';
|
| 76 |
+
GRANT ALL PRIVILEGES ON DATABASE image_project TO image_user;
|
| 77 |
|
| 78 |
+
\c image_project
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
+
GRANT ALL ON SCHEMA public TO image_user;
|
|
|
|
| 81 |
|
| 82 |
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
| 83 |
+
GRANT ALL ON TABLES TO image_user;
|
| 84 |
|
| 85 |
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
| 86 |
+
GRANT ALL ON SEQUENCES TO image_user;
|
| 87 |
|
| 88 |
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
| 89 |
+
GRANT ALL ON FUNCTIONS TO image_user;
|
| 90 |
```
|
| 91 |
|
| 92 |
+
创建完成后,业务项目可以使用以下连接格式:
|
|
|
|
|
|
|
| 93 |
|
| 94 |
+
```env
|
| 95 |
+
DATABASE_URL=postgresql://image_user:image_project_strong_password@127.0.0.1:5432/image_project
|
| 96 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
|
| 98 |
+
注意:`127.0.0.1:5432` 是容器内部地址,适用于同一个 Space 容器内的应用。如果外部项目需要访问数据库,不建议直接暴露 PostgreSQL 端口,推荐使用 API Gateway。
|
|
|
|
| 99 |
|
| 100 |
+
---
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
## 16. 创建只读账号
|
| 103 |
|
| 104 |
+
如果需要给某个用户或项目只读权限,可以创建只读用户。
|
|
|
|
|
|
|
|
|
|
|
|
|
| 105 |
|
| 106 |
+
示例:
|
|
|
|
| 107 |
|
| 108 |
+
```sql
|
| 109 |
+
CREATE USER readonly_user WITH PASSWORD 'readonly_strong_password';
|
|
|
|
|
|
|
| 110 |
|
| 111 |
+
GRANT CONNECT ON DATABASE rss_project TO readonly_user;
|
| 112 |
+
```
|
| 113 |
|
| 114 |
+
切换到目标数据库:
|
|
|
|
| 115 |
|
| 116 |
+
```sql
|
| 117 |
+
\c rss_project
|
| 118 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
| 119 |
|
| 120 |
+
授予只读权限:
|
|
|
|
|
|
|
|
|
|
| 121 |
|
| 122 |
+
```sql
|
| 123 |
+
GRANT USAGE ON SCHEMA public TO readonly_user;
|
| 124 |
+
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly_user;
|
| 125 |
|
| 126 |
+
ALTER DEFAULT PRIVILEGES IN SCHEMA public
|
| 127 |
+
GRANT SELECT ON TABLES TO readonly_user;
|
| 128 |
+
```
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
+
只读账号适合:
|
| 131 |
|
| 132 |
+
```text
|
| 133 |
+
1. 数据分析
|
| 134 |
+
2. BI 看板
|
| 135 |
+
3. 外部查询服务
|
| 136 |
+
4. 临时审查
|
| 137 |
+
5. 只读 API 服务
|
| 138 |
```
|
| 139 |
|
| 140 |
---
|
| 141 |
|
| 142 |
+
## 17. 修改用户密码
|
| 143 |
|
| 144 |
+
管理员可以使用以下 SQL 修改用户密码:
|
| 145 |
|
| 146 |
+
```sql
|
| 147 |
+
ALTER USER rss_user WITH PASSWORD 'new_strong_password';
|
|
|
|
|
|
|
| 148 |
```
|
| 149 |
|
| 150 |
+
修改后,使用旧密码的应用需要同步更新环境变量。
|
| 151 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
---
|
|
|
|
| 153 |
|
| 154 |
+
## 18. 删除用户和数据库
|
| 155 |
|
| 156 |
+
删除用户前,需要先确认该用户不再拥有数据库对象。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
+
删除用户:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
|
| 160 |
+
```sql
|
| 161 |
+
DROP USER rss_user;
|
| 162 |
```
|
| 163 |
|
| 164 |
+
删除数据库:
|
|
|
|
|
|
|
| 165 |
|
| 166 |
+
```sql
|
| 167 |
+
DROP DATABASE rss_project;
|
| 168 |
+
```
|
| 169 |
|
| 170 |
+
如果用户拥有表、sequence、函数等对象,可以先在对应数据库内执行:
|
| 171 |
|
| 172 |
+
```sql
|
| 173 |
+
REASSIGN OWNED BY rss_user TO admin;
|
| 174 |
+
DROP OWNED BY rss_user;
|
| 175 |
+
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
|
| 177 |
+
然后再删除用户:
|
| 178 |
|
| 179 |
+
```sql
|
| 180 |
+
DROP USER rss_user;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 181 |
```
|
| 182 |
|
| 183 |
+
危险操作请谨慎执行,建议先备份数据库。
|
| 184 |
|
| 185 |
---
|
| 186 |
|
| 187 |
+
## 19. 不建议直接对外暴露 PostgreSQL 端口
|
| 188 |
|
| 189 |
+
本项目默认暴露给 Hugging Face Space 的 Web 端口是:
|
| 190 |
|
| 191 |
```text
|
| 192 |
+
7860
|
| 193 |
```
|
| 194 |
|
| 195 |
+
该端口用于访问 Adminer 或 API 服务。
|
| 196 |
+
|
| 197 |
+
PostgreSQL 默认端口是:
|
| 198 |
|
| 199 |
```text
|
| 200 |
+
5432
|
|
|
|
|
|
|
| 201 |
```
|
| 202 |
|
| 203 |
+
不建议将 `5432` 直接暴露给公网。原因包括:
|
| 204 |
|
| 205 |
```text
|
| 206 |
+
1. PostgreSQL 是数据库服务,不是公开 Web 服务
|
| 207 |
+
2. 公网暴露 5432 容易被扫描和攻击
|
| 208 |
+
3. 数据库长连接在 Hugging Face Space 场景下不稳定
|
| 209 |
+
4. 权限泄露风险较高
|
| 210 |
+
5. 不便于统一鉴权、限流、日志和审计
|
| 211 |
```
|
| 212 |
|
| 213 |
+
推荐对外提供 API,而不是让外部项目直接连接数据库。
|
| 214 |
+
|
| 215 |
+
推荐架构:
|
| 216 |
+
|
| 217 |
+
```text
|
| 218 |
+
外部项目 / 用户
|
| 219 |
+
↓
|
| 220 |
+
API Token 鉴权
|
| 221 |
+
↓
|
| 222 |
+
FastAPI / API Gateway
|
| 223 |
+
↓
|
| 224 |
+
PostgreSQL
|
| 225 |
+
```
|
| 226 |
|
| 227 |
---
|
| 228 |
|
| 229 |
+
## 20. 对外提供 API 调用接口的推荐方式
|
| 230 |
|
| 231 |
+
如果需要让外部项目调用数据库,建议在当前 Space 中增加一个 FastAPI 服务。
|
| 232 |
|
| 233 |
+
推荐公开 API:
|
| 234 |
|
| 235 |
```text
|
| 236 |
+
POST /api/query 执行受控查询
|
| 237 |
+
POST /api/insert 写入数据
|
| 238 |
+
GET /api/health 健康检查
|
| 239 |
+
GET /api/files 查询文件索引
|
| 240 |
+
POST /api/files 创建文件索引
|
| 241 |
```
|
| 242 |
|
| 243 |
+
不建议开放任意 SQL 执行接口给普通用户。
|
| 244 |
+
|
| 245 |
+
推荐做法是:
|
| 246 |
|
| 247 |
```text
|
| 248 |
+
1. 管理员在 PostgreSQL 中创建项目数据库和项目用户
|
| 249 |
+
2. 管理员在 Space Secrets 中配置项目数据库连接串
|
| 250 |
+
3. FastAPI 使用项目账号连接数据库
|
| 251 |
+
4. 外部项目通过 API Token 调用 FastAPI
|
| 252 |
+
5. FastAPI 执行受控 SQL
|
| 253 |
+
6. PostgreSQL 不直接暴露给公网
|
| 254 |
```
|
| 255 |
|
| 256 |
---
|
| 257 |
|
| 258 |
+
## 21. API 鉴权方式
|
| 259 |
|
| 260 |
+
推荐使用 API Token。
|
| 261 |
|
| 262 |
+
在 Hugging Face Space 的 Secrets 中配置:
|
| 263 |
|
| 264 |
+
```env
|
| 265 |
+
API_TOKEN=replace_with_random_api_token
|
|
|
|
|
|
|
|
|
|
| 266 |
```
|
| 267 |
|
| 268 |
+
外部项目请求 API 时,在 Header 中携带:
|
| 269 |
|
| 270 |
+
```http
|
| 271 |
+
Authorization: Bearer replace_with_random_api_token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
```
|
| 273 |
|
| 274 |
+
服务端验证通过后才允许访问数据库。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 275 |
|
| 276 |
---
|
| 277 |
|
| 278 |
+
## 22. FastAPI 示例:健康检查接口
|
| 279 |
|
| 280 |
+
如果需要增加 API 服务,可以在项目中新增 `api.py`。
|
| 281 |
|
| 282 |
+
示例:
|
| 283 |
|
| 284 |
+
```python
|
| 285 |
+
import os
|
| 286 |
+
import psycopg2
|
| 287 |
+
from fastapi import FastAPI, Header, HTTPException
|
| 288 |
+
from pydantic import BaseModel
|
| 289 |
|
| 290 |
+
app = FastAPI()
|
| 291 |
|
| 292 |
+
API_TOKEN = os.environ.get("API_TOKEN")
|
| 293 |
+
DATABASE_URL = os.environ.get(
|
| 294 |
+
"DATABASE_URL",
|
| 295 |
+
"postgresql://admin:password@127.0.0.1:5432/appdb"
|
| 296 |
+
)
|
| 297 |
|
|
|
|
| 298 |
|
| 299 |
+
def verify_token(authorization: str | None):
|
| 300 |
+
if not API_TOKEN:
|
| 301 |
+
raise HTTPException(status_code=500, detail="API_TOKEN is not configured")
|
| 302 |
|
| 303 |
+
if not authorization:
|
| 304 |
+
raise HTTPException(status_code=401, detail="Missing Authorization header")
|
| 305 |
|
| 306 |
+
expected = f"Bearer {API_TOKEN}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
+
if authorization != expected:
|
| 309 |
+
raise HTTPException(status_code=403, detail="Invalid API token")
|
|
|
|
| 310 |
|
|
|
|
| 311 |
|
| 312 |
+
@app.get("/api/health")
|
| 313 |
+
def health():
|
| 314 |
+
return {
|
| 315 |
+
"status": "ok",
|
| 316 |
+
"service": "postgresql-adminer-space"
|
| 317 |
+
}
|
| 318 |
|
|
|
|
| 319 |
|
| 320 |
+
@app.get("/api/db-health")
|
| 321 |
+
def db_health(authorization: str | None = Header(default=None)):
|
| 322 |
+
verify_token(authorization)
|
|
|
|
|
|
|
| 323 |
|
| 324 |
+
conn = psycopg2.connect(DATABASE_URL)
|
| 325 |
+
cur = conn.cursor()
|
| 326 |
+
cur.execute("SELECT 1;")
|
| 327 |
+
result = cur.fetchone()
|
| 328 |
+
cur.close()
|
| 329 |
+
conn.close()
|
| 330 |
|
| 331 |
+
return {
|
| 332 |
+
"status": "ok",
|
| 333 |
+
"db": result[0]
|
| 334 |
+
}
|
|
|
|
| 335 |
```
|
| 336 |
|
| 337 |
---
|
| 338 |
|
| 339 |
+
## 23. FastAPI 示例:文件索引表
|
| 340 |
|
| 341 |
+
本项目建议大文件保存在 `/data/files`,数据库只保存文件索引。
|
| 342 |
|
| 343 |
+
管理员可以先创建文件索引表:
|
| 344 |
+
|
| 345 |
+
```sql
|
| 346 |
+
CREATE TABLE IF NOT EXISTS files (
|
| 347 |
+
id SERIAL PRIMARY KEY,
|
| 348 |
+
user_id TEXT,
|
| 349 |
+
filename TEXT NOT NULL,
|
| 350 |
+
file_path TEXT NOT NULL,
|
| 351 |
+
file_size BIGINT,
|
| 352 |
+
mime_type TEXT,
|
| 353 |
+
created_at TIMESTAMP DEFAULT now()
|
| 354 |
+
);
|
| 355 |
```
|
| 356 |
|
| 357 |
+
API 示例:
|
| 358 |
+
|
| 359 |
+
```python
|
| 360 |
+
class FileRecord(BaseModel):
|
| 361 |
+
user_id: str | None = None
|
| 362 |
+
filename: str
|
| 363 |
+
file_path: str
|
| 364 |
+
file_size: int | None = None
|
| 365 |
+
mime_type: str | None = None
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
@app.post("/api/files")
|
| 369 |
+
def create_file(record: FileRecord, authorization: str | None = Header(default=None)):
|
| 370 |
+
verify_token(authorization)
|
| 371 |
+
|
| 372 |
+
conn = psycopg2.connect(DATABASE_URL)
|
| 373 |
+
cur = conn.cursor()
|
| 374 |
+
|
| 375 |
+
cur.execute(
|
| 376 |
+
"""
|
| 377 |
+
INSERT INTO files (user_id, filename, file_path, file_size, mime_type)
|
| 378 |
+
VALUES (%s, %s, %s, %s, %s)
|
| 379 |
+
RETURNING id, created_at;
|
| 380 |
+
""",
|
| 381 |
+
(
|
| 382 |
+
record.user_id,
|
| 383 |
+
record.filename,
|
| 384 |
+
record.file_path,
|
| 385 |
+
record.file_size,
|
| 386 |
+
record.mime_type,
|
| 387 |
+
),
|
| 388 |
+
)
|
| 389 |
+
|
| 390 |
+
row = cur.fetchone()
|
| 391 |
+
conn.commit()
|
| 392 |
+
|
| 393 |
+
cur.close()
|
| 394 |
+
conn.close()
|
| 395 |
+
|
| 396 |
+
return {
|
| 397 |
+
"id": row[0],
|
| 398 |
+
"created_at": row[1],
|
| 399 |
+
"filename": record.filename,
|
| 400 |
+
"file_path": record.file_path
|
| 401 |
+
}
|
| 402 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 403 |
|
| 404 |
+
@app.get("/api/files")
|
| 405 |
+
def list_files(authorization: str | None = Header(default=None)):
|
| 406 |
+
verify_token(authorization)
|
| 407 |
|
| 408 |
+
conn = psycopg2.connect(DATABASE_URL)
|
| 409 |
+
cur = conn.cursor()
|
|
|
|
| 410 |
|
| 411 |
+
cur.execute(
|
| 412 |
+
"""
|
| 413 |
+
SELECT id, user_id, filename, file_path, file_size, mime_type, created_at
|
| 414 |
+
FROM files
|
| 415 |
+
ORDER BY id DESC
|
| 416 |
+
LIMIT 100;
|
| 417 |
+
"""
|
| 418 |
+
)
|
| 419 |
|
| 420 |
+
rows = cur.fetchall()
|
| 421 |
+
|
| 422 |
+
cur.close()
|
| 423 |
+
conn.close()
|
| 424 |
+
|
| 425 |
+
return [
|
| 426 |
+
{
|
| 427 |
+
"id": row[0],
|
| 428 |
+
"user_id": row[1],
|
| 429 |
+
"filename": row[2],
|
| 430 |
+
"file_path": row[3],
|
| 431 |
+
"file_size": row[4],
|
| 432 |
+
"mime_type": row[5],
|
| 433 |
+
"created_at": row[6],
|
| 434 |
+
}
|
| 435 |
+
for row in rows
|
| 436 |
+
]
|
| 437 |
```
|
| 438 |
|
| 439 |
---
|
| 440 |
|
| 441 |
+
## 24. FastAPI 示例:受控插入数据
|
|
|
|
|
|
|
| 442 |
|
| 443 |
+
可以为不同业务创建专门接口,而不是开放通用 SQL。
|
|
|
|
|
|
|
| 444 |
|
| 445 |
+
例如创建一个 RSS 文章表:
|
| 446 |
|
| 447 |
```sql
|
| 448 |
+
CREATE TABLE IF NOT EXISTS rss_articles (
|
| 449 |
+
id SERIAL PRIMARY KEY,
|
| 450 |
+
source TEXT,
|
| 451 |
+
title TEXT NOT NULL,
|
| 452 |
+
url TEXT UNIQUE,
|
| 453 |
+
summary TEXT,
|
| 454 |
+
published_at TIMESTAMP,
|
| 455 |
+
created_at TIMESTAMP DEFAULT now()
|
| 456 |
+
);
|
| 457 |
```
|
| 458 |
|
| 459 |
+
API 示例:
|
| 460 |
+
|
| 461 |
+
```python
|
| 462 |
+
class ArticleRecord(BaseModel):
|
| 463 |
+
source: str | None = None
|
| 464 |
+
title: str
|
| 465 |
+
url: str
|
| 466 |
+
summary: str | None = None
|
| 467 |
+
published_at: str | None = None
|
| 468 |
+
|
| 469 |
+
|
| 470 |
+
@app.post("/api/rss/articles")
|
| 471 |
+
def create_article(
|
| 472 |
+
article: ArticleRecord,
|
| 473 |
+
authorization: str | None = Header(default=None)
|
| 474 |
+
):
|
| 475 |
+
verify_token(authorization)
|
| 476 |
+
|
| 477 |
+
conn = psycopg2.connect(DATABASE_URL)
|
| 478 |
+
cur = conn.cursor()
|
| 479 |
+
|
| 480 |
+
cur.execute(
|
| 481 |
+
"""
|
| 482 |
+
INSERT INTO rss_articles (source, title, url, summary, published_at)
|
| 483 |
+
VALUES (%s, %s, %s, %s, %s)
|
| 484 |
+
ON CONFLICT (url) DO UPDATE SET
|
| 485 |
+
title = EXCLUDED.title,
|
| 486 |
+
summary = EXCLUDED.summary,
|
| 487 |
+
published_at = EXCLUDED.published_at
|
| 488 |
+
RETURNING id;
|
| 489 |
+
""",
|
| 490 |
+
(
|
| 491 |
+
article.source,
|
| 492 |
+
article.title,
|
| 493 |
+
article.url,
|
| 494 |
+
article.summary,
|
| 495 |
+
article.published_at,
|
| 496 |
+
),
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
row = cur.fetchone()
|
| 500 |
+
conn.commit()
|
| 501 |
+
|
| 502 |
+
cur.close()
|
| 503 |
+
conn.close()
|
| 504 |
+
|
| 505 |
+
return {
|
| 506 |
+
"id": row[0],
|
| 507 |
+
"status": "saved"
|
| 508 |
+
}
|
| 509 |
```
|
| 510 |
|
| 511 |
+
---
|
| 512 |
|
| 513 |
+
## 25. 不推荐开放通用 SQL API
|
|
|
|
|
|
|
| 514 |
|
| 515 |
+
不推荐提供如下接口:
|
| 516 |
|
| 517 |
+
```text
|
| 518 |
+
POST /api/sql
|
| 519 |
```
|
| 520 |
|
| 521 |
+
并允许外部用户直接提交任意 SQL:
|
| 522 |
|
| 523 |
+
```json
|
| 524 |
+
{
|
| 525 |
+
"sql": "DROP TABLE users;"
|
| 526 |
+
}
|
| 527 |
```
|
| 528 |
|
| 529 |
+
这非常危险,可能导致:
|
|
|
|
|
|
|
| 530 |
|
| 531 |
+
```text
|
| 532 |
+
1. 数据库被删除
|
| 533 |
+
2. 数据泄露
|
| 534 |
+
3. 权限绕过
|
| 535 |
+
4. SQL 注入
|
| 536 |
+
5. 系统被恶意占用
|
| 537 |
+
```
|
| 538 |
|
| 539 |
+
如果确实需要管理级 SQL API,只建议管理员内部使用,并且必须同时满足:
|
| 540 |
|
| 541 |
```text
|
| 542 |
+
1. 强 API Token
|
| 543 |
+
2. IP 白名单
|
| 544 |
+
3. Cloudflare Access
|
| 545 |
+
4. 请求日志
|
| 546 |
+
5. 限制 SQL 类型
|
| 547 |
+
6. 禁止 DROP / TRUNCATE / ALTER 等危险语句
|
| 548 |
```
|
| 549 |
|
| 550 |
+
---
|
| 551 |
|
| 552 |
+
## 26. API 服务依赖安装
|
|
|
|
|
|
|
|
|
|
| 553 |
|
| 554 |
+
如果增加 FastAPI API 服务,Dockerfile 需要额外安装 Python 依赖。
|
|
|
|
|
|
|
|
|
|
| 555 |
|
| 556 |
+
可以在 Dockerfile 中增加:
|
| 557 |
|
| 558 |
+
```dockerfile
|
| 559 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 560 |
+
python3 \
|
| 561 |
+
python3-pip \
|
| 562 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 563 |
|
| 564 |
+
RUN pip3 install --break-system-packages \
|
| 565 |
+
fastapi \
|
| 566 |
+
uvicorn \
|
| 567 |
+
psycopg2-binary \
|
| 568 |
+
pydantic
|
| 569 |
+
```
|
| 570 |
|
| 571 |
+
或者使用 `requirements.txt`:
|
| 572 |
|
| 573 |
+
```txt
|
| 574 |
+
fastapi
|
| 575 |
+
uvicorn
|
| 576 |
+
psycopg2-binary
|
| 577 |
+
pydantic
|
| 578 |
+
```
|
| 579 |
|
| 580 |
+
然后在 Dockerfile 中加入:
|
| 581 |
|
| 582 |
+
```dockerfile
|
| 583 |
+
COPY requirements.txt /requirements.txt
|
| 584 |
+
RUN pip3 install --break-system-packages -r /requirements.txt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 585 |
```
|
| 586 |
|
| 587 |
---
|
| 588 |
|
| 589 |
+
## 27. 同时运行 Adminer 和 API 服务
|
| 590 |
+
|
| 591 |
+
如果当前 Space 同时需要 Adminer 和 API 服务,可以让 FastAPI 作为主 Web 服务,并把 Adminer 放到 `/adminer.php`。
|
| 592 |
|
| 593 |
+
更简单的方式是继续让 Adminer 使用 `7860`,API 另外用内部端口。但 Hugging Face Space 通常只对外暴露一个 Web 端口,所以更推荐后续改成:
|
| 594 |
|
| 595 |
```text
|
| 596 |
+
7860:
|
| 597 |
+
/ API 首页
|
| 598 |
+
/docs FastAPI 文档
|
| 599 |
+
/api/... API 接口
|
| 600 |
+
/adminer Adminer 管理入口
|
|
|
|
|
|
|
|
|
|
| 601 |
```
|
| 602 |
|
| 603 |
+
如果暂时只需要 Adminer,可以不增加 API 服务。
|
| 604 |
+
|
| 605 |
+
如果需要 API 服务,建议后续把启动方式升级为:
|
| 606 |
|
| 607 |
```text
|
| 608 |
+
Nginx / Caddy / FastAPI 静态代理
|
| 609 |
+
├── /api
|
| 610 |
+
└── /adminer
|
|
|
|
|
|
|
| 611 |
```
|
| 612 |
|
| 613 |
---
|
| 614 |
|
| 615 |
+
## 28. 外部项目如何调用 API
|
| 616 |
|
| 617 |
+
示例:调用健康检查接口。
|
| 618 |
|
| 619 |
+
```bash
|
| 620 |
+
curl -X GET "https://your-space-name.hf.space/api/db-health" \
|
| 621 |
+
-H "Authorization: Bearer replace_with_random_api_token"
|
| 622 |
```
|
| 623 |
|
| 624 |
+
示例:创建文件索引。
|
| 625 |
|
| 626 |
+
```bash
|
| 627 |
+
curl -X POST "https://your-space-name.hf.space/api/files" \
|
| 628 |
+
-H "Authorization: Bearer replace_with_random_api_token" \
|
| 629 |
+
-H "Content-Type: application/json" \
|
| 630 |
+
-d '{
|
| 631 |
+
"user_id": "user_001",
|
| 632 |
+
"filename": "image_001.png",
|
| 633 |
+
"file_path": "/data/files/image_001.png",
|
| 634 |
+
"file_size": 245891,
|
| 635 |
+
"mime_type": "image/png"
|
| 636 |
+
}'
|
| 637 |
```
|
| 638 |
|
| 639 |
+
示例:写入 RSS 文章。
|
| 640 |
+
|
| 641 |
+
```bash
|
| 642 |
+
curl -X POST "https://your-space-name.hf.space/api/rss/articles" \
|
| 643 |
+
-H "Authorization: Bearer replace_with_random_api_token" \
|
| 644 |
+
-H "Content-Type: application/json" \
|
| 645 |
+
-d '{
|
| 646 |
+
"source": "example-rss",
|
| 647 |
+
"title": "Example Article",
|
| 648 |
+
"url": "https://example.com/article-1",
|
| 649 |
+
"summary": "This is an example article.",
|
| 650 |
+
"published_at": "2026-05-30T00:00:00"
|
| 651 |
+
}'
|
| 652 |
+
```
|
| 653 |
|
| 654 |
---
|
| 655 |
|
| 656 |
+
## 29. 推荐的生产访问方式
|
| 657 |
|
| 658 |
+
公开 Space 推荐增加一层访问保护:
|
| 659 |
|
| 660 |
```text
|
| 661 |
+
Cloudflare Access
|
| 662 |
+
↓
|
| 663 |
+
Hugging Face Space
|
| 664 |
+
↓
|
| 665 |
+
FastAPI API Token
|
| 666 |
+
↓
|
| 667 |
+
PostgreSQL 项目用户
|
| 668 |
```
|
| 669 |
|
| 670 |
+
最终安全层级:
|
| 671 |
|
| 672 |
+
```text
|
| 673 |
+
第一层:Cloudflare Access / OAuth / 登录白名单
|
| 674 |
+
第二层:API Token
|
| 675 |
+
第三层:PostgreSQL 项目账号
|
| 676 |
+
第四层:database / schema 权限隔离
|
| 677 |
+
第五层:定期备份
|
| 678 |
```
|
| 679 |
|
| 680 |
+
---
|
| 681 |
+
|
| 682 |
+
## 30. 管理员操作流程总结
|
| 683 |
|
| 684 |
+
管理员首次部署后,建议按以下步骤操作:
|
| 685 |
|
| 686 |
```text
|
| 687 |
+
1. 设置 POSTGRES_PASSWORD
|
| 688 |
+
2. 挂载 /data 存储
|
| 689 |
+
3. 启动 Space
|
| 690 |
+
4. 登录 Adminer
|
| 691 |
+
5. 创建项目 database
|
| 692 |
+
6. 创建项目 user
|
| 693 |
+
7. 授权 database 和 schema 权限
|
| 694 |
+
8. 创建业务表
|
| 695 |
+
9. 配置 API_TOKEN
|
| 696 |
+
10. 如果需要,对外开放 FastAPI 接口
|
| 697 |
+
11. 外部项目通过 API Token 调用接口
|
| 698 |
+
12. 定期检查 /data/backups 备份文件
|
| 699 |
+
```
|
| 700 |
+
|
| 701 |
+
推荐不要让外部项目直接获得 `admin` 账号,也不要让外部用户直接连接 PostgreSQL 5432 端口。
|