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") }