瀏覽代碼

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.
Tommy van der Vorst 6 月之前
父節點
當前提交
f15d50c2e8

+ 3 - 1
lib/config/config_test.go

@@ -111,6 +111,7 @@ func TestDefaultValues(t *testing.T) {
 				AutoNormalize:    true,
 				MinDiskFree:      size,
 				Versioning: VersioningConfiguration{
+					FSType:           FilesystemTypeBasic,
 					CleanupIntervalS: 3600,
 					Params:           map[string]string{},
 				},
@@ -520,7 +521,8 @@ func TestIssue1750(t *testing.T) {
 
 func TestFolderPath(t *testing.T) {
 	folder := FolderConfiguration{
-		Path: "~/tmp",
+		FilesystemType: FilesystemTypeBasic,
+		Path:           "~/tmp",
 	}
 
 	realPath := folder.Filesystem(nil).URI()

+ 13 - 30
lib/config/filesystemtype.go

@@ -8,47 +8,30 @@ package config
 
 import "github.com/syncthing/syncthing/lib/fs"
 
-type FilesystemType int32
+type FilesystemType string
 
 const (
-	FilesystemTypeBasic FilesystemType = 0
-	FilesystemTypeFake  FilesystemType = 1
+	FilesystemTypeBasic FilesystemType = "basic"
+	FilesystemTypeFake  FilesystemType = "fake"
 )
 
-func (t FilesystemType) String() string {
-	switch t {
-	case FilesystemTypeBasic:
-		return "basic"
-	case FilesystemTypeFake:
-		return "fake"
-	default:
-		return "unknown"
-	}
+func (t FilesystemType) ToFS() fs.FilesystemType {
+	return fs.FilesystemType(string(t))
 }
 
-func (t FilesystemType) ToFS() fs.FilesystemType {
-	switch t {
-	case FilesystemTypeBasic:
-		return fs.FilesystemTypeBasic
-	case FilesystemTypeFake:
-		return fs.FilesystemTypeFake
-	default:
-		return fs.FilesystemTypeBasic
-	}
+func (t FilesystemType) String() string {
+	return string(t)
 }
 
 func (t FilesystemType) MarshalText() ([]byte, error) {
-	return []byte(t.String()), nil
+	return []byte(t), nil
 }
 
 func (t *FilesystemType) UnmarshalText(bs []byte) error {
-	switch string(bs) {
-	case "basic":
-		*t = FilesystemTypeBasic
-	case "fake":
-		*t = FilesystemTypeFake
-	default:
-		*t = FilesystemTypeBasic
-	}
+	*t = FilesystemType(string(bs))
 	return nil
 }
+
+func (t *FilesystemType) ParseDefault(str string) error {
+	return t.UnmarshalText([]byte(str))
+}

+ 1 - 1
lib/config/folderconfiguration.go

@@ -47,7 +47,7 @@ type FolderDeviceConfiguration struct {
 type FolderConfiguration struct {
 	ID                      string                      `json:"id" xml:"id,attr" nodefault:"true"`
 	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:"~"`
 	Type                    FolderType                  `json:"type" xml:"type,attr"`
 	Devices                 []FolderDeviceConfiguration `json:"devices" xml:"device"`

+ 2 - 2
lib/config/versioningconfiguration.go

@@ -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"`
 	CleanupIntervalS int               `json:"cleanupIntervalS" xml:"cleanupIntervalS" default:"3600"`
 	FSPath           string            `json:"fsPath" xml:"fsPath"`
-	FSType           FilesystemType    `json:"fsType" xml:"fsType"`
+	FSType           FilesystemType    `json:"fsType" xml:"fsType" default:"basic"`
 }
 
 func (c *VersioningConfiguration) Reset() {
@@ -33,7 +33,7 @@ type internalVersioningConfiguration struct {
 	Params           []internalParam `xml:"param"`
 	CleanupIntervalS int             `xml:"cleanupIntervalS" default:"3600"`
 	FSPath           string          `xml:"fsPath"`
-	FSType           FilesystemType  `xml:"fsType"`
+	FSType           FilesystemType  `xml:"fsType" default:"basic"`
 }
 
 type internalParam struct {

+ 8 - 0
lib/fs/basicfs.go

@@ -19,6 +19,8 @@ import (
 	"github.com/syncthing/syncthing/lib/build"
 )
 
+const FilesystemTypeBasic FilesystemType = "basic"
+
 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")
@@ -56,6 +58,12 @@ type (
 	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 {
 	if root == "" {
 		root = "." // Otherwise "" becomes "/" below

+ 8 - 0
lib/fs/fakefs.go

@@ -26,6 +26,14 @@ import (
 	"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()
 const randomBlockShift = 14 // 128k
 

+ 14 - 19
lib/fs/filesystem.go

@@ -9,6 +9,7 @@ package fs
 import (
 	"context"
 	"errors"
+	"fmt"
 	"io"
 	"io/fs"
 	"os"
@@ -215,17 +216,6 @@ func IsPermission(err error) bool {
 // IsPathSeparator is the equivalent of 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 {
 	var caseOpt Option
 	var mtimeOpt Option
@@ -246,18 +236,23 @@ func NewFilesystem(fsType FilesystemType, uri string, opts ...Option) Filesystem
 	}
 	opts = opts[:i]
 
+	// Construct file system using the registered factory function
 	var fs Filesystem
-	switch fsType {
-	case FilesystemTypeBasic:
-		fs = newBasicFilesystem(uri, opts...)
-	case FilesystemTypeFake:
-		fs = newFakeFilesystem(uri, opts...)
-	default:
-		l.Debugln("Unknown filesystem", fsType, uri)
+	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:    errors.New("filesystem with type " + fsType.String() + " does not exist."),
+			err:    err,
 		}
 	}
 

+ 28 - 14
lib/fs/types.go

@@ -6,20 +6,34 @@
 
 package fs
 
-type FilesystemType int32
+import "sync"
 
-const (
-	FilesystemTypeBasic FilesystemType = 0
-	FilesystemTypeFake  FilesystemType = 1
-)
+type FilesystemType string
 
-func (t FilesystemType) String() string {
-	switch t {
-	case FilesystemTypeBasic:
-		return "basic"
-	case FilesystemTypeFake:
-		return "fake"
-	default:
-		return "unknown"
-	}
+// 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
+}
+
+// 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
lib/model/model_test.go

@@ -2774,9 +2774,10 @@ func TestIssue4903(t *testing.T) {
 	folderPath := "nonexistent"
 	cfg := defaultCfgWrapper.RawCopy()
 	fcfg := config.FolderConfiguration{
-		ID:     "folder1",
-		Path:   folderPath,
-		Paused: true,
+		ID:             "folder1",
+		Path:           folderPath,
+		FilesystemType: config.FilesystemTypeBasic,
+		Paused:         true,
 		Devices: []config.FolderDeviceConfiguration{
 			{DeviceID: device1},
 		},

+ 1 - 1
lib/scanner/walk_test.go

@@ -677,7 +677,7 @@ func TestStopWalk(t *testing.T) {
 
 	// Use an errorFs as the backing fs for the rest of the interface
 	// 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})
 
 	const numHashers = 4

+ 1 - 1
lib/versioner/external.go

@@ -74,7 +74,7 @@ func (v external) Archive(filePath string) error {
 	}
 
 	context := map[string]string{
-		"%FOLDER_FILESYSTEM%": v.filesystem.Type().String(),
+		"%FOLDER_FILESYSTEM%": string(v.filesystem.Type()),
 		"%FOLDER_PATH%":       v.filesystem.URI(),
 		"%FILE_PATH%":         filePath,
 	}

+ 4 - 2
lib/versioner/staggered_test.go

@@ -139,10 +139,12 @@ func TestCreateVersionPath(t *testing.T) {
 	}
 
 	folderCfg := config.FolderConfiguration{
-		ID:   "default",
-		Path: tmpDir,
+		ID:             "default",
+		FilesystemType: config.FilesystemTypeBasic,
+		Path:           tmpDir,
 		Versioning: config.VersioningConfiguration{
 			Type:   "staggered",
+			FSType: config.FilesystemTypeBasic,
 			FSPath: versionsDir,
 		},
 	}