packer/main.go
2023-08-05 15:14:44 +02:00

187 lines
4.6 KiB
Go

package main
import (
"archive/zip"
"errors"
"fmt"
"io"
"os"
fp "path/filepath"
re "regexp"
"github.com/akamensky/argparse"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
var ErrEmptyRegex = errors.New("empty regex after build")
func initLogger() {
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = log.With().Caller().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
// parseArguments parses CLI arguments to launch `packer`.
func parseArguments() (string, string, string, bool, error) {
parser := argparse.NewParser("packer", "zip files where filenames match a regex")
regex := parser.String("r", "regex", &argparse.Options{Required: true, Help: "filename Regex"})
outputPath := parser.String("o", "output-path", &argparse.Options{Required: true, Help: "zip output path (including zip name)"})
directory := parser.String("d", "directory", &argparse.Options{Required: false, Help: "root directory to check files", Default: "."})
remove := parser.Flag("x", "remove", &argparse.Options{Required: false, Help: "remove files after archive creation", Default: false})
if err := parser.Parse(os.Args); err != nil {
return "", "", "", false, fmt.Errorf("parsing error: %s", err)
}
return *regex, *directory, *outputPath, *remove, nil
}
// buildRegex builds POSIX regex from a string.
// WARN: lookahead and lookbehind are not supported.
func buildRegex(regex string) (re.Regexp, error) {
rc, err := re.Compile(regex)
if rc == nil {
return re.Regexp{}, ErrEmptyRegex
}
return *rc, err
}
// walkFilteredDir walks into a directory and its subdirectories and find files that match a regex.
func walkDirFiltered(directory string, regex re.Regexp) ([]string, error) {
var files []string
err := fp.Walk(directory,
func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && regex.MatchString(path) {
log.Info().Str("path", path).Msg("file found")
files = append(files, path)
}
return err
})
return files, err
}
// writeFileContent writes file content into zipped file (chunking is used to avoid memory errors with large files).
func writeFileContent(f *os.File, zf io.Writer) (err error) {
const chunkSize = 4
b := make([]byte, chunkSize)
defer f.Close()
for {
if _, err = f.Read(b); err != nil {
if err != io.EOF {
continue
}
break
}
zf.Write(b)
}
return err
}
// generateZip creates a zip archive from an output path and filepaths slice.
func generateZip(outputPath string, filepaths []string) error {
// create a zipfile from `outputPath`
zipPath, createErr := os.Create(outputPath)
if createErr != nil {
return createErr
}
defer zipPath.Close()
// create a new zip archive from `zipPath`
w := zip.NewWriter(zipPath)
for _, filepath := range filepaths {
// open matching regex file
f, openErr := os.Open(filepath)
if openErr != nil {
log.Err(openErr)
continue
}
// create a zipped file
zf, zipErr := w.Create(filepath)
if zipErr != nil {
log.Err(zipErr)
f.Close()
continue
}
// write file content into zipped file
writeErr := writeFileContent(f, zf)
// catch sentinel error `io.EOF`
if writeErr != nil && writeErr != io.EOF {
log.Err(zipErr)
f.Close()
continue
}
// clean up opened file
f.Close()
}
// make sure to check the error on Close.
if err := w.Close(); err != nil {
return fmt.Errorf("error occured while closing the zip archive : %w", err)
}
return nil
}
func removeFiles(filepaths []string) {
for _, filepath := range filepaths {
if err := os.Remove(filepath); err != nil {
log.Err(err)
}
}
}
func main() {
initLogger()
// parse arguments
regex, directory, outputPath, remove, err := parseArguments()
if err != nil {
log.Fatal().Err(err).Msg("error while parsing args")
}
// build regex from string
rc, err := buildRegex(regex)
if err != nil {
log.Fatal().Err(err).Str("regex", regex).Msg("error while compiling regex")
}
// walk into directory
log.Info().Str("regex", rc.String()).Str("directory", directory).Msg("looking for files...")
files, err := walkDirFiltered(directory, rc)
if err != nil {
log.Fatal().Err(err)
}
if len(files) == 0 {
log.Warn().Str("regex", rc.String()).Str("directory", directory).Msg("no file found in the directory")
return
}
log.Info().Int("len", len(files)).Msg("files found")
// generating zip archive
if err := generateZip(outputPath, files); err != nil {
log.Fatal().Err(err)
}
if remove {
log.Info().Msg("cleaning up archive files...")
removeFiles(files)
}
log.Info().Str("output path", outputPath).Msg("zip archive generated")
}