Skip to content
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
13 changes: 9 additions & 4 deletions server/filesystem/compress.go
Original file line number Diff line number Diff line change
Expand Up @@ -258,14 +258,19 @@ func (fs *Filesystem) extractStream(ctx context.Context, opts extractStreamOptio

// Decompress and extract archive
return ex.Extract(ctx, opts.Reader, func(ctx context.Context, f archives.FileInfo) error {
if f.IsDir() {
return nil
}
p := filepath.Join(opts.Directory, f.NameInArchive)
// If it is ignored, just don't do anything with the file and skip over it.
// If it is ignored, just don't do anything with the entry and skip over it.
if err := fs.IsIgnored(p); err != nil {
return nil
}
// Create directories explicitly; an empty one has no file to create it
// implicitly and would otherwise be dropped during extraction.
if f.IsDir() {
if err := fs.unixFS.MkdirAll(p, 0o755); err != nil {
return wrapError(err, opts.FileName)
}
return nil
}
r, err := f.Open()
if err != nil {
return err
Expand Down
97 changes: 97 additions & 0 deletions server/filesystem/compress_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package filesystem

import (
"archive/tar"
"archive/zip"
"bytes"
"compress/gzip"
"context"
"os"
"testing"
Expand Down Expand Up @@ -52,3 +56,96 @@ func TestFilesystem_DecompressFile(t *testing.T) {
})
})
}

// Empty directories have no file to create them implicitly, so extraction must
// create them explicitly or they are dropped.
func TestFilesystem_DecompressFileEmptyDirectory(t *testing.T) {
g := Goblin(t)
fs, rfs := NewFs()

g.Describe("Decompress", func() {
archives := []struct {
name string
build func() ([]byte, error)
}{
{"empty.zip", zipWithEmptyDir},
{"empty.tar.gz", tarGzWithEmptyDir},
}

for _, a := range archives {
g.It("preserves an empty directory in a "+a.name, func() {
content, err := a.build()
g.Assert(err).IsNil()
err = rfs.CreateServerFile("./"+a.name, content)
g.Assert(err).IsNil()

err = fs.DecompressFile(context.Background(), "/", a.name)
g.Assert(err).IsNil()

// The empty directory must exist, and the sibling file must still extract.
st, err := rfs.StatServerFile("empty")
g.Assert(err).IsNil()
g.Assert(st.IsDir()).IsTrue()

_, err = rfs.StatServerFile("outside.txt")
g.Assert(err).IsNil()
})
}

g.AfterEach(func() {
_ = fs.TruncateRootDirectory()
})
})
}

// zipWithEmptyDir builds a zip holding one file and an empty directory ("empty/").
func zipWithEmptyDir() ([]byte, error) {
var buf bytes.Buffer
zw := zip.NewWriter(&buf)

dh := &zip.FileHeader{Name: "empty/"}
dh.SetMode(os.ModeDir | 0o755)
if _, err := zw.CreateHeader(dh); err != nil {
return nil, err
}

w, err := zw.Create("outside.txt")
if err != nil {
return nil, err
}
if _, err := w.Write([]byte("hello")); err != nil {
return nil, err
}

if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}

// tarGzWithEmptyDir builds a tar.gz holding one file and an empty directory ("empty/").
func tarGzWithEmptyDir() ([]byte, error) {
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gw)

if err := tw.WriteHeader(&tar.Header{Name: "empty/", Typeflag: tar.TypeDir, Mode: 0o755}); err != nil {
return nil, err
}

content := []byte("hello")
if err := tw.WriteHeader(&tar.Header{Name: "outside.txt", Typeflag: tar.TypeReg, Mode: 0o644, Size: int64(len(content))}); err != nil {
return nil, err
}
if _, err := tw.Write(content); err != nil {
return nil, err
}

if err := tw.Close(); err != nil {
return nil, err
}
if err := gw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}