diff --git a/api/next/58000.txt b/api/next/58000.txt new file mode 100644 index 00000000000000..94db9637cbd26f --- /dev/null +++ b/api/next/58000.txt @@ -0,0 +1 @@ +pkg archive/tar, method (*Writer) AddFS(fs.FS) error #58000 diff --git a/src/archive/tar/writer.go b/src/archive/tar/writer.go index 1c95f0738a864f..d805e266d0594c 100644 --- a/src/archive/tar/writer.go +++ b/src/archive/tar/writer.go @@ -5,8 +5,10 @@ package tar import ( + "errors" "fmt" "io" + "io/fs" "path" "sort" "strings" @@ -403,6 +405,43 @@ func (tw *Writer) writeRawHeader(blk *block, size int64, flag byte) error { return nil } +// AddFS adds the files from fs.FS to the archive. +// It walks the directory tree starting at the root of the filesystem +// adding each file to the tar archive while maintaining the directory structure. +func (tw *Writer) AddFS(fsys fs.FS) error { + return fs.WalkDir(fsys, ".", func(name string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + info, err := d.Info() + if err != nil { + return err + } + // TODO(#49580): Handle symlinks when fs.ReadLinkFS is available. + if !info.Mode().IsRegular() { + return errors.New("tar: cannot add non-regular file") + } + h, err := FileInfoHeader(info, "") + if err != nil { + return err + } + h.Name = name + if err := tw.WriteHeader(h); err != nil { + return err + } + f, err := fsys.Open(name) + if err != nil { + return err + } + defer f.Close() + _, err = io.Copy(tw, f) + return err + }) +} + // splitUSTARPath splits a path according to USTAR prefix and suffix rules. // If the path is not splittable, then it will return ("", "", false). func splitUSTARPath(name string) (prefix, suffix string, ok bool) { diff --git a/src/archive/tar/writer_test.go b/src/archive/tar/writer_test.go index f6d75c58032305..a9936d6cd526fe 100644 --- a/src/archive/tar/writer_test.go +++ b/src/archive/tar/writer_test.go @@ -9,12 +9,14 @@ import ( "encoding/hex" "errors" "io" + "io/fs" "os" "path" "reflect" "sort" "strings" "testing" + "testing/fstest" "testing/iotest" "time" ) @@ -1333,3 +1335,67 @@ func TestFileWriter(t *testing.T) { } } } + +func TestWriterAddFS(t *testing.T) { + fsys := fstest.MapFS{ + "file.go": {Data: []byte("hello")}, + "subfolder/another.go": {Data: []byte("world")}, + } + var buf bytes.Buffer + tw := NewWriter(&buf) + if err := tw.AddFS(fsys); err != nil { + t.Fatal(err) + } + + // Test that we can get the files back from the archive + tr := NewReader(&buf) + + entries, err := fsys.ReadDir(".") + if err != nil { + t.Fatal(err) + } + + var curfname string + for _, entry := range entries { + curfname = entry.Name() + if entry.IsDir() { + curfname += "/" + continue + } + hdr, err := tr.Next() + if err == io.EOF { + break // End of archive + } + if err != nil { + t.Fatal(err) + } + + data, err := io.ReadAll(tr) + if err != nil { + t.Fatal(err) + } + + if hdr.Name != curfname { + t.Fatalf("got filename %v, want %v", + curfname, hdr.Name) + } + + origdata := fsys[curfname].Data + if string(data) != string(origdata) { + t.Fatalf("got file content %v, want %v", + data, origdata) + } + } +} + +func TestWriterAddFSNonRegularFiles(t *testing.T) { + fsys := fstest.MapFS{ + "device": {Data: []byte("hello"), Mode: 0755 | fs.ModeDevice}, + "symlink": {Data: []byte("world"), Mode: 0755 | fs.ModeSymlink}, + } + var buf bytes.Buffer + tw := NewWriter(&buf) + if err := tw.AddFS(fsys); err == nil { + t.Fatal("expected error, got nil") + } +}