Files
syncthing/lib/fs/filesystem.go
T

382 lines
10 KiB
Go
Raw Normal View History

2016-11-24 12:07:14 +00:00
// 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/.
2016-11-24 12:07:14 +00:00
package fs
import (
2017-10-20 14:52:55 +00:00
"context"
"errors"
"fmt"
2016-11-24 12:07:14 +00:00
"io"
"io/fs"
"os"
"path/filepath"
2017-09-02 05:52:38 +00:00
"strings"
2016-11-24 12:07:14 +00:00
"time"
"github.com/syncthing/syncthing/lib/ignore/ignoreresult"
"github.com/syncthing/syncthing/lib/protocol"
2016-11-24 12:07:14 +00:00
)
type XattrFilter interface {
Permit(string) bool
GetMaxSingleEntrySize() int
GetMaxTotalSize() int
}
2016-11-24 12:07:14 +00:00
// The Filesystem interface abstracts access to the file system.
type Filesystem interface {
Chmod(name string, mode FileMode) error
Lchown(name string, uid, gid string) error // uid/gid as strings; numeric on POSIX, SID on Windows, like in os/user package
2016-11-24 12:07:14 +00:00
Chtimes(name string, atime time.Time, mtime time.Time) error
Create(name string) (File, error)
CreateSymlink(target, name string) error
2016-11-24 12:07:14 +00:00
DirNames(name string) ([]string, error)
Lstat(name string) (FileInfo, error)
Mkdir(name string, perm FileMode) error
MkdirAll(name string, perm FileMode) error
2016-11-24 12:07:14 +00:00
Open(name string) (File, error)
OpenFile(name string, flags int, mode FileMode) (File, error)
ReadSymlink(name string) (string, error)
2016-11-24 12:07:14 +00:00
Remove(name string) error
RemoveAll(name string) error
2016-11-24 12:07:14 +00:00
Rename(oldname, newname string) error
Stat(name string) (FileInfo, error)
2017-10-20 14:52:55 +00:00
Walk(name string, walkFn WalkFunc) error
// If setup fails, returns non-nil error, and if afterwards a fatal (!)
// error occurs, sends that error on the channel. Afterwards this watch
// can be considered stopped.
Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, <-chan error, 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
Options() []Option
SameFile(fi1, fi2 FileInfo) bool
PlatformData(name string, withOwnership, withXattrs bool, xattrFilter XattrFilter) (protocol.PlatformData, error)
GetXattr(name string, xattrFilter XattrFilter) ([]protocol.Xattr, error)
SetXattr(path string, xattrs []protocol.Xattr, xattrFilter XattrFilter) error
}
type wrappingFilesystem interface {
// Used for unwrapping things
underlying() (Filesystem, bool)
2016-11-24 12:07:14 +00:00
}
// The File interface abstracts access to a regular file, being a somewhat
// smaller interface than os.File
type File interface {
io.Closer
2016-11-24 12:07:14 +00:00
io.Reader
io.ReaderAt
io.Seeker
io.Writer
2016-11-24 12:07:14 +00:00
io.WriterAt
Name() string
2016-11-24 12:07:14 +00:00
Truncate(size int64) error
Stat() (FileInfo, error)
Sync() error
2016-11-24 12:07:14 +00:00
}
// The FileInfo interface is almost the same as os.FileInfo, but with the
// Sys method removed (as we don't want to expose whatever is underlying)
// and with a couple of convenience methods added.
type FileInfo interface {
// Standard things present in os.FileInfo
Name() string
Mode() FileMode
Size() int64
ModTime() time.Time
IsDir() bool
Sys() interface{}
2016-11-24 12:07:14 +00:00
// Extensions
IsRegular() bool
IsSymlink() bool
Owner() int
Group() int
2016-11-24 12:07:14 +00:00
}
// FileMode is similar to os.FileMode
type FileMode uint32
func (fm FileMode) String() string {
return os.FileMode(fm).String()
}
// Usage represents filesystem space usage
type Usage struct {
Free uint64
Total uint64
}
2017-10-20 14:52:55 +00:00
type Matcher interface {
Match(name string) ignoreresult.R
2017-10-20 14:52:55 +00:00
}
type Event struct {
Name string
Type EventType
}
type EventType int
const (
NonRemove EventType = 1 + iota
Remove
Mixed // Should probably not be necessary to be used in filesystem interface implementation
)
// Merge returns Mixed, except if evType and other are the same and not Mixed.
func (evType EventType) Merge(other EventType) EventType {
return evType | other
}
2017-10-20 14:52:55 +00:00
func (evType EventType) String() string {
2025-10-21 21:58:18 +02:00
switch evType {
case NonRemove:
2017-10-20 14:52:55 +00:00
return "non-remove"
2025-10-21 21:58:18 +02:00
case Remove:
2017-10-20 14:52:55 +00:00
return "remove"
2025-10-21 21:58:18 +02:00
case Mixed:
2017-10-20 14:52:55 +00:00
return "mixed"
default:
panic("bug: Unknown event type")
}
}
var (
ErrWatchNotSupported = errors.New("watching is not supported")
ErrXattrsNotSupported = errors.New("extended attributes are not supported on this platform")
)
2017-10-20 14:52:55 +00:00
// Equivalents from os package.
const (
ModePerm = FileMode(os.ModePerm)
ModeSetgid = FileMode(os.ModeSetgid)
ModeSetuid = FileMode(os.ModeSetuid)
ModeSticky = FileMode(os.ModeSticky)
ModeSymlink = FileMode(os.ModeSymlink)
ModeType = FileMode(os.ModeType)
PathSeparator = os.PathSeparator
OptAppend = os.O_APPEND
OptCreate = os.O_CREATE
OptExclusive = os.O_EXCL
OptReadOnly = os.O_RDONLY
OptReadWrite = os.O_RDWR
OptSync = os.O_SYNC
OptTruncate = os.O_TRUNC
OptWriteOnly = os.O_WRONLY
)
2016-11-24 12:07:14 +00:00
// 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
// as an error by any function.
2025-06-06 12:03:28 +02:00
var SkipDir = filepath.SkipDir //nolint:errname
func IsExist(err error) bool {
return errors.Is(err, ErrExist)
}
// ErrExist is the equivalent of os.ErrExist
var ErrExist = fs.ErrExist
// IsNotExist is the equivalent of os.IsNotExist
func IsNotExist(err error) bool {
return errors.Is(err, ErrNotExist)
}
// ErrNotExist is the equivalent of os.ErrNotExist
var ErrNotExist = fs.ErrNotExist
// IsPermission is the equivalent of os.IsPermission
func IsPermission(err error) bool {
return errors.Is(err, fs.ErrPermission)
}
// IsPathSeparator is the equivalent of os.IsPathSeparator
var IsPathSeparator = os.IsPathSeparator
func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem {
var caseOpt Option
var mtimeOpt Option
i := 0
for _, opt := range opts {
if caseOpt != nil && mtimeOpt != nil {
break
}
switch opt.(type) {
case *OptionDetectCaseConflicts:
caseOpt = opt
case *optionMtime:
mtimeOpt = opt
default:
opts[i] = opt
i++
}
}
opts = opts[:i]
// Construct file system using the registered factory function
var fs Filesystem
var err error
filesystemFactoriesMutex.Lock()
fsFactory, factoryFound := filesystemFactories[fsType]
filesystemFactoriesMutex.Unlock()
if factoryFound {
fs, err = fsFactory(uri, opts...)
} else {
err = fmt.Errorf("File system type '%s' not recognized", fsType)
}
if err != nil {
fs = &errorFilesystem{
fsType: fsType,
uri: uri,
err: err,
}
}
// mtime handling should happen inside walking, as filesystem calls while
// walking should be mtime-resolved too
if mtimeOpt != nil {
fs = mtimeOpt.apply(fs)
}
fs = &metricsFS{next: fs}
layersAboveWalkFilesystem := 0
if caseOpt != nil {
// DirNames calls made to check the case of a name will also be
// attributed to the calling function.
layersAboveWalkFilesystem++
}
2025-06-06 12:03:28 +02:00
switch {
case l.ShouldDebug("walkfs"):
// A walkFilesystem is not a layer to skip, it embeds the underlying
// filesystem, passing calls directly trough. Except for calls made
// during walking, however those are truly originating in the walk
// filesystem.
fs = NewWalkFilesystem(newLogFilesystem(fs, layersAboveWalkFilesystem))
2025-06-06 12:03:28 +02:00
case l.ShouldDebug("fs"):
fs = newLogFilesystem(NewWalkFilesystem(fs), layersAboveWalkFilesystem)
2025-06-06 12:03:28 +02:00
default:
fs = NewWalkFilesystem(fs)
}
// Case handling is at the outermost layer to resolve all input names.
// Reason being is that the only names/paths that are potentially "wrong"
// come from outside the fs package. Any paths that result from filesystem
// operations itself already have the correct case. Thus there's e.g. no
// point to check the case on all the stating the walk filesystem does, it
// just adds overhead.
if caseOpt != nil {
fs = caseOpt.apply(fs)
}
return fs
}
2017-09-02 05:52:38 +00:00
// fs cannot import config or versioner, so we hard code .stfolder
// (config.DefaultMarkerName) and .stversions (versioner.DefaultPath)
var internals = []string{".stfolder", ".stignore", ".stversions"}
2017-09-02 05:52:38 +00:00
// IsInternal returns true if the file, as a path relative to the folder
// root, represents an internal file that should always be ignored. The file
// path must be clean (i.e., in canonical shortest form).
func IsInternal(file string) bool {
for _, internal := range internals {
if file == internal {
return true
}
if IsParent(file, internal) {
2017-09-02 05:52:38 +00:00
return true
}
}
return false
}
var (
errPathInvalid = errors.New("path is invalid")
errPathTraversingUpwards = errors.New("relative path traversing upwards (starting with ..)")
)
// Canonicalize checks that the file path is valid and returns it in the "canonical" form:
// - /foo/bar -> foo/bar
// - / -> "."
func Canonicalize(file string) (string, error) {
2021-11-23 19:55:58 +01:00
const pathSep = string(PathSeparator)
if strings.HasPrefix(file, 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 and is out of spec.
return "", errPathInvalid
}
// The relative path should be clean from internal dotdots and similar
2025-08-12 11:05:10 -07:00
// funkiness.
file = filepath.Clean(file)
// It is not acceptable to attempt to traverse upwards.
2021-03-17 23:12:26 +01:00
if file == ".." {
return "", errPathTraversingUpwards
}
if strings.HasPrefix(file, ".."+pathSep) {
return "", errPathTraversingUpwards
}
if strings.HasPrefix(file, pathSep) {
if file == pathSep {
return ".", nil
}
return file[1:], nil
}
return file, nil
}
2020-09-09 14:38:39 +02:00
// unwrapFilesystem removes "wrapping" filesystems to expose the filesystem of the requested wrapper type T, if it exists.
func unwrapFilesystem[T Filesystem](fs Filesystem) (T, bool) {
2020-09-09 14:38:39 +02:00
for {
if unwrapped, ok := fs.(T); ok {
return unwrapped, true
}
wrappingFs, ok := fs.(wrappingFilesystem)
if !ok {
var x T
return x, false
}
fs, ok = wrappingFs.underlying()
if !ok {
var x T
return x, false
2020-09-09 14:38:39 +02:00
}
}
}
// WriteFile writes data to the named file, creating it if necessary.
// If the file does not exist, WriteFile creates it with permissions perm (before umask);
// otherwise WriteFile truncates it before writing, without changing permissions.
// Since Writefile requires multiple system calls to complete, a failure mid-operation
// can leave the file in a partially written state.
func WriteFile(fs Filesystem, name string, data []byte, perm FileMode) error {
f, err := fs.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}