353 lines
		
	
	
		
			7.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			353 lines
		
	
	
		
			7.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"
 | |
| 
 | |
| // 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+1 < 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"))
 | |
| }
 | 
