diff --git a/.env.example b/.env.example index 5266799..f35f847 100644 --- a/.env.example +++ b/.env.example @@ -36,4 +36,32 @@ USE_ETAGS=True PRAGMA_HEADER= # Wartość nagłówka X-Robots-Tag, gdy BLOCK_BOTS=True -ROBOTS_TAG=noindex, nofollow, nosnippet, noarchive +ROBOTS_TAG="noindex, nofollow, nosnippet, noarchive" + + +# Rodzaj bazy: sqlite, pgsql, mysql +# Mozliwe wartosci: sqlite / pgsql / mysql +DB_ENGINE=sqlite + +# --- Konfiguracja dla sqlite --- +# Plik bazy bedzie utworzony automatycznie w katalogu ./instance +# Pozostale zmienne sa ignorowane przy DB_ENGINE=sqlite + +# --- Konfiguracja dla pgsql --- +# Ustaw DB_ENGINE=pgsql +# Domyslny port PostgreSQL to 5432 +# Wymaga dzialajacego serwera PostgreSQL (np. kontener `postgres`) + +# --- Konfiguracja dla mysql --- +# Ustaw DB_ENGINE=mysql +# Domyslny port MySQL to 3306 +# Wymaga kontenera z MySQL i uzytkownika z dostepem do bazy + +# Wspolne zmienne (dla pgsql, mysql) +# DB_HOST = pgsql lub mysql zgodnie z deployem (profil w docker-compose.yml) + +DB_HOST=pgsql +DB_PORT=5432 +DB_NAME=myapp +DB_USER=user +DB_PASSWORD=pass diff --git a/.gitignore b/.gitignore index f825246..2b3d7ec 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ venv/ .env version.txt deploy/varnish/default.vcl -*.tar.gz \ No newline at end of file +*.tar.gz +db/* \ No newline at end of file diff --git a/_tools/db/migrate.txt b/_tools/db/migrate.txt new file mode 100644 index 0000000..399ed5a --- /dev/null +++ b/_tools/db/migrate.txt @@ -0,0 +1,38 @@ +python3 -m venv venv_migrate +source venv_migrate/bin/activate +pip install sqlalchemy psycopg2-binary dotenv +docker compose --profile pgsql up -d --build +PYTHONPATH=. python3 _tools/db/migrate_sqlite_to_pgsql.py +rm -rf venv_migrate + +# reset wszystkich sekwencji w pgsql +docker exec -it pgsql-db psql -U zbiorki -d zbiorki + + +DO $$ +DECLARE + r RECORD; +BEGIN + FOR r IN + SELECT + c.relname AS seq_name, + t.relname AS table_name, + a.attname AS column_name + FROM + pg_class c + JOIN + pg_depend d ON d.objid = c.oid + JOIN + pg_class t ON d.refobjid = t.oid + JOIN + pg_attribute a ON a.attrelid = t.oid AND a.attnum = d.refobjsubid + WHERE + c.relkind = 'S' + AND d.deptype = 'a' + LOOP + EXECUTE format( + 'SELECT setval(%L, COALESCE((SELECT MAX(%I) FROM %I), 1), true)', + r.seq_name, r.column_name, r.table_name + ); + END LOOP; +END$$; \ No newline at end of file diff --git a/_tools/db/migrate_sqlite_to_pgsql.py b/_tools/db/migrate_sqlite_to_pgsql.py new file mode 100644 index 0000000..e49728d --- /dev/null +++ b/_tools/db/migrate_sqlite_to_pgsql.py @@ -0,0 +1,61 @@ +import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../../"))) + +from sqlalchemy import create_engine, MetaData +from sqlalchemy.orm import sessionmaker +from config import Config +from dotenv import load_dotenv +load_dotenv() + +# Źródło: SQLite +sqlite_engine = create_engine("sqlite:///instance/baza.db") +sqlite_meta = MetaData() +sqlite_meta.reflect(bind=sqlite_engine) + +# Cel: PostgreSQL +pg_engine = create_engine(Config.SQLALCHEMY_DATABASE_URI) +pg_meta = MetaData() +pg_meta.reflect(bind=pg_engine) + +# Sesje +SQLiteSession = sessionmaker(bind=sqlite_engine) +PGSession = sessionmaker(bind=pg_engine) + +sqlite_session = SQLiteSession() +pg_session = PGSession() + +def migrate_table(table_name): + print("➡️ Używana baza docelowa:", Config.SQLALCHEMY_DATABASE_URI) + print(f"\n➡️ Migruję tabelę: {table_name}") + source_table = sqlite_meta.tables.get(table_name) + target_table = pg_meta.tables.get(table_name) + + if source_table is None or target_table is None: + print(f"⚠️ Pominięto: {table_name} (brak w jednej z baz)") + return + + rows = sqlite_session.execute(source_table.select()).fetchall() + if not rows: + print("ℹ️ Brak danych do migracji.") + return + + insert_data = [dict(row._mapping) for row in rows] + + try: + with pg_engine.begin() as conn: + conn.execute(target_table.delete()) + conn.execute(target_table.insert(), insert_data) + print(f"✅ Przeniesiono: {len(rows)} rekordów") + except Exception as e: + print(f"❌ Błąd przy migracji {table_name}: {e}") + + +def main(): + tables = ["user", "shopping_list", "item", "expense", "receipt", "suggested_product"] + for table in tables: + migrate_table(table) + print("\n🎉 Migracja zakończona pomyślnie.") + +if __name__ == "__main__": + main() diff --git a/config.py b/config.py index 87bf745..ac25c76 100644 --- a/config.py +++ b/config.py @@ -1,5 +1,7 @@ import os +basedir = os.path.abspath(os.path.dirname(__file__)) + def _get_bool(name: str, default: bool) -> bool: val = os.environ.get(name) if val is None: @@ -24,8 +26,8 @@ class Config: - ROBOTS_TAG """ - # Baza danych - SQLALCHEMY_DATABASE_URI = _get_str("DATABASE_URL", "sqlite:///baza.db") + + #SQLALCHEMY_DATABASE_URI = _get_str("DATABASE_URL", "sqlite:///baza.db") # Flask SECRET_KEY = _get_str("SECRET_KEY", "tajny_klucz") @@ -48,4 +50,22 @@ class Config: # (opcjonalnie) wyłącz warningi track_modifications SQLALCHEMY_TRACK_MODIFICATIONS = False - HEALTHCHECK_TOKEN = _get_str("HEALTHCHECK_TOKEN", "healthcheck") \ No newline at end of file + HEALTHCHECK_TOKEN = _get_str("HEALTHCHECK_TOKEN", "healthcheck") + + # Baza danych + DB_ENGINE = os.environ.get("DB_ENGINE", "sqlite").lower() + + if DB_ENGINE == "sqlite": + SQLALCHEMY_DATABASE_URI = f"sqlite:///{os.path.join(basedir, 'db', 'database.db')}" + elif DB_ENGINE == "pgsql": + SQLALCHEMY_DATABASE_URI = ( + f"postgresql://{os.environ['DB_USER']}:{os.environ['DB_PASSWORD']}@" + f"{os.environ['DB_HOST']}:{os.environ.get('DB_PORT', 5432)}/{os.environ['DB_NAME']}" + ) + elif DB_ENGINE == "mysql": + SQLALCHEMY_DATABASE_URI = ( + f"mysql+pymysql://{os.environ['DB_USER']}:{os.environ['DB_PASSWORD']}@" + f"{os.environ['DB_HOST']}:{os.environ.get('DB_PORT', 3306)}/{os.environ['DB_NAME']}" + ) + else: + raise ValueError("Nieobsługiwany typ bazy danych.") \ No newline at end of file diff --git a/deploy.sh b/deploy.sh index 2761d0f..dc7003e 100755 --- a/deploy.sh +++ b/deploy.sh @@ -10,23 +10,30 @@ fi APP_PORT="${APP_PORT:-8080}" - -# === Konfiguracja (możesz nadpisać zmiennymi środowiskowymi) === REPO_DIR="${REPO_DIR:-$(pwd)}" -COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" # albo compose.yaml +COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" GIT_REMOTE="${GIT_REMOTE:-origin}" GIT_BRANCH="${GIT_BRANCH:-$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)}" -# Jeśli chcesz uruchomić tylko wybrane serwisy, podaj je po komendzie, -# np.: ./deploy.sh web api -SERVICES=("$@") +# Domyślny profil i usługi +DB_PROFILE="sqlite" # domyślnie sqlite, czyli brak profilu pgsql/mysql +SERVICES=() + +# Przetwarzanie argumentów +if [[ $# -gt 0 ]]; then + case "$1" in + pgsql|mysql|sqlite) + DB_PROFILE="$1" + shift + ;; + esac + SERVICES=("$@") +fi log() { printf "\n==> %s\n" "$*"; } -# --- Kontrole wstępne --- command -v git >/dev/null || { echo "Brak 'git' w PATH"; exit 1; } command -v docker >/dev/null || { echo "Brak 'docker' w PATH"; exit 1; } - if ! docker compose version >/dev/null 2>&1; then echo "Wymagany jest 'docker compose' (plugin), nie stary 'docker-compose'." exit 1 @@ -41,34 +48,37 @@ if [[ ! -f "$REPO_DIR/$COMPOSE_FILE" ]]; then fi fi -# --- Praca w katalogu repo --- cd "$REPO_DIR" -# --- Aktualizacja kodu --- log "Aktualizacja repo: git pull --ff-only ($GIT_REMOTE/$GIT_BRANCH)" git fetch --prune "$GIT_REMOTE" git checkout "$GIT_BRANCH" >/dev/null 2>&1 || true git pull --ff-only "$GIT_REMOTE" "$GIT_BRANCH" -# --- Zapisanie wersji do pliku --- log "Zapisywanie hasha commita do version.txt" git rev-parse --short HEAD > version.txt -# --- Zatrzymanie i usunięcie bieżącego stacka --- log "Docker Compose DOWN (usuwanie kontenerów i osieroconych usług)" docker compose -f "$COMPOSE_FILE" down --remove-orphans - -# --- Generowanie default.vcl z szablonu --- log "Generowanie default.vcl z APP_PORT=$APP_PORT" envsubst < deploy/varnish/default.vcl.template > deploy/varnish/default.vcl -# --- Budowanie i uruchamianie bez restartu zależności --- -log "Docker Compose UP (build bez deps) dla: ${SERVICES[*]:-(wszystkie)}" -if [[ ${#SERVICES[@]} -gt 0 ]]; then - docker compose -f "$COMPOSE_FILE" up -d --no-deps --build "${SERVICES[@]}" +# Budowanie i uruchamianie +log "Docker Compose UP (build bez deps) dla profilu: $DB_PROFILE i serwisów: ${SERVICES[*]:-(wszystkie)}" + +if [[ "$DB_PROFILE" == "pgsql" ]]; then + PROFILE_OPT="--profile pgsql" +elif [[ "$DB_PROFILE" == "mysql" ]]; then + PROFILE_OPT="--profile mysql" else - docker compose -f "$COMPOSE_FILE" up -d --no-deps --build + PROFILE_OPT="" +fi + +if [[ ${#SERVICES[@]} -gt 0 ]]; then + docker compose -f "$COMPOSE_FILE" up -d --no-deps --build $PROFILE_OPT "${SERVICES[@]}" +else + docker compose -f "$COMPOSE_FILE" up -d --no-deps --build $PROFILE_OPT fi log "Gotowe ✅ (wersja: $(cat version.txt))" diff --git a/docker-compose.yml b/docker-compose.yml index 52e3be7..d7d5085 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -39,3 +39,28 @@ services: env_file: - .env restart: unless-stopped + + pgsql: + image: postgres:17 + container_name: pgsql-db + environment: + POSTGRES_DB: ${DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - ./db/pgsql:/var/lib/postgresql/data + restart: unless-stopped + profiles: ["pgsql"] + + mysql: + image: mysql:8 + container_name: mysql-db + environment: + MYSQL_DATABASE: ${DB_NAME} + MYSQL_USER: ${DB_USER} + MYSQL_PASSWORD: ${DB_PASSWORD} + MYSQL_ROOT_PASSWORD: 89o38kUX5T4C + volumes: + - ./db/mysql:/var/lib/mysql + restart: unless-stopped + profiles: ["mysql"] \ No newline at end of file