package utils import ( "archive/tar" "compress/gzip" "context" "fmt" "io" "os" "path/filepath" "strconv" "strings" "time" "github.com/rs/zerolog/log" ) const confirmChar = "Y" // addToArchive adds a file or directory (recursively) to the tar writer. // The baseName is the desired root name in the archive. // //nolint:funlen // it's ok func addToArchive(tw *tar.Writer, filename, baseName, basePath string) error { // resolve the absolute path to eliminate relative components like ../ absPath, err := filepath.Abs(filename) if err != nil { return fmt.Errorf("unable to resolve absolute path for %s: %v", filename, err) } file, err := os.Open(absPath) if err != nil { return err } defer file.Close() //nolint: errcheck // deferred info, err := file.Stat() if err != nil { return err } // compute the relative path within the archive // start with baseName and append the relative path from basePath relPath, err := filepath.Rel(basePath, absPath) if err != nil { return fmt.Errorf("unable to compute relative path for %s: %v", absPath, err) } // combine baseName with the relative path if relPath == "." { relPath = baseName } else { relPath = filepath.Join(baseName, relPath) } relPath = filepath.ToSlash(relPath) if info.IsDir() { header, err := tar.FileInfoHeader(info, "") //nolint:govet // shadow ok if err != nil { return err } header.Name = relPath if err := tw.WriteHeader(header); err != nil { //nolint:govet // shadow ok return err } entries, err := os.ReadDir(absPath) if err != nil { return err } for _, entry := range entries { entryPath := filepath.Join(absPath, entry.Name()) if err := addToArchive(tw, entryPath, baseName, basePath); err != nil { return err } } return nil } header, err := tar.FileInfoHeader(info, "") if err != nil { return err } header.Name = relPath if err := tw.WriteHeader(header); err != nil { //nolint:govet // shadow ok return err } _, err = io.Copy(tw, file) return err } // CreateArchive creates a gzip tar archive in the `destDir` path including `files` and returns // the generated archive path. 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 // deferred gw := gzip.NewWriter(file) defer gw.Close() //nolint: errcheck // deferred tw := tar.NewWriter(gw) defer tw.Close() //nolint: errcheck // deferred for _, f := range files { absPath, err := filepath.Abs(f) if err != nil { return "", fmt.Errorf("unable to resolve absolute path for %s: %v", f, err) } baseName := filepath.Base(f) if err := addToArchive(tw, f, baseName, absPath); 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 } func NewColumn(name, value string) Column { return Column{name: name, value: value} } type Row struct { next *Row // useful for grouping rows columns []Column } func (r *Row) AddNext(row *Row) { r.next = row } type Table struct { rowSeparator string colSeparator string rows []*Row header header cursor int } func NewTable(options ...TableOption) Table { table := Table{} for _, o := range options { o(&table) } table.rows = []*Row{} return table } func (t *Table) AddRow(columns ...Column) (*Row, error) { if len(columns) == 0 { return &Row{}, nil } maxNbCols := len(t.header.columns) if len(columns) > maxNbCols { return nil, fmt.Errorf("invalid column number, should be %d", maxNbCols) } row := &Row{columns: make([]Column, maxNbCols)} t.rows = append(t.rows, row) for idx := range columns { header, ok := t.header.headerMeta[strings.ToLower(columns[idx].name)] if !ok { return nil, 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] + "..." } row.columns[header.pos].value = value row.columns[header.pos].name = columns[idx].name } t.cursor++ return row, 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.rows { lineParts := []string{} for idy := range t.rows[idx].columns { lineParts = append(lineParts, fmt.Sprintf( "%-*s", t.header.columns[idy].length, t.rows[idx].columns[idy].value), ) } if idx < len(t.rows) { if t.rows[idx].next != nil { table = append(table, strings.Join(lineParts, t.colSeparator)) } else { table = append(table, strings.Join(lineParts, t.colSeparator), rowHr) } } } fmt.Println(strings.Join(table, "\n")) }