hmdeploy/utils/utils.go
2025-04-29 14:14:40 +02:00

273 lines
5.3 KiB
Go

package utils
import (
"archive/tar"
"compress/gzip"
"context"
"fmt"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/rs/zerolog/log"
)
const confirmChar = "Y"
func addToArchive(tw *tar.Writer, filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close() //nolint: errcheck // defered
info, err := file.Stat()
if err != nil {
return err
}
header, err := tar.FileInfoHeader(info, info.Name())
if err != nil {
return err
}
header.Name = filepath.Base(file.Name())
if err = tw.WriteHeader(header); err != nil {
return err
}
_, err = io.Copy(tw, file)
return err
}
// CreateArchive creates a gzip tar archive in the `destDir` path including `files`.
func CreateArchive(destDir, name string, files ...string) (string, error) {
now := time.Now().UTC()
archivePath := filepath.Join(
destDir,
fmt.Sprintf("%s-%s.tar.gz", name, strings.ReplaceAll(now.Format(time.RFC3339), ":", "-")),
)
file, err := os.Create(archivePath)
if err != nil {
return "", fmt.Errorf("unable to create archive=%s, err=%v", archivePath, err)
}
defer file.Close() //nolint: errcheck // defered
gw := gzip.NewWriter(file)
defer gw.Close() //nolint: errcheck // defered
tw := tar.NewWriter(gw)
defer tw.Close() //nolint: errcheck // defered
for _, f := range files {
if err := addToArchive(tw, f); err != nil {
return "", fmt.Errorf(
"unable to add file=%s to archive=%s, err=%v",
f,
archivePath,
err,
)
}
}
return archivePath, nil
}
func Confirm(ctx context.Context, destroy bool) error {
logMsg := "deploy"
if destroy {
logMsg = "undeploy"
}
log.Warn().Msg(fmt.Sprintf("Confirm to %s ? Y to confirm", logMsg))
var text string
if _, err := fmt.Fscanf(os.Stdin, "%s", &text); err != nil {
if !strings.Contains(err.Error(), "newline") {
return err
}
}
if !strings.EqualFold(text, confirmChar) {
log.Info().Msg("Ok, bye !")
os.Exit(0)
}
return nil
}
// CatchAndConvertNumber finds and converts the first consecutives digits in integer.
func CatchAndConvertNumber(value string) (int, error) {
buf := []rune{}
hasLeftTrimmed := false
for _, c := range value {
if c >= '0' && c <= '9' {
hasLeftTrimmed = true
buf = append(buf, c)
continue
}
// early return if no number catch
if hasLeftTrimmed {
break
}
}
return strconv.Atoi(string(buf))
}
type HeaderColumn struct {
Name string
Length int
pos int
}
type Header struct {
BorderStyle string
headerMeta map[string]*HeaderColumn
Columns []HeaderColumn
}
type TableOption func(*Table)
func WithHeaderBorderStyle(style string) TableOption {
return func(t *Table) {
t.Header.BorderStyle = style
}
}
func WithRowSeparator(separator string) TableOption {
return func(t *Table) {
t.RowSeparator = separator
}
}
func WithColSeparator(separator string) TableOption {
return func(t *Table) {
t.ColSeparator = separator
}
}
func WithHeader(name string, length int) TableOption {
return func(t *Table) {
if t.Header.Columns == nil {
t.Header.Columns = []HeaderColumn{}
}
pos := len(t.Header.Columns)
t.Header.Columns = append(
t.Header.Columns,
HeaderColumn{Name: name, Length: length, pos: pos},
)
if t.Header.headerMeta == nil {
t.Header.headerMeta = map[string]*HeaderColumn{}
}
t.Header.headerMeta[strings.ToLower(name)] = &t.Header.Columns[pos]
}
}
type Column struct {
Name string
Value string
}
type Table struct {
RowSeparator string
ColSeparator string
data [][]string
Header Header
cursor int
}
func NewTable(options ...TableOption) Table {
table := Table{}
for _, o := range options {
o(&table)
}
table.data = [][]string{}
return table
}
func (t *Table) AddRow(columns ...Column) error {
maxNbCols := len(t.Header.Columns)
if len(columns) > maxNbCols {
return fmt.Errorf("invalid column number, should be %d", maxNbCols)
}
rowData := make([]string, maxNbCols)
t.data = append(t.data, rowData)
for idx := range columns {
header, ok := t.Header.headerMeta[strings.ToLower(columns[idx].Name)]
if !ok {
return fmt.Errorf("no corresponding %s column exist", columns[idx].Name)
}
value := columns[idx].Value
if len(value) > header.Length {
log.Debug().
Str("column", columns[idx].Name).
Str("value", value).
Msg("col value too long, trimming...")
value = value[:header.Length-3] + "..."
}
t.data[t.cursor][header.pos] = value
}
t.cursor++
return nil
}
func (t *Table) Render() {
table := []string{}
// header
headerParts := []string{}
for idx := range t.Header.Columns {
headerParts = append(
headerParts,
fmt.Sprintf("%-*s", t.Header.Columns[idx].Length, t.Header.Columns[idx].Name),
)
}
header := strings.Join(headerParts, t.ColSeparator)
border := ""
for i := 0; i < len(header); i++ {
border += t.Header.BorderStyle
}
rowHr := ""
for i := 0; i < len(header); i++ {
rowHr += t.RowSeparator
}
table = append(table, border, header, border)
// data
for idx := range t.data {
lineParts := []string{}
for idy := range t.data[idx] {
lineParts = append(lineParts, fmt.Sprintf(
"%-*s",
t.Header.Columns[idy].Length,
t.data[idx][idy]),
)
}
if idx+1 < len(t.data) {
table = append(table, strings.Join(lineParts, t.ColSeparator), rowHr)
}
}
fmt.Println(strings.Join(table, "\n"))
}