diff --git a/README.md b/README.md index 0d05ce9..7388c72 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ Contents: csrpubdump/ Dump the public key from an X.509 certificate request. fragment/ Print a fragment of a file. jlp/ JSON linter/prettifier. + kgz/ Custom gzip compressor / decompressor that handles 99% + of my use cases. pem2bin/ Dump the binary body of a PEM-encoded block. pembody/ Print the body of a PEM certificate. pemit/ Dump data to a PEM file. diff --git a/cmd/kgz/README b/cmd/kgz/README new file mode 100644 index 0000000..382ef36 --- /dev/null +++ b/cmd/kgz/README @@ -0,0 +1,23 @@ +kgz + +kgz is like gzip, but supports compressing and decompressing to a different +directory than the source file is in. + +Usage: kgz [-l] source [target] + +If target is a directory, the basename of the sourcefile will be used +as the target filename. Compression and decompression is selected +based on whether the source filename ends in ".gz". + +Flags: + -l level Compression level (0-9). Only meaninful when + compressing a file. + + + + + + + + + diff --git a/cmd/kgz/main.go b/cmd/kgz/main.go new file mode 100644 index 0000000..26949ee --- /dev/null +++ b/cmd/kgz/main.go @@ -0,0 +1,182 @@ +package main + +import ( + "compress/flate" + "compress/gzip" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" +) + +const gzipExt = ".gz" + +func compress(path, target string, level int) error { + sourceFile, err := os.Open(path) + if err != nil { + return errors.Wrap(err, "opening file for read") + } + defer sourceFile.Close() + + destFile, err := os.Create(target) + if err != nil { + return errors.Wrap(err, "opening file for write") + } + defer destFile.Close() + + gzipCompressor, err := gzip.NewWriterLevel(destFile, level) + if err != nil { + return errors.Wrap(err, "invalid compression level") + } + defer gzipCompressor.Close() + + _, err = io.Copy(gzipCompressor, sourceFile) + if err != nil { + return errors.Wrap(err, "compressing file") + } + + if err != nil { + return errors.Wrap(err, "stat(2)ing destination file") + } + + return nil +} + +func uncompress(path, target string) error { + sourceFile, err := os.Open(path) + if err != nil { + return errors.Wrap(err, "opening file for read") + } + defer sourceFile.Close() + + gzipUncompressor, err := gzip.NewReader(sourceFile) + if err != nil { + return errors.Wrap(err, "reading gzip headers") + } + defer gzipUncompressor.Close() + + destFile, err := os.Create(target) + if err != nil { + return errors.Wrap(err, "opening file for write") + } + defer destFile.Close() + + _, err = io.Copy(destFile, gzipUncompressor) + if err != nil { + return errors.Wrap(err, "uncompressing file") + } + + return nil +} + +func usage(w io.Writer) { + fmt.Fprintf(w, `Usage: %s [-l] source [target] + +kgz is like gzip, but supports compressing and decompressing to a different +directory than the source file is in. + +Flags: + -l level Compression level (0-9). Only meaninful when + compressing a file. +`, os.Args[0]) +} + +func init() { + flag.Usage = func() { usage(os.Stderr) } +} + +func isDir(path string) bool { + file, err := os.Open(path) + if err == nil { + defer file.Close() + stat, err := file.Stat() + if err != nil { + return false + } + + if stat.IsDir() { + return true + } + } + + return false +} + +func pathForUncompressing(source, dest string) (string, error) { + if !isDir(dest) { + return dest, nil + } + + source = filepath.Base(source) + if !strings.HasSuffix(source, gzipExt) { + return "", errors.Errorf("%s is a not gzip-compressed file") + } + outFile := source[:len(source)-len(gzipExt)] + outFile = filepath.Join(dest, outFile) + return outFile, nil +} + +func pathForCompressing(source, dest string) (string, error) { + if !isDir(dest) { + return dest, nil + } + + source = filepath.Base(source) + if strings.HasSuffix(source, gzipExt) { + return "", errors.Errorf("%s is a gzip-compressed file") + } + + dest = filepath.Join(dest, source+gzipExt) + return dest, nil +} + +func main() { + var level int + var path string + var target = "." + + flag.IntVar(&level, "l", flate.DefaultCompression, "compression level") + flag.Parse() + + if flag.NArg() < 1 || flag.NArg() > 2 { + usage(os.Stderr) + os.Exit(1) + } + + path = flag.Arg(0) + if flag.NArg() == 2 { + target = flag.Arg(1) + } + + if strings.HasSuffix(path, gzipExt) { + target, err := pathForUncompressing(path, target) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + + err = uncompress(path, target) + if err != nil { + os.Remove(target) + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + } else { + target, err := pathForCompressing(path, target) + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + + err = compress(path, target, level) + if err != nil { + os.Remove(target) + fmt.Fprintf(os.Stderr, "%s\n", err) + os.Exit(1) + } + } +}