clean code
This commit is contained in:
parent
60499589b1
commit
ccb1b413f8
24
README.md
24
README.md
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
A simple CLI tool to find and zip file corresponding to a given regex.
|
A simple CLI tool to find and zip file corresponding to a given regex.
|
||||||
|
|
||||||
**NOTE**: if you have experience using Linux bash command, prefer use `find` + `zip` command.
|
**NOTE**: if you have experience using Linux bash command, use `find` + `zip` command.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -17,12 +17,24 @@ make build
|
|||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
```bash
|
||||||
|
# help
|
||||||
|
./packer -h
|
||||||
|
```
|
||||||
|
```
|
||||||
|
usage: packer [-h|--help] -r|--regex "<value>" -o|--output-path "<value>"
|
||||||
|
[-d|--directory "<value>"] [-x|--remove]
|
||||||
|
|
||||||
### Arguments
|
zip files where filenames match a regex
|
||||||
* **-r** (**required**) : filename regex
|
|
||||||
* **-o** (**required**) : full path of the zip archive output
|
Arguments:
|
||||||
* **-d** (**optional**) : full path of the directory to check
|
|
||||||
* **-x** (**optional**) : remove original files after successful zip
|
-h --help Print help information
|
||||||
|
-r --regex filename Regex
|
||||||
|
-o --output-path zip output path (including zip name)
|
||||||
|
-d --directory root directory to check files. Default: .
|
||||||
|
-x --remove remove files after archive creation. Default: false
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# template usage
|
# template usage
|
||||||
|
|||||||
11
go.mod
11
go.mod
@ -2,4 +2,13 @@ module gotuto
|
|||||||
|
|
||||||
go 1.18
|
go 1.18
|
||||||
|
|
||||||
require github.com/akamensky/argparse v1.3.1
|
require (
|
||||||
|
github.com/akamensky/argparse v1.4.0
|
||||||
|
github.com/rs/zerolog v1.30.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/mattn/go-colorable v0.1.12 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.14 // indirect
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect
|
||||||
|
)
|
||||||
|
|||||||
17
go.sum
17
go.sum
@ -1,2 +1,15 @@
|
|||||||
github.com/akamensky/argparse v1.3.1 h1:kP6+OyvR0fuBH6UhbE6yh/nskrDEIQgEA1SUXDPjx4g=
|
github.com/akamensky/argparse v1.4.0 h1:YGzvsTqCvbEZhL8zZu2AiA5nq805NZh75JNj4ajn1xc=
|
||||||
github.com/akamensky/argparse v1.3.1/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
|
github.com/akamensky/argparse v1.4.0/go.mod h1:S5kwC7IuDcEr5VeXtGPRVZ5o/FdhcMlQz4IZQuw64xA=
|
||||||
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
|
||||||
|
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||||
|
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
|
github.com/rs/zerolog v1.30.0 h1:SymVODrcRsaRaSInD9yQtKbtWqwsfoPcRff/oRXLj4c=
|
||||||
|
github.com/rs/zerolog v1.30.0/go.mod h1:/tk+P47gFdPXq4QYjvCmT5/Gsug2nagsFWBWhAiSi1w=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo=
|
||||||
|
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
141
main.go
141
main.go
@ -1,69 +1,53 @@
|
|||||||
// packer - a simple CLI tool to find and zip file corresponding to a given regex
|
|
||||||
// the archive keeps the folder structure
|
|
||||||
// find + zip linux CLI works too
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
"archive/zip"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/akamensky/argparse"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
fp "path/filepath"
|
fp "path/filepath"
|
||||||
re "regexp"
|
re "regexp"
|
||||||
|
|
||||||
|
"github.com/akamensky/argparse"
|
||||||
|
"github.com/rs/zerolog"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
// WritterError
|
var ErrEmptyRegex = errors.New("empty regex after build")
|
||||||
// Useless, just to play with `errors.As()` to check specific error
|
|
||||||
type WritterError struct {
|
func initLogger() {
|
||||||
message error
|
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||||
|
log.Logger = log.With().Caller().Logger().Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrorCode interface {
|
// parseArguments parses CLI arguments to launch `packer`.
|
||||||
Code() int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (we WritterError) Error() string {
|
|
||||||
return fmt.Sprintf("error occured while creating archive : %s", we.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (we WritterError) Code() int {
|
|
||||||
return 500
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseArguments - get needed arguments to launch the `packer`
|
|
||||||
// returns 3 strings :
|
|
||||||
// * regex
|
|
||||||
// * outputPath - full path of the zip archive
|
|
||||||
// * directory - which directory to check
|
|
||||||
// * remove - delete all file after creating zip archive
|
|
||||||
func parseArguments() (string, string, string, bool, error) {
|
func parseArguments() (string, string, string, bool, error) {
|
||||||
parser := argparse.NewParser("packer", "packer - zip file where filenames match with a regex")
|
parser := argparse.NewParser("packer", "zip files where filenames match a regex")
|
||||||
var err error
|
|
||||||
|
|
||||||
regex := parser.String("r", "regex", &argparse.Options{Required: true, Help: "filename 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)"})
|
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: "."})
|
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})
|
remove := parser.Flag("x", "remove", &argparse.Options{Required: false, Help: "remove files after archive creation", Default: false})
|
||||||
|
|
||||||
parseError := parser.Parse(os.Args)
|
if err := parser.Parse(os.Args); err != nil {
|
||||||
if parseError != nil {
|
return "", "", "", false, fmt.Errorf("parsing error: %s", err)
|
||||||
err = fmt.Errorf("parsing error: %s", parseError)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return *regex, *directory, *outputPath, *remove, err
|
return *regex, *directory, *outputPath, *remove, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRegex - build POSIX regex from a string
|
// buildRegex builds POSIX regex from a string.
|
||||||
// WARN: lookahead and lookbehind are not supported
|
// WARN: lookahead and lookbehind are not supported.
|
||||||
func buildRegex(regex string) (*re.Regexp, error) {
|
func buildRegex(regex string) (re.Regexp, error) {
|
||||||
rc, err := re.CompilePOSIX(regex)
|
rc, err := re.Compile(regex)
|
||||||
return rc, err
|
if rc == nil {
|
||||||
|
return re.Regexp{}, ErrEmptyRegex
|
||||||
|
}
|
||||||
|
return *rc, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// walkFilteredDir - walk into a directory and its subdirectories and find files that match a regex
|
// walkFilteredDir walks into a directory and its subdirectories and find files that match a regex.
|
||||||
func walkDirFiltered(directory string, regex re.Regexp) ([]string, error) {
|
func walkDirFiltered(directory string, regex re.Regexp) ([]string, error) {
|
||||||
var files []string
|
var files []string
|
||||||
err := fp.Walk(directory,
|
err := fp.Walk(directory,
|
||||||
@ -72,7 +56,7 @@ func walkDirFiltered(directory string, regex re.Regexp) ([]string, error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !info.IsDir() && regex.MatchString(path) {
|
if !info.IsDir() && regex.MatchString(path) {
|
||||||
log.Println("file found :", path)
|
log.Info().Str("path", path).Msg("file found")
|
||||||
files = append(files, path)
|
files = append(files, path)
|
||||||
}
|
}
|
||||||
return err
|
return err
|
||||||
@ -80,21 +64,15 @@ func walkDirFiltered(directory string, regex re.Regexp) ([]string, error) {
|
|||||||
return files, err
|
return files, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// getFilesFromDirectoryPath - wrapper calling `walkDirFiltered()`
|
// writeFileContent writes file content into zipped file (chunking is used to avoid memory errors with large files).
|
||||||
func getFilesFromDirectoryPath(directory string, regex re.Regexp) ([]string, error) {
|
|
||||||
files, err := walkDirFiltered(directory, regex)
|
|
||||||
return files, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// writeFileContent - write 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) {
|
func writeFileContent(f *os.File, zf io.Writer) (err error) {
|
||||||
const chunkSize = 4
|
const chunkSize = 4
|
||||||
|
|
||||||
b := make([]byte, chunkSize)
|
b := make([]byte, chunkSize)
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
for {
|
for {
|
||||||
_, err = f.Read(b)
|
if _, err = f.Read(b); err != nil {
|
||||||
if err != nil {
|
|
||||||
if err != io.EOF {
|
if err != io.EOF {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -106,9 +84,7 @@ func writeFileContent(f *os.File, zf io.Writer) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateZip - create a zip archive
|
// generateZip creates a zip archive from an output path and filepaths slice.
|
||||||
// * outputPath: zip archive full path
|
|
||||||
// * filePaths: files path
|
|
||||||
func generateZip(outputPath string, filepaths []string) error {
|
func generateZip(outputPath string, filepaths []string) error {
|
||||||
|
|
||||||
// create a zipfile from `outputPath`
|
// create a zipfile from `outputPath`
|
||||||
@ -125,14 +101,14 @@ func generateZip(outputPath string, filepaths []string) error {
|
|||||||
// open matching regex file
|
// open matching regex file
|
||||||
f, openErr := os.Open(filepath)
|
f, openErr := os.Open(filepath)
|
||||||
if openErr != nil {
|
if openErr != nil {
|
||||||
log.Println("[ERROR] :", openErr)
|
log.Err(openErr)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// create a zipped file
|
// create a zipped file
|
||||||
zf, zipErr := w.Create(filepath)
|
zf, zipErr := w.Create(filepath)
|
||||||
if zipErr != nil {
|
if zipErr != nil {
|
||||||
log.Println("[ERROR] :", zipErr)
|
log.Err(zipErr)
|
||||||
f.Close()
|
f.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -141,7 +117,7 @@ func generateZip(outputPath string, filepaths []string) error {
|
|||||||
writeErr := writeFileContent(f, zf)
|
writeErr := writeFileContent(f, zf)
|
||||||
// catch sentinel error `io.EOF`
|
// catch sentinel error `io.EOF`
|
||||||
if writeErr != nil && writeErr != io.EOF {
|
if writeErr != nil && writeErr != io.EOF {
|
||||||
log.Println("[ERROR] :", writeErr)
|
log.Err(zipErr)
|
||||||
f.Close()
|
f.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -151,69 +127,60 @@ func generateZip(outputPath string, filepaths []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// make sure to check the error on Close.
|
// make sure to check the error on Close.
|
||||||
closeErr := w.Close()
|
if err := w.Close(); err != nil {
|
||||||
if closeErr != nil {
|
return fmt.Errorf("error occured while closing the zip archive : %w", err)
|
||||||
return WritterError{message: fmt.Errorf("error occured while closing the zip archive : %w", closeErr)}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// removeFiles - can't be more obvious...
|
|
||||||
func removeFiles(filepaths []string) {
|
func removeFiles(filepaths []string) {
|
||||||
for _, filepath := range filepaths {
|
for _, filepath := range filepaths {
|
||||||
err := os.Remove(filepath)
|
if err := os.Remove(filepath); err != nil {
|
||||||
if err != nil {
|
log.Err(err)
|
||||||
log.Println("[ERROR] :", err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
initLogger()
|
||||||
|
|
||||||
// parse arguments
|
// parse arguments
|
||||||
regex, directory, outputPath, remove, err := parseArguments()
|
regex, directory, outputPath, remove, err := parseArguments()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("error while parsing args : ", err)
|
log.Fatal().Err(err).Msg("error while parsing args")
|
||||||
}
|
}
|
||||||
|
|
||||||
// build regex from string
|
// build regex from string
|
||||||
rc, err := buildRegex(regex)
|
rc, err := buildRegex(regex)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("error while compiling regex : '", regex, "'\nWARN: lookahead and lookbehind not implemented")
|
log.Fatal().Err(err).Str("regex", regex).Msg("error while compiling regex")
|
||||||
}
|
}
|
||||||
|
|
||||||
// deferencing Regexp pointer
|
|
||||||
rd := *rc
|
|
||||||
|
|
||||||
// walk into directory
|
// walk into directory
|
||||||
log.Printf("looking for file '%s' in '%s' ...", rd.String(), directory)
|
log.Info().Str("regex", rc.String()).Str("directory", directory).Msg("looking for files...")
|
||||||
files, err := getFilesFromDirectoryPath(directory, rd)
|
|
||||||
|
|
||||||
log.Printf("files found : %d", len(files))
|
files, err := walkDirFiltered(directory, rc)
|
||||||
if len(files) == 0 {
|
if err != nil {
|
||||||
message := fmt.Sprintf("no file found in the directory : %s with the regex : %s", directory, regex)
|
log.Fatal().Err(err)
|
||||||
log.Fatal(message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
// generating zip archive
|
||||||
err = generateZip(outputPath, files)
|
if err := generateZip(outputPath, files); err != nil {
|
||||||
|
log.Fatal().Err(err)
|
||||||
// check if errors occured during zip
|
|
||||||
var errorCode interface {
|
|
||||||
Code() int
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
if errors.As(err, &errorCode) {
|
|
||||||
log.Fatal("zip archive writing error : ", err)
|
|
||||||
}
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if remove {
|
if remove {
|
||||||
log.Printf("clean up archive files...")
|
log.Info().Msg("cleaning up archive files...")
|
||||||
removeFiles(files)
|
removeFiles(files)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("zip archive generated in %s", outputPath)
|
log.Info().Str("output path", outputPath).Msg("zip archive generated")
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user