Skip to content

Commit

Permalink
feat: handle hardlinks
Browse files Browse the repository at this point in the history
  • Loading branch information
zhijie-yang committed Jul 18, 2024
1 parent 504ad63 commit e1abecc
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 3 deletions.
8 changes: 7 additions & 1 deletion internal/deb/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,17 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error {
}
}
// Create the entry itself.
link := tarHeader.Linkname
if tarHeader.Typeflag == tar.TypeLink {
// The hard link requires the real path of the target file.
link = filepath.Join(options.TargetDir, link)
}

createOptions := &fsutil.CreateOptions{
Path: filepath.Join(options.TargetDir, targetPath),
Mode: tarHeader.FileInfo().Mode(),
Data: pathReader,
Link: tarHeader.Linkname,
Link: link,
MakeParents: true,
}
err := options.Create(extractInfos, createOptions)
Expand Down
34 changes: 34 additions & 0 deletions internal/deb/extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,40 @@ var extractTests = []extractTest{{
},
},
error: `cannot extract from package "test-package": path /dir/ requested twice with diverging mode: 0777 != 0000`,
}, {
summary: "Hard link must be created if specified in the tarball",
pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{
testutil.Dir(0755, "./"),
testutil.Reg(0644, "./file1.txt", "text for file1.txt"),
testutil.Hln(0644, "./file2.txt", "./file1.txt"),
}),
options: deb.ExtractOptions{
Extract: map[string][]deb.ExtractInfo{
"/*.txt": []deb.ExtractInfo{{
Path: "/*.txt",
}},
},
},
result: map[string]string{
"/file1.txt": "file 0644 e926a7fb",
"/file2.txt": "file 0644 e926a7fb",
},
notCreated: []string{},
}, {
summary: "Dangling hard link must raise an error",
pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{
testutil.Dir(0755, "./"),
// testutil.Reg(0644, "./file1.txt", "text for file1.txt"),
testutil.Hln(0644, "./file2.txt", "./file1.txt"),
}),
options: deb.ExtractOptions{
Extract: map[string][]deb.ExtractInfo{
"/*.txt": []deb.ExtractInfo{{
Path: "/*.txt",
}},
},
},
error: "cannot extract from package \"test-package\": the target file does not exist: .*/file1.txt",
}}

func (s *S) TestExtract(c *C) {
Expand Down
30 changes: 28 additions & 2 deletions internal/fsutil/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,12 @@ func Create(options *CreateOptions) (*Entry, error) {

switch o.Mode & fs.ModeType {
case 0:
err = createFile(o)
hash = hex.EncodeToString(rp.h.Sum(nil))
if o.Link != "" {
err = createHardLink(o)
} else {
err = createFile(o)
hash = hex.EncodeToString(rp.h.Sum(nil))
}
case fs.ModeDir:
err = createDir(o)
case fs.ModeSymlink:
Expand Down Expand Up @@ -121,6 +125,28 @@ func createSymlink(o *CreateOptions) error {
return os.Symlink(o.Link, o.Path)
}

func createHardLink(o *CreateOptions) error {
debugf("Creating hard link: %s => %s", o.Path, o.Link)
targetInfo, err := os.Lstat(o.Link)
if err != nil && os.IsNotExist(err) {
return fmt.Errorf("the target file does not exist: %s", o.Link)
} else if err != nil {
return err
}

linkInfo, err := os.Lstat(o.Path)
if err == nil || os.IsExist(err) {
if os.SameFile(targetInfo, linkInfo) {
return nil
}
return fmt.Errorf("the link already exists: %s", o.Path)
} else if !os.IsNotExist(err) {
return err
}

return os.Link(o.Link, o.Path)
}

// readerProxy implements the io.Reader interface proxying the calls to its
// inner io.Reader. On each read, the proxy keeps track of the file size and hash.
type readerProxy struct {
Expand Down
13 changes: 13 additions & 0 deletions internal/testutil/pkgdata.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,3 +197,16 @@ func Lnk(mode int64, path, target string) TarEntry {
},
}
}

// Hln is a shortcut for creating a hard link TarEntry structure (with
// tar.Typeflag set to tar.TypeLink). Hln stands for "Hard LiNk".
func Hln(mode int64, path, target string) TarEntry {
return TarEntry{
Header: tar.Header{
Typeflag: tar.TypeLink,
Name: path,
Mode: mode,
Linkname: target,
},
}
}

0 comments on commit e1abecc

Please sign in to comment.