backend optimizations + redundant commits cleanup

This commit is contained in:
Vladless
2026-04-19 22:53:14 +00:00
parent ed30a21842
commit bfd628b24e
67 changed files with 1157 additions and 1137 deletions
+32 -3
View File
@@ -4,6 +4,7 @@ import os
from time import perf_counter
from fastapi import Depends, FastAPI, Request
from fastapi.responses import ORJSONResponse
from sqlalchemy.ext.asyncio import AsyncSession
from starlette.middleware.cors import CORSMiddleware
from starlette.middleware.gzip import GZipMiddleware
@@ -28,6 +29,7 @@ app = FastAPI(
docs_url="/api/docs",
redoc_url="/api/redoc",
openapi_url="/api/openapi.json",
default_response_class=ORJSONResponse,
)
_cors_origins = API_CORS_ORIGINS if API_CORS_ORIGINS != ["*"] else API_CORS_ORIGINS
@@ -43,6 +45,8 @@ app.add_middleware(
app.add_middleware(GZipMiddleware, minimum_size=1024, compresslevel=6)
_ETAG_MAX_BODY_BYTES = 256 * 1024
@app.middleware("http")
async def security_and_cache_middleware(request: Request, call_next):
@@ -60,9 +64,32 @@ async def security_and_cache_middleware(request: Request, call_next):
return response
if request.method == "GET" and response.status_code == 200 and "application/json" in content_type:
body = b""
content_length_header = response.headers.get("content-length")
try:
cl = int(content_length_header) if content_length_header is not None else None
except (TypeError, ValueError):
cl = None
if cl is not None and cl > _ETAG_MAX_BODY_BYTES:
response.headers.setdefault("Cache-Control", "no-cache")
return response
chunks: list[bytes] = []
total = 0
too_big = False
async for chunk in response.body_iterator:
body += chunk
total += len(chunk)
if total > _ETAG_MAX_BODY_BYTES:
chunks.append(chunk)
too_big = True
async for remaining in response.body_iterator:
chunks.append(remaining)
break
chunks.append(chunk)
body = b"".join(chunks)
if too_big:
headers = dict(response.headers)
headers["Cache-Control"] = "no-cache"
headers.pop("content-length", None)
return StarletteResponse(content=body, status_code=200, headers=headers, media_type=response.media_type)
etag = '"' + hashlib.md5(body).hexdigest() + '"'
if_none_match = request.headers.get("if-none-match", "")
client_etags = [t.strip() for t in if_none_match.split(",") if t.strip()]
@@ -80,12 +107,13 @@ async def security_and_cache_middleware(request: Request, call_next):
@app.middleware("http")
async def api_access_log_middleware(request: Request, call_next):
context = ensure_api_context(request)
started = perf_counter()
if not API_LOGGING:
response = await call_next(request)
response.headers["X-Request-Id"] = context.request_id
response.headers["X-Response-Time"] = f"{int((perf_counter() - started) * 1000)}ms"
return response
started = perf_counter()
try:
response = await call_next(request)
except Exception as exc:
@@ -110,6 +138,7 @@ async def api_access_log_middleware(request: Request, call_next):
duration_ms = int((perf_counter() - started) * 1000)
response.headers["X-Request-Id"] = context.request_id
response.headers["X-Response-Time"] = f"{duration_ms}ms"
result = "success" if response.status_code < 400 else "fail"
log_api_access(
request,
-3
View File
@@ -155,7 +155,6 @@ def generate_crud_router(
data["days"] = None
obj = model(**data)
session.add(obj)
await session.commit()
await session.refresh(obj)
return to_schema(schema_response, obj)
@@ -181,7 +180,6 @@ def generate_crud_router(
for k, v in validated.model_dump(exclude_unset=True).items():
setattr(obj, k, v)
await session.commit()
await session.refresh(obj)
return to_schema(schema_response, obj)
@@ -203,7 +201,6 @@ def generate_crud_router(
raise HTTPException(status_code=404, detail=f"{model.__name__} not found")
await session.delete(obj)
await session.commit()
return {"detail": f"{model.__name__} deleted"}
return router
-1
View File
@@ -65,5 +65,4 @@ async def delete_gift_with_usages(
await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id))
await session.delete(gift)
await session.commit()
return {"message": "Подарок и связанные использования удалены"}
-2
View File
@@ -44,7 +44,6 @@ async def delete_key_by_email(
cluster_id=db_key.server_id,
)
await session.delete(db_key)
await session.commit()
logger.info(f"[API] Ключ удалён: {db_key.client_id}")
return {"message": "Ключ успешно удалён"}
@@ -108,7 +107,6 @@ async def edit_key_by_email(
hwid_device_limit=getattr(db_key, "device_limit", None),
reset_traffic=True,
)
await session.commit()
logger.info(f"[API] Ключ обновлён: {db_key.client_id}")
return db_key
-2
View File
@@ -195,7 +195,6 @@ async def change_domain(
)
)
result = await session.execute(stmt)
await session.commit()
return {"updated": result.rowcount or 0}
@@ -214,7 +213,6 @@ async def restore_trials(
.values(trial=0)
)
result = await session.execute(stmt)
await session.commit()
return {"restored": result.rowcount or 0}
+954 -964
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -42,5 +42,4 @@ async def delete_one_referral(
if not obj:
raise HTTPException(status_code=404, detail="Referral not found")
await session.delete(obj)
await session.commit()
return {"status": "deleted_one"}
-2
View File
@@ -125,7 +125,6 @@ async def upsert_setting(
value=payload.value,
description=payload.description,
)
await session.commit()
await session.refresh(obj)
settings_cache.update(
key,
@@ -148,6 +147,5 @@ async def delete_setting(
if not obj:
raise HTTPException(status_code=404, detail="Setting not found")
await session.delete(obj)
await session.commit()
settings_cache.delete(key)
return {"detail": "Setting deleted"}
-1
View File
@@ -56,7 +56,6 @@ async def apply_coupon(
tg_id=tg_id,
code=str(body.code or ""),
)
await session.commit()
return CouponApplyResponse(
ok=True,
message="Купон успешно активирован",
-3
View File
@@ -76,7 +76,6 @@ async def create_flow(
)
session.add(flow)
await bump_site_revision(session)
await session.commit()
await session.refresh(flow)
return _flow_to_response(flow)
@@ -101,7 +100,6 @@ async def update_flow(
flow.updated_at = datetime.now(UTC)
await bump_site_revision(session)
await session.commit()
await session.refresh(flow)
return _flow_to_response(flow)
@@ -117,4 +115,3 @@ async def delete_flow(
raise HTTPException(404, "Flow not found")
await session.delete(flow)
await bump_site_revision(session)
await session.commit()
-2
View File
@@ -244,7 +244,6 @@ async def delete_my_gift(
raise HTTPException(status_code=404, detail="Подарок не найден")
await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id))
await session.delete(gift)
await session.commit()
return {"ok": True, "message": "Подарок удалён"}
@@ -330,5 +329,4 @@ async def delete_gift_with_usages(
raise HTTPException(status_code=404, detail="Gift not found")
await session.execute(delete(GiftUsage).where(GiftUsage.gift_id == gift_id))
await session.delete(gift)
await session.commit()
return {"message": "Подарок и связанные использования удалены"}
-2
View File
@@ -21,7 +21,6 @@ async def delete_key_by_email(
cluster_id=db_key.server_id,
)
await session.delete(db_key)
await session.commit()
logger.info(f"[API] Ключ удалён: {db_key.client_id}")
return {"message": "Ключ успешно удалён"}
except Exception as e:
@@ -82,7 +81,6 @@ async def edit_key_by_email(
hwid_device_limit=getattr(db_key, "device_limit", None),
reset_traffic=True,
)
await session.commit()
logger.info(f"[API] Ключ обновлён: {db_key.client_id}")
return db_key
except Exception as e:
-1
View File
@@ -647,7 +647,6 @@ async def user_key_apply_addons(
await update_balance(session, int(billing_user_id), -int(final_extra_price_rub))
if coupon_id is not None:
await mark_coupon_used(session, int(coupon_id), int(billing_user_id))
await session.commit()
return AccountKeyApplyAddonsResponse(
ok=True,
message="Доп. опции применены",
-2
View File
@@ -195,7 +195,6 @@ async def user_key_update_alias(
if db_key is None:
raise HTTPException(status_code=404, detail="Подписка не найдена")
db_key.alias = alias
await session.commit()
return AccountKeyResponse(
email=str(getattr(db_key, "email", "") or ""),
alias=getattr(db_key, "alias", None),
@@ -237,5 +236,4 @@ async def user_key_delete(
session=session,
)
await session.delete(db_key)
await session.commit()
return AccountKeyActionResponse(ok=True, message="Подписка удалена")
-1
View File
@@ -215,7 +215,6 @@ async def user_key_change_location(
db_key.client_id = key_client_id
db_key.key = public_link if isinstance(public_link, str) and public_link.strip() else None
db_key.remnawave_link = remnawave_link
await session.commit()
return AccountKeyChangeLocationResponse(
ok=True,
message="Локация успешно изменена",
-1
View File
@@ -174,7 +174,6 @@ async def user_key_renew(
)
except ServiceError as e:
raise HTTPException(status_code=400, detail=e.message)
await session.commit()
return AccountKeyRenewResponse(
ok=True,
message="Подписка продлена",
-2
View File
@@ -215,7 +215,6 @@ async def change_domain(
)
)
result = await session.execute(stmt)
await session.commit()
return {"updated": result.rowcount or 0}
@@ -235,7 +234,6 @@ async def restore_trials(
.values(trial=0)
)
result = await session.execute(stmt)
await session.commit()
return {"restored": result.rowcount or 0}
+44 -56
View File
@@ -9,7 +9,7 @@ from urllib.parse import urlsplit
import qrcode
from fastapi import APIRouter, Depends, HTTPException, Path, Query, Request
from fastapi.responses import JSONResponse, StreamingResponse
from fastapi.responses import ORJSONResponse, StreamingResponse
from sqlalchemy import text
from sqlalchemy.ext.asyncio import AsyncSession
@@ -227,7 +227,6 @@ async def partner_apply(
)
except Exception:
pass
await session.commit()
return PartnerApplyResponse(
ok=True,
message="Партнерский код применен",
@@ -516,7 +515,6 @@ async def partner_create_payout_request(
text("UPDATE users SET partner_balance = :balance WHERE id = :id"),
{"balance": new_balance, "id": int(user_id)},
)
await session.commit()
return PartnerPayoutRequestResponse(
ok=True,
message="Заявка на вывод создана",
@@ -577,7 +575,7 @@ async def get_all_partners(
"method": partner[5] or None,
"referred_count": int(partner[6] or 0),
})
return JSONResponse(content={"total": total, "items": partners_list})
return ORJSONResponse(content={"total": total, "items": partners_list})
@router.get("/stats/all")
@@ -623,7 +621,7 @@ async def get_partners_stats(
"top_partner_tg_id": 0,
"top_partner_refs": 0,
}
return JSONResponse(content=stats)
return ORJSONResponse(content=stats)
@router.patch("/{tg_id}")
@@ -644,15 +642,14 @@ async def update_partner(
"""
)
result = await session.execute(stmt, {"tg_id": tg_id, "balance": balance, "percent": percent})
await session.commit()
if result.rowcount > 0:
return JSONResponse(
return ORJSONResponse(
content={"success": True, "message": f"Партнёр {tg_id} успешно обновлён"}, status_code=200
)
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
except Exception as e:
await session.rollback()
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
@router.get("/{tg_id}")
@@ -706,7 +703,7 @@ async def get_partner_data(
for row in invited_rows
],
}
return JSONResponse(content=response)
return ORJSONResponse(content=response)
@router.post("/{tg_id}/invited")
@@ -718,18 +715,18 @@ async def add_partner_invited(
):
"""Добавляет приглашённого пользователя партнёру."""
if joined_tg_id == tg_id:
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": "Нельзя привязать пользователя к самому себе"}, status_code=400
)
try:
partner_exists = await session.execute(text("SELECT 1 FROM users WHERE tg_id = :tg_id"), {"tg_id": tg_id})
if not partner_exists.scalar():
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
invited_exists = await session.execute(
text("SELECT 1 FROM users WHERE tg_id = :joined_tg_id"), {"joined_tg_id": joined_tg_id}
)
if not invited_exists.scalar():
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": "Приглашённый пользователь не найден"}, status_code=404
)
existing = await session.execute(
@@ -738,7 +735,7 @@ async def add_partner_invited(
)
existing_partner = existing.scalar()
if existing_partner is not None:
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": f"Пользователь уже привязан к партнёру {existing_partner}"},
status_code=409,
)
@@ -746,8 +743,7 @@ async def add_partner_invited(
text("INSERT INTO partners (partner_tg_id, joined_tg_id) VALUES (:partner_tg_id, :joined_tg_id)"),
{"partner_tg_id": tg_id, "joined_tg_id": joined_tg_id},
)
await session.commit()
return JSONResponse(
return ORJSONResponse(
content={
"success": True,
"message": "Приглашённый добавлен",
@@ -758,7 +754,7 @@ async def add_partner_invited(
)
except Exception as e:
await session.rollback()
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
@router.delete("/{tg_id}/invited/{joined_tg_id}")
@@ -774,9 +770,8 @@ async def delete_partner_invited(
text("DELETE FROM partners WHERE partner_tg_id = :partner_tg_id AND joined_tg_id = :joined_tg_id"),
{"partner_tg_id": tg_id, "joined_tg_id": joined_tg_id},
)
await session.commit()
if result.rowcount > 0:
return JSONResponse(
return ORJSONResponse(
content={
"success": True,
"message": "Приглашённый удалён",
@@ -785,12 +780,12 @@ async def delete_partner_invited(
},
status_code=200,
)
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": "Связка партнёр-приглашённый не найдена"}, status_code=404
)
except Exception as e:
await session.rollback()
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
@router.patch("/{tg_id}/percent")
@@ -803,7 +798,7 @@ async def update_partner_percent(
"""Обновляет персональный процент партнёра."""
normalized = _parse_percent(percent)
if normalized is None:
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": "Неверный процент. Допустимо 0-100 или 0.0-1.0"}, status_code=400
)
try:
@@ -811,15 +806,14 @@ async def update_partner_percent(
text("UPDATE users SET partner_percent = :percent, partner_percent_custom = true WHERE tg_id = :tg_id"),
{"tg_id": tg_id, "percent": normalized},
)
await session.commit()
if result.rowcount > 0:
return JSONResponse(
return ORJSONResponse(
content={"success": True, "message": "Процент обновлён", "percent": normalized}, status_code=200
)
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
except Exception as e:
await session.rollback()
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
@router.patch("/{tg_id}/balance")
@@ -833,22 +827,22 @@ async def update_partner_balance(
"""Изменяет баланс партнёрской программы."""
mode_normalized = (mode or "set").strip().lower()
if mode_normalized not in {"set", "add", "subtract"}:
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": "Неверный режим. Используйте set, add или subtract"}, status_code=400
)
try:
amount_val = float(amount)
except (TypeError, ValueError):
return JSONResponse(content={"success": False, "message": "Неверная сумма"}, status_code=400)
return ORJSONResponse(content={"success": False, "message": "Неверная сумма"}, status_code=400)
if amount_val < 0:
return JSONResponse(content={"success": False, "message": "Сумма не может быть отрицательной"}, status_code=400)
return ORJSONResponse(content={"success": False, "message": "Сумма не может быть отрицательной"}, status_code=400)
try:
current_res = await session.execute(
text("SELECT partner_balance FROM users WHERE tg_id = :tg_id"), {"tg_id": tg_id}
)
current_balance = current_res.scalar()
if current_balance is None:
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
current_balance = float(current_balance or 0.0)
if mode_normalized == "set":
new_balance = amount_val
@@ -856,19 +850,18 @@ async def update_partner_balance(
new_balance = current_balance + amount_val
else:
if current_balance < amount_val:
return JSONResponse(content={"success": False, "message": "Недостаточно средств"}, status_code=400)
return ORJSONResponse(content={"success": False, "message": "Недостаточно средств"}, status_code=400)
new_balance = current_balance - amount_val
await session.execute(
text("UPDATE users SET partner_balance = :balance WHERE tg_id = :tg_id"),
{"tg_id": tg_id, "balance": new_balance},
)
await session.commit()
return JSONResponse(
return ORJSONResponse(
content={"success": True, "message": "Баланс обновлён", "balance": new_balance}, status_code=200
)
except Exception as e:
await session.rollback()
return JSONResponse(content={"success": False, "message": str(e)}, status_code=500)
return ORJSONResponse(content={"success": False, "message": str(e)}, status_code=500)
@router.get("/{tg_id}/invited")
@@ -901,7 +894,7 @@ async def get_partner_invited(
}
for row in invited_rows
]
return JSONResponse(content=invited_list)
return ORJSONResponse(content=invited_list)
@router.get("/payouts/pending")
@@ -944,7 +937,7 @@ async def get_partner_payouts_pending(
}
for row in result.fetchall()
]
return JSONResponse(content={"total": int(total), "items": items})
return ORJSONResponse(content={"total": int(total), "items": items})
@router.get("/payouts/history")
@@ -987,7 +980,7 @@ async def get_partner_payouts_history(
}
for row in result.fetchall()
]
return JSONResponse(content={"total": int(total), "items": items})
return ORJSONResponse(content={"total": int(total), "items": items})
@router.post("/payouts/{payout_id}/approve")
@@ -1003,7 +996,7 @@ async def approve_partner_payout(
)
req = req_row.fetchone()
if not req:
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": "Заявка не найдена или уже обработана"}, status_code=404
)
user_row = await session.execute(
@@ -1019,8 +1012,7 @@ async def approve_partner_payout(
),
{"id": payout_id, "method": payout_method, "destination": destination},
)
await session.commit()
return JSONResponse(content={"success": True, "message": "Заявка одобрена"}, status_code=200)
return ORJSONResponse(content={"success": True, "message": "Заявка одобрена"}, status_code=200)
@router.post("/payouts/{payout_id}/reject")
@@ -1036,7 +1028,7 @@ async def reject_partner_payout(
)
req = req_row.fetchone()
if not req:
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": "Заявка не найдена или уже обработана"}, status_code=404
)
user_row = await session.execute(
@@ -1058,8 +1050,7 @@ async def reject_partner_payout(
text("UPDATE users SET partner_balance = :balance WHERE tg_id = :tg_id"),
{"balance": current_balance + float(req[2] or 0.0), "tg_id": req[1]},
)
await session.commit()
return JSONResponse(content={"success": True, "message": "Заявка отклонена"}, status_code=200)
return ORJSONResponse(content={"success": True, "message": "Заявка отклонена"}, status_code=200)
@router.patch("/{tg_id}/percent/reset")
@@ -1073,10 +1064,9 @@ async def reset_partner_percent(
text("UPDATE users SET partner_percent = NULL, partner_percent_custom = false WHERE tg_id = :tg_id"),
{"tg_id": tg_id},
)
await session.commit()
if result.rowcount > 0:
return JSONResponse(content={"success": True, "message": "Процент сброшен"}, status_code=200)
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
return ORJSONResponse(content={"success": True, "message": "Процент сброшен"}, status_code=200)
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
@router.patch("/{tg_id}/code")
@@ -1089,23 +1079,22 @@ async def update_partner_code(
"""Обновляет код партнёрской ссылки."""
raw = (code or "").strip().lower()
if not raw:
return JSONResponse(content={"success": False, "message": "Код не может быть пустым"}, status_code=400)
return ORJSONResponse(content={"success": False, "message": "Код не может быть пустым"}, status_code=400)
if not re.fullmatch(r"[a-z0-9_]{3,32}", raw):
return JSONResponse(
return ORJSONResponse(
content={"success": False, "message": "Неверный код. Разрешены a-z, 0-9, _ (3-32 символа)"}, status_code=400
)
exists = await session.execute(
text("SELECT 1 FROM users WHERE partner_code = :code AND tg_id != :tg_id"), {"code": raw, "tg_id": tg_id}
)
if exists.first():
return JSONResponse(content={"success": False, "message": "Такой код уже занят"}, status_code=409)
return ORJSONResponse(content={"success": False, "message": "Такой код уже занят"}, status_code=409)
result = await session.execute(
text("UPDATE users SET partner_code = :code WHERE tg_id = :tg_id"), {"code": raw, "tg_id": tg_id}
)
await session.commit()
if result.rowcount > 0:
return JSONResponse(content={"success": True, "message": "Код обновлён", "code": raw}, status_code=200)
return JSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
return ORJSONResponse(content={"success": True, "message": "Код обновлён", "code": raw}, status_code=200)
return ORJSONResponse(content={"success": False, "message": "Партнёр не найден"}, status_code=404)
@router.post("/reset-disabled-methods")
@@ -1138,12 +1127,11 @@ async def reset_disabled_payout_methods(
if not ENABLE_PAYOUT_SBP and B:
disabled.append(B.METHOD_SBP)
if not disabled:
return JSONResponse(content={"success": True, "message": "Отключённых методов нет"}, status_code=200)
return ORJSONResponse(content={"success": True, "message": "Отключённых методов нет"}, status_code=200)
await session.execute(
text("UPDATE users SET card_number = NULL WHERE payout_method = ANY(:methods)"), {"methods": disabled}
)
await session.commit()
return JSONResponse(content={"success": True, "message": "Отключённые методы сброшены"}, status_code=200)
return ORJSONResponse(content={"success": True, "message": "Отключённые методы сброшены"}, status_code=200)
@router.get("/{tg_id}/export")
@@ -1159,7 +1147,7 @@ async def export_partner_invites_csv(
)
data = rows.fetchall()
if not data:
return JSONResponse(content={"success": False, "message": "Нет приглашённых"}, status_code=404)
return ORJSONResponse(content={"success": False, "message": "Нет приглашённых"}, status_code=404)
buffer = StringIO()
writer = csv.writer(buffer, delimiter=";")
writer.writerow(["joined_tg_id", "created_at"])
-2
View File
@@ -120,7 +120,6 @@ async def upsert_setting(
value=payload.value,
description=payload.description,
)
await session.commit()
await session.refresh(obj)
settings_cache.update(
key,
@@ -144,6 +143,5 @@ async def delete_setting(
if not obj:
raise HTTPException(status_code=404, detail="Setting not found")
await session.delete(obj)
await session.commit()
settings_cache.delete(key)
return {"detail": "Setting deleted"}
-1
View File
@@ -302,7 +302,6 @@ async def purchase_tariff_with_balance(
)
if coupon_id is not None:
await mark_coupon_used(session, int(coupon_id), int(tg_id))
await session.commit()
except Exception:
logger.exception("web tariff purchase failed")
raise HTTPException(status_code=500, detail="Не удалось оформить подписку") from None
+41
View File
@@ -40,6 +40,10 @@ from logger import logger
UPLOAD_DIR = Path("static/web_uploads")
ALLOWED_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".mp4", ".webm"})
MAX_FILE_SIZE = 100 * 1024 * 1024
_IMAGE_RESIZE_EXTENSIONS = frozenset({".png", ".jpg", ".jpeg", ".webp"})
_IMAGE_MAX_SIDE = 2048
_IMAGE_JPEG_QUALITY = 85
_IMAGE_WEBP_QUALITY = 85
_SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9\-]*$")
@@ -55,6 +59,39 @@ EXTENSION_CONTENT_TYPES: dict[str, frozenset[str]] = {
}
def _optimize_image_bytes(data: bytes, ext: str) -> bytes:
"""Уменьшает большие картинки до _IMAGE_MAX_SIDE и пережимает с разумным качеством."""
try:
from io import BytesIO
from PIL import Image, ImageOps
with Image.open(BytesIO(data)) as img:
img = ImageOps.exif_transpose(img)
w, h = img.size
if max(w, h) <= _IMAGE_MAX_SIDE and len(data) < 500_000:
return data
if max(w, h) > _IMAGE_MAX_SIDE:
img.thumbnail((_IMAGE_MAX_SIDE, _IMAGE_MAX_SIDE), Image.Resampling.LANCZOS)
buffer = BytesIO()
save_kwargs: dict = {}
if ext in (".jpg", ".jpeg"):
if img.mode not in ("RGB", "L"):
img = img.convert("RGB")
save_kwargs = {"format": "JPEG", "quality": _IMAGE_JPEG_QUALITY, "optimize": True, "progressive": True}
elif ext == ".webp":
save_kwargs = {"format": "WEBP", "quality": _IMAGE_WEBP_QUALITY, "method": 6}
elif ext == ".png":
save_kwargs = {"format": "PNG", "optimize": True}
else:
return data
img.save(buffer, **save_kwargs)
optimized = buffer.getvalue()
return optimized if len(optimized) < len(data) else data
except Exception:
return data
def _sanitize_svg(data: bytes) -> bytes:
import re as _re
@@ -529,6 +566,10 @@ async def upload_media(
file_data = b"".join(chunks)
if ext == ".svg":
file_data = _sanitize_svg(file_data)
elif ext in _IMAGE_RESIZE_EXTENSIONS:
from core.executor import run_cpu
file_data = await run_cpu(_optimize_image_bytes, file_data, ext)
with open(path, "wb") as f:
f.write(file_data)
url = f"/api/web/uploads/{name}"