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 }