diff --git a/CLAUDE.md b/CLAUDE.md index db055aa..65b3dcb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -59,6 +59,8 @@ npm test # backend (cargo test, MongoDB açık olmalı) + frontend ( npm run lint # clippy + oxlint npm run types # Rust DTO → frontend/src/types/*.ts (ts-rs; cargo test de üretir) npm run icons # frontend/brand/*.svg → favicon, PWA, Apple, Tauri ikonları (rsvg-convert + magick) +npm run db:backup # kakeibo_dev → ./backups (canlı yedek: docs/DEPLOY.md §6) +npm run db:restore -- [--force] ``` - `npm run dev` öncesinde DB otomatik başlar (`predev`). - Yerel MongoDB host portu **27020** (27017 sistem mongod, 27018/27019 diğer projeler). diff --git a/deploy/systemd/kakeibo-backup-pull.service b/deploy/systemd/kakeibo-backup-pull.service new file mode 100644 index 0000000..7a87dfd --- /dev/null +++ b/deploy/systemd/kakeibo-backup-pull.service @@ -0,0 +1,11 @@ +# Bu bilgisayar: ~/.config/systemd/user/kakeibo-backup-pull.service +[Unit] +Description=Kakeibo yedeklerini sunucudan çek +Wants=network-online.target +After=network-online.target + +[Service] +Type=oneshot +# ~/.ssh/config'teki sunucu adı "atlas" değilse başındaki # işaretini kaldırıp düzeltin +#Environment=KAKEIBO_BACKUP_HOST=atlas +ExecStart=%h/Developer/Rust/kakeibo/scripts/backup-pull.sh diff --git a/deploy/systemd/kakeibo-backup-pull.timer b/deploy/systemd/kakeibo-backup-pull.timer new file mode 100644 index 0000000..a11984f --- /dev/null +++ b/deploy/systemd/kakeibo-backup-pull.timer @@ -0,0 +1,12 @@ +# Bu bilgisayar: ~/.config/systemd/user/kakeibo-backup-pull.timer +[Unit] +Description=Kakeibo yedeklerini her gün çek + +[Timer] +# Sunucudaki yedekten sonra; bilgisayar kapalıysa açıldıktan 10 dk sonra +OnCalendar=*-*-* 09:00:00 +OnStartupSec=10min +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/deploy/systemd/kakeibo-backup.service b/deploy/systemd/kakeibo-backup.service new file mode 100644 index 0000000..816f339 --- /dev/null +++ b/deploy/systemd/kakeibo-backup.service @@ -0,0 +1,13 @@ +# Atlas (sunucu): /etc/systemd/system/kakeibo-backup.service +[Unit] +Description=Kakeibo MongoDB günlük yedeği +Wants=docker.service +After=docker.service + +[Service] +Type=oneshot +# Yedeği çeken SSH kullanıcısı root değilse: başındaki # işaretini kaldırıp grubunu yazın. +#Environment=BACKUP_GROUP= +ExecStart=/usr/local/lib/kakeibo/backup.sh +Nice=10 +IOSchedulingClass=idle diff --git a/deploy/systemd/kakeibo-backup.timer b/deploy/systemd/kakeibo-backup.timer new file mode 100644 index 0000000..5432f7d --- /dev/null +++ b/deploy/systemd/kakeibo-backup.timer @@ -0,0 +1,12 @@ +# Atlas (sunucu): /etc/systemd/system/kakeibo-backup.timer +[Unit] +Description=Kakeibo yedeği her gece 03:30 + +[Timer] +OnCalendar=*-*-* 03:30:00 +# Sunucu o saatte kapalıysa açılınca hemen çalışır +Persistent=true +RandomizedDelaySec=5m + +[Install] +WantedBy=timers.target diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index 5b512e4..d9283b1 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -1,6 +1,6 @@ # 🚀 Canlıya Alma (Atlas Homeserver + Dokploy) -> Durum: ✅ Yayında — https://kakeibo.ta1sph.website (2026-11-12) · Yedekleme ⏳ (Faz 5 bitmeden) +> Durum: ✅ Yayında — https://kakeibo.ta1sph.website (2026-11-12) · Yedekleme: betikler hazır, sunucuda kurulum bekliyor (§6) ## 1. Mimari @@ -95,4 +95,78 @@ Notlar: ## 6. Yedekleme ve geri yükleme -> Faz 3 – Adım 5'te doldurulacak (günlük `mongodump`, son 14 yedek). +``` +Atlas 03:30 backup.sh ──▶ /var/backups/kakeibo/kakeibo-YYYY-MM-DD_HHMM.archive.gz (son 14) + │ rsync (SSH) +Bu bilgisayar 09:00 / açılışta ──▶ ~/Yedekler/kakeibo/ (son 60, 2 günden eskiyse masaüstü uyarısı) +``` + +| Dosya | Nerede çalışır | İş | +|---|---|---| +| `scripts/backup.sh` | Atlas → `/usr/local/lib/kakeibo/` | `mongodump --archive --gzip` → gzip + `mongorestore --dryRun` denetimi → son 14'ü tutar | +| `scripts/restore.sh` | Atlas (ve yerelde `npm run db:restore`) | yedeği **başka adla** geri yükler, kayıt sayılarını gösterir/karşılaştırır | +| `scripts/backup-pull.sh` | bu bilgisayar | yeni yedekleri çeker, eskiyse uyarır | +| `deploy/systemd/*` | ikisi | zamanlayıcılar (`Persistent=true`: kapalıyken kaçan çalışma açılınca yapılır) | + +Yerel deneme: `npm run db:backup` (kakeibo_dev → `./backups`, git'e girmez) · +`COMPARE_DB=kakeibo_dev npm run db:restore -- latest kakeibo_restore_test`. + +### 6.1 Atlas'ta kurulum (bir kez) +Bu bilgisayarda, proje klasöründe: +```bash +scp scripts/backup.sh scripts/restore.sh deploy/systemd/kakeibo-backup.service deploy/systemd/kakeibo-backup.timer atlas:/tmp/ +ssh atlas +``` +Atlas'ta: +```bash +sudo install -Dm755 /tmp/backup.sh /usr/local/lib/kakeibo/backup.sh +sudo install -Dm755 /tmp/restore.sh /usr/local/lib/kakeibo/restore.sh +sudo install -m644 /tmp/kakeibo-backup.service /tmp/kakeibo-backup.timer /etc/systemd/system/ +``` +- SSH'ye **root olmayan** bir kullanıcıyla bağlanıyorsanız: `sudo systemctl edit --full kakeibo-backup.service` → + `#Environment=BACKUP_GROUP=…` satırının başındaki `#`'i kaldırıp kendi kullanıcı grubunuzu yazın (yoksa yedekleri çekemezsiniz). + +İlk yedek ve kontrol: +```bash +sudo systemctl daemon-reload +sudo systemctl start kakeibo-backup.service +journalctl -u kakeibo-backup -n 20 --no-pager # "tamam: …" görülmeli +``` + +### 6.2 Geri yükleme denemesi (kurulumda bir kez, sonra ayda bir önerilir) +Canlı veriye dokunmaz; ayrı `kakeibo_restore_test` veritabanına yükler ve sayıları karşılaştırır: +```bash +sudo COMPARE_DB=kakeibo /usr/local/lib/kakeibo/restore.sh latest kakeibo_restore_test +# "✅ kakeibo ile aynı" görülmeli. Sonra deneme veritabanını silin: +sudo docker exec $(sudo docker ps -q -f name=kakeibo-kakeibomongo-fibzhc) mongosh --quiet kakeibo_restore_test --eval 'db.dropDatabase()' +``` +Sonra zamanlayıcıyı açın: +```bash +sudo systemctl enable --now kakeibo-backup.timer +systemctl list-timers kakeibo-backup.timer # bir sonraki çalışma saati +``` + +### 6.3 Bu bilgisayarda (sunucu dışı kopya) +Önce şifresiz SSH çalışmalı: `ssh -o BatchMode=yes atlas ls /var/backups/kakeibo` yedek dosyasını listelemeli. +```bash +scripts/backup-pull.sh # elle bir kez: ~/Yedekler/kakeibo/ +mkdir -p ~/.config/systemd/user +cp deploy/systemd/kakeibo-backup-pull.service deploy/systemd/kakeibo-backup-pull.timer ~/.config/systemd/user/ +systemctl --user daemon-reload +systemctl --user enable --now kakeibo-backup-pull.timer +``` +- SSH'de sunucunun adı `atlas` değilse: `systemctl --user edit --full kakeibo-backup-pull.service` → `KAKEIBO_BACKUP_HOST` satırı. +- Günlük: `journalctl --user -u kakeibo-backup-pull -n 20`. + +### 6.4 Felaket sonrası geri yükleme (canlı veritabanının yerine) +1. Dokploy → `kakeibo-app` → **Stop** (yükleme sırasında yazma olmasın). +2. Gerekirse yedeği sunucuya kopyalayın: `scp ~/Yedekler/kakeibo/ atlas:/tmp/`. +3. Atlas'ta: `sudo /usr/local/lib/kakeibo/restore.sh /tmp/ kakeibo --force` (önce mevcut `kakeibo` silinir). +4. `kakeibo-app` → **Start**, `/api/health` ve giriş kontrolü. +- MongoDB servisi tamamen yeniden kurulduysa önce §4'teki kullanıcıları (`kakeibo_app`) yeniden oluşturun. +- Yedekte kullanıcı şifre özetleri (argon2) ve oturum bilgisi bulunur → yedek dosyalarını paylaşmayın. + +### Notlar +- MongoDB'de kimlik doğrulaması zorunlu olmadığından (§4) `mongodump` konteynerin içinden şifresiz çalışır. + Auth açıldığında yedek için ayrı bir `backup` rolü kullanıcısı ve betiğe `--config` ile şifre dosyası eklenmeli. +- Yedek boyutu şu an birkaç KB–MB; 14 + 60 dosya önemsiz yer tutar. diff --git a/docs/MEMORY.md b/docs/MEMORY.md index 824f426..716d7cb 100644 --- a/docs/MEMORY.md +++ b/docs/MEMORY.md @@ -1,7 +1,7 @@ # 🧠 Proje Hafızası ## ▶️ Sıradaki Adım -**Faz 5 – Adım 5:** Canlı yedekleme (gerçek veri girişinden önce): Dokploy'da günlük `mongodump --archive --gzip`, 14 gün sakla (eskisini sil), yedeği sunucu dışına da kopyala; boş bir DB'ye `mongorestore` ile geri yükleme denemesi yap ve `docs/DEPLOY.md`'ye adımları yaz. `scripts/backup` yerel karşılığı. Bitince Faz 5 ✅ → Faz 6 (ay sonu değerlendirmesi + dönem kapatma). +**Faz 5 – Adım 5 (kullanıcıda):** `docs/DEPLOY.md` §6.1–6.3: Atlas'a betik + systemd timer kurulumu, ilk yedek, `restore.sh latest kakeibo_restore_test` ile karşılaştırma, bu bilgisayarda pull timer. Kullanıcı çıktıları (journal satırları, "✅ kakeibo ile aynı") bildirince Faz 5 ✅ → Faz 6 (ay sonu değerlendirmesi + dönem kapatma/devir). ## 📌 Alınan Kararlar - **Yerel demo hesapları** (`kakeibo_dev`): `demo@ornek.com` / `demo12345` (owner, "Demo Hanesi") · `es@ornek.com` / `demo12345` (member, aynı hane). `npm run db:reset` silerse API ile yeniden oluştur (ileride `db:seed`). @@ -47,7 +47,7 @@ ## ❓ Açık Konular - Geliştirme makinesi: CachyOS nodejs 26.13.0-2 ↔ simdjson .so.36 uyumsuzluğu → simdjson 2:5.0.5-1'e düşürüldü (2026-11-12). Düzeltilmiş nodejs gelene kadar `pacman -Syu --ignore simdjson`. `node: libsimdjson.so.34` hatası görülürse sebep budur. -- **Canlı yedekleme yok** (Faz 3 Adım 5 ertelendi) → gerçek veri girişinden önce, en geç Faz 5 sonunda: günlük mongodump, 14 yedek, geri yükleme testi. +- **Canlı yedekleme kurulumu bekliyor** (betikler hazır, `docs/DEPLOY.md` §6) → gerçek veri girişinden önce. - **Canlı MongoDB auth kapalı** (Dokploy replica set betiği keyFile kullanmıyor, özel Command'ı yok sayıyor). İç ağda, port kapalı. Ayrıntı: `docs/DEPLOY.md` §4. - Üye çıkarma / rol değiştirme / sahipliği devretme henüz yok (Ayarlar ekranıyla birlikte, Faz 2+). - Kapalı dönem yeniden açılırken başka açık dönem varsa davranış (Faz 6). @@ -103,3 +103,4 @@ - **Adım 2:** Gerçekleşen harcama (saklanmaz, her istekte aggregation): `GET /summary` → `spent_total`, `remaining` (= harcanabilir − harcanan, negatifse aşılmış). `GET /budget` → grupta `spent` (dönem), haftada `spent`. `spent_by_group_and_date` (grup×gün toplanır, Rust'ta haftalara dağıtılır; tarih metin karşılaştırması). Silinen kayıt sayılmaz, kategori değişince harcama yeni gruba geçer. Test `actual_spending_in_summary_and_budget` (hafta sınırları 6/7 Aralık, dönem son günü, önceki dönem hariç, geri al). Mutasyon: hafta sınırı `<=`→`<` ve silinenleri sayma → ikisi de yakalandı. Backend 98 test. - **Adım 3:** Defter ekranı (`features/ledger/`): `WeekPicker` (◀ ▶, "Bu hafta", haftanın harcanan/bütçe toplamı), `EntryForm` (büyük `MoneyInput`, 4 renkli grup düğmesi, kategori/ödeme türü çipleri, tarih, açıklama; kayıttan sonra tutar+açıklama temizlenir, odak tutara döner), `schema.ts` (`entrySchema(start,last)`, `fieldForError`: sunucu mesajı → alan). Hafta adreste: `/defter?hafta=N` (bugünün haftasında parametre yok); başka haftaya tarihli kayıt → o haftaya geçer; hafta değişince tarih o haftaya çekilir. `lib/ledger.ts` (`weekIndexFor`, `defaultEntryDate`, `parseWeekParam`, son seçim localStorage `kakeibo.ledger.last`: grup + **her grubun son kategorisi** + ödeme türü; pasifleşen seçim düşer). `lib/catalog.ts` (`useCategories`, `usePaymentMethods`, 5 dk staleTime), `lib/entries.ts` (`useEntries`, `useCreateEntry`), `queryKeys.entries`. Frontend 79 test; e2e 31 kontrol (ayrı 8091 sunucu, DB silindi). Mobilde "Beklenmeyen" taşması düzeltildi. - **Adım 4:** Defter listesi ve özet: `entry-list.tsx` (gün gün `groupByDay`, gün adı `formatWeekday` = Intl tr-TR UTC; satır: kategori emojisi grup renginde, grup · ödeme · açıklama; düzenle/sil 44 px), sil → sonner `action` "↩️ Geri al" (8 sn) → `useRestoreEntry`. Düzenleme aynı `EntryForm` (`entry` + `key`, Güncelle/Vazgeç; `version` gönderilir, 409 Türkçe root hatası; `''` → ödeme/açıklama kaldır). Katalog **pasifler dahil** okunur (eski kayıtların adı; seçenekte yalnız aktifler + kaydın mevcut değeri). `week-summary.tsx` grup bazlı bütçe/harcanan/kalan + çubuk, aşımda "Aşıldı" kırmızı. Masaüstü iki sütun (form solda yapışkan); dar sütunda taşmaya karşı **container query** (`@container`, `@lg:grid-cols-4`). Frontend 81 test; e2e 29 kontrol (+ Adım 3'ün 31'i yeniden geçti). **Not:** e2e betikleri aynı DB'yi paylaşırsa birbirini bozar → her biri temiz DB ile. +- **Adım 5:** Yedekleme betikleri: `scripts/backup.sh` (Swarm etiketiyle konteyneri bulur, `mongodump --archive --gzip` → `.partial` → `gzip -t` + `mongorestore --dryRun` → ad verilir; son `KEEP=14`, `BACKUP_GROUP` ile okuma izni), `scripts/restore.sh` (dosya adından kaynak DB, `--nsFrom/--nsTo` ile **başka adla**; mevcut hedefe yalnızca `--force` + `--drop`; `COMPARE_DB` ile sayım karşılaştırma), `scripts/backup-pull.sh` (rsync SSH BatchMode, `~/Yedekler/kakeibo`, son 60, 48 saatten eski → `notify-send`). `deploy/systemd/` (sunucu 03:30, bu bilgisayar 09:00 + açılış, `Persistent=true`). Kullanıcı kararı: sunucu dışı kopya **bu bilgisayara** (S3 değil). `npm run db:backup` / `db:restore` (yerel, `backups/` gitignore). Yerel test: yedek → geri yükleme sayıları kakeibo_dev ile aynı, koruma, bozuk dosya hatası, saklama ✅. diff --git a/docs/PHASES.md b/docs/PHASES.md index 85d0d9f..28e921c 100644 --- a/docs/PHASES.md +++ b/docs/PHASES.md @@ -58,4 +58,4 @@ - ✅ 2. Backend: haftalık/aylık gerçekleşen (grup bazlı bütçe–harcanan–kalan), özet ve önerilere gerçekleşen verisi - ✅ 3. Frontend: Defter — hafta seçici + hızlı giriş formu (tutar, 4 grup düğmesi, kategori, ödeme türü, tarih, açıklama; son seçim hatırlanır) - ✅ 4. Frontend: haftanın girişleri (gün gün), düzenle/sil + "Geri al", haftalık özet -- 🔄 5. Canlı yedekleme (mongodump günlük, 14 gün) + geri yükleme denemesi +- 🔄 5. Canlı yedekleme (betikler + test ✅, sunucuda kurulum kullanıcıda) (mongodump günlük, 14 gün) + geri yükleme denemesi diff --git a/package.json b/package.json index 7d0b973..db45c07 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "dev:host": "concurrently -k -n api,web -c blue,green \"npm:dev:api\" \"npm --prefix frontend run dev -- --host\"", "db:up": "docker compose -f docker-compose.dev.yml up -d --wait", "db:down": "docker compose -f docker-compose.dev.yml stop", + "db:backup": "MONGO_CONTAINER=kakeibo-mongo DB_NAME=kakeibo_dev BACKUP_DIR=./backups scripts/backup.sh", + "db:restore": "MONGO_CONTAINER=kakeibo-mongo BACKUP_DIR=./backups scripts/restore.sh", "db:seed": "echo 'Seed betiği Faz 1-4 arasında eklenecek.'", "db:reset": "docker exec kakeibo-mongo mongosh --quiet --eval \"db.getSiblingDB('kakeibo_dev').dropDatabase(); db.adminCommand({listDatabases:1}).databases.filter(d => d.name.startsWith('kakeibo_test_')).forEach(d => db.getSiblingDB(d.name).dropDatabase()); print('kakeibo_dev ve test veritabanları sıfırlandı.')\"", "test": "cd backend && cargo test && cd .. && npm --prefix frontend test", diff --git a/scripts/backup-pull.sh b/scripts/backup-pull.sh new file mode 100755 index 0000000..d988e7c --- /dev/null +++ b/scripts/backup-pull.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Canlı yedekleri bu bilgisayara çeker (sunucu dışı kopya). systemd kullanıcı zamanlayıcısı çalıştırır; +# elle de çalıştırılabilir: scripts/backup-pull.sh +# +# KAKEIBO_BACKUP_HOST ~/.ssh/config'teki sunucu adı [atlas] +# REMOTE_DIR Sunucudaki yedek klasörü [/var/backups/kakeibo] +# LOCAL_DIR Bu bilgisayardaki klasör [~/Yedekler/kakeibo] +# KEEP Burada saklanacak yedek sayısı [60] +# +# En yeni yedek 2 günden eskiyse masaüstü bildirimi verir (sunucudaki günlük yedek durmuş olabilir). +set -euo pipefail + +HOST=${KAKEIBO_BACKUP_HOST:-atlas} +REMOTE_DIR=${REMOTE_DIR:-/var/backups/kakeibo} +LOCAL_DIR=${LOCAL_DIR:-$HOME/Yedekler/kakeibo} +KEEP=${KEEP:-60} + +warn() { + echo "[kakeibo-pull] UYARI: $*" >&2 + command -v notify-send >/dev/null && notify-send -u critical "Kakeibo yedeği" "$*" || true +} + +mkdir -p "$LOCAL_DIR" +chmod 700 "$LOCAL_DIR" +# Yalnızca tamamlanmış yedekler (.partial hariç); var olanlar yeniden indirilmez +if ! rsync -a --ignore-existing --include='*.archive.gz' --exclude='*' \ + -e 'ssh -o BatchMode=yes -o ConnectTimeout=15' "$HOST:$REMOTE_DIR/" "$LOCAL_DIR/"; then + warn "sunucudan yedek çekilemedi ($HOST). Sunucu açık mı, SSH anahtarı çalışıyor mu?" + exit 1 +fi + +newest=$(ls -1t "$LOCAL_DIR"/*.archive.gz 2>/dev/null | head -n1 || true) +if [[ -z $newest ]]; then + warn "hiç yedek yok" + exit 1 +fi +age_h=$(( ($(date +%s) - $(stat -c %Y "$newest")) / 3600 )) +echo "[kakeibo-pull] en yeni: $(basename "$newest") ($age_h saat önce)" +(( age_h <= 48 )) || warn "en yeni yedek $age_h saat önce alınmış; sunucudaki günlük yedeği kontrol edin." + +ls -1t "$LOCAL_DIR"/*.archive.gz | tail -n +"$((KEEP + 1))" | while read -r f; do rm -f -- "$f"; done diff --git a/scripts/backup.sh b/scripts/backup.sh new file mode 100755 index 0000000..4648054 --- /dev/null +++ b/scripts/backup.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Kakeibo veritabanı yedeği: MongoDB konteynerinde mongodump → sıkıştırılmış tek dosya. +# +# Canlı (Atlas, root): /usr/local/lib/kakeibo/backup.sh +# Yerel: npm run db:backup (kakeibo_dev → ./backups) +# +# Ayarlar (ortam değişkeni): +# MONGO_SERVICE Dokploy/Swarm servis adı (konteyner bundan bulunur) [kakeibo-kakeibomongo-fibzhc] +# MONGO_CONTAINER Konteyner adı doğrudan (verilirse MONGO_SERVICE kullanılmaz) +# DB_NAME Yedeklenecek veritabanı [kakeibo] +# BACKUP_DIR Yedek klasörü [/var/backups/kakeibo] +# KEEP Saklanacak yedek sayısı (en yeniler) [14] +# BACKUP_GROUP Verilirse klasör/dosyalar bu gruba okunur yapılır (yedeği çeken SSH kullanıcısı için) +# +# Her yedek: yazılır → gzip bütünlüğü + mongorestore --dryRun ile denetlenir → ancak sonra adı verilir. +# Hata olursa çıkış kodu ≠ 0 (systemd günlüğünde görünür), yarım dosya kalmaz. +set -euo pipefail + +MONGO_SERVICE=${MONGO_SERVICE:-kakeibo-kakeibomongo-fibzhc} +DB_NAME=${DB_NAME:-kakeibo} +BACKUP_DIR=${BACKUP_DIR:-/var/backups/kakeibo} +KEEP=${KEEP:-14} + +log() { echo "[kakeibo-backup] $*"; } +die() { echo "[kakeibo-backup] HATA: $*" >&2; exit 1; } + +container=${MONGO_CONTAINER:-} +if [[ -z $container ]]; then + container=$(docker ps -q -f "label=com.docker.swarm.service.name=$MONGO_SERVICE" | head -n1) + [[ -n $container ]] || container=$(docker ps -q -f "name=$MONGO_SERVICE" | head -n1) +fi +[[ -n $container ]] || die "MongoDB konteyneri bulunamadı ($MONGO_SERVICE). Servis çalışıyor mu?" + +umask 027 +mkdir -p "$BACKUP_DIR" +if [[ -n ${BACKUP_GROUP:-} ]]; then + chgrp "$BACKUP_GROUP" "$BACKUP_DIR" + chmod 750 "$BACKUP_DIR" +fi + +stamp=$(date +%Y-%m-%d_%H%M) +file="$BACKUP_DIR/$DB_NAME-$stamp.archive.gz" +tmp="$file.partial" +trap 'rm -f "$tmp"' EXIT + +log "$DB_NAME yedekleniyor → $file" +docker exec "$container" mongodump --db="$DB_NAME" --archive --gzip --quiet > "$tmp" \ + || die "mongodump başarısız" +[[ -s $tmp ]] || die "yedek dosyası boş" +gzip -t "$tmp" || die "yedek dosyası bozuk (gzip)" +docker exec -i "$container" mongorestore --archive --gzip --dryRun --quiet \ + --nsInclude="$DB_NAME.*" < "$tmp" || die "yedek okunamadı (mongorestore --dryRun)" + +mv "$tmp" "$file" +[[ -n ${BACKUP_GROUP:-} ]] && chgrp "$BACKUP_GROUP" "$file" +log "tamam: $(du -h "$file" | cut -f1)" + +# Eski yedekler: en yeni $KEEP tanesi kalır (gün atlansa da sayı korunur) +mapfile -t old < <(ls -1t "$BACKUP_DIR"/"$DB_NAME"-*.archive.gz | tail -n +"$((KEEP + 1))") +for f in "${old[@]}"; do + rm -f -- "$f" + log "silindi (eski): $(basename "$f")" +done diff --git a/scripts/restore.sh b/scripts/restore.sh new file mode 100755 index 0000000..ad32730 --- /dev/null +++ b/scripts/restore.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Yedeği geri yükler ve koleksiyon sayılarını gösterir. +# +# restore.sh [--force] +# +# - Yedek hangi DB'den alındıysa (dosya adının başı, ör. "kakeibo-…") ondan 'ye aktarılır. +# - Deneme için ayrı bir hedef kullanın (ör. kakeibo_restore_test); bitince silin: +# docker exec mongosh --quiet kakeibo_restore_test --eval 'db.dropDatabase()' +# - Hedef **canlı veritabanının kendisiyse** (felaket sonrası) `--force` gerekir; hedef önce silinir (--drop). +# - COMPARE_DB=kakeibo verilirse hedefteki sayılar o veritabanıyla karşılaştırılır (fark → çıkış kodu 1). +# +# Konteyner backup.sh ile aynı ayarlarla bulunur (MONGO_SERVICE / MONGO_CONTAINER, BACKUP_DIR). +set -euo pipefail + +MONGO_SERVICE=${MONGO_SERVICE:-kakeibo-kakeibomongo-fibzhc} +BACKUP_DIR=${BACKUP_DIR:-/var/backups/kakeibo} +die() { echo "[kakeibo-restore] HATA: $*" >&2; exit 1; } + +[[ $# -ge 2 ]] || die "kullanım: restore.sh [--force]" +src=$1 target=$2 force=${3:-} +if [[ $src == latest ]]; then + src=$(ls -1t "$BACKUP_DIR"/*.archive.gz 2>/dev/null | head -n1) || true + [[ -n $src ]] || die "$BACKUP_DIR içinde yedek yok" +fi +[[ -f $src ]] || die "dosya yok: $src" +[[ $target =~ ^[A-Za-z0-9_-]+$ ]] || die "hedef veritabanı adı geçersiz: $target" + +# Dosya adı: -YYYY-MM-DD_HHMM.archive.gz +base=$(basename "$src") +from=${base%-????-??-??_????.archive.gz} +[[ $from != "$base" ]] || die "dosya adından kaynak veritabanı anlaşılamadı: $base" + +container=${MONGO_CONTAINER:-} +if [[ -z $container ]]; then + container=$(docker ps -q -f "label=com.docker.swarm.service.name=$MONGO_SERVICE" | head -n1) + [[ -n $container ]] || container=$(docker ps -q -f "name=$MONGO_SERVICE" | head -n1) +fi +[[ -n $container ]] || die "MongoDB konteyneri bulunamadı" + +drop=() +exists=$(docker exec "$container" mongosh --quiet --eval \ + "db.adminCommand({listDatabases:1}).databases.some(d => d.name === '$target')") +if [[ $exists == true ]]; then + [[ $force == --force ]] || die "'$target' zaten var. Üzerine yazmak için --force (önce silinir)." + drop=(--drop) +fi + +echo "[kakeibo-restore] $base → $target" +docker exec -i "$container" mongorestore --archive --gzip --quiet "${drop[@]}" \ + --nsInclude="$from.*" --nsFrom="$from.*" --nsTo="$target.*" < "$src" \ + || die "geri yükleme başarısız (yedek bozuk olabilir). '$target' yarım kalmış olabilir; kontrol edip silin." + +counts() { + docker exec "$container" mongosh --quiet "$1" --eval \ + 'db.getCollectionNames().sort().map(c => c + " " + db[c].countDocuments({})).join("\n")' +} +echo "[kakeibo-restore] $target koleksiyonları (kayıt sayısı):" +after=$(counts "$target"); echo "$after" | sed 's/^/ /' + +if [[ -n ${COMPARE_DB:-} ]]; then + if diff <(counts "$COMPARE_DB") <(echo "$after") >/dev/null; then + echo "[kakeibo-restore] ✅ $COMPARE_DB ile aynı" + else + echo "[kakeibo-restore] ⚠️ $COMPARE_DB ile fark var (yedekten sonra yeni kayıt girildiyse normaldir):" + diff <(counts "$COMPARE_DB") <(echo "$after") | sed 's/^/ /' + exit 1 + fi +fi