mirror of
https://github.com/wowlikon/LiB.git
synced 2026-03-21 23:53:38 +00:00
исправление обработки 404
This commit is contained in:
+1
-18
@@ -11,7 +11,7 @@ services:
|
|||||||
- ./data/db:/var/lib/postgresql/data
|
- ./data/db:/var/lib/postgresql/data
|
||||||
networks:
|
networks:
|
||||||
- proxy
|
- proxy
|
||||||
ports: # !сменить внешний порт перед использованием!
|
ports: # !только локальный тест!
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
env_file:
|
env_file:
|
||||||
- ./.env
|
- ./.env
|
||||||
@@ -31,23 +31,6 @@ services:
|
|||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
replication-setup:
|
|
||||||
image: postgres:17-alpine
|
|
||||||
container_name: replication-setup
|
|
||||||
restart: "no"
|
|
||||||
networks:
|
|
||||||
- proxy
|
|
||||||
env_file:
|
|
||||||
- ./.env
|
|
||||||
volumes:
|
|
||||||
- ./setup-replication.sh:/setup-replication.sh
|
|
||||||
entrypoint: ["/bin/sh", "/setup-replication.sh"]
|
|
||||||
depends_on:
|
|
||||||
api:
|
|
||||||
condition: service_started
|
|
||||||
db:
|
|
||||||
condition: service_healthy
|
|
||||||
|
|
||||||
llm:
|
llm:
|
||||||
image: ollama/ollama:latest
|
image: ollama/ollama:latest
|
||||||
container_name: llm
|
container_name: llm
|
||||||
|
|||||||
@@ -4,9 +4,6 @@ POSTGRES_PORT=5432
|
|||||||
POSTGRES_USER=postgres
|
POSTGRES_USER=postgres
|
||||||
POSTGRES_PASSWORD=postgres
|
POSTGRES_PASSWORD=postgres
|
||||||
POSTGRES_DB=lib
|
POSTGRES_DB=lib
|
||||||
REMOTE_HOST=
|
|
||||||
REMOTE_PORT=
|
|
||||||
NODE_ID=
|
|
||||||
|
|
||||||
# Ollama
|
# Ollama
|
||||||
OLLAMA_URL="http://llm:11434"
|
OLLAMA_URL="http://llm:11434"
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
CREATE PUBLICATION all_tables_pub FOR ALL TABLES;
|
|
||||||
|
|
||||||
ALTER SYSTEM SET password_encryption = 'scram-sha-256';
|
|
||||||
SELECT pg_reload_conf();
|
|
||||||
+12
-7
@@ -73,15 +73,20 @@ app = get_app(lifespan)
|
|||||||
|
|
||||||
@app.exception_handler(status.HTTP_404_NOT_FOUND)
|
@app.exception_handler(status.HTTP_404_NOT_FOUND)
|
||||||
async def custom_not_found_handler(request: Request, exc: HTTPException):
|
async def custom_not_found_handler(request: Request, exc: HTTPException):
|
||||||
path = request.url.path
|
if exc.detail == "Not Found":
|
||||||
|
path = request.url.path
|
||||||
|
|
||||||
if path.startswith("/api"):
|
if path.startswith("/api/"):
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
status_code=status.HTTP_404_NOT_FOUND,
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
content={"detail": "API endpoint not found", "path": path},
|
content={"detail": "API endpoint not found", "path": path},
|
||||||
)
|
)
|
||||||
|
return await unknown(request, app)
|
||||||
|
|
||||||
return await unknown(request, app)
|
return JSONResponse(
|
||||||
|
status_code=exc.status_code,
|
||||||
|
content={"detail": exc.detail},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
|
|||||||
@@ -287,7 +287,8 @@ def enable_2fa(
|
|||||||
|
|
||||||
if not current_user.totp_secret:
|
if not current_user.totp_secret:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Secret key not generated"
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Secret key not generated",
|
||||||
)
|
)
|
||||||
|
|
||||||
if not verify_totp_code(secret, data.code):
|
if not verify_totp_code(secret, data.code):
|
||||||
@@ -299,7 +300,8 @@ def enable_2fa(
|
|||||||
decrypted = cipher.decrypt(base64.b64decode(current_user.totp_secret.encode()))
|
decrypted = cipher.decrypt(base64.b64decode(current_user.totp_secret.encode()))
|
||||||
if secret != decrypted.decode():
|
if secret != decrypted.decode():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Incorret secret"
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Incorret secret",
|
||||||
)
|
)
|
||||||
|
|
||||||
current_user.is_2fa_enabled = True
|
current_user.is_2fa_enabled = True
|
||||||
|
|||||||
@@ -67,7 +67,8 @@ def get_author(
|
|||||||
author = session.get(Author, author_id)
|
author = session.get(Author, author_id)
|
||||||
if not author:
|
if not author:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Author not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
books = session.exec(
|
books = session.exec(
|
||||||
@@ -98,7 +99,8 @@ def update_author(
|
|||||||
db_author = session.get(Author, author_id)
|
db_author = session.get(Author, author_id)
|
||||||
if not db_author:
|
if not db_author:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Author not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
update_data = author.model_dump(exclude_unset=True)
|
update_data = author.model_dump(exclude_unset=True)
|
||||||
@@ -125,7 +127,8 @@ def delete_author(
|
|||||||
author = session.get(Author, author_id)
|
author = session.get(Author, author_id)
|
||||||
if not author:
|
if not author:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Author not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Author not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
author_read = AuthorRead(**author.model_dump())
|
author_read = AuthorRead(**author.model_dump())
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
"""Модуль работы с книгами"""
|
"""Модуль работы с книгами"""
|
||||||
from library_service.services import transcode_image
|
|
||||||
import shutil
|
|
||||||
from uuid import uuid4
|
|
||||||
|
|
||||||
from pydantic import Field
|
|
||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
|
from uuid import uuid4
|
||||||
from sqlalchemy.orm import selectinload, defer
|
import shutil
|
||||||
|
|
||||||
from sqlalchemy import text, case, distinct
|
|
||||||
|
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status, UploadFile, File
|
from fastapi import APIRouter, Depends, HTTPException, Path, Query, status, UploadFile, File
|
||||||
from ollama import Client
|
from ollama import Client
|
||||||
|
from pydantic import Field
|
||||||
|
from sqlalchemy import text, case, distinct
|
||||||
|
from sqlalchemy.orm import selectinload, defer
|
||||||
from sqlmodel import Session, select, col, func
|
from sqlmodel import Session, select, col, func
|
||||||
|
|
||||||
from library_service.auth import RequireStaff
|
from library_service.auth import RequireStaff
|
||||||
|
from library_service.services import transcode_image
|
||||||
from library_service.settings import get_session, OLLAMA_URL, BOOKS_PREVIEW_DIR
|
from library_service.settings import get_session, OLLAMA_URL, BOOKS_PREVIEW_DIR
|
||||||
from library_service.models.enums import BookStatus
|
from library_service.models.enums import BookStatus
|
||||||
from library_service.models.db import (
|
from library_service.models.db import (
|
||||||
@@ -139,7 +136,8 @@ def create_book(
|
|||||||
session.commit()
|
session.commit()
|
||||||
session.refresh(db_book)
|
session.refresh(db_book)
|
||||||
|
|
||||||
book_data = db_book.model_dump(exclude={"embedding", "preview_id"})
|
book_dict = {k: v for k, v in db_book.__dict__.items() if not k.startswith('_')}
|
||||||
|
book_data = {k: v for k, v in book_dict.items() if k not in {"embedding", "preview_id"}}
|
||||||
book_data["preview_urls"] = {
|
book_data["preview_urls"] = {
|
||||||
"png": f"/static/books/{db_book.preview_id}.png",
|
"png": f"/static/books/{db_book.preview_id}.png",
|
||||||
"jpeg": f"/static/books/{db_book.preview_id}.jpg",
|
"jpeg": f"/static/books/{db_book.preview_id}.jpg",
|
||||||
@@ -160,12 +158,17 @@ def read_books(session: Session = Depends(get_session)):
|
|||||||
|
|
||||||
books_data = []
|
books_data = []
|
||||||
for book in books:
|
for book in books:
|
||||||
book_data = book.model_dump(exclude={"embedding", "preview_id"})
|
book = book[0]
|
||||||
|
book_dict = dict(book)
|
||||||
|
|
||||||
|
book_data = {k: v for k, v in book_dict.items() if k not in {"embedding", "preview_id"}}
|
||||||
|
|
||||||
book_data["preview_urls"] = {
|
book_data["preview_urls"] = {
|
||||||
"png": f"/static/books/{book.preview_id}.png",
|
"png": f"/static/books/{book.preview_id}.png",
|
||||||
"jpeg": f"/static/books/{book.preview_id}.jpg",
|
"jpeg": f"/static/books/{book.preview_id}.jpg",
|
||||||
"webp": f"/static/books/{book.preview_id}.webp",
|
"webp": f"/static/books/{book.preview_id}.webp",
|
||||||
} if book.preview_id else {}
|
} if book.preview_id else {}
|
||||||
|
|
||||||
books_data.append(book_data)
|
books_data.append(book_data)
|
||||||
|
|
||||||
return BookList(
|
return BookList(
|
||||||
@@ -188,7 +191,8 @@ def get_book(
|
|||||||
book = session.get(Book, book_id)
|
book = session.get(Book, book_id)
|
||||||
if not book:
|
if not book:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Book not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
authors = session.scalars(
|
authors = session.scalars(
|
||||||
@@ -203,7 +207,8 @@ def get_book(
|
|||||||
|
|
||||||
genre_reads = [GenreRead(**genre.model_dump()) for genre in genres]
|
genre_reads = [GenreRead(**genre.model_dump()) for genre in genres]
|
||||||
|
|
||||||
book_data = book.model_dump(exclude={"embedding", "preview_id"})
|
book_dict = {k: v for k, v in book.__dict__.items() if not k.startswith('_')}
|
||||||
|
book_data = {k: v for k, v in book_dict.items() if k not in {"embedding", "preview_id"}}
|
||||||
book_data["preview_urls"] = {
|
book_data["preview_urls"] = {
|
||||||
"png": f"/static/books/{book.preview_id}.png",
|
"png": f"/static/books/{book.preview_id}.png",
|
||||||
"jpeg": f"/static/books/{book.preview_id}.jpg",
|
"jpeg": f"/static/books/{book.preview_id}.jpg",
|
||||||
@@ -231,7 +236,8 @@ def update_book(
|
|||||||
db_book = session.get(Book, book_id)
|
db_book = session.get(Book, book_id)
|
||||||
if not db_book:
|
if not db_book:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Book not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
if book_update.status is not None:
|
if book_update.status is not None:
|
||||||
@@ -292,7 +298,8 @@ def delete_book(
|
|||||||
book = session.get(Book, book_id)
|
book = session.get(Book, book_id)
|
||||||
if not book:
|
if not book:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Book not found",
|
||||||
)
|
)
|
||||||
book_read = BookRead(
|
book_read = BookRead(
|
||||||
id=(book.id or 0),
|
id=(book.id or 0),
|
||||||
@@ -313,10 +320,16 @@ async def upload_book_preview(
|
|||||||
session: Session = Depends(get_session)
|
session: Session = Depends(get_session)
|
||||||
):
|
):
|
||||||
if not (file.content_type or "").startswith("image/"):
|
if not (file.content_type or "").startswith("image/"):
|
||||||
raise HTTPException(status.HTTP_415_UNSUPPORTED_MEDIA_TYPE, "Image required")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_415_UNSUPPORTED_MEDIA_TYPE,
|
||||||
|
detail="Image required",
|
||||||
|
)
|
||||||
|
|
||||||
if (file.size or 0) > 32 * 1024 * 1024:
|
if (file.size or 0) > 32 * 1024 * 1024:
|
||||||
raise HTTPException(status.HTTP_413_REQUEST_ENTITY_TOO_LARGE, "File larger than 10 MB")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||||
|
detail="File larger than 10 MB",
|
||||||
|
)
|
||||||
|
|
||||||
file_uuid= uuid4()
|
file_uuid= uuid4()
|
||||||
tmp_path = BOOKS_PREVIEW_DIR / f"{file_uuid}.upload"
|
tmp_path = BOOKS_PREVIEW_DIR / f"{file_uuid}.upload"
|
||||||
@@ -327,7 +340,10 @@ async def upload_book_preview(
|
|||||||
book = session.get(Book, book_id)
|
book = session.get(Book, book_id)
|
||||||
if not book:
|
if not book:
|
||||||
tmp_path.unlink()
|
tmp_path.unlink()
|
||||||
raise HTTPException(status.HTTP_404_NOT_FOUND, "Book not found")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Book not found",
|
||||||
|
)
|
||||||
|
|
||||||
transcode_image(tmp_path)
|
transcode_image(tmp_path)
|
||||||
tmp_path.unlink()
|
tmp_path.unlink()
|
||||||
|
|||||||
@@ -27,11 +27,13 @@ async def challenge(request: Request, ip: str = Depends(get_ip)):
|
|||||||
"""Возвращает задачу capjs"""
|
"""Возвращает задачу capjs"""
|
||||||
if challenges_by_ip[ip] >= MAX_CHALLENGES_PER_IP:
|
if challenges_by_ip[ip] >= MAX_CHALLENGES_PER_IP:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_429_TOO_MANY_REQUESTS, detail="Too many challenges"
|
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
|
||||||
|
detail="Too many challenges",
|
||||||
)
|
)
|
||||||
if len(active_challenges) >= MAX_TOTAL_CHALLENGES:
|
if len(active_challenges) >= MAX_TOTAL_CHALLENGES:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE, detail="Server busy"
|
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||||
|
detail="Server busy",
|
||||||
)
|
)
|
||||||
|
|
||||||
token = secrets.token_hex(25)
|
token = secrets.token_hex(25)
|
||||||
@@ -60,17 +62,22 @@ async def redeem(request: Request, payload: dict, ip: str = Depends(get_ip)):
|
|||||||
|
|
||||||
if token not in active_challenges:
|
if token not in active_challenges:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Invalid challenge"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Invalid challenge",
|
||||||
)
|
)
|
||||||
|
|
||||||
ch = active_challenges.pop(token)
|
ch = active_challenges.pop(token)
|
||||||
challenges_by_ip[ch["ip"]] -= 1
|
challenges_by_ip[ch["ip"]] -= 1
|
||||||
|
|
||||||
if now_ms() > ch["expires"]:
|
if now_ms() > ch["expires"]:
|
||||||
raise HTTPException(status_code=status.HTTP_410_GONE, detail="Expired")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_410_GONE,
|
||||||
|
detail="Expired"
|
||||||
|
)
|
||||||
if len(solutions) < ch["c"]:
|
if len(solutions) < ch["c"]:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Bad solutions"
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Bad solutions",
|
||||||
)
|
)
|
||||||
|
|
||||||
def verify(i: int) -> bool:
|
def verify(i: int) -> bool:
|
||||||
@@ -84,7 +91,8 @@ async def redeem(request: Request, payload: dict, ip: str = Depends(get_ip)):
|
|||||||
)
|
)
|
||||||
if not all(results):
|
if not all(results):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid solution"
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid solution",
|
||||||
)
|
)
|
||||||
|
|
||||||
r_token = ch["redeem_token"]
|
r_token = ch["redeem_token"]
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ def get_genre(
|
|||||||
genre = session.get(Genre, genre_id)
|
genre = session.get(Genre, genre_id)
|
||||||
if not genre:
|
if not genre:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Genre not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
books = session.exec(
|
books = session.exec(
|
||||||
@@ -97,7 +98,8 @@ def update_genre(
|
|||||||
db_genre = session.get(Genre, genre_id)
|
db_genre = session.get(Genre, genre_id)
|
||||||
if not db_genre:
|
if not db_genre:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Genre not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
update_data = genre.model_dump(exclude_unset=True)
|
update_data = genre.model_dump(exclude_unset=True)
|
||||||
@@ -124,7 +126,8 @@ def delete_genre(
|
|||||||
genre = session.get(Genre, genre_id)
|
genre = session.get(Genre, genre_id)
|
||||||
if not genre:
|
if not genre:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Genre not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Genre not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
genre_read = GenreRead(**genre.model_dump())
|
genre_read = GenreRead(**genre.model_dump())
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ def create_loan(
|
|||||||
book = session.get(Book, loan.book_id)
|
book = session.get(Book, loan.book_id)
|
||||||
if not book:
|
if not book:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Book not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
if book.status != BookStatus.ACTIVE:
|
if book.status != BookStatus.ACTIVE:
|
||||||
@@ -53,7 +54,8 @@ def create_loan(
|
|||||||
target_user = session.get(User, loan.user_id)
|
target_user = session.get(User, loan.user_id)
|
||||||
if not target_user:
|
if not target_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
db_loan = BookUserLink(
|
db_loan = BookUserLink(
|
||||||
@@ -253,14 +255,16 @@ def get_loan(
|
|||||||
|
|
||||||
if not loan:
|
if not loan:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Loan not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_staff = is_user_staff(current_user)
|
is_staff = is_user_staff(current_user)
|
||||||
|
|
||||||
if not is_staff and loan.user_id != current_user.id:
|
if not is_staff and loan.user_id != current_user.id:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to this loan"
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail="Access denied to this loan",
|
||||||
)
|
)
|
||||||
|
|
||||||
return LoanRead(**loan.model_dump())
|
return LoanRead(**loan.model_dump())
|
||||||
@@ -282,7 +286,8 @@ def update_loan(
|
|||||||
db_loan = session.get(BookUserLink, loan_id)
|
db_loan = session.get(BookUserLink, loan_id)
|
||||||
if not db_loan:
|
if not db_loan:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Loan not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_staff = is_user_staff(current_user)
|
is_staff = is_user_staff(current_user)
|
||||||
@@ -296,7 +301,8 @@ def update_loan(
|
|||||||
book = session.get(Book, db_loan.book_id)
|
book = session.get(Book, db_loan.book_id)
|
||||||
if not book:
|
if not book:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Book not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
if loan_update.user_id is not None:
|
if loan_update.user_id is not None:
|
||||||
@@ -308,7 +314,8 @@ def update_loan(
|
|||||||
new_user = session.get(User, loan_update.user_id)
|
new_user = session.get(User, loan_update.user_id)
|
||||||
if not new_user:
|
if not new_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found",
|
||||||
)
|
)
|
||||||
db_loan.user_id = loan_update.user_id
|
db_loan.user_id = loan_update.user_id
|
||||||
|
|
||||||
@@ -347,18 +354,21 @@ def confirm_loan(
|
|||||||
loan = session.get(BookUserLink, loan_id)
|
loan = session.get(BookUserLink, loan_id)
|
||||||
if not loan:
|
if not loan:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Loan not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
if loan.returned_at:
|
if loan.returned_at:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Loan is already returned"
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Loan is already returned",
|
||||||
)
|
)
|
||||||
|
|
||||||
book = session.get(Book, loan.book_id)
|
book = session.get(Book, loan.book_id)
|
||||||
if not book:
|
if not book:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Book not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
if book.status not in [BookStatus.RESERVED, BookStatus.ACTIVE]:
|
if book.status not in [BookStatus.RESERVED, BookStatus.ACTIVE]:
|
||||||
@@ -392,12 +402,14 @@ def return_loan(
|
|||||||
loan = session.get(BookUserLink, loan_id)
|
loan = session.get(BookUserLink, loan_id)
|
||||||
if not loan:
|
if not loan:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Loan not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
if loan.returned_at:
|
if loan.returned_at:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_400_BAD_REQUEST, detail="Loan is already returned"
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Loan is already returned",
|
||||||
)
|
)
|
||||||
|
|
||||||
loan.returned_at = datetime.now(timezone.utc)
|
loan.returned_at = datetime.now(timezone.utc)
|
||||||
@@ -429,7 +441,8 @@ def delete_loan(
|
|||||||
loan = session.get(BookUserLink, loan_id)
|
loan = session.get(BookUserLink, loan_id)
|
||||||
if not loan:
|
if not loan:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Loan not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Loan not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
is_staff = is_user_staff(current_user)
|
is_staff = is_user_staff(current_user)
|
||||||
@@ -499,7 +512,8 @@ def issue_book_directly(
|
|||||||
book = session.get(Book, loan.book_id)
|
book = session.get(Book, loan.book_id)
|
||||||
if not book:
|
if not book:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Book not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Book not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
if book.status != BookStatus.ACTIVE:
|
if book.status != BookStatus.ACTIVE:
|
||||||
@@ -511,7 +525,8 @@ def issue_book_directly(
|
|||||||
target_user = session.get(User, loan.user_id)
|
target_user = session.get(User, loan.user_id)
|
||||||
if not target_user:
|
if not target_user:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="User not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
db_loan = BookUserLink(
|
db_loan = BookUserLink(
|
||||||
|
|||||||
@@ -56,8 +56,14 @@ async def create_genre(request: Request, app=Depends(lambda: get_app())):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/genre/{genre_id}/edit", include_in_schema=False)
|
@router.get("/genre/{genre_id}/edit", include_in_schema=False)
|
||||||
async def edit_genre(request: Request, genre_id: int, app=Depends(lambda: get_app())):
|
async def edit_genre(request: Request, genre_id: str, app=Depends(lambda: get_app())):
|
||||||
"""Рендерит страницу редактирования жанра"""
|
"""Рендерит страницу редактирования жанра"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
assert int(genre_id) > 0
|
||||||
|
except:
|
||||||
|
return await unknown(request, app)
|
||||||
|
|
||||||
return templates.TemplateResponse(request, "edit_genre.html", get_info(app) | {"request": request, "title": "LiB - Редактировать жанр", "id": genre_id})
|
return templates.TemplateResponse(request, "edit_genre.html", get_info(app) | {"request": request, "title": "LiB - Редактировать жанр", "id": genre_id})
|
||||||
|
|
||||||
|
|
||||||
@@ -74,15 +80,32 @@ async def create_author(request: Request, app=Depends(lambda: get_app())):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/author/{author_id}/edit", include_in_schema=False)
|
@router.get("/author/{author_id}/edit", include_in_schema=False)
|
||||||
async def edit_author(request: Request, author_id: int, app=Depends(lambda: get_app())):
|
async def edit_author(request: Request, author_id: str, app=Depends(lambda: get_app()), session=Depends(get_session)):
|
||||||
"""Рендерит страницу редактирования автора"""
|
"""Рендерит страницу редактирования автора"""
|
||||||
return templates.TemplateResponse(request, "edit_author.html", get_info(app) | {"request": request, "title": "LiB - Редактировать автора", "id": author_id})
|
|
||||||
|
try:
|
||||||
|
author = session.get(Author, int(author_id))
|
||||||
|
assert author is not None
|
||||||
|
except:
|
||||||
|
return await unknown(request, app)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(request, "edit_author.html", get_info(app) | {"request": request, "title": f"LiB - Редактировать автора \"{author.name}\"", "id": author_id})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/author/{author_id}", include_in_schema=False)
|
@router.get("/author/{author_id}", include_in_schema=False)
|
||||||
async def author(request: Request, author_id: int, app=Depends(lambda: get_app())):
|
async def author(request: Request, author_id: str, app=Depends(lambda: get_app()), session=Depends(get_session)):
|
||||||
"""Рендерит страницу просмотра автора"""
|
"""Рендерит страницу просмотра автора"""
|
||||||
return templates.TemplateResponse(request, "author.html", get_info(app) | {"request": request, "title": "LiB - Автор", "id": author_id})
|
|
||||||
|
if author_id == "":
|
||||||
|
return RedirectResponse("/authors")
|
||||||
|
|
||||||
|
try:
|
||||||
|
author = session.get(Author, int(author_id))
|
||||||
|
assert author is not None
|
||||||
|
except:
|
||||||
|
return await unknown(request, app)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(request, "author.html", get_info(app) | {"request": request, "title": f"LiB - Автор \"{author.name}\"", "id": author_id})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/books", include_in_schema=False)
|
@router.get("/books", include_in_schema=False)
|
||||||
@@ -98,16 +121,32 @@ async def create_book(request: Request, app=Depends(lambda: get_app())):
|
|||||||
|
|
||||||
|
|
||||||
@router.get("/book/{book_id}/edit", include_in_schema=False)
|
@router.get("/book/{book_id}/edit", include_in_schema=False)
|
||||||
async def edit_book(request: Request, book_id: int, app=Depends(lambda: get_app())):
|
async def edit_book(request: Request, book_id: str, app=Depends(lambda: get_app()), session=Depends(get_session)):
|
||||||
"""Рендерит страницу редактирования книги"""
|
"""Рендерит страницу редактирования книги"""
|
||||||
return templates.TemplateResponse(request, "edit_book.html", get_info(app) | {"request": request, "title": "LiB - Редактировать книгу", "id": book_id})
|
|
||||||
|
try:
|
||||||
|
book = session.get(Book, int(book_id))
|
||||||
|
assert book is not None
|
||||||
|
except:
|
||||||
|
return await unknown(request, app)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(request, "edit_book.html", get_info(app) | {"request": request, "title": f"LiB - Редактировать книгу \"{book.title}\"", "id": book_id})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/book/{book_id}", include_in_schema=False)
|
@router.get("/book/{book_id}", include_in_schema=False)
|
||||||
async def book(request: Request, book_id: int, app=Depends(lambda: get_app()), session=Depends(get_session)):
|
async def book(request: Request, book_id: str, app=Depends(lambda: get_app()), session=Depends(get_session)):
|
||||||
"""Рендерит страницу просмотра книги"""
|
"""Рендерит страницу просмотра книги"""
|
||||||
book = session.get(Book, book_id)
|
|
||||||
return templates.TemplateResponse(request, "book.html", get_info(app) | {"request": request, "title": "LiB - Книга", "id": book_id, "img": book.preview_id})
|
if book_id == "":
|
||||||
|
return RedirectResponse("/books")
|
||||||
|
|
||||||
|
try:
|
||||||
|
book = session.get(Book, int(book_id))
|
||||||
|
assert book is not None
|
||||||
|
except:
|
||||||
|
return await unknown(request, app)
|
||||||
|
|
||||||
|
return templates.TemplateResponse(request, "book.html", get_info(app) | {"request": request, "title": f"LiB - Книга \"{book.title}\"", "id": book_id, "img": book.preview_id})
|
||||||
|
|
||||||
|
|
||||||
@router.get("/auth", include_in_schema=False)
|
@router.get("/auth", include_in_schema=False)
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ def check_entity_exists(session, model, entity_id, entity_name):
|
|||||||
entity = session.get(model, entity_id)
|
entity = session.get(model, entity_id)
|
||||||
if not entity:
|
if not entity:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail=f"{entity_name} not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail=f"{entity_name} not found",
|
||||||
)
|
)
|
||||||
return entity
|
return entity
|
||||||
|
|
||||||
@@ -33,7 +34,10 @@ def add_relationship(session, link_model, id1, field1, id2, field2, detail):
|
|||||||
).first()
|
).first()
|
||||||
|
|
||||||
if existing_link:
|
if existing_link:
|
||||||
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=detail)
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail=detail,
|
||||||
|
)
|
||||||
|
|
||||||
link = link_model(**{field1: id1, field2: id2})
|
link = link_model(**{field1: id1, field2: id2})
|
||||||
session.add(link)
|
session.add(link)
|
||||||
@@ -52,7 +56,8 @@ def remove_relationship(session, link_model, id1, field1, id2, field2):
|
|||||||
|
|
||||||
if not link:
|
if not link:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_404_NOT_FOUND, detail="Relationship not found"
|
status_code=status.HTTP_404_NOT_FOUND,
|
||||||
|
detail="Relationship not found",
|
||||||
)
|
)
|
||||||
|
|
||||||
session.delete(link)
|
session.delete(link)
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ async def require_captcha(request: Request):
|
|||||||
token = request.cookies.get("capjs_token")
|
token = request.cookies.get("capjs_token")
|
||||||
if not token or token not in redeem_tokens or redeem_tokens[token] < now_ms():
|
if not token or token not in redeem_tokens or redeem_tokens[token] < now_ms():
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=status.HTTP_403_FORBIDDEN, detail={"error": "captcha_required"}
|
status_code=status.HTTP_403_FORBIDDEN,
|
||||||
|
detail={"error": "captcha_required"},
|
||||||
)
|
)
|
||||||
del redeem_tokens[token]
|
del redeem_tokens[token]
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ $(document).ready(() => {
|
|||||||
|
|
||||||
Api.get(`/api/authors/${authorId}`)
|
Api.get(`/api/authors/${authorId}`)
|
||||||
.then((author) => {
|
.then((author) => {
|
||||||
document.title = `LiB - ${author.name}`;
|
|
||||||
renderAuthor(author);
|
renderAuthor(author);
|
||||||
renderBooks(author.books);
|
renderBooks(author.books);
|
||||||
if (window.canManage()) {
|
if (window.canManage()) {
|
||||||
|
|||||||
@@ -366,7 +366,6 @@ $(document).ready(() => {
|
|||||||
Api.get(`/api/books/${bookId}`)
|
Api.get(`/api/books/${bookId}`)
|
||||||
.then((book) => {
|
.then((book) => {
|
||||||
currentBook = book;
|
currentBook = book;
|
||||||
document.title = `LiB - ${book.title}`;
|
|
||||||
renderBook(book);
|
renderBook(book);
|
||||||
if (window.canManage()) {
|
if (window.canManage()) {
|
||||||
$("#edit-book-btn")
|
$("#edit-book-btn")
|
||||||
|
|||||||
@@ -32,7 +32,6 @@ $(document).ready(() => {
|
|||||||
originalAuthor = author;
|
originalAuthor = author;
|
||||||
authorBooks = booksData.books || booksData || [];
|
authorBooks = booksData.books || booksData || [];
|
||||||
|
|
||||||
document.title = `Редактирование: ${author.name} | LiB`;
|
|
||||||
populateForm(author);
|
populateForm(author);
|
||||||
renderAuthorBooks(authorBooks);
|
renderAuthorBooks(authorBooks);
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ $(document).ready(() => {
|
|||||||
currentGenres.set(g.id, g.name),
|
currentGenres.set(g.id, g.name),
|
||||||
);
|
);
|
||||||
|
|
||||||
document.title = `Редактирование: ${book.title} | LiB`;
|
|
||||||
populateForm(book);
|
populateForm(book);
|
||||||
initAuthorsDropdown();
|
initAuthorsDropdown();
|
||||||
initGenresDropdown();
|
initGenresDropdown();
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ $(document).ready(() => {
|
|||||||
originalGenre = genre;
|
originalGenre = genre;
|
||||||
genreBooks = booksData.books || booksData || [];
|
genreBooks = booksData.books || booksData || [];
|
||||||
|
|
||||||
document.title = `Редактирование: ${genre.name} | LiB`;
|
|
||||||
populateForm(genre);
|
populateForm(genre);
|
||||||
renderGenreBooks(genreBooks);
|
renderGenreBooks(genreBooks);
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ $(document).ready(() => {
|
|||||||
Api.get("/api/auth/recovery-codes/status").catch(() => null),
|
Api.get("/api/auth/recovery-codes/status").catch(() => null),
|
||||||
])
|
])
|
||||||
.then(async ([user, rolesData, recoveryStatus]) => {
|
.then(async ([user, rolesData, recoveryStatus]) => {
|
||||||
document.title = `LiB - ${user.full_name || user.username}`;
|
document.title = `LiB - Профиль ${user.full_name || user.username}`;
|
||||||
currentUsername = user.username;
|
currentUsername = user.username;
|
||||||
|
|
||||||
await renderProfileHeader(user);
|
await renderProfileHeader(user);
|
||||||
|
|||||||
@@ -2,10 +2,15 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<title id="pageTitle">Loading...</title>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta property="og:title" content="LiB - API Документация" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:description" content="Ваша персональная библиотека книг" />
|
||||||
|
<meta property="og:url" content="{{ request.url.scheme }}://{{ domain }}/" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/dagre/0.8.5/dagre.min.js"></script>
|
||||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.6/js/jsplumb.min.js"></script>
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/jsPlumb/2.15.6/js/jsplumb.min.js"></script>
|
||||||
|
<title id="pageTitle">Loading...</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo "=== Настройка репликации ==="
|
|
||||||
echo "Этот узел: NODE_ID=${NODE_ID}"
|
|
||||||
echo "Удаленный хост: ${REMOTE_HOST}"
|
|
||||||
|
|
||||||
echo "Ждем локальную базу..."
|
|
||||||
sleep 10
|
|
||||||
|
|
||||||
until PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c '\q' 2>/dev/null; do
|
|
||||||
echo "Локальная база не готова, ждем..."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "Локальная база готова"
|
|
||||||
|
|
||||||
echo "Настройка генераторов ID (NODE_ID=${NODE_ID})..."
|
|
||||||
|
|
||||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
|
|
||||||
DO \$\$
|
|
||||||
DECLARE
|
|
||||||
r RECORD;
|
|
||||||
BEGIN
|
|
||||||
FOR r IN
|
|
||||||
SELECT table_schema, table_name, column_name
|
|
||||||
FROM information_schema.columns
|
|
||||||
WHERE is_identity = 'YES'
|
|
||||||
AND table_schema = 'public'
|
|
||||||
LOOP
|
|
||||||
EXECUTE format(
|
|
||||||
'ALTER TABLE %I.%I ALTER COLUMN %I SET GENERATED BY DEFAULT AS IDENTITY (START WITH %s INCREMENT BY 2)',
|
|
||||||
r.table_schema, r.table_name, r.column_name, ${NODE_ID}
|
|
||||||
);
|
|
||||||
RAISE NOTICE 'Настроен ID для %.%', r.table_name, r.column_name;
|
|
||||||
END LOOP;
|
|
||||||
END \$\$;
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Проверяем/создаем публикацию..."
|
|
||||||
|
|
||||||
PUB_EXISTS=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -tAc "SELECT COUNT(*) FROM pg_publication WHERE pubname = 'all_tables_pub';")
|
|
||||||
|
|
||||||
if [ "$PUB_EXISTS" -gt 0 ]; then
|
|
||||||
echo "Публикация уже существует"
|
|
||||||
else
|
|
||||||
echo "Создаем публикацию..."
|
|
||||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
|
|
||||||
CREATE PUBLICATION all_tables_pub FOR ALL TABLES;
|
|
||||||
EOF
|
|
||||||
echo "Публикация создана!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Ждем удаленный хост ${REMOTE_HOST}:${REMOTE_PORT}..."
|
|
||||||
TIMEOUT=300
|
|
||||||
ELAPSED=0
|
|
||||||
|
|
||||||
while ! PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${REMOTE_HOST}" -p ${REMOTE_PORT} -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -c '\q' 2>/dev/null; do
|
|
||||||
sleep 5
|
|
||||||
ELAPSED=$((ELAPSED + 5))
|
|
||||||
if [ $ELAPSED -ge $TIMEOUT ]; then
|
|
||||||
echo "Таймаут ожидания удаленного хоста. Подписка НЕ настроена."
|
|
||||||
echo "Публикация создана - удаленный хост сможет подписаться на нас."
|
|
||||||
echo "Для создания подписки запустите позже:"
|
|
||||||
echo "docker compose restart replication-setup"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
echo "Удаленный хост недоступен, ждем... (${ELAPSED}s/${TIMEOUT}s)"
|
|
||||||
done
|
|
||||||
echo "Удаленный хост доступен"
|
|
||||||
|
|
||||||
REMOTE_PUB=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${REMOTE_HOST}" -p ${REMOTE_PORT} -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -tAc "SELECT COUNT(*) FROM pg_publication WHERE pubname = 'all_tables_pub';" 2>/dev/null || echo "0")
|
|
||||||
|
|
||||||
if [ "$REMOTE_PUB" -eq 0 ]; then
|
|
||||||
echo "ВНИМАНИЕ: На удалённом хосте нет публикации 'all_tables_pub'!"
|
|
||||||
echo "Подписка не будет создана. Сначала запустите скрипт на удалённом хосте."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
EXISTING=$(PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" -tAc "SELECT COUNT(*) FROM pg_subscription WHERE subname = 'sub_from_remote';")
|
|
||||||
|
|
||||||
if [ "$EXISTING" -gt 0 ]; then
|
|
||||||
echo "Подписка уже существует, пропускаем создание"
|
|
||||||
else
|
|
||||||
echo "Создаем подписку на удаленный хост..."
|
|
||||||
|
|
||||||
PGPASSWORD="${POSTGRES_PASSWORD}" psql -h db -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <<EOF
|
|
||||||
CREATE SUBSCRIPTION sub_from_remote
|
|
||||||
CONNECTION 'host=${REMOTE_HOST} port=${REMOTE_PORT} user=${POSTGRES_USER} password=${POSTGRES_PASSWORD} dbname=${POSTGRES_DB}'
|
|
||||||
PUBLICATION all_tables_pub
|
|
||||||
WITH (
|
|
||||||
origin = none
|
|
||||||
);
|
|
||||||
EOF
|
|
||||||
|
|
||||||
echo "Подписка создана!"
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "=== Репликация настроена! ==="
|
|
||||||
echo "Публикация: all_tables_pub (другие могут подписаться на нас)"
|
|
||||||
echo "Подписка: sub_from_remote (мы получаем данные от ${REMOTE_HOST})"
|
|
||||||
Reference in New Issue
Block a user