localfs: fixed support for snapshotting UNC path root (\\server\path) (#1362)

Fixes #1288
Fixes #1035
Fixes #751
This commit is contained in:
Jarek Kowalski authored and GitHub committed 2021-10-06 20:27:45 -07:00
1 parent 73e492c9db
commit 2fa7cfb042
2 files changed
+65 -22

No files matched your search

+36 -22
View File
@@ -29,7 +29,7 @@ type filesystemEntry struct {
owner fs.OwnerInfo
device fs.DeviceInfo
parentDir string
prefix string
}
func (e *filesystemEntry) Name() string {
@@ -57,7 +57,7 @@ func (e *filesystemEntry) Sys() interface{} {
}
func (e *filesystemEntry) fullPath() string {
return filepath.Join(e.parentDir, e.Name())
return e.prefix + e.Name()
}
func (e *filesystemEntry) Owner() fs.OwnerInfo {
@@ -74,7 +74,7 @@ func (e *filesystemEntry) LocalFilesystemPath() string {
var _ os.FileInfo = (*filesystemEntry)(nil)
func newEntry(fi os.FileInfo, parentDir string) filesystemEntry {
func newEntry(fi os.FileInfo, prefix string) filesystemEntry {
return filesystemEntry{
TrimShallowSuffix(fi.Name()),
fi.Size(),
@@ -82,7 +82,7 @@ func newEntry(fi os.FileInfo, parentDir string) filesystemEntry {
fi.Mode(),
platformSpecificOwnerInfo(fi),
platformSpecificDeviceInfo(fi),
parentDir,
prefix,
}
}
@@ -120,7 +120,7 @@ func (fsd *filesystemDirectory) Child(ctx context.Context, name string) (fs.Entr
return nil, errors.Wrap(err, "unable to get child")
}
return entryFromDirEntry(st, fullPath), nil
return entryFromDirEntry(st, fullPath+string(filepath.Separator)), nil
}
type entryWithError struct {
@@ -128,8 +128,8 @@ type entryWithError struct {
err error
}
func toDirEntryOrNil(basename, dirPath string) (fs.Entry, error) {
fi, err := os.Lstat(dirPath + "/" + basename)
func toDirEntryOrNil(basename, prefix string) (fs.Entry, error) {
fi, err := os.Lstat(prefix + basename)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
@@ -138,7 +138,7 @@ func toDirEntryOrNil(basename, dirPath string) (fs.Entry, error) {
return nil, errors.Wrap(err, "error reading directory")
}
return entryFromDirEntry(fi, dirPath), nil
return entryFromDirEntry(fi, prefix), nil
}
func (fsd *filesystemDirectory) Readdir(ctx context.Context) (fs.Entries, error) {
@@ -158,8 +158,10 @@ func (fsd *filesystemDirectory) Readdir(ctx context.Context) (fs.Entries, error)
return nil, errors.Wrap(firstBatchErr, "unable to read directory entries")
}
childPrefix := fullPath + string(filepath.Separator)
for _, de := range firstBatch {
e, err := toDirEntryOrNil(de, fullPath)
e, err := toDirEntryOrNil(de, childPrefix)
if err != nil {
return nil, errors.Wrap(err, "error reading entry")
}
@@ -185,7 +187,7 @@ func (fsd *filesystemDirectory) Readdir(ctx context.Context) (fs.Entries, error)
// process results in case it's not EOF.
for _, de := range secondBatch {
e, err := toDirEntryOrNil(de, fullPath)
e, err := toDirEntryOrNil(de, childPrefix)
if err != nil {
return nil, errors.Wrap(err, "error reading entry")
}
@@ -203,10 +205,10 @@ func (fsd *filesystemDirectory) Readdir(ctx context.Context) (fs.Entries, error)
}
}
return fsd.readRemainingDirEntriesInParallel(fullPath, entries, f)
return fsd.readRemainingDirEntriesInParallel(childPrefix, entries, f)
}
func (fsd *filesystemDirectory) readRemainingDirEntriesInParallel(fullPath string, entries fs.Entries, f *os.File) (fs.Entries, error) {
func (fsd *filesystemDirectory) readRemainingDirEntriesInParallel(childPrefix string, entries fs.Entries, f *os.File) (fs.Entries, error) {
// start feeding directory entries to dirEntryCh
dirEntryCh := make(chan string, dirListingPrefetch)
@@ -246,7 +248,7 @@ func (fsd *filesystemDirectory) readRemainingDirEntriesInParallel(fullPath strin
defer workersWG.Done()
for de := range dirEntryCh {
e, err := toDirEntryOrNil(de, fullPath)
e, err := toDirEntryOrNil(de, childPrefix)
if err != nil {
entriesCh <- entryWithError{err: errors.Errorf("unable to stat directory entry %q: %v", de, err)}
continue
@@ -296,7 +298,7 @@ func (f *fileWithMetadata) Entry() (fs.Entry, error) {
return nil, errors.Wrap(err, "unable to stat() local file")
}
return &filesystemFile{newEntry(fi, filepath.Dir(f.Name()))}, nil
return &filesystemFile{newEntry(fi, dirPrefix(f.Name()))}, nil
}
func (fsf *filesystemFile) Open(ctx context.Context) (fs.Reader, error) {
@@ -317,6 +319,18 @@ func (e *filesystemErrorEntry) ErrorInfo() error {
return e.err
}
// dirPrefix returns the directory prefix for a given path - the initial part of the path up to and including the final slash (or backslash on Windows).
// this is similar to filepath.Dir() except dirPrefix("\\foo\bar") == "\\foo\", which is unsupported in filepath.
func dirPrefix(s string) string {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == filepath.Separator || s[i] == '/' {
return s[0 : i+1]
}
}
return ""
}
// NewEntry returns fs.Entry for the specified path, the result will be one of supported entry types: fs.File, fs.Directory, fs.Symlink
// or fs.UnsupportedEntry.
func NewEntry(path string) (fs.Entry, error) {
@@ -325,7 +339,7 @@ func NewEntry(path string) (fs.Entry, error) {
return nil, errors.Wrap(err, "unable to determine entry type")
}
return entryFromDirEntry(fi, filepath.Dir(path)), nil
return entryFromDirEntry(fi, dirPrefix(path)), nil
}
// Directory returns fs.Directory for the specified path.
@@ -342,28 +356,28 @@ func Directory(path string) (fs.Directory, error) {
return nil, errors.Errorf("not a directory: %v", path)
}
func entryFromDirEntry(fi os.FileInfo, parentDir string) fs.Entry {
func entryFromDirEntry(fi os.FileInfo, prefix string) fs.Entry {
isplaceholder := strings.HasSuffix(fi.Name(), ShallowEntrySuffix)
maskedmode := fi.Mode() & os.ModeType
switch {
case maskedmode == os.ModeDir && !isplaceholder:
return &filesystemDirectory{newEntry(fi, parentDir)}
return &filesystemDirectory{newEntry(fi, prefix)}
case maskedmode == os.ModeDir && isplaceholder:
return &shallowFilesystemDirectory{newEntry(fi, parentDir)}
return &shallowFilesystemDirectory{newEntry(fi, prefix)}
case maskedmode == os.ModeSymlink && !isplaceholder:
return &filesystemSymlink{newEntry(fi, parentDir)}
return &filesystemSymlink{newEntry(fi, prefix)}
case maskedmode == 0 && !isplaceholder:
return &filesystemFile{newEntry(fi, parentDir)}
return &filesystemFile{newEntry(fi, prefix)}
case maskedmode == 0 && isplaceholder:
return &shallowFilesystemFile{newEntry(fi, parentDir)}
return &shallowFilesystemFile{newEntry(fi, prefix)}
default:
return &filesystemErrorEntry{newEntry(fi, parentDir), fs.ErrUnknown}
return &filesystemErrorEntry{newEntry(fi, prefix), fs.ErrUnknown}
}
}
+29
View File
@@ -4,9 +4,11 @@ import (
"fmt"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/pkg/errors"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/fs"
"github.com/kopia/kopia/internal/clock"
@@ -137,6 +139,33 @@ func verifyChild(t *testing.T, dir fs.Directory) {
}
}
func TestDirPrefix(t *testing.T) {
cases := map[string]string{
"foo": "",
"/": "/",
"/tmp": "/",
"/tmp/": "/tmp/",
"/tmp/foo": "/tmp/",
}
if runtime.GOOS == "windows" {
cases["c:/"] = "c:/"
cases["c:\\"] = "c:\\"
cases["c:/temp"] = "c:/"
cases["c:\\temp"] = "c:\\"
cases["c:/temp/orary"] = "c:/temp/"
cases["c:\\temp\\orary"] = "c:\\temp\\"
cases["c:/temp\\orary"] = "c:/temp\\"
cases["c:\\temp/orary"] = "c:\\temp/"
cases["\\\\server\\path"] = "\\\\server\\"
cases["\\\\server\\path\\subdir"] = "\\\\server\\path\\"
}
for input, want := range cases {
require.Equal(t, want, dirPrefix(input), input)
}
}
func assertNoError(t *testing.T, err error) {
t.Helper()