Files

137 lines
3.9 KiB
Go
Raw Permalink Normal View History

package service
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"time"
"dating-app/internal/config"
)
var (
ErrBackupNotFound = errors.New("فایل بک‌آپ یافت نشد")
backupNamePattern = regexp.MustCompile(`^backup_\d{8}_\d{6}\.sql\.gz$`)
)
type BackupInfo struct {
Filename string `json:"filename"`
SizeBytes int64 `json:"size_bytes"`
CreatedAt time.Time `json:"created_at"`
}
// BackupService با اجرای pg_dump یک نسخه‌ی پشتیبان فشرده از کل دیتابیس
// می‌سازه. برای گرفتن بک‌آپ نیازی به هیچ ابزار بیرونی/سرویس ابری نیست؛
// فایل‌ها روی همون دیسک سرور (پوشه‌ی dir) نگه داشته می‌شن.
type BackupService struct {
cfg *config.Config
dir string
}
func NewBackupService(cfg *config.Config, dir string) *BackupService {
return &BackupService{cfg: cfg, dir: dir}
}
// Create یک بک‌آپ جدید می‌گیره: خروجی pg_dump به‌صورت stream به gzip داده
// می‌شه تا حافظه و فضای دیسک مصرفی کمتر باشه.
func (s *BackupService) Create() (*BackupInfo, error) {
if err := os.MkdirAll(s.dir, 0o755); err != nil {
return nil, fmt.Errorf("ساخت پوشه‌ی بک‌آپ ناموفق بود: %w", err)
}
filename := fmt.Sprintf("backup_%s.sql.gz", time.Now().Format("20060102_150405"))
fullPath := filepath.Join(s.dir, filename)
out, err := os.Create(fullPath)
if err != nil {
return nil, fmt.Errorf("ساخت فایل بک‌آپ ناموفق بود: %w", err)
}
gz := gzip.NewWriter(out)
cmd := exec.Command("pg_dump",
"-h", s.cfg.DBHost,
"-p", s.cfg.DBPort,
"-U", s.cfg.DBUser,
"-d", s.cfg.DBName,
"--no-password",
"--format=plain",
)
cmd.Env = append(os.Environ(), "PGPASSWORD="+s.cfg.DBPassword)
cmd.Stdout = gz
var stderr bytes.Buffer
cmd.Stderr = &stderr
runErr := cmd.Run()
gzErr := gz.Close()
outErr := out.Close()
if runErr != nil {
os.Remove(fullPath)
return nil, fmt.Errorf("pg_dump ناموفق بود: %v - %s", runErr, stderr.String())
}
if gzErr != nil {
os.Remove(fullPath)
return nil, gzErr
}
if outErr != nil {
return nil, outErr
}
info, err := os.Stat(fullPath)
if err != nil {
return nil, err
}
return &BackupInfo{Filename: filename, SizeBytes: info.Size(), CreatedAt: info.ModTime()}, nil
}
// List بک‌آپ‌های موجود رو، جدیدترین اول، برمی‌گردونه.
func (s *BackupService) List() ([]BackupInfo, error) {
entries, err := os.ReadDir(s.dir)
if err != nil {
if os.IsNotExist(err) {
return []BackupInfo{}, nil
}
return nil, err
}
backups := make([]BackupInfo, 0, len(entries))
for _, e := range entries {
if e.IsDir() || !backupNamePattern.MatchString(e.Name()) {
continue
}
info, err := e.Info()
if err != nil {
continue
}
backups = append(backups, BackupInfo{
Filename: e.Name(),
SizeBytes: info.Size(),
CreatedAt: info.ModTime(),
})
}
sort.Slice(backups, func(i, j int) bool { return backups[i].CreatedAt.After(backups[j].CreatedAt) })
return backups, nil
}
// ResolvePath یک نام فایل بک‌آپ (که مستقیماً از پارامتر URL میاد) رو به مسیر
// کامل تبدیل می‌کنه. چون ورودی از کاربره، صراحتاً باید در برابر path traversal
// (مثلاً "../../etc/passwd") محافظت بشه؛ برای همین فقط اسم‌هایی که دقیقاً با
// الگوی خودمون ساخته شدن (backup_YYYYMMDD_HHMMSS.sql.gz) قبول می‌شن.
func (s *BackupService) ResolvePath(filename string) (string, error) {
if !backupNamePattern.MatchString(filename) {
return "", ErrBackupNotFound
}
full := filepath.Join(s.dir, filename)
if _, err := os.Stat(full); err != nil {
return "", ErrBackupNotFound
}
return full, nil
}