mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-06 20:02:41 +08:00
Three community-requested features: 1. CLI password reset: `backupx reset-password --username admin --password xxx` Docker users can run via `docker exec`. No full app init needed. 2. Audit logging: async fire-and-forget audit trail for all key operations (login, CRUD on tasks/targets/records, settings changes). New UI page at /audit with category filter and pagination. 3. Multi-source path backup: file backup tasks now support multiple source directories packed into a single tar archive. Backward compatible with existing single sourcePath field.
201 lines
7.1 KiB
Go
201 lines
7.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
stdhttp "net/http"
|
|
"time"
|
|
|
|
"backupx/server/internal/backup"
|
|
backupretention "backupx/server/internal/backup/retention"
|
|
"backupx/server/internal/config"
|
|
"backupx/server/internal/database"
|
|
aphttp "backupx/server/internal/http"
|
|
"backupx/server/internal/logger"
|
|
"backupx/server/internal/notify"
|
|
"backupx/server/internal/repository"
|
|
"backupx/server/internal/scheduler"
|
|
"backupx/server/internal/security"
|
|
"backupx/server/internal/service"
|
|
"backupx/server/internal/storage"
|
|
"backupx/server/internal/storage/codec"
|
|
"backupx/server/internal/storage/googledrive"
|
|
"backupx/server/internal/storage/localdisk"
|
|
storageAliyun "backupx/server/internal/storage/aliyun"
|
|
storageFTP "backupx/server/internal/storage/ftp"
|
|
storageTencent "backupx/server/internal/storage/tencent"
|
|
storageQiniu "backupx/server/internal/storage/qiniu"
|
|
storageS3 "backupx/server/internal/storage/s3"
|
|
storageWebDAV "backupx/server/internal/storage/webdav"
|
|
"go.uber.org/zap"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type Application struct {
|
|
cfg config.Config
|
|
version string
|
|
logger *zap.Logger
|
|
db *gorm.DB
|
|
httpServer *stdhttp.Server
|
|
scheduler *scheduler.Service
|
|
}
|
|
|
|
func New(ctx context.Context, cfg config.Config, version string) (*Application, error) {
|
|
appLogger, err := logger.New(cfg.Log)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("init logger: %w", err)
|
|
}
|
|
|
|
db, err := database.Open(cfg.Database, appLogger)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("init database: %w", err)
|
|
}
|
|
|
|
userRepo := repository.NewUserRepository(db)
|
|
systemConfigRepo := repository.NewSystemConfigRepository(db)
|
|
storageTargetRepo := repository.NewStorageTargetRepository(db)
|
|
backupTaskRepo := repository.NewBackupTaskRepository(db)
|
|
backupRecordRepo := repository.NewBackupRecordRepository(db)
|
|
notificationRepo := repository.NewNotificationRepository(db)
|
|
oauthSessionRepo := repository.NewOAuthSessionRepository(db)
|
|
resolvedSecurity, err := service.ResolveSecurity(ctx, cfg.Security, systemConfigRepo)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve security config: %w", err)
|
|
}
|
|
|
|
jwtManager := security.NewJWTManager(resolvedSecurity.JWTSecret, config.MustJWTDuration(cfg.Security))
|
|
rateLimiter := security.NewLoginRateLimiter(5, time.Minute)
|
|
authService := service.NewAuthService(userRepo, systemConfigRepo, jwtManager, rateLimiter)
|
|
systemService := service.NewSystemService(cfg, version, time.Now().UTC())
|
|
configCipher := codec.NewConfigCipher(resolvedSecurity.EncryptionKey)
|
|
storageRegistry := storage.NewRegistry(
|
|
localdisk.NewFactory(),
|
|
storageS3.NewFactory(),
|
|
storageWebDAV.NewFactory(),
|
|
googledrive.NewFactory(),
|
|
storageAliyun.NewFactory(),
|
|
storageTencent.NewFactory(),
|
|
storageQiniu.NewFactory(),
|
|
storageFTP.NewFactory(),
|
|
)
|
|
storageTargetService := service.NewStorageTargetService(storageTargetRepo, oauthSessionRepo, storageRegistry, configCipher)
|
|
storageTargetService.SetBackupTaskRepository(backupTaskRepo)
|
|
storageTargetService.SetBackupRecordRepository(backupRecordRepo)
|
|
backupTaskService := service.NewBackupTaskService(backupTaskRepo, storageTargetRepo, configCipher)
|
|
backupRunnerRegistry := backup.NewRegistry(backup.NewFileRunner(), backup.NewSQLiteRunner(), backup.NewMySQLRunner(nil), backup.NewPostgreSQLRunner(nil), backup.NewSAPHANARunner(nil))
|
|
logHub := backup.NewLogHub()
|
|
retentionService := backupretention.NewService(backupRecordRepo)
|
|
notifyRegistry := notify.NewRegistry(notify.NewEmailNotifier(), notify.NewWebhookNotifier(), notify.NewTelegramNotifier())
|
|
notificationService := service.NewNotificationService(notificationRepo, notifyRegistry, configCipher)
|
|
backupExecutionService := service.NewBackupExecutionService(backupTaskRepo, backupRecordRepo, storageTargetRepo, storageRegistry, backupRunnerRegistry, logHub, retentionService, configCipher, notificationService, cfg.Backup.TempDir, cfg.Backup.MaxConcurrent)
|
|
schedulerService := scheduler.NewService(backupTaskRepo, backupExecutionService, appLogger)
|
|
backupTaskService.SetScheduler(schedulerService)
|
|
backupRecordService := service.NewBackupRecordService(backupRecordRepo, backupExecutionService, logHub)
|
|
dashboardService := service.NewDashboardService(backupTaskRepo, backupRecordRepo, storageTargetRepo)
|
|
settingsService := service.NewSettingsService(systemConfigRepo)
|
|
|
|
// Audit
|
|
auditLogRepo := repository.NewAuditLogRepository(db)
|
|
auditService := service.NewAuditService(auditLogRepo)
|
|
authService.SetAuditService(auditService)
|
|
|
|
// Database discovery
|
|
databaseDiscoveryService := service.NewDatabaseDiscoveryService(backup.NewOSCommandExecutor())
|
|
|
|
// Cluster: Node management
|
|
nodeRepo := repository.NewNodeRepository(db)
|
|
nodeService := service.NewNodeService(nodeRepo)
|
|
if err := nodeService.EnsureLocalNode(ctx); err != nil {
|
|
appLogger.Warn("failed to ensure local node", zap.Error(err))
|
|
}
|
|
|
|
router := aphttp.NewRouter(aphttp.RouterDependencies{
|
|
Config: cfg,
|
|
Version: version,
|
|
Logger: appLogger,
|
|
AuthService: authService,
|
|
SystemService: systemService,
|
|
StorageTargetService: storageTargetService,
|
|
BackupTaskService: backupTaskService,
|
|
BackupExecutionService: backupExecutionService,
|
|
BackupRecordService: backupRecordService,
|
|
NotificationService: notificationService,
|
|
DashboardService: dashboardService,
|
|
SettingsService: settingsService,
|
|
NodeService: nodeService,
|
|
DatabaseDiscoveryService: databaseDiscoveryService,
|
|
AuditService: auditService,
|
|
JWTManager: jwtManager,
|
|
UserRepository: userRepo,
|
|
SystemConfigRepo: systemConfigRepo,
|
|
})
|
|
|
|
httpServer := &stdhttp.Server{
|
|
Addr: cfg.Address(),
|
|
Handler: router,
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
}
|
|
|
|
return &Application{
|
|
cfg: cfg,
|
|
version: version,
|
|
logger: appLogger,
|
|
db: db,
|
|
httpServer: httpServer,
|
|
scheduler: schedulerService,
|
|
}, nil
|
|
}
|
|
|
|
func (a *Application) Run(ctx context.Context) error {
|
|
if a.scheduler != nil {
|
|
if err := a.scheduler.Start(context.Background()); err != nil {
|
|
return fmt.Errorf("start scheduler: %w", err)
|
|
}
|
|
}
|
|
errCh := make(chan error, 1)
|
|
go func() {
|
|
a.logger.Info("http server listening", zap.String("addr", a.cfg.Address()), zap.String("version", a.version))
|
|
if err := a.httpServer.ListenAndServe(); err != nil && !errors.Is(err, stdhttp.ErrServerClosed) {
|
|
errCh <- err
|
|
return
|
|
}
|
|
errCh <- nil
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
a.logger.Info("shutdown signal received")
|
|
if err := a.httpServer.Shutdown(shutdownCtx); err != nil {
|
|
return fmt.Errorf("shutdown http server: %w", err)
|
|
}
|
|
if a.scheduler != nil {
|
|
if err := a.scheduler.Stop(context.Background()); err != nil {
|
|
return fmt.Errorf("stop scheduler: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
case err := <-errCh:
|
|
if err != nil {
|
|
return fmt.Errorf("serve http: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (a *Application) Close() {
|
|
if a.logger != nil {
|
|
_ = a.logger.Sync()
|
|
}
|
|
}
|
|
|
|
func (a *Application) Logger() *zap.Logger {
|
|
return a.logger
|
|
}
|
|
|
|
func ErrorField(err error) zap.Field {
|
|
return zap.Error(err)
|
|
}
|