feat(fs, config): add support for custom filesystem type construction (#9887)

For Synctrain I would like to create a virtual filesystem that exposes
iOS' photo library. This can only be accessed through APIs.
This commit is contained in:
Tommy van der Vorst
2025-04-03 10:12:23 +02:00
committed by GitHub
parent f9007ed106
commit f15d50c2e8
12 changed files with 87 additions and 74 deletions
+3 -1
View File
@@ -111,6 +111,7 @@ func TestDefaultValues(t *testing.T) {
AutoNormalize: true, AutoNormalize: true,
MinDiskFree: size, MinDiskFree: size,
Versioning: VersioningConfiguration{ Versioning: VersioningConfiguration{
FSType: FilesystemTypeBasic,
CleanupIntervalS: 3600, CleanupIntervalS: 3600,
Params: map[string]string{}, Params: map[string]string{},
}, },
@@ -520,7 +521,8 @@ func TestIssue1750(t *testing.T) {
func TestFolderPath(t *testing.T) { func TestFolderPath(t *testing.T) {
folder := FolderConfiguration{ folder := FolderConfiguration{
Path: "~/tmp", FilesystemType: FilesystemTypeBasic,
Path: "~/tmp",
} }
realPath := folder.Filesystem(nil).URI() realPath := folder.Filesystem(nil).URI()
+13 -30
View File
@@ -8,47 +8,30 @@ package config
import "github.com/syncthing/syncthing/lib/fs" import "github.com/syncthing/syncthing/lib/fs"
type FilesystemType int32 type FilesystemType string
const ( const (
FilesystemTypeBasic FilesystemType = 0 FilesystemTypeBasic FilesystemType = "basic"
FilesystemTypeFake FilesystemType = 1 FilesystemTypeFake FilesystemType = "fake"
) )
func (t FilesystemType) String() string { func (t FilesystemType) ToFS() fs.FilesystemType {
switch t { return fs.FilesystemType(string(t))
case FilesystemTypeBasic:
return "basic"
case FilesystemTypeFake:
return "fake"
default:
return "unknown"
}
} }
func (t FilesystemType) ToFS() fs.FilesystemType { func (t FilesystemType) String() string {
switch t { return string(t)
case FilesystemTypeBasic:
return fs.FilesystemTypeBasic
case FilesystemTypeFake:
return fs.FilesystemTypeFake
default:
return fs.FilesystemTypeBasic
}
} }
func (t FilesystemType) MarshalText() ([]byte, error) { func (t FilesystemType) MarshalText() ([]byte, error) {
return []byte(t.String()), nil return []byte(t), nil
} }
func (t *FilesystemType) UnmarshalText(bs []byte) error { func (t *FilesystemType) UnmarshalText(bs []byte) error {
switch string(bs) { *t = FilesystemType(string(bs))
case "basic":
*t = FilesystemTypeBasic
case "fake":
*t = FilesystemTypeFake
default:
*t = FilesystemTypeBasic
}
return nil return nil
} }
func (t *FilesystemType) ParseDefault(str string) error {
return t.UnmarshalText([]byte(str))
}
+1 -1
View File
@@ -47,7 +47,7 @@ type FolderDeviceConfiguration struct {
type FolderConfiguration struct { type FolderConfiguration struct {
ID string `json:"id" xml:"id,attr" nodefault:"true"` ID string `json:"id" xml:"id,attr" nodefault:"true"`
Label string `json:"label" xml:"label,attr" restart:"false"` Label string `json:"label" xml:"label,attr" restart:"false"`
FilesystemType FilesystemType `json:"filesystemType" xml:"filesystemType"` FilesystemType FilesystemType `json:"filesystemType" xml:"filesystemType" default:"basic"`
Path string `json:"path" xml:"path,attr" default:"~"` Path string `json:"path" xml:"path,attr" default:"~"`
Type FolderType `json:"type" xml:"type,attr"` Type FolderType `json:"type" xml:"type,attr"`
Devices []FolderDeviceConfiguration `json:"devices" xml:"device"` Devices []FolderDeviceConfiguration `json:"devices" xml:"device"`
+2 -2
View File
@@ -20,7 +20,7 @@ type VersioningConfiguration struct {
Params map[string]string `json:"params" xml:"parameter" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` Params map[string]string `json:"params" xml:"parameter" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
CleanupIntervalS int `json:"cleanupIntervalS" xml:"cleanupIntervalS" default:"3600"` CleanupIntervalS int `json:"cleanupIntervalS" xml:"cleanupIntervalS" default:"3600"`
FSPath string `json:"fsPath" xml:"fsPath"` FSPath string `json:"fsPath" xml:"fsPath"`
FSType FilesystemType `json:"fsType" xml:"fsType"` FSType FilesystemType `json:"fsType" xml:"fsType" default:"basic"`
} }
func (c *VersioningConfiguration) Reset() { func (c *VersioningConfiguration) Reset() {
@@ -33,7 +33,7 @@ type internalVersioningConfiguration struct {
Params []internalParam `xml:"param"` Params []internalParam `xml:"param"`
CleanupIntervalS int `xml:"cleanupIntervalS" default:"3600"` CleanupIntervalS int `xml:"cleanupIntervalS" default:"3600"`
FSPath string `xml:"fsPath"` FSPath string `xml:"fsPath"`
FSType FilesystemType `xml:"fsType"` FSType FilesystemType `xml:"fsType" default:"basic"`
} }
type internalParam struct { type internalParam struct {
+8
View File
@@ -19,6 +19,8 @@ import (
"github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/build"
) )
const FilesystemTypeBasic FilesystemType = "basic"
var ( var (
errInvalidFilenameEmpty = errors.New("name is invalid, must not be empty") 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") errInvalidFilenameWindowsSpacePeriod = errors.New("name is invalid, must not end in space or period on Windows")
@@ -56,6 +58,12 @@ type (
groupCache = valueCache[string, *user.Group] groupCache = valueCache[string, *user.Group]
) )
func init() {
RegisterFilesystemType(FilesystemTypeBasic, func(root string, opts ...Option) (Filesystem, error) {
return newBasicFilesystem(root, opts...), nil
})
}
func newBasicFilesystem(root string, opts ...Option) *BasicFilesystem { func newBasicFilesystem(root string, opts ...Option) *BasicFilesystem {
if root == "" { if root == "" {
root = "." // Otherwise "" becomes "/" below root = "." // Otherwise "" becomes "/" below
+8
View File
@@ -26,6 +26,14 @@ import (
"github.com/syncthing/syncthing/lib/protocol" "github.com/syncthing/syncthing/lib/protocol"
) )
const FilesystemTypeFake FilesystemType = "fake"
func init() {
RegisterFilesystemType(FilesystemTypeFake, func(root string, opts ...Option) (Filesystem, error) {
return newFakeFilesystem(root, opts...), nil
})
}
// see readShortAt() // see readShortAt()
const randomBlockShift = 14 // 128k const randomBlockShift = 14 // 128k
+14 -19
View File
@@ -9,6 +9,7 @@ package fs
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"io" "io"
"io/fs" "io/fs"
"os" "os"
@@ -215,17 +216,6 @@ func IsPermission(err error) bool {
// IsPathSeparator is the equivalent of os.IsPathSeparator // IsPathSeparator is the equivalent of os.IsPathSeparator
var IsPathSeparator = os.IsPathSeparator var IsPathSeparator = os.IsPathSeparator
// Option modifies a filesystem at creation. An option might be specific
// to a filesystem-type.
//
// String is used to detect options with the same effect, i.e. must be different
// for options with different effects. Meaning if an option has parameters, a
// representation of those must be part of the returned string.
type Option interface {
String() string
apply(Filesystem) Filesystem
}
func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem { func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem {
var caseOpt Option var caseOpt Option
var mtimeOpt Option var mtimeOpt Option
@@ -246,18 +236,23 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
} }
opts = opts[:i] opts = opts[:i]
// Construct file system using the registered factory function
var fs Filesystem var fs Filesystem
switch fsType { var err error
case FilesystemTypeBasic: filesystemFactoriesMutex.Lock()
fs = newBasicFilesystem(uri, opts...) fsFactory, factoryFound := filesystemFactories[fsType]
case FilesystemTypeFake: filesystemFactoriesMutex.Unlock()
fs = newFakeFilesystem(uri, opts...) if factoryFound {
default: fs, err = fsFactory(uri, opts...)
l.Debugln("Unknown filesystem", fsType, uri) } else {
err = fmt.Errorf("File system type '%s' not recognized", fsType)
}
if err != nil {
fs = &errorFilesystem{ fs = &errorFilesystem{
fsType: fsType, fsType: fsType,
uri: uri, uri: uri,
err: errors.New("filesystem with type " + fsType.String() + " does not exist."), err: err,
} }
} }
+28 -14
View File
@@ -6,20 +6,34 @@
package fs package fs
type FilesystemType int32 import "sync"
const ( type FilesystemType string
FilesystemTypeBasic FilesystemType = 0
FilesystemTypeFake FilesystemType = 1
)
func (t FilesystemType) String() string { // Option modifies a filesystem at creation. An option might be specific
switch t { // to a filesystem-type.
case FilesystemTypeBasic: //
return "basic" // String is used to detect options with the same effect, i.e. must be different
case FilesystemTypeFake: // for options with different effects. Meaning if an option has parameters, a
return "fake" // representation of those must be part of the returned string.
default: type Option interface {
return "unknown" String() string
} apply(Filesystem) Filesystem
}
// Factory function type for constructing a custom file system. It takes the URI
// and options as its parameters.
type FilesystemFactory func(string, ...Option) (Filesystem, error)
// For each registered file system type, a function to construct a file system.
var filesystemFactories map[FilesystemType]FilesystemFactory = make(map[FilesystemType]FilesystemFactory)
var filesystemFactoriesMutex sync.Mutex = sync.Mutex{}
// Register a function to be called when a filesystem is to be constructed with
// the specified fsType. The function will receive the URI for the file system as well
// as all options.
func RegisterFilesystemType(fsType FilesystemType, fn FilesystemFactory) {
filesystemFactoriesMutex.Lock()
defer filesystemFactoriesMutex.Unlock()
filesystemFactories[fsType] = fn
} }
+4 -3
View File
@@ -2774,9 +2774,10 @@ func TestIssue4903(t *testing.T) {
folderPath := "nonexistent" folderPath := "nonexistent"
cfg := defaultCfgWrapper.RawCopy() cfg := defaultCfgWrapper.RawCopy()
fcfg := config.FolderConfiguration{ fcfg := config.FolderConfiguration{
ID: "folder1", ID: "folder1",
Path: folderPath, Path: folderPath,
Paused: true, FilesystemType: config.FilesystemTypeBasic,
Paused: true,
Devices: []config.FolderDeviceConfiguration{ Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1}, {DeviceID: device1},
}, },
+1 -1
View File
@@ -677,7 +677,7 @@ func TestStopWalk(t *testing.T) {
// Use an errorFs as the backing fs for the rest of the interface // Use an errorFs as the backing fs for the rest of the interface
// The way we get it is a bit hacky tho. // The way we get it is a bit hacky tho.
errorFs := fs.NewFilesystem(fs.FilesystemType(-1), ".") errorFs := fs.NewFilesystem(fs.FilesystemType("error"), ".")
fs := fs.NewWalkFilesystem(&infiniteFS{errorFs, 100, 100, 1e6}) fs := fs.NewWalkFilesystem(&infiniteFS{errorFs, 100, 100, 1e6})
const numHashers = 4 const numHashers = 4
+1 -1
View File
@@ -74,7 +74,7 @@ func (v external) Archive(filePath string) error {
} }
context := map[string]string{ context := map[string]string{
"%FOLDER_FILESYSTEM%": v.filesystem.Type().String(), "%FOLDER_FILESYSTEM%": string(v.filesystem.Type()),
"%FOLDER_PATH%": v.filesystem.URI(), "%FOLDER_PATH%": v.filesystem.URI(),
"%FILE_PATH%": filePath, "%FILE_PATH%": filePath,
} }
+4 -2
View File
@@ -139,10 +139,12 @@ func TestCreateVersionPath(t *testing.T) {
} }
folderCfg := config.FolderConfiguration{ folderCfg := config.FolderConfiguration{
ID: "default", ID: "default",
Path: tmpDir, FilesystemType: config.FilesystemTypeBasic,
Path: tmpDir,
Versioning: config.VersioningConfiguration{ Versioning: config.VersioningConfiguration{
Type: "staggered", Type: "staggered",
FSType: config.FilesystemTypeBasic,
FSPath: versionsDir, FSPath: versionsDir,
}, },
} }