Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for including zip directory entries #4

Merged
merged 4 commits into from
Aug 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 0 additions & 10 deletions docs/differences/Directories.md

This file was deleted.

4 changes: 4 additions & 0 deletions pkg/cli/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ type Configuration struct {
// Recursive includes all child folders automatically
Recursive bool

// Directories includes directory entries in the zip file
Directories bool

// Exclude file patterns from the archive
Exclude []string

Expand Down Expand Up @@ -59,6 +62,7 @@ func (conf *Configuration) addStringFlag(field *string, long string, short strin

func (conf *Configuration) defineFlags() {
conf.addBoolFlag(&conf.Verbose, "verbose", "v", false, "Verbose mode or print diagnostic version info.")
conf.addBoolFlag(&conf.Directories, "directories", "D", false, "Include directories in the zip file.")
conf.addBoolFlag(&conf.Recursive, "recurse-paths", "r", false, "Include all files verbose")
conf.addBoolFlag(&conf.Quiet, "quiet", "q", false, "Quiet mode; eliminate informational messages")
conf.addStringsFlag(&conf.Exclude, "exclude", "x", []string{}, "Exclude specific file patterns")
Expand Down
8 changes: 8 additions & 0 deletions pkg/features/conditions/contains_key.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package conditions

func ContainsKey(haystack *map[string]string, needle string) bool {
if _, ok := (*haystack)[needle]; ok {
return ok
}
return false
}
19 changes: 19 additions & 0 deletions pkg/features/conditions/contains_key_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package conditions

import (
"testing"
)

func TestContainsKey(t *testing.T) {
val := map[string]string{
"zzzzz": "zzzzz",
"element": "element",
}

if !ContainsKey(&val, "element") {
t.Fatal("Contains check not working")
}
if ContainsKey(&val, "missing element") {
t.Fatal("Contains check found element not in the slice")
}
}
70 changes: 70 additions & 0 deletions pkg/features/fileset/directories.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package fileset

import (
"github.com/timo-reymann/deterministic-zip/pkg/cli"
"github.com/timo-reymann/deterministic-zip/pkg/features/conditions"
"github.com/timo-reymann/deterministic-zip/pkg/file"
"os"
"path/filepath"
"sort"
)

// Directories adds all children folders recursively
type Directories struct{}

// DebugName prints the debuggable name
func (r Directories) DebugName() string {
return "Directories"
}

// IsEnabled checks if recursive adding was requested
func (r Directories) IsEnabled(c *cli.Configuration) bool {
return conditions.OnFlag(c.Directories)
}

// Execute and read all directories recursively and add them back to source files
func (r Directories) Execute(c *cli.Configuration) error {
files := make(map[string]string, 0)
sort.Strings(c.SourceFiles)

for _, f := range c.SourceFiles {
isDir, err := file.IsDir(f)
if err != nil {
return err
}

cf := filepath.Clean(f)
if cf != "." {
if isDir {
cf += string(os.PathSeparator)
}
files[cf] = f
}
includeParentDirs(&files, f)
}

c.SourceFiles = sortedKeySlice(&files)

return nil
}

func includeParentDirs(m *map[string]string, path string) {
parent := filepath.Dir(path)

for parent != "." {
if !conditions.ContainsKey(m, parent) {
suffixed := parent + string(os.PathSeparator)
(*m)[suffixed] = parent
}
parent = filepath.Dir(parent)
}
}

func sortedKeySlice(m *map[string]string) []string {
keys := make([]string, 0, len(*m))
for k := range *m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
89 changes: 89 additions & 0 deletions pkg/features/fileset/directories_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package fileset

import (
"github.com/timo-reymann/deterministic-zip/pkg/cli"
"reflect"
"testing"
)

func TestDirectories_IsEnabled(t *testing.T) {
c := cli.Configuration{Directories: true}
directories := Directories{}
if !directories.IsEnabled(&c) {
t.Fatal("Execution for directories fallback not working")
}
}

func TestDirectories_Execute(t *testing.T) {
directories := Directories{}
testCases := []struct {
sources []string
err *error
sourcesAfter []string
}{
{
sources: []string{
"testdata/recursive",
},
err: nil,
sourcesAfter: []string{
"testdata/",
"testdata/recursive/",
},
},
{
sources: []string{
"testdata/recursive/folder/file.txt",
},
err: nil,
sourcesAfter: []string{
"testdata/",
"testdata/recursive/",
"testdata/recursive/folder/",
"testdata/recursive/folder/file.txt",
},
},
{
sources: []string{
"testdata/recursive/folder/subfolder/",
},
err: nil,
sourcesAfter: []string{
"testdata/",
"testdata/recursive/",
"testdata/recursive/folder/",
"testdata/recursive/folder/subfolder/",
},
},
{
sources: []string{
"nonExistent",
},
err: mockErr("stat nonExistent: no such file or directory"),
sourcesAfter: []string{},
},
}

for _, tc := range testCases {
c := cli.Configuration{
Directories: true,
SourceFiles: tc.sources,
}
err := directories.Execute(&c)
if tc.err == nil && err != nil {
t.Fatalf("Expected no error but got %v", err)
} else if err != nil {
if (*tc.err).Error() != err.Error() {
t.Fatalf("Expected error %v, but got %v", *tc.err, err)
} else {
// Skip checking -> error thrown
continue
}
}

if !reflect.DeepEqual(tc.sourcesAfter, c.SourceFiles) {
t.Fatalf("Expected %v, but got %v", tc.sourcesAfter, c.SourceFiles)
}
}

}
2 changes: 2 additions & 0 deletions pkg/features/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,6 @@ func init() {
register(fileset.Recursive{})
register(filter.Exclude{})
register(filter.Include{})
// Directories must always be processed after the Recursive, Exclude, Include modules
register(fileset.Directories{})
}
25 changes: 20 additions & 5 deletions pkg/zip/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"archive/zip"
"compress/flate"
"github.com/timo-reymann/deterministic-zip/pkg/cli"
"github.com/timo-reymann/deterministic-zip/pkg/features/conditions"
"github.com/timo-reymann/deterministic-zip/pkg/output"
"io"
"os"
Expand Down Expand Up @@ -42,7 +43,7 @@ func Create(c *cli.Configuration, compression uint16) error {
registerCompressors(zipWriter)

for _, srcFile := range c.SourceFiles {
if err := appendFile(srcFile, zipWriter, compression); err != nil {
if err := appendFile(srcFile, zipWriter, compression, conditions.OnFlag(c.Directories)); err != nil {
return err
}
}
Expand All @@ -56,10 +57,17 @@ func registerCompressors(zipWriter *zip.Writer) {
})
}

func appendFile(srcFile string, zipWriter *zip.Writer, compression uint16) error {
func appendFile(srcFile string, zipWriter *zip.Writer, compression uint16, includeDirs bool) error {
output.Infof("Adding file %s", srcFile)

f, err := os.Open(srcFile)
// Ensure the open file is always closed, ignoring any errors during the close
defer func(f *os.File) {
if f != nil {
_ = f.Close()
}
}(f)

if err != nil {
return err
}
Expand All @@ -69,8 +77,7 @@ func appendFile(srcFile string, zipWriter *zip.Writer, compression uint16) error
return err
}

// Directories are currently not supported.
if stat.IsDir() {
if !includeDirs && stat.IsDir() {
return nil
}

Expand All @@ -83,6 +90,13 @@ func appendFile(srcFile string, zipWriter *zip.Writer, compression uint16) error
h.Name = srcFile
h.Extra = extra

// If dealing with a directory, we must ensure the h.Name field ends with a `/`
if stat.IsDir() {
if !strings.HasSuffix(h.Name, "/") {
h.Name += "/"
}
}

fw, err := zipWriter.CreateHeader(h)
if err != nil {
return err
Expand All @@ -93,7 +107,8 @@ func appendFile(srcFile string, zipWriter *zip.Writer, compression uint16) error
return err
}
}
zipWriter.Flush()
// explicitly ignore any errors during the flush
_ = zipWriter.Flush()

return nil
}
63 changes: 61 additions & 2 deletions pkg/zip/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,18 @@ func TestCreate(t *testing.T) {
},
{
config: cli.Configuration{
Directories: true,
SourceFiles: []string{
"testdata/folder",
},
},
sha256: "8739c76e681f900923b900c9df0ef75cf421d39cabb54650c4b9ad19b6a76d85",
sha256: "7839c2a3939b278e9b24e02621e6bdb07f4c32f79e111661ee9948f7516009c3",
compression: zip.Store,
zipFiles: []expectedFile{},
zipFiles: []expectedFile{
{
name: "testdata/folder/",
},
},
},
{
config: cli.Configuration{
Expand Down Expand Up @@ -114,6 +119,33 @@ func TestCreate(t *testing.T) {
},
},
},
{
config: cli.Configuration{
Directories: true,
SourceFiles: []string{
"testdata",
"testdata/file.txt",
"testdata/folder",
"testdata/folder/file.txt",
},
},
sha256: "e2431c807ee3f202e84f66ed3756ae736eb890916cf1737420708bed2181c5e0",
compression: zip.Store,
zipFiles: []expectedFile{
{
name: "testdata/",
},
{
name: "testdata/file.txt",
},
{
name: "testdata/folder/",
},
{
name: "testdata/folder/file.txt",
},
},
},
{
config: cli.Configuration{
SourceFiles: []string{
Expand All @@ -134,6 +166,33 @@ func TestCreate(t *testing.T) {
},
},
},
{
config: cli.Configuration{
Directories: true,
SourceFiles: []string{
"testdata",
"testdata/file.txt",
"testdata/folder",
"testdata/folder/file.txt",
},
},
sha256: "9501f16697415f9de62aab8e28925111abfac435842b455ea1bd36852a5b6adc",
compression: zip.Deflate,
zipFiles: []expectedFile{
{
name: "testdata/",
},
{
name: "testdata/file.txt",
},
{
name: "testdata/folder/",
},
{
name: "testdata/folder/file.txt",
},
},
},
{
config: cli.Configuration{
SourceFiles: []string{
Expand Down