Compare commits

..

11 Commits

Author SHA1 Message Date
498a948b34 gofmt some files 2023-10-04 01:23:03 -05:00
2679751896 Fix handling of hardlinks and special files
Also, don't attempt xattr ops on special files on the BSD likes.

TODO: Should have a way to allow restore without special files,
otherwise very cumbersome for a regular user.
2023-10-04 01:12:13 -05:00
66a938af67 Support for hardlinks to symlinks
This is a specialized use-case, but it is indeed possible to do this
and can be used if xattrs are attached to the symlink.
2023-10-04 01:12:11 -05:00
015d2200da Fix handling of xattrs with symlinks
Fix Linux, Darwin, and other BSD (untested) to allow proper
handling of xattrs with symlinks. On Linux we cannot use the f*
syscalls for symlinks because symlinks cannot be opened.
File flags must be handled differently on darwin and other BSD due
to the lack of the LCHFLAGS syscall on darwin, and the fact that it
is emulated in libc. However, we do have O_SYMLINK on darwin.
2023-10-04 01:11:20 -05:00
96e7c93a2c Support backup and restore of special files on POSIX style systems
Special files are device nodes and named pipes. The necessity of the
former is clear, the latter is debatable.
In order to preserve backward compatibility, the device number is
encoded in the StartChunk/StartOffset fields of the entry.
2023-10-03 16:26:19 -05:00
f06779659e Don't overwrite symlinks if file already exists 2023-10-03 15:08:47 -05:00
16885eaa61 Support backup and restore of hardlinks
This tracks inode/device from the stat info and creates backward
compatible snapshots that allow preserving hardlinks. Backwards
compatibility is preserved by saving a virtual inode number index in the
Link field of the file entry. Since this field was previously only used
for symlinks, this won't break old versions. Additionally, the entry
data is cloned so restoration with an old version works.

Current limitations are primarility with restore. They include:
- no command line option to prevent hard link restore
- if a file has the immutable or append only flag it will be set before
hardlinks are restored, so hardlinking will fail.
- if a partial restore includes a hardlink but not the parent
directories the hardlink will fail.

These will be solved by grouping restore of hardlinks together
with file, prior to applying final metadata.

- if a file is changed and is being rewritten by a restore hardlinks are
not preserved.
2023-10-03 12:21:46 -05:00
bf2565b5c3 Initial implementation of file/inode flags (Linux, BSD, darwin)
Basic support for BSD and Darwin style chflags (stat flags). Applies
these flags at the end of file restore.
Supports linux style ioctl_iflags(2) in a 2 step process. Flags that
need to be applied prior to writes such as compress and especially no-COW
are applied immediately upon file open.

The flags format is backwards compatible. An attribute starting with a
null byte is used to store flags in the entry attributes table. With
an old version of duplicacy the restore of this attribute should silently
fail (effectively be ignored).

Fixes xattr restore to use O_NOFOLLOW so attributes are applied to symlink.

TODO: Tests, possible option to switch off mutable/append prior to
restore of existing file similar to rsync. Does not apply attributes
or flags to the top most directory.
2023-10-03 12:15:54 -05:00
c07eef5063 Increase b2 client max file listing count to 10000
Considerable speed improvement with listing large storage.
2023-10-02 12:46:02 -05:00
2fdedcb9dd Fix exclude_by_attribute feature on POSIX
The exclude by attribute function is broken on non-Darwin POSIX: linux and freebsd.
This is because those xattrs must be prefixed by a legal namespace. The old xattr
library implicitly appended the user namespace to the xattr, but the current
official go pkg does not (which is just as well).

Also fix the test to remove the discordant old xattr dependency and provide
test cases for both darwin and non-darwin POSIX.
2023-10-02 12:41:50 -05:00
7bdd1cabd3 Use S3 ListObjectsV2 for listing files
ListObjects has been deprecated since 2016 and ListObjectsV2 with use of
explicit pagination tokens is more performant for large listings as well.

This also mitigates an issue with iDrive E2 where the StartAfter/Marker
is included in the output, leading to duplicate entries. Right now this
causes an exhaustive prune to delete chunks erroneously flagged as
duplicate, destroying the storage.
2023-10-02 12:41:50 -05:00
12 changed files with 86 additions and 133 deletions

View File

@@ -2262,7 +2262,7 @@ func main() {
app.Name = "duplicacy"
app.HelpName = "duplicacy"
app.Usage = "A new generation cloud backup tool based on lock-free deduplication"
app.Version = "3.2.2" + " (" + GitCommit + ")"
app.Version = "3.2.1" + " (" + GitCommit + ")"
// Exit with code 2 if an invalid command is provided
app.CommandNotFound = func(context *cli.Context, command string) {

2
go.mod
View File

@@ -27,7 +27,6 @@ require (
golang.org/x/crypto v0.12.0
golang.org/x/net v0.10.0
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d
golang.org/x/sys v0.11.0
google.golang.org/api v0.21.0
storj.io/uplink v1.12.0
)
@@ -64,6 +63,7 @@ require (
go.opencensus.io v0.22.3 // indirect
golang.org/x/mod v0.10.0 // indirect
golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.11.0 // indirect
golang.org/x/term v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/tools v0.9.1 // indirect

View File

@@ -710,23 +710,23 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
var hardLinkTable []hardLinkEntry
var hardLinks []*Entry
restoreHardLink := func(entry *Entry, fullPath string) bool {
if entry.IsHardLinkRoot() {
restoreHardlink := func(entry *Entry, fullPath string) bool {
if entry.IsHardlinkRoot() {
hardLinkTable[len(hardLinkTable)-1].willExist = true
} else if entry.IsHardLinkChild() {
i, err := entry.GetHardLinkId()
} else if entry.IsHardlinkedFrom() {
i, err := entry.GetHardlinkId()
if err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Decode error for hard link entry %s: %v", entry.Path, err)
LOG_ERROR("RESTORE_HARDLINK", "Decode error for hardlinked entry %s, %v", entry.Path, err)
return false
}
if !hardLinkTable[i].willExist {
hardLinkTable[i] = hardLinkEntry{entry, true}
} else {
sourcePath := joinPath(top, hardLinkTable[i].entry.Path)
LOG_INFO("RESTORE_HARDLINK", "Hard linking %s to %s", fullPath, sourcePath)
if err := MakeHardlink(sourcePath, fullPath); err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Failed to create hard link %s to %s: %v", fullPath, sourcePath, err)
LOG_ERROR("RESTORE_HARDLINK", "Failed to create hard link %s to %s %v", fullPath, sourcePath, err)
}
LOG_TRACE("DOWNLOAD_DONE", "Hard linked %s to %s", entry.Path, hardLinkTable[i].entry.Path)
return true
}
}
@@ -735,7 +735,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
for remoteEntry := range remoteListingChannel {
if remoteEntry.IsHardLinkRoot() {
if remoteEntry.IsHardlinkRoot() {
hardLinkTable = append(hardLinkTable, hardLinkEntry{remoteEntry, false})
}
@@ -782,7 +782,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
isRegular, link, err := Readlink(fullPath)
if err == nil && link == remoteEntry.Link && !isRegular {
remoteEntry.RestoreMetadata(fullPath, nil, setOwner)
if remoteEntry.IsHardLinkRoot() {
if remoteEntry.IsHardlinkRoot() {
hardLinkTable[len(hardLinkTable)-1].willExist = true
}
continue
@@ -798,7 +798,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
os.Remove(fullPath)
}
if restoreHardLink(remoteEntry, fullPath) {
if restoreHardlink(remoteEntry, fullPath) {
continue
}
@@ -807,8 +807,8 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
return 0
}
remoteEntry.RestoreMetadata(fullPath, nil, setOwner)
LOG_TRACE("DOWNLOAD_DONE", "Symlink %s updated", remoteEntry.Path)
LOG_TRACE("DOWNLOAD_DONE", "Symlink %s updated", remoteEntry.Path)
} else if remoteEntry.IsDir() {
stat, err := os.Stat(fullPath)
@@ -833,7 +833,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
if stat, _ := os.Lstat(fullPath); stat != nil {
if remoteEntry.IsSameSpecial(stat) {
remoteEntry.RestoreMetadata(fullPath, nil, setOwner)
if remoteEntry.IsHardLinkRoot() {
if remoteEntry.IsHardlinkRoot() {
hardLinkTable[len(hardLinkTable)-1].willExist = true
}
}
@@ -845,24 +845,22 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
os.Remove(fullPath)
}
if restoreHardLink(remoteEntry, fullPath) {
if restoreHardlink(remoteEntry, fullPath) {
continue
}
if err := remoteEntry.RestoreSpecial(fullPath); err != nil {
LOG_ERROR("RESTORE_SPECIAL", "Failed to restore special file %s: %v", fullPath, err)
LOG_ERROR("RESTORE_SPECIAL", "Unable to restore special file %s: %v", remoteEntry.Path, err)
return 0
}
remoteEntry.RestoreMetadata(fullPath, nil, setOwner)
LOG_TRACE("DOWNLOAD_DONE", "Special %s %s restored", remoteEntry.Path, remoteEntry.FmtSpecial())
} else {
if remoteEntry.IsHardLinkRoot() {
if remoteEntry.IsHardlinkRoot() {
hardLinkTable[len(hardLinkTable)-1].willExist = true
} else if remoteEntry.IsHardLinkChild() {
i, err := remoteEntry.GetHardLinkId()
} else if remoteEntry.IsHardlinkedFrom() {
i, err := remoteEntry.GetHardlinkId()
if err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Decode error for hard link entry %s: %v", remoteEntry.Path, err)
LOG_ERROR("RESTORE_HARDLINK", "Decode error for hardlinked entry %s, %v", remoteEntry.Path, err)
return 0
}
if !hardLinkTable[i].willExist {
@@ -994,7 +992,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
for _, linkEntry := range hardLinks {
i, _ := linkEntry.GetHardLinkId()
i, _ := linkEntry.GetHardlinkId()
sourcePath := joinPath(top, hardLinkTable[i].entry.Path)
fullPath := joinPath(top, linkEntry.Path)
@@ -1006,7 +1004,7 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
if sourceStat == nil {
LOG_WERROR(allowFailures, "RESTORE_HARDLINK",
"Target %s for hard link %s is missing", sourcePath, linkEntry.Path)
"Target %s for hardlink %s is missing", sourcePath, linkEntry.Path)
continue
}
if !overwrite {
@@ -1017,11 +1015,11 @@ func (manager *BackupManager) Restore(top string, revision int, inPlace bool, qu
os.Remove(fullPath)
}
LOG_DEBUG("RESTORE_HARDLINK", "Hard linking %s to %s", fullPath, sourcePath)
if err := MakeHardlink(sourcePath, fullPath); err != nil {
LOG_ERROR("RESTORE_HARDLINK", "Failed to create hard link %s to %s: %v", fullPath, sourcePath, err)
LOG_ERROR("RESTORE_HARDLINK", "Failed to create hard link %s to %s", fullPath, sourcePath)
return 0
}
LOG_TRACE("RESTORE_HARDLINK", "Hard linked %s to %s", linkEntry.Path, hardLinkTable[i].entry.Path)
}
if deleteMode && len(patterns) == 0 {
@@ -1199,7 +1197,8 @@ func (manager *BackupManager) UploadSnapshot(chunkOperator *ChunkOperator, top s
entry.StartChunk -= delta
entry.EndChunk -= delta
if entry.IsHardLinkRoot() {
if entry.IsHardlinkRoot() {
LOG_DEBUG("SNAPSHOT_UPLOAD", "Hard link root %s %v %v", entry.Path, entry.StartChunk, entry.EndChunk)
hardLinkTable = append(hardLinkTable, hardLinkEntry{entry, entry.StartChunk})
}
@@ -1207,24 +1206,28 @@ func (manager *BackupManager) UploadSnapshot(chunkOperator *ChunkOperator, top s
entry.StartChunk -= lastEndChunk
lastEndChunk = entry.EndChunk
entry.EndChunk = delta
} else if entry.IsHardLinkChild() {
i, err := entry.GetHardLinkId()
} else if entry.IsHardlinkedFrom() && !entry.IsLink() {
i, err := entry.GetHardlinkId()
if err != nil {
LOG_ERROR("SNAPSHOT_UPLOAD", "Decode error for hard link entry %s: %v", entry.Link, err)
LOG_ERROR("SNAPSHOT_UPLOAD", "Decode error for hardlinked entry %s, %v", entry.Link, err)
return err
}
targetEntry := hardLinkTable[i].entry
var startChunk, endChunk int
if targetEntry.IsFile() && targetEntry.Size > 0 {
if targetEntry.Size > 0 {
startChunk = hardLinkTable[i].startChunk - lastEndChunk
endChunk = targetEntry.EndChunk
lastEndChunk = hardLinkTable[i].startChunk + endChunk
}
entry = entry.HardLinkTo(targetEntry, startChunk, endChunk)
} else if entry.IsHardLinkRoot() {
if targetEntry.Size > 0 {
lastEndChunk = hardLinkTable[i].startChunk + endChunk
}
LOG_DEBUG("SNAPSHOT_UPLOAD", "Uploading cloned hardlink for %s to %s (%v %v)", entry.Path, targetEntry.Path, startChunk, endChunk)
} else if entry.IsHardlinkRoot() {
hardLinkTable = append(hardLinkTable, hardLinkEntry{entry, 0})
}

View File

@@ -85,8 +85,8 @@ type Config struct {
FileKey []byte `json:"-"`
// for erasure coding
DataShards int `json:"data-shards"`
ParityShards int `json:"parity-shards"`
DataShards int `json:'data-shards'`
ParityShards int `json:'parity-shards'`
// for RSA encryption
rsaPrivateKey *rsa.PrivateKey

View File

@@ -25,8 +25,8 @@ import (
)
const (
entryHardLinkRootChunkMarker = -9
entryHardLinkTargetChunkMarker = -10
entrySymHardLinkRootChunkMarker = -72
entrySymHardLinkTargetChunkMarker = -73
)
// This is the hidden directory in the repository for storing various files.
@@ -126,13 +126,6 @@ func (entry *Entry) Copy() *Entry {
}
func (entry *Entry) HardLinkTo(target *Entry, startChunk int, endChunk int) *Entry {
endOffset := target.EndOffset
if !target.IsFile() {
startChunk = target.StartChunk
endChunk = entry.EndChunk
endOffset = entry.EndOffset
}
return &Entry{
Path: entry.Path,
Size: target.Size,
@@ -147,7 +140,7 @@ func (entry *Entry) HardLinkTo(target *Entry, startChunk int, endChunk int) *Ent
StartChunk: startChunk,
StartOffset: target.StartOffset,
EndChunk: endChunk,
EndOffset: endOffset,
EndOffset: target.EndOffset,
Attributes: target.Attributes,
}
@@ -523,30 +516,34 @@ func (entry *Entry) IsLink() bool {
}
func (entry *Entry) IsSpecial() bool {
return entry.Mode&uint32(os.ModeNamedPipe|os.ModeDevice|os.ModeCharDevice|os.ModeSocket) != 0
return entry.Mode&uint32(os.ModeNamedPipe|os.ModeDevice|os.ModeCharDevice) != 0
}
func (entry *Entry) IsFileOrSpecial() bool {
return entry.Mode&uint32(os.ModeDir|os.ModeSymlink|os.ModeIrregular) == 0
}
func (entry *Entry) IsComplete() bool {
return entry.Size >= 0
}
func (entry *Entry) IsHardLinkChild() bool {
return (entry.IsFile() && len(entry.Link) > 0 && entry.Link != "/") || (!entry.IsDir() && entry.EndChunk == entryHardLinkTargetChunkMarker)
func (entry *Entry) IsHardlinkedFrom() bool {
return (entry.IsFileOrSpecial() && len(entry.Link) > 0 && entry.Link != "/") || (entry.IsLink() && entry.StartChunk == entrySymHardLinkTargetChunkMarker)
}
func (entry *Entry) IsHardLinkRoot() bool {
return (entry.IsFile() && entry.Link == "/") || (!entry.IsDir() && entry.EndChunk == entryHardLinkRootChunkMarker)
func (entry *Entry) IsHardlinkRoot() bool {
return (entry.IsFileOrSpecial() && entry.Link == "/") || (entry.IsLink() && entry.StartChunk == entrySymHardLinkRootChunkMarker)
}
func (entry *Entry) GetHardLinkId() (int, error) {
if entry.IsFile() {
func (entry *Entry) GetHardlinkId() (int, error) {
if entry.IsLink() {
if entry.StartChunk != entrySymHardLinkTargetChunkMarker {
return 0, errors.New("Symlink entry not marked as hardlinked")
}
return entry.StartOffset, nil
} else {
i, err := strconv.ParseUint(entry.Link, 16, 64)
return int(i), err
} else {
if entry.EndChunk != entryHardLinkTargetChunkMarker {
return 0, errors.New("Entry not marked as hard link child")
}
return entry.EndOffset, nil
}
}
@@ -810,6 +807,9 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string
if f.Name() == DUPLICACY_DIRECTORY {
continue
}
if f.Mode()&os.ModeSocket != 0 {
continue
}
entry := CreateEntryFromFileInfo(f, normalizedPath)
if len(patterns) > 0 && !MatchPath(entry.Path, patterns) {
@@ -823,23 +823,21 @@ func ListEntries(top string, path string, patterns []string, nobackupFile string
k := listEntryLinkKey{dev: uint64(stat.Dev), ino: uint64(stat.Ino)}
if linkIndex, seen := listingState.linkTable[k]; seen {
if linkIndex == -1 {
LOG_DEBUG("LIST_EXCLUDE", "%s is excluded by attribute (hard link)", entry.Path)
LOG_DEBUG("LIST_EXCLUDE", "%s is excluded by attribute (hardlink)", entry.Path)
continue
}
entry.Size = 0
if entry.IsFile() {
if entry.IsLink() {
entry.StartChunk = entrySymHardLinkTargetChunkMarker
entry.StartOffset = linkIndex
} else {
entry.Link = strconv.FormatInt(int64(linkIndex), 16)
} else {
entry.EndChunk = entryHardLinkTargetChunkMarker
entry.EndOffset = linkIndex
}
listingChannel <- entry
continue
} else {
if entry.IsFile() {
entry.Link = "/"
if entry.IsLink() {
entry.StartChunk = entrySymHardLinkRootChunkMarker
} else {
entry.EndChunk = entryHardLinkRootChunkMarker
entry.Link = "/"
}
listingState.linkTable[k] = -1
linkKey = &k

View File

@@ -172,8 +172,6 @@ func TestEntryOrder(t *testing.T) {
}
}
listingState := NewListingState()
directories := make([]*Entry, 0, 4)
directories = append(directories, CreateEntry("", 0, 0, 0))
@@ -184,7 +182,7 @@ func TestEntryOrder(t *testing.T) {
for len(directories) > 0 {
directory := directories[len(directories)-1]
directories = directories[:len(directories)-1]
subdirectories, _, err := ListEntries(testDir, directory.Path, nil, "", false, listingState, entryChannel)
subdirectories, _, err := ListEntries(testDir, directory.Path, nil, "", false, entryChannel)
if err != nil {
t.Errorf("ListEntries(%s, %s) returned an error: %s", testDir, directory.Path, err)
}
@@ -290,8 +288,6 @@ func TestEntryExcludeByAttribute(t *testing.T) {
for _, excludeByAttribute := range [2]bool{true, false} {
t.Logf("testing excludeByAttribute: %t", excludeByAttribute)
listingState := NewListingState()
directories := make([]*Entry, 0, 4)
directories = append(directories, CreateEntry("", 0, 0, 0))
@@ -302,7 +298,7 @@ func TestEntryExcludeByAttribute(t *testing.T) {
for len(directories) > 0 {
directory := directories[len(directories)-1]
directories = directories[:len(directories)-1]
subdirectories, _, err := ListEntries(testDir, directory.Path, nil, "", excludeByAttribute, listingState, entryChannel)
subdirectories, _, err := ListEntries(testDir, directory.Path, nil, "", excludeByAttribute, entryChannel)
if err != nil {
t.Errorf("ListEntries(%s, %s) returned an error: %s", testDir, directory.Path, err)
}

View File

@@ -2568,7 +2568,7 @@ func (manager *SnapshotManager) CheckSnapshot(snapshot *Snapshot) (err error) {
}
if entry.EndChunk < entry.StartChunk {
err = fmt.Errorf("The file %s starts at chunk %d and ends at chunk %d",
fmt.Errorf("The file %s starts at chunk %d and ends at chunk %d",
entry.Path, entry.StartChunk, entry.EndChunk)
return false
}

View File

@@ -756,8 +756,6 @@ func CreateStorage(preference Preference, resetPassword bool, threads int) (stor
LOG_ERROR("STORAGE_CREATE", "Failed to load the Storj storage at %s: %v", storageURL, err)
return nil
}
SavePassword(preference, "storj_key", apiKey)
SavePassword(preference, "storj_passphrase", passphrase)
return storjStorage
} else if matched[1] == "smb" {
server := matched[3]

View File

@@ -92,18 +92,3 @@ func (entry *Entry) RestoreEarlyDirFlags(path string) error {
func (entry *Entry) RestoreEarlyFileFlags(f *os.File) error {
return nil
}
func (entry *Entry) RestoreSpecial(fullPath string) error {
mode := entry.Mode & uint32(fileModeMask)
if entry.Mode&uint32(os.ModeNamedPipe) != 0 {
mode |= syscall.S_IFIFO
} else if entry.Mode&uint32(os.ModeCharDevice) != 0 {
mode |= syscall.S_IFCHR
} else if entry.Mode&uint32(os.ModeDevice) != 0 {
mode |= syscall.S_IFBLK
} else {
return nil
}
return syscall.Mknod(fullPath, mode, int(entry.GetRdev()))
}

View File

@@ -219,23 +219,6 @@ func (entry *Entry) restoreLateFileFlags(f *os.File) error {
return nil
}
func (entry *Entry) RestoreSpecial(fullPath string) error {
mode := entry.Mode & uint32(fileModeMask)
if entry.Mode&uint32(os.ModeNamedPipe) != 0 {
mode |= syscall.S_IFIFO
} else if entry.Mode&uint32(os.ModeCharDevice) != 0 {
mode |= syscall.S_IFCHR
} else if entry.Mode&uint32(os.ModeDevice) != 0 {
mode |= syscall.S_IFBLK
} else if entry.Mode&uint32(os.ModeSocket) != 0 {
mode |= syscall.S_IFSOCK
} else {
return nil
}
return syscall.Mknod(fullPath, mode, int(entry.GetRdev()))
}
func excludedByAttribute(attributes map[string][]byte) bool {
_, ok := attributes["user.duplicacy_exclude"]
return ok

View File

@@ -8,7 +8,6 @@
package duplicacy
import (
"fmt"
"os"
"path"
"syscall"
@@ -66,6 +65,21 @@ func (entry *Entry) GetRdev() uint64 {
return uint64(entry.StartChunk) | uint64(entry.StartOffset)<<32
}
func (entry *Entry) RestoreSpecial(fullPath string) error {
mode := entry.Mode & uint32(fileModeMask)
if entry.Mode&uint32(os.ModeNamedPipe) != 0 {
mode |= syscall.S_IFIFO
} else if entry.Mode&uint32(os.ModeCharDevice) != 0 {
mode |= syscall.S_IFCHR
} else if entry.Mode&uint32(os.ModeDevice) != 0 {
mode |= syscall.S_IFBLK
} else {
return nil
}
return syscall.Mknod(fullPath, mode, int(entry.GetRdev()))
}
func (entry *Entry) IsSameSpecial(fileInfo os.FileInfo) bool {
stat := fileInfo.Sys().(*syscall.Stat_t)
if stat == nil {
@@ -74,26 +88,6 @@ func (entry *Entry) IsSameSpecial(fileInfo os.FileInfo) bool {
return (uint32(fileInfo.Mode()) == entry.Mode) && (uint64(stat.Rdev) == entry.GetRdev())
}
func (entry *Entry) FmtSpecial() string {
var c string
mode := entry.Mode & uint32(os.ModeType)
if mode&uint32(os.ModeNamedPipe) != 0 {
c = "p"
} else if mode&uint32(os.ModeCharDevice) != 0 {
c = "c"
} else if mode&uint32(os.ModeDevice) != 0 {
c = "b"
} else if mode&uint32(os.ModeSocket) != 0 {
c = "s"
} else {
return ""
}
rdev := entry.GetRdev()
return fmt.Sprintf("%s (%d, %d)", c, unix.Major(rdev), unix.Minor(rdev))
}
func MakeHardlink(source string, target string) error {
return unix.Linkat(unix.AT_FDCWD, source, unix.AT_FDCWD, target, 0)
}

View File

@@ -125,10 +125,6 @@ func (entry *Entry) RestoreSpecial(fullPath string) error {
return nil
}
func (entry *Entry) FmtSpecial() string {
return ""
}
func MakeHardlink(source string, target string) error {
return os.Link(source, target)
}