Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
git \
libpq-dev \
gcc \
openssl \
&& rm -rf /var/lib/apt/lists/*

RUN pip install --no-cache-dir uv
Expand Down
3 changes: 3 additions & 0 deletions api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from api import accounts, ai, image_tasks, register, system
from api.support import resolve_web_asset, start_limited_account_watcher
from services.backup_service import backup_service
from services.config import config


Expand All @@ -20,12 +21,14 @@ def create_app() -> FastAPI:
async def lifespan(_: FastAPI):
stop_event = Event()
thread = start_limited_account_watcher(stop_event)
backup_service.start()
config.cleanup_old_images()
try:
yield
finally:
stop_event.set()
thread.join(timeout=1)
backup_service.stop()

app = FastAPI(title="chatgpt2api", version=app_version, lifespan=lifespan)
app.add_middleware(
Expand Down
72 changes: 72 additions & 0 deletions api/system.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
from __future__ import annotations

from urllib.parse import quote

from fastapi import APIRouter, Header, HTTPException, Request
from fastapi.concurrency import run_in_threadpool
from fastapi.responses import Response
from pydantic import BaseModel, ConfigDict

from api.support import require_admin, require_identity, resolve_image_base_url
from services.backup_service import BackupError, backup_service
from services.config import config
from services.image_service import delete_images, get_thumbnail_response, list_images
from services.log_service import log_service
Expand All @@ -26,6 +30,10 @@ class ImageDeleteRequest(BaseModel):
all_matching: bool = False


class BackupDeleteRequest(BaseModel):
key: str = ""


def create_router(app_version: str) -> APIRouter:
router = APIRouter()

Expand Down Expand Up @@ -90,4 +98,68 @@ async def get_storage_info(authorization: str | None = Header(default=None)):
"health": storage.health_check(),
}

@router.post("/api/backup/test")
async def test_backup_connection(authorization: str | None = Header(default=None)):
require_admin(authorization)
try:
return {"result": await run_in_threadpool(backup_service.test_connection)}
except BackupError as exc:
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc

@router.get("/api/backups")
async def get_backups(authorization: str | None = Header(default=None)):
require_admin(authorization)
try:
return {
"items": await run_in_threadpool(backup_service.list_backups),
"state": backup_service.get_status(),
"settings": backup_service.get_settings(),
}
except BackupError as exc:
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc

@router.post("/api/backups/run")
async def run_backup_endpoint(authorization: str | None = Header(default=None)):
require_admin(authorization)
try:
return {"result": await run_in_threadpool(backup_service.run_backup)}
except BackupError as exc:
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc

@router.post("/api/backups/delete")
async def delete_backup_endpoint(body: BackupDeleteRequest, authorization: str | None = Header(default=None)):
require_admin(authorization)
try:
await run_in_threadpool(backup_service.delete_backup, body.key)
return {"ok": True}
except BackupError as exc:
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc

@router.get("/api/backups/detail")
async def get_backup_detail(key: str = "", authorization: str | None = Header(default=None)):
require_admin(authorization)
try:
return {"item": await run_in_threadpool(backup_service.get_backup_detail, key)}
except BackupError as exc:
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc

@router.get("/api/backups/download")
async def download_backup_endpoint(key: str = "", authorization: str | None = Header(default=None)):
require_admin(authorization)
try:
item = await run_in_threadpool(backup_service.download_backup, key)
except BackupError as exc:
raise HTTPException(status_code=400, detail={"error": str(exc)}) from exc
filename = str(item.get("name") or "backup.bin")
quoted = quote(filename)
headers = {
"Content-Disposition": f"attachment; filename*=UTF-8''{quoted}",
"Content-Length": str(int(item.get("size") or 0)),
}
return Response(
content=bytes(item.get("payload") or b""),
media_type=str(item.get("content_type") or "application/octet-stream"),
headers=headers,
)

return router
26 changes: 25 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,29 @@
"api_key": "",
"model": "gpt-5.4-mini",
"prompt": "判断用户请求是否允许。只回答 ALLOW 或 REJECT。"
},
"backup": {
"enabled": false,
"provider": "cloudflare_r2",
"account_id": "",
"access_key_id": "",
"secret_access_key": "",
"bucket": "",
"prefix": "backups",
"interval_minutes": 1440,
"rotation_keep": 10,
"encrypt": false,
"passphrase": "",
"include": {
"config": true,
"register": true,
"cpa": true,
"sub2api": true,
"logs": true,
"image_tasks": true,
"accounts_snapshot": true,
"auth_keys_snapshot": true,
"images": false
}
}
}
}
Loading