all: Convert folders to use filesystem abstraction
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4228
This commit is contained in:
committed by
Jakob Borg
parent
ab8c2fb5c7
commit
3d8b4a42b7
@@ -9,30 +9,156 @@ package fs
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/du"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidFilename = errors.New("filename is invalid")
|
||||
ErrNotRelative = errors.New("not a relative path")
|
||||
)
|
||||
|
||||
// The BasicFilesystem implements all aspects by delegating to package os.
|
||||
// All paths are relative to the root and cannot (should not) escape the root directory.
|
||||
type BasicFilesystem struct {
|
||||
root string
|
||||
}
|
||||
|
||||
func NewBasicFilesystem() *BasicFilesystem {
|
||||
return new(BasicFilesystem)
|
||||
func newBasicFilesystem(root string) *BasicFilesystem {
|
||||
// The reason it's done like this:
|
||||
// C: -> C:\ -> C:\ (issue that this is trying to fix)
|
||||
// C:\somedir -> C:\somedir\ -> C:\somedir
|
||||
// C:\somedir\ -> C:\somedir\\ -> C:\somedir
|
||||
// This way in the tests, we get away without OS specific separators
|
||||
// in the test configs.
|
||||
root = filepath.Dir(root + string(filepath.Separator))
|
||||
|
||||
// Attempt tilde expansion; leave unchanged in case of error
|
||||
if path, err := ExpandTilde(root); err == nil {
|
||||
root = path
|
||||
}
|
||||
|
||||
// Attempt absolutification; leave unchanged in case of error
|
||||
if !filepath.IsAbs(root) {
|
||||
// Abs() looks like a fairly expensive syscall on Windows, while
|
||||
// IsAbs() is a whole bunch of string mangling. I think IsAbs() may be
|
||||
// somewhat faster in the general case, hence the outer if...
|
||||
if path, err := filepath.Abs(root); err == nil {
|
||||
root = path
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to enable long filename support on Windows. We may still not
|
||||
// have an absolute path here if the previous steps failed.
|
||||
if runtime.GOOS == "windows" {
|
||||
if filepath.IsAbs(root) && !strings.HasPrefix(root, `\\`) {
|
||||
root = `\\?\` + root
|
||||
}
|
||||
// If we're not on Windows, we want the path to end with a slash to
|
||||
// penetrate symlinks. On Windows, paths must not end with a slash.
|
||||
} else if root[len(root)-1] != filepath.Separator {
|
||||
root = root + string(filepath.Separator)
|
||||
}
|
||||
|
||||
return &BasicFilesystem{
|
||||
root: root,
|
||||
}
|
||||
}
|
||||
|
||||
// rooted expands the relative path to the full path that is then used with os
|
||||
// package. If the relative path somehow causes the final path to escape the root
|
||||
// directoy, this returns an error, to prevent accessing files that are not in the
|
||||
// shared directory.
|
||||
func (f *BasicFilesystem) rooted(rel string) (string, error) {
|
||||
// The root must not be empty.
|
||||
if f.root == "" {
|
||||
return "", ErrInvalidFilename
|
||||
}
|
||||
|
||||
pathSep := string(PathSeparator)
|
||||
|
||||
// The expected prefix for the resulting path is the root, with a path
|
||||
// separator at the end.
|
||||
expectedPrefix := filepath.FromSlash(f.root)
|
||||
if !strings.HasSuffix(expectedPrefix, pathSep) {
|
||||
expectedPrefix += pathSep
|
||||
}
|
||||
|
||||
// The relative path should be clean from internal dotdots and similar
|
||||
// funkyness.
|
||||
rel = filepath.FromSlash(rel)
|
||||
if filepath.Clean(rel) != rel {
|
||||
return "", ErrInvalidFilename
|
||||
}
|
||||
|
||||
// It is not acceptable to attempt to traverse upwards.
|
||||
switch rel {
|
||||
case "..", pathSep:
|
||||
return "", ErrNotRelative
|
||||
}
|
||||
if strings.HasPrefix(rel, ".."+pathSep) {
|
||||
return "", ErrNotRelative
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rel, pathSep+pathSep) {
|
||||
// The relative path may pretend to be an absolute path within the
|
||||
// root, but the double path separator on Windows implies something
|
||||
// else. It would get cleaned by the Join below, but it's out of
|
||||
// spec anyway.
|
||||
return "", ErrNotRelative
|
||||
}
|
||||
|
||||
// The supposedly correct path is the one filepath.Join will return, as
|
||||
// it does cleaning and so on. Check that one first to make sure no
|
||||
// obvious escape attempts have been made.
|
||||
joined := filepath.Join(f.root, rel)
|
||||
if rel == "." && !strings.HasSuffix(joined, pathSep) {
|
||||
joined += pathSep
|
||||
}
|
||||
if !strings.HasPrefix(joined, expectedPrefix) {
|
||||
return "", ErrNotRelative
|
||||
}
|
||||
|
||||
return joined, nil
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) unrooted(path string) string {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(path, f.root), string(PathSeparator))
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Chmod(name string, mode FileMode) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(name, os.FileMode(mode))
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chtimes(name, atime, mtime)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Mkdir(name string, perm FileMode) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Mkdir(name, os.FileMode(perm))
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := underlyingLstat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -41,14 +167,38 @@ func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Remove(name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Remove(name)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) RemoveAll(name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.RemoveAll(name)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Rename(oldpath, newpath string) error {
|
||||
oldpath, err := f.rooted(oldpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newpath, err = f.rooted(newpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(oldpath, newpath)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Stat(name string) (FileInfo, error) {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -57,7 +207,11 @@ func (f *BasicFilesystem) Stat(name string) (FileInfo, error) {
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
|
||||
fd, err := os.OpenFile(name, os.O_RDONLY, 0777)
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := os.OpenFile(name, OptReadOnly, 0777)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -72,19 +226,39 @@ func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Open(name string) (File, error) {
|
||||
fd, err := os.Open(name)
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFile{fd}, err
|
||||
fd, err := os.Open(rootedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFile{fd, name}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fd, err := os.OpenFile(rootedName, flags, os.FileMode(mode))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFile{fd, name}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Create(name string) (File, error) {
|
||||
fd, err := os.Create(name)
|
||||
rootedName, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFile{fd}, err
|
||||
fd, err := os.Create(rootedName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFile{fd, name}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
@@ -92,9 +266,47 @@ func (f *BasicFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Glob(pattern string) ([]string, error) {
|
||||
pattern, err := f.rooted(pattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files, err := filepath.Glob(pattern)
|
||||
unrooted := make([]string, len(files))
|
||||
for i := range files {
|
||||
unrooted[i] = f.unrooted(files[i])
|
||||
}
|
||||
return unrooted, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Usage(name string) (Usage, error) {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return Usage{}, err
|
||||
}
|
||||
u, err := du.Get(name)
|
||||
return Usage{
|
||||
Free: u.FreeBytes,
|
||||
Total: u.TotalBytes,
|
||||
}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Type() FilesystemType {
|
||||
return FilesystemTypeBasic
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) URI() string {
|
||||
return strings.TrimPrefix(f.root, `\\?\`)
|
||||
}
|
||||
|
||||
// fsFile implements the fs.File interface on top of an os.File
|
||||
type fsFile struct {
|
||||
*os.File
|
||||
name string
|
||||
}
|
||||
|
||||
func (f fsFile) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f fsFile) Stat() (FileInfo, error) {
|
||||
@@ -105,6 +317,17 @@ func (f fsFile) Stat() (FileInfo, error) {
|
||||
return fsFileInfo{info}, nil
|
||||
}
|
||||
|
||||
func (f fsFile) Sync() error {
|
||||
err := f.File.Sync()
|
||||
// On Windows, fsyncing a directory returns a "handle is invalid"
|
||||
// So we swallow that and let things go through in order not to have to add
|
||||
// a separate way of syncing directories versus files.
|
||||
if err != nil && (runtime.GOOS != "windows" || !strings.Contains(err.Error(), "handle is invalid")) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fsFileInfo implements the fs.FileInfo interface on top of an os.FileInfo.
|
||||
type fsFileInfo struct {
|
||||
os.FileInfo
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import "os"
|
||||
|
||||
var symlinksSupported = true
|
||||
|
||||
func DisableSymlinks() {
|
||||
symlinksSupported = false
|
||||
}
|
||||
|
||||
func (BasicFilesystem) SymlinksSupported() bool {
|
||||
return symlinksSupported
|
||||
}
|
||||
|
||||
func (BasicFilesystem) CreateSymlink(name, target string) error {
|
||||
return os.Symlink(target, name)
|
||||
}
|
||||
|
||||
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
|
||||
return os.Readlink(path)
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build windows
|
||||
|
||||
package fs
|
||||
|
||||
import "errors"
|
||||
|
||||
var errNotSupported = errors.New("symlinks not supported")
|
||||
|
||||
func DisableSymlinks() {}
|
||||
|
||||
func (BasicFilesystem) SymlinksSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
|
||||
return "", errNotSupported
|
||||
}
|
||||
|
||||
func (BasicFilesystem) CreateSymlink(path, target string) error {
|
||||
return errNotSupported
|
||||
}
|
||||
486
lib/fs/basicfs_test.go
Normal file
486
lib/fs/basicfs_test.go
Normal file
@@ -0,0 +1,486 @@
|
||||
// Copyright (C) 2017 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func setup(t *testing.T) (Filesystem, string) {
|
||||
dir, err := ioutil.TempDir("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return newBasicFilesystem(dir), dir
|
||||
}
|
||||
|
||||
func TestChmodFile(t *testing.T) {
|
||||
fs, dir := setup(t)
|
||||
path := filepath.Join(dir, "file")
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
defer os.Chmod(path, 0666)
|
||||
|
||||
fd, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fd.Close()
|
||||
|
||||
if err := os.Chmod(path, 0666); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(path); err != nil || stat.Mode()&os.ModePerm != 0666 {
|
||||
t.Errorf("wrong perm: %t %#o", err == nil, stat.Mode()&os.ModePerm)
|
||||
}
|
||||
|
||||
if err := fs.Chmod("file", 0444); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(path); err != nil || stat.Mode()&os.ModePerm != 0444 {
|
||||
t.Errorf("wrong perm: %t %#o", err == nil, stat.Mode()&os.ModePerm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChmodDir(t *testing.T) {
|
||||
fs, dir := setup(t)
|
||||
path := filepath.Join(dir, "dir")
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
mode := os.FileMode(0755)
|
||||
if runtime.GOOS == "windows" {
|
||||
mode = os.FileMode(0777)
|
||||
}
|
||||
|
||||
defer os.Chmod(path, mode)
|
||||
|
||||
if err := os.Mkdir(path, mode); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(path); err != nil || stat.Mode()&os.ModePerm != mode {
|
||||
t.Errorf("wrong perm: %t %#o", err == nil, stat.Mode()&os.ModePerm)
|
||||
}
|
||||
|
||||
if err := fs.Chmod("dir", 0555); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if stat, err := os.Stat(path); err != nil || stat.Mode()&os.ModePerm != 0555 {
|
||||
t.Errorf("wrong perm: %t %#o", err == nil, stat.Mode()&os.ModePerm)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChtimes(t *testing.T) {
|
||||
fs, dir := setup(t)
|
||||
path := filepath.Join(dir, "file")
|
||||
defer os.RemoveAll(dir)
|
||||
fd, err := os.Create(path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fd.Close()
|
||||
|
||||
mtime := time.Now().Add(-time.Hour)
|
||||
|
||||
fs.Chtimes("file", mtime, mtime)
|
||||
|
||||
stat, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
diff := stat.ModTime().Sub(mtime)
|
||||
if diff > 3*time.Second || diff < -3*time.Second {
|
||||
t.Errorf("%s != %s", stat.Mode(), mtime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate(t *testing.T) {
|
||||
fs, dir := setup(t)
|
||||
path := filepath.Join(dir, "file")
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
t.Errorf("exists?")
|
||||
}
|
||||
|
||||
fd, err := fs.Create("file")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
fd.Close()
|
||||
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateSymlink(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("windows not supported")
|
||||
}
|
||||
|
||||
fs, dir := setup(t)
|
||||
path := filepath.Join(dir, "file")
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
if err := fs.CreateSymlink("blah", "file"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if target, err := os.Readlink(path); err != nil || target != "blah" {
|
||||
t.Error("target", target, "err", err)
|
||||
}
|
||||
|
||||
if err := os.Remove(path); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if err := fs.CreateSymlink(filepath.Join("..", "blah"), "file"); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if target, err := os.Readlink(path); err != nil || target != filepath.Join("..", "blah") {
|
||||
t.Error("target", target, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirNames(t *testing.T) {
|
||||
fs, dir := setup(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
// Case differences
|
||||
testCases := []string{
|
||||
"a",
|
||||
"bC",
|
||||
}
|
||||
sort.Strings(testCases)
|
||||
|
||||
for _, sub := range testCases {
|
||||
if err := os.Mkdir(filepath.Join(dir, sub), 0777); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
if dirs, err := fs.DirNames("."); err != nil || len(dirs) != len(testCases) {
|
||||
t.Errorf("%s %s %s", err, dirs, testCases)
|
||||
} else {
|
||||
sort.Strings(dirs)
|
||||
for i := range dirs {
|
||||
if dirs[i] != testCases[i] {
|
||||
t.Errorf("%s != %s", dirs[i], testCases[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestNames(t *testing.T) {
|
||||
// Tests that all names are without the root directory.
|
||||
fs, dir := setup(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := "file"
|
||||
fd, err := fs.Create(expected)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if fd.Name() != expected {
|
||||
t.Errorf("incorrect %s != %s", fd.Name(), expected)
|
||||
}
|
||||
if stat, err := fd.Stat(); err != nil || stat.Name() != expected {
|
||||
t.Errorf("incorrect %s != %s (%v)", stat.Name(), expected, err)
|
||||
}
|
||||
|
||||
if err := fs.Mkdir("dir", 0777); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
expected = filepath.Join("dir", "file")
|
||||
fd, err = fs.Create(expected)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
if fd.Name() != expected {
|
||||
t.Errorf("incorrect %s != %s", fd.Name(), expected)
|
||||
}
|
||||
|
||||
// os.fd.Stat() returns just base, so do we.
|
||||
if stat, err := fd.Stat(); err != nil || stat.Name() != filepath.Base(expected) {
|
||||
t.Errorf("incorrect %s != %s (%v)", stat.Name(), filepath.Base(expected), err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlob(t *testing.T) {
|
||||
// Tests that all names are without the root directory.
|
||||
fs, dir := setup(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
for _, dirToCreate := range []string{
|
||||
filepath.Join("a", "test", "b"),
|
||||
filepath.Join("a", "best", "b"),
|
||||
filepath.Join("a", "best", "c"),
|
||||
} {
|
||||
if err := fs.MkdirAll(dirToCreate, 0777); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
pattern string
|
||||
matches []string
|
||||
}{
|
||||
{
|
||||
filepath.Join("a", "?est", "?"),
|
||||
[]string{
|
||||
filepath.Join("a", "test", "b"),
|
||||
filepath.Join("a", "best", "b"),
|
||||
filepath.Join("a", "best", "c"),
|
||||
},
|
||||
},
|
||||
{
|
||||
filepath.Join("a", "?est", "b"),
|
||||
[]string{
|
||||
filepath.Join("a", "test", "b"),
|
||||
filepath.Join("a", "best", "b"),
|
||||
},
|
||||
},
|
||||
{
|
||||
filepath.Join("a", "best", "?"),
|
||||
[]string{
|
||||
filepath.Join("a", "best", "b"),
|
||||
filepath.Join("a", "best", "c"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
results, err := fs.Glob(testCase.pattern)
|
||||
sort.Strings(results)
|
||||
sort.Strings(testCase.matches)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if len(results) != len(testCase.matches) {
|
||||
t.Errorf("result count mismatch")
|
||||
}
|
||||
for i := range testCase.matches {
|
||||
if results[i] != testCase.matches[i] {
|
||||
t.Errorf("%s != %s", results[i], testCase.matches[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUsage(t *testing.T) {
|
||||
fs, dir := setup(t)
|
||||
defer os.RemoveAll(dir)
|
||||
usage, err := fs.Usage(".")
|
||||
if err != nil {
|
||||
if runtime.GOOS == "netbsd" || runtime.GOOS == "openbsd" || runtime.GOOS == "solaris" {
|
||||
t.Skip()
|
||||
}
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
if usage.Free < 1 {
|
||||
t.Error("Disk is full?", usage.Free)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWindowsPaths(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skip("Not useful on non-Windows")
|
||||
return
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
input string
|
||||
expectedRoot string
|
||||
expectedURI string
|
||||
}{
|
||||
{`e:\`, `\\?\e:\`, `e:\`},
|
||||
{`\\?\e:\`, `\\?\e:\`, `e:\`},
|
||||
{`\\192.0.2.22\network\share`, `\\192.0.2.22\network\share`, `\\192.0.2.22\network\share`},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
fs := newBasicFilesystem(testCase.input)
|
||||
if fs.root != testCase.expectedRoot {
|
||||
t.Errorf("root %q != %q", fs.root, testCase.expectedRoot)
|
||||
}
|
||||
if fs.URI() != testCase.expectedURI {
|
||||
t.Errorf("uri %q != %q", fs.URI(), testCase.expectedURI)
|
||||
}
|
||||
}
|
||||
|
||||
fs := newBasicFilesystem(`relative\path`)
|
||||
if fs.root == `relative\path` || !strings.HasPrefix(fs.root, "\\\\?\\") {
|
||||
t.Errorf("%q == %q, expected absolutification", fs.root, `relative\path`)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRooted(t *testing.T) {
|
||||
type testcase struct {
|
||||
root string
|
||||
rel string
|
||||
joined string
|
||||
ok bool
|
||||
}
|
||||
cases := []testcase{
|
||||
// Valid cases
|
||||
{"foo", "bar", "foo/bar", true},
|
||||
{"foo", "/bar", "foo/bar", true},
|
||||
{"foo/", "bar", "foo/bar", true},
|
||||
{"foo/", "/bar", "foo/bar", true},
|
||||
{"baz/foo", "bar", "baz/foo/bar", true},
|
||||
{"baz/foo", "/bar", "baz/foo/bar", true},
|
||||
{"baz/foo/", "bar", "baz/foo/bar", true},
|
||||
{"baz/foo/", "/bar", "baz/foo/bar", true},
|
||||
{"foo", "bar/baz", "foo/bar/baz", true},
|
||||
{"foo", "/bar/baz", "foo/bar/baz", true},
|
||||
{"foo/", "bar/baz", "foo/bar/baz", true},
|
||||
{"foo/", "/bar/baz", "foo/bar/baz", true},
|
||||
{"baz/foo", "bar/baz", "baz/foo/bar/baz", true},
|
||||
{"baz/foo", "/bar/baz", "baz/foo/bar/baz", true},
|
||||
{"baz/foo/", "bar/baz", "baz/foo/bar/baz", true},
|
||||
{"baz/foo/", "/bar/baz", "baz/foo/bar/baz", true},
|
||||
|
||||
// Not escape attempts, but oddly formatted relative paths. Disallowed.
|
||||
{"foo", "./bar", "", false},
|
||||
{"baz/foo", "./bar", "", false},
|
||||
{"foo", "./bar/baz", "", false},
|
||||
{"baz/foo", "./bar/baz", "", false},
|
||||
{"baz/foo", "bar/../baz", "", false},
|
||||
{"baz/foo", "/bar/../baz", "", false},
|
||||
{"baz/foo", "./bar/../baz", "", false},
|
||||
{"baz/foo", "bar/../baz", "", false},
|
||||
{"baz/foo", "/bar/../baz", "", false},
|
||||
{"baz/foo", "./bar/../baz", "", false},
|
||||
|
||||
// Results in an allowed path, but does it by probing. Disallowed.
|
||||
{"foo", "../foo", "", false},
|
||||
{"foo", "../foo/bar", "", false},
|
||||
{"baz/foo", "../foo/bar", "", false},
|
||||
{"baz/foo", "../../baz/foo/bar", "", false},
|
||||
{"baz/foo", "bar/../../foo/bar", "", false},
|
||||
{"baz/foo", "bar/../../../baz/foo/bar", "", false},
|
||||
|
||||
// Escape attempts.
|
||||
{"foo", "", "", false},
|
||||
{"foo", "/", "", false},
|
||||
{"foo", "..", "", false},
|
||||
{"foo", "/..", "", false},
|
||||
{"foo", "../", "", false},
|
||||
{"foo", "../bar", "", false},
|
||||
{"foo", "../foobar", "", false},
|
||||
{"foo/", "../bar", "", false},
|
||||
{"foo/", "../foobar", "", false},
|
||||
{"baz/foo", "../bar", "", false},
|
||||
{"baz/foo", "../foobar", "", false},
|
||||
{"baz/foo/", "../bar", "", false},
|
||||
{"baz/foo/", "../foobar", "", false},
|
||||
{"baz/foo/", "bar/../../quux/baz", "", false},
|
||||
|
||||
// Empty root is a misconfiguration.
|
||||
{"", "/foo", "", false},
|
||||
{"", "foo", "", false},
|
||||
{"", ".", "", false},
|
||||
{"", "..", "", false},
|
||||
{"", "/", "", false},
|
||||
{"", "", "", false},
|
||||
|
||||
// Root=/ is valid, and things should be verified as usual.
|
||||
{"/", "foo", "/foo", true},
|
||||
{"/", "/foo", "/foo", true},
|
||||
{"/", "../foo", "", false},
|
||||
{"/", "..", "", false},
|
||||
{"/", "/", "", false},
|
||||
{"/", "", "", false},
|
||||
|
||||
// special case for filesystems to be able to MkdirAll('.') for example
|
||||
{"/", ".", "/", true},
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
extraCases := []testcase{
|
||||
{`c:\`, `foo`, `c:\foo`, true},
|
||||
{`\\?\c:\`, `foo`, `\\?\c:\foo`, true},
|
||||
{`c:\`, `\foo`, `c:\foo`, true},
|
||||
{`\\?\c:\`, `\foo`, `\\?\c:\foo`, true},
|
||||
{`c:\`, `\\foo`, ``, false},
|
||||
{`c:\`, ``, ``, false},
|
||||
{`c:\`, `\`, ``, false},
|
||||
{`\\?\c:\`, `\\foo`, ``, false},
|
||||
{`\\?\c:\`, ``, ``, false},
|
||||
{`\\?\c:\`, `\`, ``, false},
|
||||
|
||||
// makes no sense, but will be treated simply as a bad filename
|
||||
{`c:\foo`, `d:\bar`, `c:\foo\d:\bar`, true},
|
||||
|
||||
// special case for filesystems to be able to MkdirAll('.') for example
|
||||
{`c:\`, `.`, `c:\`, true},
|
||||
{`\\?\c:\`, `.`, `\\?\c:\`, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
// Add case where root is backslashed, rel is forward slashed
|
||||
extraCases = append(extraCases, testcase{
|
||||
root: filepath.FromSlash(tc.root),
|
||||
rel: tc.rel,
|
||||
joined: tc.joined,
|
||||
ok: tc.ok,
|
||||
})
|
||||
// and the opposite
|
||||
extraCases = append(extraCases, testcase{
|
||||
root: tc.root,
|
||||
rel: filepath.FromSlash(tc.rel),
|
||||
joined: tc.joined,
|
||||
ok: tc.ok,
|
||||
})
|
||||
// and both backslashed
|
||||
extraCases = append(extraCases, testcase{
|
||||
root: filepath.FromSlash(tc.root),
|
||||
rel: filepath.FromSlash(tc.rel),
|
||||
joined: tc.joined,
|
||||
ok: tc.ok,
|
||||
})
|
||||
}
|
||||
|
||||
cases = append(cases, extraCases...)
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
fs := BasicFilesystem{root: tc.root}
|
||||
res, err := fs.rooted(tc.rel)
|
||||
if tc.ok {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for rooted(%q, %q): %v", tc.root, tc.rel, err)
|
||||
continue
|
||||
}
|
||||
exp := filepath.FromSlash(tc.joined)
|
||||
if res != exp {
|
||||
t.Errorf("Unexpected result for rooted(%q, %q): %q != expected %q", tc.root, tc.rel, res, exp)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Errorf("Unexpected pass for rooted(%q, %q) => %q", tc.root, tc.rel, res)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
57
lib/fs/basicfs_unix.go
Normal file
57
lib/fs/basicfs_unix.go
Normal file
@@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import "os"
|
||||
|
||||
func (BasicFilesystem) SymlinksSupported() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) CreateSymlink(target, name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Symlink(target, name)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) ReadSymlink(name string) (string, error) {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return os.Readlink(name)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) MkdirAll(name string, perm FileMode) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(name, os.FileMode(perm))
|
||||
}
|
||||
|
||||
// Unhide is a noop on unix, as unhiding files requires renaming them.
|
||||
// We still check that the relative path does not try to escape the root
|
||||
func (f *BasicFilesystem) Unhide(name string) error {
|
||||
_, err := f.rooted(name)
|
||||
return err
|
||||
}
|
||||
|
||||
// Hide is a noop on unix, as hiding files requires renaming them.
|
||||
// We still check that the relative path does not try to escape the root
|
||||
func (f *BasicFilesystem) Hide(name string) error {
|
||||
_, err := f.rooted(name)
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Roots() ([]string, error) {
|
||||
return []string{"/"}, nil
|
||||
}
|
||||
165
lib/fs/basicfs_windows.go
Normal file
165
lib/fs/basicfs_windows.go
Normal file
@@ -0,0 +1,165 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var errNotSupported = errors.New("symlinks not supported")
|
||||
|
||||
func (BasicFilesystem) SymlinksSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (BasicFilesystem) ReadSymlink(path string) (string, error) {
|
||||
return "", errNotSupported
|
||||
}
|
||||
|
||||
func (BasicFilesystem) CreateSymlink(path, target string) error {
|
||||
return errNotSupported
|
||||
}
|
||||
|
||||
// MkdirAll creates a directory named path, along with any necessary parents,
|
||||
// and returns nil, or else returns an error.
|
||||
// The permission bits perm are used for all directories that MkdirAll creates.
|
||||
// If path is already a directory, MkdirAll does nothing and returns nil.
|
||||
func (f *BasicFilesystem) MkdirAll(path string, perm FileMode) error {
|
||||
path, err := f.rooted(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return f.mkdirAll(path, os.FileMode(perm))
|
||||
}
|
||||
|
||||
// Required due to https://github.com/golang/go/issues/10900
|
||||
func (f *BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
|
||||
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
|
||||
dir, err := os.Stat(path)
|
||||
if err == nil {
|
||||
if dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return &os.PathError{
|
||||
Op: "mkdir",
|
||||
Path: path,
|
||||
Err: syscall.ENOTDIR,
|
||||
}
|
||||
}
|
||||
|
||||
// Slow path: make sure parent exists and then call Mkdir for path.
|
||||
i := len(path)
|
||||
for i > 0 && IsPathSeparator(path[i-1]) { // Skip trailing path separator.
|
||||
i--
|
||||
}
|
||||
|
||||
j := i
|
||||
for j > 0 && !IsPathSeparator(path[j-1]) { // Scan backward over element.
|
||||
j--
|
||||
}
|
||||
|
||||
if j > 1 {
|
||||
// Create parent
|
||||
parent := path[0 : j-1]
|
||||
if parent != filepath.VolumeName(parent) {
|
||||
err = os.MkdirAll(parent, perm)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parent now exists; invoke Mkdir and use its result.
|
||||
err = os.Mkdir(path, perm)
|
||||
if err != nil {
|
||||
// Handle arguments like "foo/." by
|
||||
// double-checking that directory doesn't exist.
|
||||
dir, err1 := os.Lstat(path)
|
||||
if err1 == nil && dir.IsDir() {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Unhide(name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Hide(name string) error {
|
||||
name, err := f.rooted(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p, err := syscall.UTF16PtrFromString(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs |= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Roots() ([]string, error) {
|
||||
kernel32, err := syscall.LoadDLL("kernel32.dll")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
getLogicalDriveStringsHandle, err := kernel32.FindProc("GetLogicalDriveStringsA")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buffer := [1024]byte{}
|
||||
bufferSize := uint32(len(buffer))
|
||||
|
||||
hr, _, _ := getLogicalDriveStringsHandle.Call(uintptr(unsafe.Pointer(&bufferSize)), uintptr(unsafe.Pointer(&buffer)))
|
||||
if hr == 0 {
|
||||
return nil, fmt.Errorf("Syscall failed")
|
||||
}
|
||||
|
||||
var drives []string
|
||||
parts := bytes.Split(buffer[:], []byte{0})
|
||||
for _, part := range parts {
|
||||
if len(part) == 0 {
|
||||
break
|
||||
}
|
||||
drives = append(drives, string(part))
|
||||
}
|
||||
|
||||
return drives, nil
|
||||
}
|
||||
22
lib/fs/debug.go
Normal file
22
lib/fs/debug.go
Normal file
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
l = logger.DefaultLogger.NewFacility("filesystem", "Filesystem access")
|
||||
)
|
||||
|
||||
func init() {
|
||||
l.SetDebug("filesystem", strings.Contains(os.Getenv("STTRACE"), "filesystem") || os.Getenv("STTRACE") == "all")
|
||||
}
|
||||
41
lib/fs/errorfs.go
Normal file
41
lib/fs/errorfs.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import "time"
|
||||
|
||||
type errorFilesystem struct {
|
||||
err error
|
||||
fsType FilesystemType
|
||||
uri string
|
||||
}
|
||||
|
||||
func (fs *errorFilesystem) Chmod(name string, mode FileMode) error { return fs.err }
|
||||
func (fs *errorFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error { return fs.err }
|
||||
func (fs *errorFilesystem) Create(name string) (File, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) CreateSymlink(name, target string) error { return fs.err }
|
||||
func (fs *errorFilesystem) DirNames(name string) ([]string, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) Lstat(name string) (FileInfo, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) Mkdir(name string, perm FileMode) error { return fs.err }
|
||||
func (fs *errorFilesystem) MkdirAll(name string, perm FileMode) error { return fs.err }
|
||||
func (fs *errorFilesystem) Open(name string) (File, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) OpenFile(string, int, FileMode) (File, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) ReadSymlink(name string) (string, error) { return "", fs.err }
|
||||
func (fs *errorFilesystem) Remove(name string) error { return fs.err }
|
||||
func (fs *errorFilesystem) RemoveAll(name string) error { return fs.err }
|
||||
func (fs *errorFilesystem) Rename(oldname, newname string) error { return fs.err }
|
||||
func (fs *errorFilesystem) Stat(name string) (FileInfo, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) SymlinksSupported() bool { return false }
|
||||
func (fs *errorFilesystem) Walk(root string, walkFn WalkFunc) error { return fs.err }
|
||||
func (fs *errorFilesystem) Unhide(name string) error { return fs.err }
|
||||
func (fs *errorFilesystem) Hide(name string) error { return fs.err }
|
||||
func (fs *errorFilesystem) Glob(pattern string) ([]string, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) SyncDir(name string) error { return fs.err }
|
||||
func (fs *errorFilesystem) Roots() ([]string, error) { return nil, fs.err }
|
||||
func (fs *errorFilesystem) Usage(name string) (Usage, error) { return Usage{}, fs.err }
|
||||
func (fs *errorFilesystem) Type() FilesystemType { return fs.fsType }
|
||||
func (fs *errorFilesystem) URI() string { return fs.uri }
|
||||
@@ -7,6 +7,7 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -22,23 +23,38 @@ type Filesystem interface {
|
||||
DirNames(name string) ([]string, error)
|
||||
Lstat(name string) (FileInfo, error)
|
||||
Mkdir(name string, perm FileMode) error
|
||||
MkdirAll(name string, perm FileMode) error
|
||||
Open(name string) (File, error)
|
||||
OpenFile(name string, flags int, mode FileMode) (File, error)
|
||||
ReadSymlink(name string) (string, error)
|
||||
Remove(name string) error
|
||||
RemoveAll(name string) error
|
||||
Rename(oldname, newname string) error
|
||||
Stat(name string) (FileInfo, error)
|
||||
SymlinksSupported() bool
|
||||
Walk(root string, walkFn WalkFunc) error
|
||||
Hide(name string) error
|
||||
Unhide(name string) error
|
||||
Glob(pattern string) ([]string, error)
|
||||
Roots() ([]string, error)
|
||||
Usage(name string) (Usage, error)
|
||||
Type() FilesystemType
|
||||
URI() string
|
||||
}
|
||||
|
||||
// The File interface abstracts access to a regular file, being a somewhat
|
||||
// smaller interface than os.File
|
||||
type File interface {
|
||||
io.Reader
|
||||
io.WriterAt
|
||||
io.Closer
|
||||
io.Reader
|
||||
io.ReaderAt
|
||||
io.Seeker
|
||||
io.Writer
|
||||
io.WriterAt
|
||||
Name() string
|
||||
Truncate(size int64) error
|
||||
Stat() (FileInfo, error)
|
||||
Sync() error
|
||||
}
|
||||
|
||||
// The FileInfo interface is almost the same as os.FileInfo, but with the
|
||||
@@ -59,12 +75,27 @@ type FileInfo interface {
|
||||
// FileMode is similar to os.FileMode
|
||||
type FileMode uint32
|
||||
|
||||
// ModePerm is the equivalent of os.ModePerm
|
||||
const ModePerm = FileMode(os.ModePerm)
|
||||
// Usage represents filesystem space usage
|
||||
type Usage struct {
|
||||
Free int64
|
||||
Total int64
|
||||
}
|
||||
|
||||
// DefaultFilesystem is the fallback to use when nothing explicitly has
|
||||
// been passed.
|
||||
var DefaultFilesystem Filesystem = NewWalkFilesystem(NewBasicFilesystem())
|
||||
// Equivalents from os package.
|
||||
|
||||
const ModePerm = FileMode(os.ModePerm)
|
||||
const ModeSetgid = FileMode(os.ModeSetgid)
|
||||
const ModeSetuid = FileMode(os.ModeSetuid)
|
||||
const ModeSticky = FileMode(os.ModeSticky)
|
||||
const PathSeparator = os.PathSeparator
|
||||
const OptAppend = os.O_APPEND
|
||||
const OptCreate = os.O_CREATE
|
||||
const OptExclusive = os.O_EXCL
|
||||
const OptReadOnly = os.O_RDONLY
|
||||
const OptReadWrite = os.O_RDWR
|
||||
const OptSync = os.O_SYNC
|
||||
const OptTruncate = os.O_TRUNC
|
||||
const OptWriteOnly = os.O_WRONLY
|
||||
|
||||
// SkipDir is used as a return value from WalkFuncs to indicate that
|
||||
// the directory named in the call is to be skipped. It is not returned
|
||||
@@ -76,3 +107,29 @@ var IsExist = os.IsExist
|
||||
|
||||
// IsNotExist is the equivalent of os.IsNotExist
|
||||
var IsNotExist = os.IsNotExist
|
||||
|
||||
// IsPermission is the equivalent of os.IsPermission
|
||||
var IsPermission = os.IsPermission
|
||||
|
||||
// IsPathSeparator is the equivalent of os.IsPathSeparator
|
||||
var IsPathSeparator = os.IsPathSeparator
|
||||
|
||||
func NewFilesystem(fsType FilesystemType, uri string) Filesystem {
|
||||
var fs Filesystem
|
||||
switch fsType {
|
||||
case FilesystemTypeBasic:
|
||||
fs = NewWalkFilesystem(newBasicFilesystem(uri))
|
||||
default:
|
||||
l.Debugln("Unknown filesystem", fsType, uri)
|
||||
fs = &errorFilesystem{
|
||||
fsType: fsType,
|
||||
uri: uri,
|
||||
err: errors.New("filesystem with type " + fsType.String() + " does not exist."),
|
||||
}
|
||||
}
|
||||
|
||||
if l.ShouldDebug("filesystem") {
|
||||
fs = &logFilesystem{fs}
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
158
lib/fs/logfs.go
Normal file
158
lib/fs/logfs.go
Normal file
@@ -0,0 +1,158 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
type logFilesystem struct {
|
||||
Filesystem
|
||||
}
|
||||
|
||||
func getCaller() string {
|
||||
_, file, line, ok := runtime.Caller(2)
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", filepath.Base(file), line)
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Chmod(name string, mode FileMode) error {
|
||||
err := fs.Filesystem.Chmod(name, mode)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Chmod", name, mode, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
err := fs.Filesystem.Chtimes(name, atime, mtime)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Chtimes", name, atime, mtime, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Create(name string) (File, error) {
|
||||
file, err := fs.Filesystem.Create(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Create", name, file, err)
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) CreateSymlink(name, target string) error {
|
||||
err := fs.Filesystem.CreateSymlink(name, target)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "CreateSymlink", name, target, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) DirNames(name string) ([]string, error) {
|
||||
names, err := fs.Filesystem.DirNames(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "DirNames", name, names, err)
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
info, err := fs.Filesystem.Lstat(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Lstat", name, info, err)
|
||||
return info, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Mkdir(name string, perm FileMode) error {
|
||||
err := fs.Filesystem.Mkdir(name, perm)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Mkdir", name, perm, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) MkdirAll(name string, perm FileMode) error {
|
||||
err := fs.Filesystem.MkdirAll(name, perm)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "MkdirAll", name, perm, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Open(name string) (File, error) {
|
||||
file, err := fs.Filesystem.Open(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Open", name, file, err)
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
file, err := fs.Filesystem.OpenFile(name, flags, mode)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "OpenFile", name, flags, mode, file, err)
|
||||
return file, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) ReadSymlink(name string) (string, error) {
|
||||
target, err := fs.Filesystem.ReadSymlink(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "ReadSymlink", name, target, err)
|
||||
return target, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Remove(name string) error {
|
||||
err := fs.Filesystem.Remove(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Remove", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) RemoveAll(name string) error {
|
||||
err := fs.Filesystem.RemoveAll(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "RemoveAll", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Rename(oldname, newname string) error {
|
||||
err := fs.Filesystem.Rename(oldname, newname)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Rename", oldname, newname, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Stat(name string) (FileInfo, error) {
|
||||
info, err := fs.Filesystem.Stat(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Stat", name, info, err)
|
||||
return info, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) SymlinksSupported() bool {
|
||||
supported := fs.Filesystem.SymlinksSupported()
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "SymlinksSupported", supported)
|
||||
return supported
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
err := fs.Filesystem.Walk(root, walkFn)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Walk", root, walkFn, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Unhide(name string) error {
|
||||
err := fs.Filesystem.Unhide(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Unhide", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Hide(name string) error {
|
||||
err := fs.Filesystem.Hide(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Hide", name, err)
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Glob(name string) ([]string, error) {
|
||||
names, err := fs.Filesystem.Glob(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Glob", name, names, err)
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Roots() ([]string, error) {
|
||||
roots, err := fs.Filesystem.Roots()
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Roots", roots, err)
|
||||
return roots, err
|
||||
}
|
||||
|
||||
func (fs *logFilesystem) Usage(name string) (Usage, error) {
|
||||
usage, err := fs.Filesystem.Usage(name)
|
||||
l.Debugln(getCaller(), fs.Type(), fs.URI(), "Usage", name, usage, err)
|
||||
return usage, err
|
||||
}
|
||||
@@ -6,12 +6,7 @@
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// The database is where we store the virtual mtimes
|
||||
type database interface {
|
||||
@@ -20,36 +15,34 @@ type database interface {
|
||||
Delete(key string)
|
||||
}
|
||||
|
||||
// variable so that we can mock it for testing
|
||||
var osChtimes = os.Chtimes
|
||||
|
||||
// The MtimeFS is a filesystem with nanosecond mtime precision, regardless
|
||||
// of what shenanigans the underlying filesystem gets up to. A nil MtimeFS
|
||||
// just does the underlying operations with no additions.
|
||||
type MtimeFS struct {
|
||||
Filesystem
|
||||
db database
|
||||
chtimes func(string, time.Time, time.Time) error
|
||||
db database
|
||||
}
|
||||
|
||||
func NewMtimeFS(underlying Filesystem, db database) *MtimeFS {
|
||||
return &MtimeFS{
|
||||
Filesystem: underlying,
|
||||
chtimes: underlying.Chtimes, // for mocking it out in the tests
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (f *MtimeFS) Chtimes(name string, atime, mtime time.Time) error {
|
||||
if f == nil {
|
||||
return osChtimes(name, atime, mtime)
|
||||
return f.chtimes(name, atime, mtime)
|
||||
}
|
||||
|
||||
// Do a normal Chtimes call, don't care if it succeeds or not.
|
||||
osChtimes(name, atime, mtime)
|
||||
f.chtimes(name, atime, mtime)
|
||||
|
||||
// Stat the file to see what happened. Here we *do* return an error,
|
||||
// because it might be "does not exist" or similar. osutil.Lstat is the
|
||||
// souped up version to account for Android breakage.
|
||||
info, err := osutil.Lstat(name)
|
||||
// because it might be "does not exist" or similar.
|
||||
info, err := f.Filesystem.Lstat(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -25,22 +25,22 @@ func TestMtimeFS(t *testing.T) {
|
||||
// a random time with nanosecond precision
|
||||
testTime := time.Unix(1234567890, 123456789)
|
||||
|
||||
mtimefs := NewMtimeFS(DefaultFilesystem, make(mapStore))
|
||||
mtimefs := NewMtimeFS(newBasicFilesystem("."), make(mapStore))
|
||||
|
||||
// Do one Chtimes call that will go through to the normal filesystem
|
||||
osChtimes = os.Chtimes
|
||||
mtimefs.chtimes = os.Chtimes
|
||||
if err := mtimefs.Chtimes("testdata/exists0", testTime, testTime); err != nil {
|
||||
t.Error("Should not have failed:", err)
|
||||
}
|
||||
|
||||
// Do one call that gets an error back from the underlying Chtimes
|
||||
osChtimes = failChtimes
|
||||
mtimefs.chtimes = failChtimes
|
||||
if err := mtimefs.Chtimes("testdata/exists1", testTime, testTime); err != nil {
|
||||
t.Error("Should not have failed:", err)
|
||||
}
|
||||
|
||||
// Do one call that gets struck by an exceptionally evil Chtimes
|
||||
osChtimes = evilChtimes
|
||||
mtimefs.chtimes = evilChtimes
|
||||
if err := mtimefs.Chtimes("testdata/exists2", testTime, testTime); err != nil {
|
||||
t.Error("Should not have failed:", err)
|
||||
}
|
||||
|
||||
36
lib/fs/types.go
Normal file
36
lib/fs/types.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
type FilesystemType int
|
||||
|
||||
const (
|
||||
FilesystemTypeBasic FilesystemType = iota // default is basic
|
||||
)
|
||||
|
||||
func (t FilesystemType) String() string {
|
||||
switch t {
|
||||
case FilesystemTypeBasic:
|
||||
return "basic"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func (t FilesystemType) MarshalText() ([]byte, error) {
|
||||
return []byte(t.String()), nil
|
||||
}
|
||||
|
||||
func (t *FilesystemType) UnmarshalText(bs []byte) error {
|
||||
switch string(bs) {
|
||||
case "basic":
|
||||
*t = FilesystemTypeBasic
|
||||
default:
|
||||
*t = FilesystemTypeBasic
|
||||
}
|
||||
return nil
|
||||
}
|
||||
55
lib/fs/util.go
Normal file
55
lib/fs/util.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var errNoHome = errors.New("no home directory found - set $HOME (or the platform equivalent)")
|
||||
|
||||
func ExpandTilde(path string) (string, error) {
|
||||
if path == "~" {
|
||||
return getHomeDir()
|
||||
}
|
||||
|
||||
path = filepath.FromSlash(path)
|
||||
if !strings.HasPrefix(path, fmt.Sprintf("~%c", PathSeparator)) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
home, err := getHomeDir()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return filepath.Join(home, path[2:]), nil
|
||||
}
|
||||
|
||||
func getHomeDir() (string, error) {
|
||||
var home string
|
||||
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
home = filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath"))
|
||||
if home == "" {
|
||||
home = os.Getenv("UserProfile")
|
||||
}
|
||||
default:
|
||||
home = os.Getenv("HOME")
|
||||
}
|
||||
|
||||
if home == "" {
|
||||
return "", errNoHome
|
||||
}
|
||||
|
||||
return home, nil
|
||||
}
|
||||
@@ -28,16 +28,16 @@ import "path/filepath"
|
||||
// Walk skips the remaining files in the containing directory.
|
||||
type WalkFunc func(path string, info FileInfo, err error) error
|
||||
|
||||
type WalkFilesystem struct {
|
||||
type walkFilesystem struct {
|
||||
Filesystem
|
||||
}
|
||||
|
||||
func NewWalkFilesystem(next Filesystem) *WalkFilesystem {
|
||||
return &WalkFilesystem{next}
|
||||
func NewWalkFilesystem(next Filesystem) Filesystem {
|
||||
return &walkFilesystem{next}
|
||||
}
|
||||
|
||||
// walk recursively descends path, calling walkFn.
|
||||
func (f *WalkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc) error {
|
||||
func (f *walkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc) error {
|
||||
err := walkFn(path, info, nil)
|
||||
if err != nil {
|
||||
if info.IsDir() && err == SkipDir {
|
||||
@@ -80,7 +80,7 @@ func (f *WalkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc) error
|
||||
// order, which makes the output deterministic but means that for very
|
||||
// large directories Walk can be inefficient.
|
||||
// Walk does not follow symbolic links.
|
||||
func (f *WalkFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
func (f *walkFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
info, err := f.Lstat(root)
|
||||
if err != nil {
|
||||
return walkFn(root, nil, err)
|
||||
|
||||
Reference in New Issue
Block a user