diff --git a/zip/README.md b/zip/README.md new file mode 100644 index 0000000..e2047bb --- /dev/null +++ b/zip/README.md @@ -0,0 +1,34 @@ +# Go Zip Example + +An example of how to zip a directory in Go. + +- Utilizes `filepath.Walk` to traverse a directory (or single file) +- Handles each of + - Files (deflated, compressed) + - Directories (empty, not compressed) + - Symlinks (not compressed) + - Skips irregular files (pipes, sockets, devices, chars) +- Names zip file after the name of the directory +- Trims path prefix + +```bash +git clone https://git.coolaj86.com/coolaj86/go-examples.git +pushd go-examples/zip +``` + +```bash +go run . path/to/whatever +``` + +```txt +wrote whatever.zip +``` + +Separates concerns into functions for readability: + +- func main() +- func Zip(w io.Writer, src string, trim string) error + - zipOne + - zipDirectory + - zipFile + - zipSymlink diff --git a/zip/main.go b/zip/main.go new file mode 100644 index 0000000..6089d1c --- /dev/null +++ b/zip/main.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" +) + +func usage() { + fmt.Println("Usage: go run go-zip.go ") +} + +func main() { + if len(os.Args) < 2 || len(os.Args) > 3 { + usage() + os.Exit(1) + } + + dir := strings.TrimSuffix(os.Args[1], string(filepath.Separator)) + base := filepath.Base(dir) + // ../foo/whatever => ../foo/ + trim := strings.TrimSuffix(dir, base) + + // ./ => error + // ../../ => error + if "" == base || "." == base || ".." == base { + // TODO also don't allow ../self + fmt.Println("Error: Cannot zip the directory containing the output file") + os.Exit(1) + } + + f, err := os.OpenFile(base+".zip", os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644) + if nil != err { + panic(err) + } + defer f.Close() + + // ./whatever => whatever.zip + // ./path/to/whatever => whatever.zip + if err := Zip(f, dir, trim); nil != err { + panic(err) + } + fmt.Println("wrote", base+".zip") +} diff --git a/zip/zip.go b/zip/zip.go new file mode 100644 index 0000000..124368a --- /dev/null +++ b/zip/zip.go @@ -0,0 +1,113 @@ +package main + +import ( + "archive/zip" + "fmt" + "io" + "os" + "path/filepath" + "strings" +) + +// Zip walks `src`, omitting `trim`, writing to `w` +func Zip(w io.Writer, src string, trim string) error { + zw := zip.NewWriter(w) + defer zw.Close() + + return filepath.Walk(src, func(path string, fi os.FileInfo, err error) error { + // path includes fi.Name() already + if nil != err { + fmt.Println("warning: skipped", path+": ", err) + return nil + } + + zipOne(zw, path, fi, trim) + return nil + }) +} + +func zipOne(zw *zip.Writer, path string, fi os.FileInfo, trim string) error { + h, err := zip.FileInfoHeader(fi) + if nil != err { + return err + } + h.Name = strings.TrimPrefix(strings.TrimPrefix(path, trim), string(filepath.Separator)) + + if fi.IsDir() { + fmt.Printf("directory: %s\n\t%q\n", path, h.Name) + return zipDirectory(zw, h) + } + + // Allow zipping a single file + if "" == h.Name { + h.Name = path + } + if fi.Mode().IsRegular() { + fmt.Printf("file: %s\n\t%q\n", path, h.Name) + return zipFile(zw, h, path) + } + + if os.ModeSymlink == (fi.Mode() & os.ModeType) { + fmt.Printf("symlink: %s\n\t%q\n", path, h.Name) + return zipSymlink(zw, h, path) + } + + fmt.Printf("skipping: %s\n\t(irregular file type)\n", path) + return nil +} + +func zipDirectory(zw *zip.Writer, h *zip.FileHeader) error { + // directories must end in / for go + h.Name = strings.TrimPrefix(h.Name+"/", "/") + + // skip top-level, trimmed directory + if "" == h.Name { + return nil + } + + if _, err := zw.CreateHeader(h); nil != err { + return err + } + + return nil +} + +func zipFile(zw *zip.Writer, h *zip.FileHeader, path string) error { + r, err := os.Open(path) + if nil != err { + return err + } + defer r.Close() + + // Files should be zipped (not dirs, and symlinks... meh) + // TODO investigate if files below a certain size shouldn't be deflated + h.Method = zip.Deflate + w, err := zw.CreateHeader(h) + if nil != err { + return err + } + + if _, err := io.Copy(w, r); nil != err { + return err + } + + return nil +} + +func zipSymlink(zw *zip.Writer, h *zip.FileHeader, path string) error { + w, err := zw.CreateHeader(h) + if nil != err { + return err + } + + // TODO make sure that this is within the root directory + targetpath, err := os.Readlink(path) + if nil != err { + return err + } + if _, err := w.Write([]byte(targetpath)); nil != err { + return err + } + + return nil +}