123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387 |
- // 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"
- "os/user"
- "path/filepath"
- "strings"
- "time"
- "github.com/shirou/gopsutil/v4/disk"
- "github.com/syncthing/syncthing/lib/build"
- )
- var (
- errInvalidFilenameEmpty = errors.New("name is invalid, must not be empty")
- errInvalidFilenameWindowsSpacePeriod = errors.New("name is invalid, must not end in space or period on Windows")
- errInvalidFilenameWindowsReservedName = errors.New("name is invalid, contains Windows reserved name")
- errInvalidFilenameWindowsReservedChar = errors.New("name is invalid, contains Windows reserved character")
- )
- type OptionJunctionsAsDirs struct{}
- func (*OptionJunctionsAsDirs) apply(fs Filesystem) Filesystem {
- if basic, ok := fs.(*BasicFilesystem); !ok {
- l.Warnln("WithJunctionsAsDirs must only be used with FilesystemTypeBasic")
- } else {
- basic.junctionsAsDirs = true
- }
- return fs
- }
- func (*OptionJunctionsAsDirs) String() string {
- return "junctionsAsDirs"
- }
- // 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
- junctionsAsDirs bool
- options []Option
- userCache *userCache
- groupCache *groupCache
- }
- type (
- userCache = valueCache[string, *user.User]
- groupCache = valueCache[string, *user.Group]
- )
- func newBasicFilesystem(root string, opts ...Option) *BasicFilesystem {
- if root == "" {
- root = "." // Otherwise "" becomes "/" below
- }
- // 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.
- sep := string(filepath.Separator)
- root = filepath.Clean(filepath.Dir(root + sep))
- // 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 build.IsWindows {
- root = longFilenameSupport(root)
- }
- fs := &BasicFilesystem{
- root: root,
- options: opts,
- userCache: newValueCache(time.Hour, user.LookupId),
- groupCache: newValueCache(time.Hour, user.LookupGroupId),
- }
- for _, opt := range opts {
- opt.apply(fs)
- }
- return fs
- }
- // 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
- // directory, this returns an error, to prevent accessing files that are not in the
- // shared directory.
- func (f *BasicFilesystem) rooted(rel string) (string, error) {
- return rooted(rel, f.root)
- }
- func rooted(rel, root string) (string, error) {
- // The root must not be empty.
- if root == "" {
- return "", errInvalidFilenameEmpty
- }
- var err error
- // Takes care that rel does not try to escape
- rel, err = Canonicalize(rel)
- if err != nil {
- return "", err
- }
- return filepath.Join(root, rel), nil
- }
- func (f *BasicFilesystem) unrooted(path string) string {
- return rel(path, f.root)
- }
- 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))
- }
- // 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))
- }
- func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
- name, err := f.rooted(name)
- if err != nil {
- return nil, err
- }
- fi, err := f.underlyingLstat(name)
- if err != nil {
- return nil, err
- }
- return basicFileInfo{fi}, err
- }
- 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
- }
- return basicFileInfo{fi}, err
- }
- func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
- name, err := f.rooted(name)
- if err != nil {
- return nil, err
- }
- fd, err := os.OpenFile(name, OptReadOnly, 0o777)
- if err != nil {
- return nil, err
- }
- defer fd.Close()
- names, err := fd.Readdirnames(-1)
- if err != nil {
- return nil, err
- }
- return names, nil
- }
- func (f *BasicFilesystem) Open(name string) (File, error) {
- rootedName, err := f.rooted(name)
- if err != nil {
- return nil, err
- }
- fd, err := os.Open(rootedName)
- if err != nil {
- return nil, err
- }
- return basicFile{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 basicFile{fd, name}, err
- }
- func (f *BasicFilesystem) Create(name string) (File, error) {
- rootedName, err := f.rooted(name)
- if err != nil {
- return nil, err
- }
- fd, err := os.Create(rootedName)
- if err != nil {
- return nil, err
- }
- return basicFile{fd, name}, err
- }
- func (*BasicFilesystem) Walk(_ string, _ WalkFunc) error {
- // implemented in WalkFilesystem
- 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 := disk.Usage(name)
- if err != nil {
- return Usage{}, err
- }
- return Usage{
- Free: u.Free,
- Total: u.Total,
- }, nil
- }
- func (*BasicFilesystem) Type() FilesystemType {
- return FilesystemTypeBasic
- }
- func (f *BasicFilesystem) URI() string {
- return strings.TrimPrefix(f.root, `\\?\`)
- }
- func (f *BasicFilesystem) Options() []Option {
- return f.options
- }
- func (*BasicFilesystem) SameFile(fi1, fi2 FileInfo) bool {
- // Like os.SameFile, we always return false unless fi1 and fi2 were created
- // by this package's Stat/Lstat method.
- f1, ok1 := fi1.(basicFileInfo)
- f2, ok2 := fi2.(basicFileInfo)
- if !ok1 || !ok2 {
- return false
- }
- return os.SameFile(f1.osFileInfo(), f2.osFileInfo())
- }
- func (*BasicFilesystem) underlying() (Filesystem, bool) {
- return nil, false
- }
- func (*BasicFilesystem) wrapperType() filesystemWrapperType {
- return filesystemWrapperTypeNone
- }
- // basicFile implements the fs.File interface on top of an os.File
- type basicFile struct {
- *os.File
- name string
- }
- func (f basicFile) Name() string {
- return f.name
- }
- func (f basicFile) Stat() (FileInfo, error) {
- info, err := f.File.Stat()
- if err != nil {
- return nil, err
- }
- return basicFileInfo{info}, nil
- }
- // basicFileInfo implements the fs.FileInfo interface on top of an os.FileInfo.
- type basicFileInfo struct {
- os.FileInfo
- }
- func (e basicFileInfo) IsSymlink() bool {
- // Must use basicFileInfo.Mode() because it may apply magic.
- return e.Mode()&ModeSymlink != 0
- }
- func (e basicFileInfo) IsRegular() bool {
- // Must use basicFileInfo.Mode() because it may apply magic.
- return e.Mode()&ModeType == 0
- }
- // longFilenameSupport adds the necessary prefix to the path to enable long
- // filename support on windows if necessary.
- // This does NOT check the current system, i.e. will also take effect on unix paths.
- func longFilenameSupport(path string) string {
- if filepath.IsAbs(path) && !strings.HasPrefix(path, `\\`) {
- return `\\?\` + path
- }
- return path
- }
- type ErrWatchEventOutsideRoot struct{ msg string }
- func (e *ErrWatchEventOutsideRoot) Error() string {
- return e.msg
- }
- func (f *BasicFilesystem) newErrWatchEventOutsideRoot(absPath string, roots []string) *ErrWatchEventOutsideRoot {
- return &ErrWatchEventOutsideRoot{fmt.Sprintf("Watching for changes encountered an event outside of the filesystem root: f.root==%v, roots==%v, path==%v. This should never happen, please report this message to forum.syncthing.net.", f.root, roots, absPath)}
- }
|