archive: preserve hardlinks in Tar and Untar
* integration test for preserving hardlinks Signed-off-by: Vincent Batts <vbatts@redhat.com> Signed-off-by: Vincent Batts <vbatts@hashbangbash.com>
This commit is contained in:
parent
0f0aef0590
commit
b17f754fff
3 changed files with 108 additions and 20 deletions
|
@ -153,7 +153,15 @@ func (compression *Compression) Extension() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func addTarFile(path, name string, tw *tar.Writer, twBuf *bufio.Writer) error {
|
type tarAppender struct {
|
||||||
|
TarWriter *tar.Writer
|
||||||
|
Buffer *bufio.Writer
|
||||||
|
|
||||||
|
// for hardlink mapping
|
||||||
|
SeenFiles map[uint64]string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ta *tarAppender) addTarFile(path, name string) error {
|
||||||
fi, err := os.Lstat(path)
|
fi, err := os.Lstat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -188,13 +196,28 @@ func addTarFile(path, name string, tw *tar.Writer, twBuf *bufio.Writer) error {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// if it's a regular file and has more than 1 link,
|
||||||
|
// it's hardlinked, so set the type flag accordingly
|
||||||
|
if fi.Mode().IsRegular() && stat.Nlink > 1 {
|
||||||
|
// a link should have a name that it links too
|
||||||
|
// and that linked name should be first in the tar archive
|
||||||
|
ino := uint64(stat.Ino)
|
||||||
|
if oldpath, ok := ta.SeenFiles[ino]; ok {
|
||||||
|
hdr.Typeflag = tar.TypeLink
|
||||||
|
hdr.Linkname = oldpath
|
||||||
|
hdr.Size = 0 // This Must be here for the writer math to add up!
|
||||||
|
} else {
|
||||||
|
ta.SeenFiles[ino] = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
capability, _ := system.Lgetxattr(path, "security.capability")
|
capability, _ := system.Lgetxattr(path, "security.capability")
|
||||||
if capability != nil {
|
if capability != nil {
|
||||||
hdr.Xattrs = make(map[string]string)
|
hdr.Xattrs = make(map[string]string)
|
||||||
hdr.Xattrs["security.capability"] = string(capability)
|
hdr.Xattrs["security.capability"] = string(capability)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tw.WriteHeader(hdr); err != nil {
|
if err := ta.TarWriter.WriteHeader(hdr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -204,17 +227,17 @@ func addTarFile(path, name string, tw *tar.Writer, twBuf *bufio.Writer) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
twBuf.Reset(tw)
|
ta.Buffer.Reset(ta.TarWriter)
|
||||||
_, err = io.Copy(twBuf, file)
|
_, err = io.Copy(ta.Buffer, file)
|
||||||
file.Close()
|
file.Close()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = twBuf.Flush()
|
err = ta.Buffer.Flush()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
twBuf.Reset(nil)
|
ta.Buffer.Reset(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -345,9 +368,15 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
tw := tar.NewWriter(compressWriter)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
ta := &tarAppender{
|
||||||
|
TarWriter: tar.NewWriter(compressWriter),
|
||||||
|
Buffer: pools.BufioWriter32KPool.Get(nil),
|
||||||
|
SeenFiles: make(map[uint64]string),
|
||||||
|
}
|
||||||
|
// this buffer is needed for the duration of this piped stream
|
||||||
|
defer pools.BufioWriter32KPool.Put(ta.Buffer)
|
||||||
|
|
||||||
// In general we log errors here but ignore them because
|
// In general we log errors here but ignore them because
|
||||||
// during e.g. a diff operation the container can continue
|
// during e.g. a diff operation the container can continue
|
||||||
// mutating the filesystem and we can see transient errors
|
// mutating the filesystem and we can see transient errors
|
||||||
|
@ -357,9 +386,6 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
|
||||||
options.Includes = []string{"."}
|
options.Includes = []string{"."}
|
||||||
}
|
}
|
||||||
|
|
||||||
twBuf := pools.BufioWriter32KPool.Get(nil)
|
|
||||||
defer pools.BufioWriter32KPool.Put(twBuf)
|
|
||||||
|
|
||||||
var renamedRelFilePath string // For when tar.Options.Name is set
|
var renamedRelFilePath string // For when tar.Options.Name is set
|
||||||
for _, include := range options.Includes {
|
for _, include := range options.Includes {
|
||||||
filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
|
filepath.Walk(filepath.Join(srcPath, include), func(filePath string, f os.FileInfo, err error) error {
|
||||||
|
@ -395,7 +421,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
|
||||||
relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1)
|
relFilePath = strings.Replace(relFilePath, renamedRelFilePath, options.Name, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := addTarFile(filePath, relFilePath, tw, twBuf); err != nil {
|
if err := ta.addTarFile(filePath, relFilePath); err != nil {
|
||||||
log.Debugf("Can't add file %s to tar: %s", srcPath, err)
|
log.Debugf("Can't add file %s to tar: %s", srcPath, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
@ -403,7 +429,7 @@ func TarWithOptions(srcPath string, options *TarOptions) (io.ReadCloser, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure to check the error on Close.
|
// Make sure to check the error on Close.
|
||||||
if err := tw.Close(); err != nil {
|
if err := ta.TarWriter.Close(); err != nil {
|
||||||
log.Debugf("Can't close tar writer: %s", err)
|
log.Debugf("Can't close tar writer: %s", err)
|
||||||
}
|
}
|
||||||
if err := compressWriter.Close(); err != nil {
|
if err := compressWriter.Close(); err != nil {
|
||||||
|
|
|
@ -249,6 +249,64 @@ func TestUntarUstarGnuConflict(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTarWithHardLink(t *testing.T) {
|
||||||
|
origin, err := ioutil.TempDir("", "docker-test-tar-hardlink")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(origin)
|
||||||
|
if err := ioutil.WriteFile(path.Join(origin, "1"), []byte("hello world"), 0700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Link(path.Join(origin, "1"), path.Join(origin, "2")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var i1, i2 uint64
|
||||||
|
if i1, err = getNlink(path.Join(origin, "1")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// sanity check that we can hardlink
|
||||||
|
if i1 != 2 {
|
||||||
|
t.Skipf("skipping since hardlinks don't work here; expected 2 links, got %d", i1)
|
||||||
|
}
|
||||||
|
|
||||||
|
dest, err := ioutil.TempDir("", "docker-test-tar-hardlink-dest")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(dest)
|
||||||
|
|
||||||
|
// we'll do this in two steps to separate failure
|
||||||
|
fh, err := Tar(origin, Uncompressed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure we can read the whole thing with no error, before writing back out
|
||||||
|
buf, err := ioutil.ReadAll(fh)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bRdr := bytes.NewReader(buf)
|
||||||
|
err = Untar(bRdr, dest, &TarOptions{Compression: Uncompressed})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i1, err = getInode(path.Join(dest, "1")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if i2, err = getInode(path.Join(dest, "2")); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if i1 != i2 {
|
||||||
|
t.Errorf("expected matching inodes, but got %d and %d", i1, i2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func getNlink(path string) (uint64, error) {
|
func getNlink(path string) (uint64, error) {
|
||||||
stat, err := os.Stat(path)
|
stat, err := os.Stat(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -368,11 +368,15 @@ func minor(device uint64) uint64 {
|
||||||
// ExportChanges produces an Archive from the provided changes, relative to dir.
|
// ExportChanges produces an Archive from the provided changes, relative to dir.
|
||||||
func ExportChanges(dir string, changes []Change) (Archive, error) {
|
func ExportChanges(dir string, changes []Change) (Archive, error) {
|
||||||
reader, writer := io.Pipe()
|
reader, writer := io.Pipe()
|
||||||
tw := tar.NewWriter(writer)
|
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
twBuf := pools.BufioWriter32KPool.Get(nil)
|
ta := &tarAppender{
|
||||||
defer pools.BufioWriter32KPool.Put(twBuf)
|
TarWriter: tar.NewWriter(writer),
|
||||||
|
Buffer: pools.BufioWriter32KPool.Get(nil),
|
||||||
|
SeenFiles: make(map[uint64]string),
|
||||||
|
}
|
||||||
|
// this buffer is needed for the duration of this piped stream
|
||||||
|
defer pools.BufioWriter32KPool.Put(ta.Buffer)
|
||||||
|
|
||||||
// In general we log errors here but ignore them because
|
// In general we log errors here but ignore them because
|
||||||
// during e.g. a diff operation the container can continue
|
// during e.g. a diff operation the container can continue
|
||||||
// mutating the filesystem and we can see transient errors
|
// mutating the filesystem and we can see transient errors
|
||||||
|
@ -390,19 +394,19 @@ func ExportChanges(dir string, changes []Change) (Archive, error) {
|
||||||
AccessTime: timestamp,
|
AccessTime: timestamp,
|
||||||
ChangeTime: timestamp,
|
ChangeTime: timestamp,
|
||||||
}
|
}
|
||||||
if err := tw.WriteHeader(hdr); err != nil {
|
if err := ta.TarWriter.WriteHeader(hdr); err != nil {
|
||||||
log.Debugf("Can't write whiteout header: %s", err)
|
log.Debugf("Can't write whiteout header: %s", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
path := filepath.Join(dir, change.Path)
|
path := filepath.Join(dir, change.Path)
|
||||||
if err := addTarFile(path, change.Path[1:], tw, twBuf); err != nil {
|
if err := ta.addTarFile(path, change.Path[1:]); err != nil {
|
||||||
log.Debugf("Can't add file %s to tar: %s", path, err)
|
log.Debugf("Can't add file %s to tar: %s", path, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure to check the error on Close.
|
// Make sure to check the error on Close.
|
||||||
if err := tw.Close(); err != nil {
|
if err := ta.TarWriter.Close(); err != nil {
|
||||||
log.Debugf("Can't close layer: %s", err)
|
log.Debugf("Can't close layer: %s", err)
|
||||||
}
|
}
|
||||||
writer.Close()
|
writer.Close()
|
||||||
|
|
Loading…
Reference in a new issue