From b8ce9ded76e2583e554f1ea07f87adbc460e0272 Mon Sep 17 00:00:00 2001 From: Alex Muszynski Date: Fri, 30 May 2025 18:49:40 -0400 Subject: [PATCH] feat: crud and backups are working --- .gitignore | 1 + backend/Dockerfile | 6 ++- backend/app/__pycache__/crud.cpython-313.pyc | Bin 0 -> 2039 bytes .../app/__pycache__/database.cpython-313.pyc | Bin 0 -> 534 bytes backend/app/__pycache__/main.cpython-313.pyc | Bin 399 -> 2673 bytes .../app/__pycache__/models.cpython-313.pyc | Bin 0 -> 714 bytes .../app/__pycache__/schemas.cpython-313.pyc | Bin 0 -> 1094 bytes backend/app/crud.py | 26 ++++++++++ backend/app/database.py | 11 ++++ backend/app/main.py | 48 ++++++++++++++++-- backend/app/models.py | 11 ++++ backend/app/schemas.py | 17 +++++++ backup/Dockerfile | 9 ++++ backup/backup.sh | 46 +++++++++++++++++ blogs-app/.vite/deps/_metadata.json | 8 --- blogs-app/.vite/deps/package.json | 3 -- docker-compose.yaml | 11 +++- frontend/src/utils/BlogList.tsx | 13 +++-- frontend/src/utils/BlogViewer.tsx | 2 +- 19 files changed, 191 insertions(+), 21 deletions(-) create mode 100644 .gitignore create mode 100644 backend/app/__pycache__/crud.cpython-313.pyc create mode 100644 backend/app/__pycache__/database.cpython-313.pyc create mode 100644 backend/app/__pycache__/models.cpython-313.pyc create mode 100644 backend/app/__pycache__/schemas.cpython-313.pyc create mode 100644 backend/app/crud.py create mode 100644 backend/app/database.py create mode 100644 backend/app/models.py create mode 100644 backend/app/schemas.py create mode 100644 backup/Dockerfile create mode 100644 backup/backup.sh delete mode 100644 blogs-app/.vite/deps/_metadata.json delete mode 100644 blogs-app/.vite/deps/package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf9a613 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/backups diff --git a/backend/Dockerfile b/backend/Dockerfile index ec0ee93..2370126 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,3 +1,4 @@ + FROM ghcr.io/astral-sh/uv:python3.13-alpine WORKDIR /app @@ -6,6 +7,9 @@ COPY pyproject.toml /app/ RUN uv sync -COPY ./ /app +COPY . /app + +# Run the FastAPI app CMD ["uv","run","uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] + diff --git a/backend/app/__pycache__/crud.cpython-313.pyc b/backend/app/__pycache__/crud.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ab6117ad8d4d394c6225cb7ad698b4530abd8bf6 GIT binary patch literal 2039 zcmcIl&1)M+6rWk`hop&QHEwYU3Ejp9XAf};j+0=D8#j()OJGw8onkPGb`@_dS<0+T z@l6IplM=8CHAznKF@{1+PCfN+h)_Xd=*OX_+*F+0``)Zpwp7Qb^ufM)^FC&0fA3=t zW3ea!`qi3Td8H8YH*NwlI>O-=2)iUhGBib!lv5LBC8Z=oJd{+q3j5F!&8SP-6*Z%s zBllIVXJS&PFKHPA^m-6sJKcyh z`uEX9Cn6nSbIg8vWIBAzwIgQfa6xSA7!R21nQn(Iwj#u>XYfMulzQ|Q7^2zgcd#th{!uOo&GNQyW4I27h1y&<1 z7Tdo5==x8yPf|ao8q936(K;LbS%1m)*~|ET{Fl-CrRfIyrpe~&Y_7qQuhrxsM8bu< zRktFvqFZvSc?3EjlqK*_LHO{%XK5G0Nbt&|HLG;3rvl6|*3volg9MnPyU@#pg6CBE zAZ|jrV$llu@Sa6K)Oz@7rfKp4bcqDGdVC1>c&K*;*!zHep*Gm%CL6D_@q=3pHrZrT zbvD&tH(skZ_(yo6idpI@WK6Hb^A!lYG=yA)w*8W1X&Q6fM?rbb(5rM! zfrdDttdHyb&Tr0drBFz2CLezHn>y5DXKM>G3qAPb#U=hKjDrtf&;REj zfh;SR;QC0p&k1G}aq6Rc7TEjx=#&9foB7jVt#EWZ@+h)1@_3*lK4vX8P)o`5F2K;l z-g$%FZ!r<~9u#v${Gn`&m3fI{wxKdB6~Ab)l|;y0tIFXi8(l8XerZ-6pW?U26_c)Q2bD;-;O@Xu>xn_VVvQm5`VD7gQohW&i*H literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/database.cpython-313.pyc b/backend/app/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f29c0905de45ad4c3eb5f0626a21318adbf8115 GIT binary patch literal 534 zcmYjNJ#W)M7(V;XA2ALoRip~Bps0(5Xrw}hMhKBoIwTcC4NR7zi+v;(w$Hq07a4U3 zD+?^_to#E;egdcqJ%m7FVguz5a882YmEP~?{aiGg4yx%|fA&a2=*QAo3v;a(r^gS)V!L&4zNO) zz8ToQwTGb*kPhmtk_enB_Uxav=V`JT0;2^Rvuqk?%+sYSm=H0~Qu>5}+=y71P@qM8 z!p0LS*nR093?2;j2mZbB!{ejp*K;n4DKPOg>36%OV9?L05YIS7ccMwZck{+>H?1OC z(7O82!_uK;!9$*=aUmPE{WvMbOp-~QMcxJJs7^EqXDqD%s~zRVe}NqiDG(I|qt57D zNOQ^xmYqQTT4}DXkgmVF;fRMckz`*@euowcfn>W&^L)vFciPD-Y2QkDR3_{;v=yj+ zqc~9F7uGcGE86~s_P(IT_tvFX)ywKL+5FhPyTJN}K6iiH$k}>*de+#Uo9C@7Z_D?_ Whr>^;(cC?^w$7}}F9>X^Mt=a|^^YY0 literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/main.cpython-313.pyc b/backend/app/__pycache__/main.cpython-313.pyc index 0bd08c019fc7566a02e8524f111801f13b4c3e7d..05b2ec2c658e1dd2aa61c77fe9662ccc24662a75 100644 GIT binary patch literal 2673 zcmcgu&2JM&6rb5$dmXRsm;@5XfnXBy;e?U^Ns~~ZDkXe4QHrvJR>D%Wu{X(rzdE~V zOG;IzJwT~iP*tQHQ}xh99N^MJ#f3k?DF?!c3aa*!o6~UUp@+V=wqxRgihAfuGw;pJ zd%t;K^RydYubaSUbRVACKO}`;LCJuSD7zyQAKqQQqZ8yq_4Qi zs#Q5im_J%Q)>4uyQ`OZ+3oErMSk|cl>GrZziBsKCd90(fDpOS%6rw^iiTX=uy;Svp zY-d@f#Hrq>qgPUWL2^;LNH?vldx?GSX#I=tguoNL7ou5Rakr@YqXBgr47|&!)>Pcp z@RGY~UwqRGz1vVHG8t3cm5T^|mtA#7OW@=49K-a31R%O~`V zo{F2?d-~kDvqK-n^o(VsQ(?+wRX0sw*D%dtfy-uWMo(%cmtt%-4j7)Iz(_i#CAd>h zO&cklOUE@+2X$%ENX27iF9?xffrYeT*-QwE{6evW$YuehND~<+ACOx#1awRU(TcB> z_z;;Ud;V2siX0VplPTISPSIsL8WtG7K<()eklGVbw2Zo6SyhgI*an+75o3h9Ag6ODS_4%*6p1RyOTwl1pa{qZvC@_DS+d=p< zWq^JywUBSN4bbmv`otf_0XpaHhCs$Hy*8t332SCch~VWDba?}fgdRXup@;5;gpG3N zX z?|SE?T>jXx`m z3&$TQtvRXH_GHaYtYls?GTf0cl7qsDP|n#eHj77SMX&s11Vu#h(Vk#AcFm2iK*hCpwn?-FIkpGjc?&O%=ylM0ijgd3QeVF zQ*n6yifL)qtT_>b(%{axZfQmW{I@gdDn*1LF6_1du>->8_+%jhTOw1n85!0Iq9|@Z zF37E~=0@^*5;E)S$evo2ee<6__BAevx!w~izM-6JXjQ4nj@r@se?r1q3oaE(7M_J( zEZKkfYFF@<3Sj#%4mfq8hv;fW^>$=Wtjfy#l}aD0v%|KJqv2g#o`PPjWekQPWcd2* ztGUm7KcOYCDPP{7W=ZCR6HXZZM-BB?rF1|r6k>Dcq zKfBj)5%Otw4lZTVro|)dmoa(3JEMs<&d`i z>hykYWW_&R4qBV{6Txw{e!l+N?gzACU7)mMLnJ~lk6hk`hk^OPweaGpA4a|#$p&)t zz@vdT?>p~RuMsYDl$m3#Z`WX_`KZyEm1$_i>SnOSr?|7CyD=mH5`W$_I zwQ=Xt!Jpo}fBxPC0(IR~5u$6cK<@SC~N(~eAId1Zrknd zbt2K)n`R!#qWlaAsvgb@z~z1DL~uTs^E54ec<0LUm25Cak38CU=A0T5YLoh=yqc?*WV-ceQLky!5gDF(Hh$)6C zh#ks7qJo)B86n(YW+{YfmUI?P)|Vg&O~za7Zi&Sujsc#x81<8pv@$ROIn6-)SqMld zGbn(pXAEXCU{ql+XHH|(WcItonvt55lYfh~JijO>WhFxq2S_;(Gu#r;Pb??^n-*)pxY7)GK)araElpe<}K#*REP;++ln}W5=Ef!yTxIXo1apelWJGQ59ETpU91fx vJ}@&fGTvp7e#pk#oY}zqfsKKQwOzbXyhFHI<|7j$qugg^1|~i*3v3twdA3YD diff --git a/backend/app/__pycache__/models.cpython-313.pyc b/backend/app/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bb40868e61ffa02920e590144cfd867c19f3e650 GIT binary patch literal 714 zcmYLH&ubGw6rS0g{gG_bYG|dTf~C|Sdoh<-h)55$2;E==r^mps$xhS7-A(#-1LhVG z#A7c7k2%_#|4Q&;2SmhEZbJ0#%xOBRmf@LT?xJn9hbXBbYIMvedXW!S-*m0HA}$fc zqp#Y+d_IUqaU!gfB;$P!!lW65Nq;UiAQuaA;7<Eq!T)TQS}eJO29^B#TO+?l-mQQEk) zrl+4bXQhM5(T#Ka^1+nMocojGo9gz*JD>Jv)%v8R{QJ|bne*`7G3=~rwOw13(~HQD zdgnYI=OX|2Q}kj7zU)?BF4(KITxwC}uJj4}SuwgpxF@OlA<~*OzjTc8chvZT8sAXu Qw`t->9~ys literal 0 HcmV?d00001 diff --git a/backend/app/__pycache__/schemas.cpython-313.pyc b/backend/app/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e0ca0df098dccf81c490a8718a1c801e3cf47713 GIT binary patch literal 1094 zcmah|&ubGw6rS0g-E5MEsI?d(q#7x*qV-}yL_FAo)M5{=$H1^An{*-Bjc+!G+=QYQ zyu?HQ2>&4o1D-s^laPp4-KTo0;#ueZO~JsgxM55BHuN|HShLJCjL|{KCFov8koCAfP?Yr7L z$f5z6p9xgeA_)i4kuG;4)rNSO#cA@D`JVQqB#D%|B;_FOjQVunkn-Iq>@Ou%DZ4SK ztRE*)lA_pF8E9m~BuTT7Y$++hBR^YGDx%sl%rc1Ij5PGrjpR91hJ^?c-<= zs@>rjsNe=DR<(ujiA_tDlY?oa`TlUSImy1%zlkPXJ=a?)7e|)X{5ryFxdICWmP6+T6 zQUc0fQ5$d~yzEquF599qWCVA6g literal 0 HcmV?d00001 diff --git a/backend/app/crud.py b/backend/app/crud.py new file mode 100644 index 0000000..c2fb5dc --- /dev/null +++ b/backend/app/crud.py @@ -0,0 +1,26 @@ +from sqlalchemy.orm import Session +from app import models, schemas + + +def get_item(db: Session, item_id: int): + return db.query(models.Item).filter(models.Item.id == item_id).first() + + +def get_items(db: Session, skip: int = 0, limit: int = 10): + return db.query(models.Item).offset(skip).limit(limit).all() + + +def create_item(db: Session, item: schemas.ItemCreate): + db_item = models.Item(**item.model_dump()) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + + +def delete_item(db: Session, item_id: int): + item = db.query(models.Item).filter(models.Item.id == item_id).first() + if item: + db.delete(item) + db.commit() + return item diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..22c96b1 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,11 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base +import os + +DATABASE_URL = os.getenv( + "DATABASE_URL", "postgresql://user:password@db:5432/mydatabase" +) + +engine = create_engine(DATABASE_URL) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() diff --git a/backend/app/main.py b/backend/app/main.py index 29b6f66..cbdb685 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -1,8 +1,48 @@ -from fastapi import FastAPI +from fastapi import FastAPI, Depends, HTTPException +from sqlalchemy.orm import Session +from . import schemas, crud +from .database import SessionLocal, engine, Base + +Base.metadata.create_all(bind=engine) app = FastAPI() -@app.get("/") -async def read_root(): - return {"hello": "world"} +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +@app.get("/check-health") +def health_check(): + return {"Health": "Super Healthy!"} + + +@app.post("/items/", response_model=schemas.Item) +def create_item(item: schemas.ItemCreate, db: Session = Depends(get_db)): + return crud.create_item(db, item) + + +@app.get("/items/", response_model=list[schemas.Item]) +def read_items(skip: int = 0, limit: int = 10, db: Session = Depends(get_db)): + return crud.get_items(db, skip, limit) + + +@app.get("/items/{item_id}", response_model=schemas.Item) +def read_item(item_id: int, db: Session = Depends(get_db)): + db_item = crud.get_item(db, item_id) + if db_item is None: + raise HTTPException(status_code=404, detail="Item not found") + return db_item + + +@app.delete("/items/{item_id}", response_model=schemas.Item) +def delete_item(item_id: int, db: Session = Depends(get_db)): + item = crud.delete_item(db, item_id) + if item is None: + raise HTTPException(status_code=404, detail="Item not found") + return item diff --git a/backend/app/models.py b/backend/app/models.py new file mode 100644 index 0000000..355eca9 --- /dev/null +++ b/backend/app/models.py @@ -0,0 +1,11 @@ +from sqlalchemy import JSON, Column, Integer, String +from .database import Base + + +class Item(Base): + __tablename__ = "items" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String, index=True) + description = Column(String, nullable=True) + body = Column(JSON, nullable=False) diff --git a/backend/app/schemas.py b/backend/app/schemas.py new file mode 100644 index 0000000..c900f57 --- /dev/null +++ b/backend/app/schemas.py @@ -0,0 +1,17 @@ +from pydantic import BaseModel + + +class ItemBase(BaseModel): + name: str + description: str | None = None + + +class ItemCreate(ItemBase): + pass + + +class Item(ItemBase): + id: int + + class Config: + from_attributes = True diff --git a/backup/Dockerfile b/backup/Dockerfile new file mode 100644 index 0000000..522fd59 --- /dev/null +++ b/backup/Dockerfile @@ -0,0 +1,9 @@ +FROM alpine:latest + +RUN apk add --no-cache docker-cli bash findutils postgresql-client + +COPY backup.sh /backup.sh +RUN chmod +x /backup.sh + +CMD ["/backup.sh"] + diff --git a/backup/backup.sh b/backup/backup.sh new file mode 100644 index 0000000..f018d88 --- /dev/null +++ b/backup/backup.sh @@ -0,0 +1,46 @@ +#!/bin/sh + +echo "[INFO] Backup entry success" + +# === Configuration === +PG_CONTAINER_NAME=$(docker ps --filter "name=_db" --format "{{.Names}}" | head -n 1) +PG_USER="user" +PG_PASSWORD="password" +PG_DB="mydatabase" +BACKUP_DIR="/backups" +LOG_DIR="/backups/logs" +RETENTION_DAYS=7 +TIMESTAMP=$(date +"%Y%m%d-%H%M%S") +FILENAME="$BACKUP_DIR/backup-$TIMESTAMP.sql" +LOGFILE="$LOG_DIR/backup-$TIMESTAMP.log" + +# === Setup Directories === +mkdir -p "$BACKUP_DIR" +mkdir -p "$LOG_DIR" + +# === Begin Logging === +exec > "$LOGFILE" 2>&1 + +echo "[INFO] Backup script started at $TIMESTAMP" +echo "[INFO] Backing up database '$PG_DB' from container '$PG_CONTAINER_NAME'..." + +# === Perform Backup === +PGPASSWORD="$PG_PASSWORD" pg_dump -h db -U "$PG_USER" -d "$PG_DB" > "$FILENAME" + +if [ $? -eq 0 ]; then + echo "[INFO] Backup successful: $FILENAME" +else + echo "[ERROR] Backup failed!" >&2 + exit 1 +fi + +# === Rotate Old Backups === +echo "[INFO] Removing backups older than $RETENTION_DAYS days..." +find "$BACKUP_DIR" -type f -name "backup-*.sql" -mtime +$RETENTION_DAYS -exec rm -f {} \; + +# === Rotate Old Logs === +echo "[INFO] Removing logs older than $RETENTION_DAYS days..." +find "$LOG_DIR" -type f -name "backup-*.log" -mtime +$RETENTION_DAYS -exec rm -f {} \; + +echo "[INFO] Backup and cleanup completed at $(date +"%Y%m%d-%H%M%S")" + diff --git a/blogs-app/.vite/deps/_metadata.json b/blogs-app/.vite/deps/_metadata.json deleted file mode 100644 index 9222614..0000000 --- a/blogs-app/.vite/deps/_metadata.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "hash": "d81b31a4", - "configHash": "5834786f", - "lockfileHash": "e3b0c442", - "browserHash": "a25f5c91", - "optimized": {}, - "chunks": {} -} \ No newline at end of file diff --git a/blogs-app/.vite/deps/package.json b/blogs-app/.vite/deps/package.json deleted file mode 100644 index 3dbc1ca..0000000 --- a/blogs-app/.vite/deps/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/docker-compose.yaml b/docker-compose.yaml index 63bebdc..6c93478 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -11,13 +11,22 @@ services: - postgres_data:/var/lib/postgresql/data networks: - app-network + backup: + build: ./backup + volumes: + - ./backups:/backups + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + - db + networks: + - app-network backend: build: ./backend volumes: - ./backend/app:/app/app environment: - DATABASE_URL: postgres://user:password@db:5432/mydatabase + DATABASE_URL: postgresql://user:password@db:5432/mydatabase depends_on: - db ports: diff --git a/frontend/src/utils/BlogList.tsx b/frontend/src/utils/BlogList.tsx index cf084e7..5e441b7 100644 --- a/frontend/src/utils/BlogList.tsx +++ b/frontend/src/utils/BlogList.tsx @@ -1,5 +1,12 @@ -export function BlogList() { - fetch("public/blogs/*.md").then((ret) => console.log(ret.text())); +import { useState, useEffect } from "react"; - return
; +export function BlogList() { + const [content, setContent] = useState(""); + useEffect(() => { + fetch(`localhost:8000/get-blogs`) + .then((res) => res.text()) + .then(setContent); + }, []); + + return
{content}
; } diff --git a/frontend/src/utils/BlogViewer.tsx b/frontend/src/utils/BlogViewer.tsx index 60bb15d..e0e6802 100644 --- a/frontend/src/utils/BlogViewer.tsx +++ b/frontend/src/utils/BlogViewer.tsx @@ -7,7 +7,7 @@ export function BlogViewer() { const { slug } = useParams(); useEffect(() => { - fetch(`/blogs/${slug ?? "home"}.md`) + fetch(`localhost:8000/get-blogs/${slug}`) .then((res) => res.text()) .then(setContent); }, []);