// 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 import ( "archive/zip" "errors" "fmt" "github.com/akamensky/argparse" "io" "log" "os" fp "path/filepath" re "regexp" ) // WritterError // Useless, just to play with `errors.As()` to check specific error type WritterError struct { message error } type ErrorCode interface { 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) { parser := argparse.NewParser("packer", "packer - zip file where filenames match with a regex") var err error 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}) parseError := parser.Parse(os.Args) if parseError != nil { err = fmt.Errorf("parsing error: %s", parseError) } return *regex, *directory, *outputPath, *remove, err } // buildRegex - build POSIX regex from a string // WARN: lookahead and lookbehind are not supported func buildRegex(regex string) (*re.Regexp, error) { rc, err := re.CompilePOSIX(regex) return rc, err } // walkFilteredDir - walk 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.Println("file found :", path) files = append(files, path) } return err }) return files, err } // getFilesFromDirectoryPath - wrapper calling `walkDirFiltered()` 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) { const chunkSize = 4 b := make([]byte, chunkSize) for { _, err = f.Read(b) if err != nil { if err != io.EOF { continue } break } zf.Write(b) } return err } // generateZip - create a zip archive // * outputPath: zip archive full path // * filePaths: files path 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.Println("[ERROR] :", openErr) continue } // create a zipped file zf, zipErr := w.Create(filepath) if zipErr != nil { log.Println("[ERROR] :", 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.Println("[ERROR] :", writeErr) f.Close() continue } // clean up opened file f.Close() } // make sure to check the error on Close. closeErr := w.Close() if closeErr != nil { return WritterError{message: fmt.Errorf("error occured while closing the zip archive : %w", closeErr)} } return nil } // removeFiles - can't be more obvious... func removeFiles(filepaths []string) { for _, filepath := range filepaths { err := os.Remove(filepath) if err != nil { log.Println("[ERROR] :", err) } } } func main() { // parse arguments regex, directory, outputPath, remove, err := parseArguments() if err != nil { log.Fatal("error while parsing args : ", err) } // build regex from string rc, err := buildRegex(regex) if err != nil { log.Fatal("error while compiling regex : '", regex, "'\nWARN: lookahead and lookbehind not implemented") } // deferencing Regexp pointer rd := *rc // walk into directory log.Printf("looking for file '%s' in '%s' ...", rd.String(), directory) files, err := getFilesFromDirectoryPath(directory, rd) log.Printf("files found : %d", len(files)) if len(files) == 0 { message := fmt.Sprintf("no file found in the directory : %s with the regex : %s", directory, regex) log.Fatal(message) } // generating zip archive err = generateZip(outputPath, files) // 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 { log.Printf("clean up archive files...") removeFiles(files) } log.Printf("zip archive generated in %s", outputPath) }