137 lines
3.9 KiB
Go
137 lines
3.9 KiB
Go
|
|
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
|
||
|
|
}
|