package cmd import ( "bytes" "context" "errors" "fmt" "log" "net/smtp" "os" "strings" "time" "gitea.urkob.com/urko/backblaze-backup/internal/services/backblaze" "gitea.urkob.com/urko/backblaze-backup/internal/services/email" "gitea.urkob.com/urko/backblaze-backup/kit" "gitea.urkob.com/urko/backblaze-backup/kit/config" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) var Check = &cobra.Command{ Use: "check", Short: "Compares local backup files with those in a Backblaze B2 bucket and sends a summary email.", Long: `This command compares the list of files in a local backup directory against the files in a specified Backblaze B2 bucket. The operation is performed concurrently and is time-bound, set by default to 5 minutes. If discrepancies are found, i.e., some files exist only locally or only on the cloud, these are logged and sent via email as attachments. The email contains two text attachments: - 'Local-Files-Not-In-B2.txt': Lists files that are in the local backup directory but not in the B2 bucket. - 'B2-Files-Not-In-Local.txt': Lists files that are in the B2 bucket but not in the local backup directory. The command requires two flags: - '--dir': The path of the local backup directory - '--bucket': The name of the Backblaze B2 bucket An environment variable 'BACKBLAZE_ENV' can be set to 'dev' to load environment variables from a .env file in the root directory.`, Run: func(cmd *cobra.Command, args []string) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) // or any appropriate time defer cancel() log.SetFlags(log.Ldate | log.Lmicroseconds) backupDir, err := cmd.Flags().GetString("dir") if err != nil { panic(fmt.Errorf("dir %w", err)) } bucketName, err := cmd.Flags().GetString("bucket") if err != nil { panic(fmt.Errorf("bucket %w", err)) } envFile := "" if os.Getenv("BACKBLAZE_ENV") == "dev" { envFile = kit.RootDir() + "/.env" } cfg := config.NewConfig(envFile) mailSrv := email.NewMailService(email.MailServiceConfig{ Auth: smtp.PlainAuth("", cfg.MailUser, cfg.MailPassword, cfg.MailHost), Host: cfg.MailHost, Port: cfg.MailPort, From: cfg.MailFrom, }, ) logger := logrus.New() logger.SetLevel(logrus.Level(cfg.LogLevel)) b, err := backblaze.NewBackBlaze(ctx, logger, cfg.BbId, cfg.BbKey) if err != nil { panic(fmt.Errorf("NewBackBlaze %w", err)) } logger.Info("start check") defer logger.Info("end check") msgChan := make(chan backblaze.B2Local) go b.CompareConcurrent(ctx, backupDir, bucketName, msgChan) reportBuilder := strings.Builder{} countLocal := 0 countOK := 0 reportBuilder.WriteString(fmt.Sprintf("Local files within `%s` path already in `%s` bucket:\n", backupDir, bucketName)) cloudBuilder := strings.Builder{} cloudBuilder.WriteString(fmt.Sprintf("List of B2 files within `%s` bucket not found in local path `%s`\n", bucketName, backupDir)) countNotInLocal := 0 localBuilder := strings.Builder{} localBuilder.WriteString(fmt.Sprintf("List of local files in `%s` not found in B2 bucket `%s`\n", backupDir, bucketName)) countNotInCloud := 0 loop: for { select { case <-ctx.Done(): // Handle the timeout or cancellation // Release any resources here if ctx.Err() == context.DeadlineExceeded { logger.Error("Operation timed out") } else if ctx.Err() == context.Canceled { logger.Error("Operation canceled") } break loop case msg := <-msgChan: if msg.Err == nil && msg.File == "" { countLocal = msg.LocalCount break loop } if errors.Is(msg.Err, backblaze.ErrCloudNotInLocal) { logger.Debug(msg.File + ": B2 file not found in local") cloudBuilder.WriteString(msg.File + "\n") countNotInLocal++ continue } if errors.Is(msg.Err, backblaze.ErrLocalNotInCloud) { logger.Debug(msg.File + ": local file not found in B2") localBuilder.WriteString(msg.File + "\n") countNotInCloud++ continue } reportBuilder.WriteString(msg.File + " OK" + "\n") countOK++ } } if countNotInLocal > 0 { reportBuilder.WriteString("\n") reportBuilder.WriteString(cloudBuilder.String()) } if countNotInCloud > 0 { reportBuilder.WriteString("\n") reportBuilder.WriteString(localBuilder.String()) } if err := mailSrv.SendOK(email.EmailWithAttachments{ To: cfg.MailTo, Bucket: bucketName, BackupDir: backupDir, CountNotInLocal: countNotInLocal, CountNotInCloud: countNotInCloud, CountOK: countOK, CountLocal: countLocal, Attachments: []email.EmailAttachment{{File: bytes.NewReader([]byte(reportBuilder.String())), Title: fmt.Sprintf("%s-check-report.txt", bucketName)}}, }); err != nil { panic(fmt.Errorf("error while send email: %w", err)) } }, }