From f661efa7fa2e718aea2ab800f108179da7090fb4 Mon Sep 17 00:00:00 2001 From: frm Date: Sat, 28 Jan 2017 16:28:29 +0100 Subject: [PATCH] Support for hardlinks with the restore command --- src/restic/fs/file.go | 6 +++ src/restic/hardlinks_index.go | 91 +++++++++++++++++++++++++++++++++++ src/restic/node.go | 19 ++++++++ src/restic/restorer.go | 4 +- 4 files changed, 119 insertions(+), 1 deletion(-) create mode 100644 src/restic/hardlinks_index.go diff --git a/src/restic/fs/file.go b/src/restic/fs/file.go index b7e81a38b..ba2a27192 100644 --- a/src/restic/fs/file.go +++ b/src/restic/fs/file.go @@ -102,6 +102,12 @@ func Symlink(oldname, newname string) error { return os.Symlink(fixpath(oldname), fixpath(newname)) } +// Hardlink creates newname as a hard link to oldname. +// If there is an error, it will be of type *LinkError. +func Hardlink(oldname, newname string) error { + return os.Link(fixpath(oldname), fixpath(newname)) +} + // Stat returns a FileInfo structure describing the named file. // If there is an error, it will be of type *PathError. func Stat(name string) (os.FileInfo, error) { diff --git a/src/restic/hardlinks_index.go b/src/restic/hardlinks_index.go new file mode 100644 index 000000000..907d65950 --- /dev/null +++ b/src/restic/hardlinks_index.go @@ -0,0 +1,91 @@ +package restic + +import ( + "sync" + "fmt" +) + +type HardlinkKey struct { + Inode, Device uint64 +} + +type HardlinkData struct { + Links uint64 + Name string +} + +var ( + hardLinkIndex = make(map[HardlinkKey]*HardlinkData) + hardLinkIndexMutex = sync.RWMutex{} +) + +func ExistsLink(inode uint64, device uint64) bool { + hardLinkIndexMutex.RLock() + defer hardLinkIndexMutex.RUnlock() + _, ok := hardLinkIndex[HardlinkKey{inode, device}] + + return ok +} + +func AddLink(inode uint64, device uint64, links uint64, name string) { + hardLinkIndexMutex.RLock() + _, ok := hardLinkIndex[HardlinkKey{inode, device}] + hardLinkIndexMutex.RUnlock() + + if !ok { + hardLinkIndexMutex.Lock() + hardLinkIndex[HardlinkKey{inode,device}] = &HardlinkData{links, name}; + hardLinkIndexMutex.Unlock() + } +} + +func GetLink(inode uint64, device uint64) *HardlinkData { + hardLinkIndexMutex.RLock() + defer hardLinkIndexMutex.RUnlock() + return hardLinkIndex[HardlinkKey{inode, device}] +} + +func RemoveLink(inode uint64, device uint64) { + hardLinkIndexMutex.Lock() + defer hardLinkIndexMutex.Unlock() + delete(hardLinkIndex, HardlinkKey{inode, device}) +} + +func DecrementLink(inode uint64, device uint64) { + hardLinkIndexMutex.RLock() + _, ok := hardLinkIndex[HardlinkKey{inode, device}] + hardLinkIndexMutex.RUnlock() + + if ok { + hardLinkIndexMutex.RLock() + if hardLinkIndex[HardlinkKey{inode, device}].Links > 0 { + hardLinkIndexMutex.RUnlock() + hardLinkIndexMutex.Lock() + hardLinkIndex[HardlinkKey{inode, device}].Links-- + hardLinkIndexMutex.Unlock() + } else { + hardLinkIndexMutex.RUnlock() + } + } +} + +/* return the number of links for a given inode-device combination */ +func CountLink(inode uint64, device uint64) uint64 { + hardLinkIndexMutex.RLock() + defer hardLinkIndexMutex.RUnlock() + + _, ok := hardLinkIndex[HardlinkKey{inode, device}] + if ok { + return hardLinkIndex[HardlinkKey{inode, device}].Links + } + return 0 +} + +func PrintLinkSummary() { + hardLinkIndexMutex.RLock() + defer hardLinkIndexMutex.RUnlock() + + for _, value := range hardLinkIndex { + fmt.Printf("File %v has unrestored links; probably you did not backup the complete volume\n", value.Name) + } +} diff --git a/src/restic/node.go b/src/restic/node.go index bf41f4201..f5c8dbbea 100644 --- a/src/restic/node.go +++ b/src/restic/node.go @@ -192,6 +192,24 @@ func (node Node) createDirAt(path string) error { } func (node Node) createFileAt(path string, repo Repository) error { + + if node.Links > 1 { + if !ExistsLink(node.Inode, node.Device) { + AddLink(node.Inode, node.Device, node.Links, path) + DecrementLink(node.Inode, node.Device) + } else { + err :=fs.Hardlink(GetLink(node.Inode, node.Device).Name, path) + if err != nil { + return errors.Wrap(err, "CreateHardlink") + } + DecrementLink(node.Inode, node.Device) + if CountLink(node.Inode, node.Device) == 0 { + RemoveLink(node.Inode, node.Device) + } + return nil + } + } + f, err := fs.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0600) defer f.Close() @@ -482,6 +500,7 @@ func (node *Node) fillExtra(path string, fi os.FileInfo) error { case "file": node.Size = uint64(stat.size()) node.Links = uint64(stat.nlink()) + node.Device = uint64(stat.dev()) case "dir": case "symlink": node.LinkTarget, err = fs.Readlink(path) diff --git a/src/restic/restorer.go b/src/restic/restorer.go index 5397c8d52..8b600fdd6 100644 --- a/src/restic/restorer.go +++ b/src/restic/restorer.go @@ -119,7 +119,9 @@ func (res *Restorer) restoreNodeTo(node *Node, dir string, dst string) error { // RestoreTo creates the directories and files in the snapshot below dir. // Before an item is created, res.Filter is called. func (res *Restorer) RestoreTo(dir string) error { - return res.restoreTo(dir, "", *res.sn.Tree) + err := res.restoreTo(dir, "", *res.sn.Tree) + PrintLinkSummary(); + return err } // Snapshot returns the snapshot this restorer is configured to use.