Pārlūkot izejas kodu

lib/model: Use factories for creating folders

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3029
Audrius Butkevicius 9 gadi atpakaļ
vecāks
revīzija
eabd2fc936

+ 2 - 8
cmd/syncthing/main.go

@@ -691,13 +691,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
 			}
 			m.Index(device, folderCfg.ID, nil, 0, nil)
 		}
-		// Routine to pull blocks from other devices to synchronize the local
-		// folder. Does not run when we are in read only (publish only) mode.
-		if folderCfg.ReadOnly {
-			m.StartFolderRO(folderCfg.ID)
-		} else {
-			m.StartFolderRW(folderCfg.ID)
-		}
+		m.StartFolder(folderCfg.ID)
 	}
 
 	mainService.Add(m)
@@ -994,7 +988,7 @@ func defaultConfig(myName string) config.Configuration {
 	if !noDefaultFolder {
 		l.Infoln("Default folder created and/or linked to new config")
 
-		defaultFolder = config.NewFolderConfiguration("default", locations[locDefFolder])
+		defaultFolder = config.NewFolderConfiguration("default", locations[locDefFolder], config.FolderTypeReadWrite)
 		defaultFolder.RescanIntervalS = 60
 		defaultFolder.MinDiskFreePct = 1
 		defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}

+ 1 - 1
cmd/syncthing/usage_report.go

@@ -133,7 +133,7 @@ func reportData(cfg configIntf, m modelIntf) map[string]interface{} {
 	for _, cfg := range cfg.Folders() {
 		rescanIntvs = append(rescanIntvs, cfg.RescanIntervalS)
 
-		if cfg.ReadOnly {
+		if cfg.Type == config.FolderTypeReadOnly {
 			folderUses["readonly"]++
 		}
 		if cfg.IgnorePerms {

+ 8 - 7
gui/default/index.html

@@ -231,7 +231,7 @@
               <div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
               <div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id)}}%"></div>
               <h4 class="panel-title">
-                <span class="fa hidden-xs fa-fw" ng-class="[folder.readOnly ? 'fa-lock' : 'fa-folder']"></span>
+                <span class="fa hidden-xs fa-fw" ng-class="[folder.type == 'readonly' ? 'fa-lock' : 'fa-folder']"></span>
                 <a href="#folder-{{$index}}">
                   <span ng-show="folder.label.length == 0">{{folder.id}}</span>
                   <span tooltip data-original-title="{{folder.id}}" ng-show="folder.label.length != 0">{{folder.label}}</span>
@@ -294,7 +294,7 @@
                         <span tooltip data-original-title="{{scanRate(folder.id) | binary}}B/s">~ {{scanRemaining(folder.id)}}</span>
                       </td>
                     </tr>
-                    <tr ng-if="!folder.readOnly && (folderStatus(folder) === 'outofsync' || hasFailedFiles(folder.id))">
+                    <tr ng-if="folder.type != 'readonly' && (folderStatus(folder) === 'outofsync' || hasFailedFiles(folder.id))">
                       <th><span class="fa fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Failed Items</span></th>
                       <!-- Show the number of failed items as a link to bring up the list. -->
                       <td ng-if="hasFailedFiles(folder.id)" class="text-right">
@@ -305,10 +305,11 @@
                         <span class="fa fa-spinner fa-pulse"></span>
                       </td>
                     </tr>
-                    <tr ng-if="folder.readOnly">
-                      <th><span class="fa fa-fw fa-lock"></span>&nbsp;<span translate>Folder Master</span></th>
+                    <tr ng-if="folder.type != 'readwrite'">
+                      <th><span class="fa fa-fw fa-lock"></span>&nbsp;<span translate>Folder Type</span></th>
                       <td class="text-right">
-                        <span translate>Yes</span>
+                        <span ng-if="folder.type == 'readonly'" translate>Master</span>
+                        <span ng-if="folder.type != 'readonly'">{{ folder.type.charAt(0).toUpperCase() + folder.type.slice(1) }}</span>
                       </td>
                     </tr>
                     <tr ng-if="model[folder.id].ignorePatterns">
@@ -351,7 +352,7 @@
                       <th><span class="fa fa-fw fa-share-alt"></span>&nbsp;<span translate>Shared With</span></th>
                       <td class="text-right">{{sharesFolder(folder)}}</td>
                     </tr>
-                    <tr ng-if="!folder.readOnly && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
+                    <tr ng-if="folder.type != 'readonly' && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
                       <th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span translate>Last File Received</span></th>
                       <td class="text-right">
                         <span tooltip data-original-title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
@@ -365,7 +366,7 @@
                 </table>
               </div>
               <div class="panel-footer">
-                <button type="button" class="btn btn-sm btn-danger pull-left" ng-click="override(folder.id)" ng-if="folderStatus(folder) == 'outofsync' && folder.readOnly">
+                <button type="button" class="btn btn-sm btn-danger pull-left" ng-click="override(folder.id)" ng-if="folderStatus(folder) == 'outofsync' && folder.type = 'readonly'">
                   <span class="fa fa-arrow-circle-up"></span>&nbsp;<span translate>Override Changes</span>
                 </button>
                 <span class="pull-right">

+ 1 - 0
gui/default/syncthing/core/syncthingController.js

@@ -1201,6 +1201,7 @@ angular.module('syncthing.core')
             $scope.currentFolder = {
                 selectedDevices: {},
                 id: $scope.createRandomFolderId(),
+                type: "readwrite",
                 rescanIntervalS: 60,
                 minDiskFreePct: 1,
                 maxConflicts: 10,

+ 7 - 7
gui/default/syncthing/folder/editFolderModalView.html

@@ -91,13 +91,13 @@
               <!-- Left column -->
               <div class="col-md-6">
                 <div class="form-group">
-                  <div class="checkbox">
-                    <label>
-                      <input type="checkbox" ng-model="currentFolder.readOnly"> <span translate>Folder Master</span>
-                    </label>
-                    &nbsp;<a href="http://docs.syncthing.net/users/foldermaster.html" target="_blank"><span class="fa fa-book"></span>&nbsp;<span translate>Help</span></a>
-                  </div>
-                  <p translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
+                  <label translate>Folder Type</label>
+                  &nbsp;<a href="http://docs.syncthing.net/users/foldermaster.html" target="_blank"><span class="fa fa-book"></span>&nbsp;<span translate>Help</span></a>
+                  <select class="form-control" ng-model="currentFolder.type">
+                    <option value="readwrite" translate>Normal</option>
+                    <option value="readonly" translate>Master</option>
+                  </select>
+                  <p ng-if="currentFolder.type == 'readonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
                 </div>
                 <div class="form-group">
                   <div class="checkbox">

+ 12 - 0
lib/config/config.go

@@ -23,6 +23,9 @@ const (
 	OldestHandledVersion = 10
 	CurrentVersion       = 13
 	MaxRescanIntervalS   = 365 * 24 * 60 * 60
+
+	FolderTypeReadWrite = "readwrite"
+	FolderTypeReadOnly  = "readonly"
 )
 
 var (
@@ -247,6 +250,15 @@ func convertV12V13(cfg *Configuration) {
 	cfg.Options.NATRenewalM = cfg.Options.DeprecatedUPnPRenewalM
 	cfg.Options.NATTimeoutS = cfg.Options.DeprecatedUPnPTimeoutS
 	cfg.Version = 13
+
+	for i, fcfg := range cfg.Folders {
+		if fcfg.DeprecatedReadOnly {
+			cfg.Folders[i].Type = FolderTypeReadOnly
+		} else {
+			cfg.Folders[i].Type = FolderTypeReadWrite
+		}
+		cfg.Folders[i].DeprecatedReadOnly = false
+	}
 }
 
 func convertV11V12(cfg *Configuration) {

+ 1 - 1
lib/config/config_test.go

@@ -94,7 +94,7 @@ func TestDeviceConfig(t *testing.T) {
 				ID:              "test",
 				RawPath:         "testdata",
 				Devices:         []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}},
-				ReadOnly:        true,
+				Type:            FolderTypeReadOnly,
 				RescanIntervalS: 600,
 				Copiers:         0,
 				Pullers:         0,

+ 5 - 2
lib/config/folderconfiguration.go

@@ -20,8 +20,8 @@ type FolderConfiguration struct {
 	ID                    string                      `xml:"id,attr" json:"id"`
 	Label                 string                      `xml:"label,attr" json:"label"`
 	RawPath               string                      `xml:"path,attr" json:"path"`
+	Type                  string                      `xml:"type,attr" json:"type"`
 	Devices               []FolderDeviceConfiguration `xml:"device" json:"devices"`
-	ReadOnly              bool                        `xml:"ro,attr" json:"readOnly"`
 	RescanIntervalS       int                         `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
 	IgnorePerms           bool                        `xml:"ignorePerms,attr" json:"ignorePerms"`
 	AutoNormalize         bool                        `xml:"autoNormalize,attr" json:"autoNormalize"`
@@ -41,16 +41,19 @@ type FolderConfiguration struct {
 
 	Invalid    string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
 	cachedPath string
+
+	DeprecatedReadOnly bool `xml:"ro,attr" json:"-"`
 }
 
 type FolderDeviceConfiguration struct {
 	DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
 }
 
-func NewFolderConfiguration(id, path string) FolderConfiguration {
+func NewFolderConfiguration(id, path, foldertype string) FolderConfiguration {
 	f := FolderConfiguration{
 		ID:      id,
 		RawPath: path,
+		Type:    foldertype,
 	}
 	f.prepare()
 	return f

+ 1 - 1
lib/config/testdata/v13.xml

@@ -1,5 +1,5 @@
 <configuration version="13">
-    <folder id="test" path="testdata" ro="true" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
+    <folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
         <device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
         <device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
         <minDiskFreePct>1</minDiskFreePct>

+ 24 - 45
lib/model/model.go

@@ -100,8 +100,11 @@ type Model struct {
 	pmut              sync.RWMutex // protects the above
 }
 
+type folderFactory func(*Model, config.FolderConfiguration, versioner.Versioner) service
+
 var (
-	symlinkWarning = stdsync.Once{}
+	symlinkWarning  = stdsync.Once{}
+	folderFactories = make(map[string]folderFactory, 0)
 )
 
 // NewModel creates and starts a new model. The model starts in read-only mode,
@@ -158,10 +161,8 @@ func (m *Model) StartDeadlockDetector(timeout time.Duration) {
 	deadlockDetect(m.pmut, timeout)
 }
 
-// StartFolderRW starts read/write processing on the current model. When in
-// read/write mode the model will attempt to keep in sync with the cluster by
-// pulling needed files from peer devices.
-func (m *Model) StartFolderRW(folder string) {
+// StartFolder constrcuts the folder service and starts it.
+func (m *Model) StartFolder(folder string) {
 	m.fmut.Lock()
 	cfg, ok := m.folderCfgs[folder]
 	if !ok {
@@ -172,37 +173,43 @@ func (m *Model) StartFolderRW(folder string) {
 	if ok {
 		panic("cannot start already running folder " + folder)
 	}
-	p := newRWFolder(m, cfg)
-	m.folderRunners[folder] = p
 
+	folderFactory, ok := folderFactories[cfg.Type]
+	if !ok {
+		panic("unknown folder type " + cfg.Type)
+	}
+
+	var ver versioner.Versioner
 	if len(cfg.Versioning.Type) > 0 {
-		factory, ok := versioner.Factories[cfg.Versioning.Type]
+		versionerFactory, ok := versioner.Factories[cfg.Versioning.Type]
 		if !ok {
 			l.Fatalf("Requested versioning type %q that does not exist", cfg.Versioning.Type)
 		}
 
-		versioner := factory(folder, cfg.Path(), cfg.Versioning.Params)
-		if service, ok := versioner.(suture.Service); ok {
+		ver = versionerFactory(folder, cfg.Path(), cfg.Versioning.Params)
+		if service, ok := ver.(suture.Service); ok {
 			// The versioner implements the suture.Service interface, so
 			// expects to be run in the background in addition to being called
 			// when files are going to be archived.
 			token := m.Add(service)
 			m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
 		}
-		p.versioner = versioner
 	}
 
+	p := folderFactory(m, cfg, ver)
+	m.folderRunners[folder] = p
+
 	m.warnAboutOverwritingProtectedFiles(folder)
 
 	token := m.Add(p)
 	m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
 	m.fmut.Unlock()
 
-	l.Infoln("Ready to synchronize", folder, "(read-write)")
+	l.Infoln("Ready to synchronize", folder, fmt.Sprintf("(%s)", cfg.Type))
 }
 
 func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
-	if m.folderCfgs[folder].ReadOnly {
+	if m.folderCfgs[folder].Type == config.FolderTypeReadOnly {
 		return
 	}
 
@@ -229,30 +236,6 @@ func (m *Model) warnAboutOverwritingProtectedFiles(folder string) {
 	}
 }
 
-// StartFolderRO starts read only processing on the current model. When in
-// read only mode the model will announce files to the cluster but not pull in
-// any external changes.
-func (m *Model) StartFolderRO(folder string) {
-	m.fmut.Lock()
-	cfg, ok := m.folderCfgs[folder]
-	if !ok {
-		panic("cannot start nonexistent folder " + folder)
-	}
-
-	_, ok = m.folderRunners[folder]
-	if ok {
-		panic("cannot start already running folder " + folder)
-	}
-	s := newROFolder(m, cfg)
-	m.folderRunners[folder] = s
-
-	token := m.Add(s)
-	m.folderRunnerTokens[folder] = append(m.folderRunnerTokens[folder], token)
-	m.fmut.Unlock()
-
-	l.Infoln("Ready to synchronize", folder, "(read only; no external updates accepted)")
-}
-
 func (m *Model) RemoveFolder(folder string) {
 	m.fmut.Lock()
 	m.pmut.Lock()
@@ -1093,7 +1076,7 @@ func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, update
 	cfg, ok := m.folderCfgs[folder]
 	m.fmut.RUnlock()
 
-	if !ok || cfg.ReadOnly || cfg.DisableTempIndexes {
+	if !ok || cfg.Type == config.FolderTypeReadOnly || cfg.DisableTempIndexes {
 		return
 	}
 
@@ -1606,7 +1589,7 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
 			Label: folderCfg.Label,
 		}
 		var flags uint32
-		if folderCfg.ReadOnly {
+		if folderCfg.Type == config.FolderTypeReadOnly {
 			flags |= protocol.FlagFolderReadOnly
 		}
 		if folderCfg.IgnorePerms {
@@ -1874,7 +1857,7 @@ func (m *Model) CheckFolderHealth(id string) error {
 		case !folder.HasMarker():
 			err = errors.New("folder marker missing")
 
-		case !folder.ReadOnly:
+		case folder.Type != config.FolderTypeReadOnly:
 			// Check for free space, if it isn't a master folder. We aren't
 			// going to change the contents of master folders, so we don't
 			// care about the amount of free space there.
@@ -1951,11 +1934,7 @@ func (m *Model) CommitConfiguration(from, to config.Configuration) bool {
 			// A folder was added.
 			l.Debugln(m, "adding folder", folderID)
 			m.AddFolder(cfg)
-			if cfg.ReadOnly {
-				m.StartFolderRO(folderID)
-			} else {
-				m.StartFolderRW(folderID)
-			}
+			m.StartFolder(folderID)
 
 			// Drop connections to all devices that can now share the new
 			// folder.

+ 10 - 8
lib/model/model_test.go

@@ -34,7 +34,7 @@ func init() {
 	device1, _ = protocol.DeviceIDFromString("AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR")
 	device2, _ = protocol.DeviceIDFromString("GYRZZQB-IRNPV4Z-T7TC52W-EQYJ3TT-FDQW6MW-DFLMU42-SSSU6EM-FBK2VAY")
 
-	defaultFolderConfig = config.NewFolderConfiguration("default", "testdata")
+	defaultFolderConfig = config.NewFolderConfiguration("default", "testdata", config.FolderTypeReadWrite)
 	defaultFolderConfig.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
 	_defaultConfig := config.Configuration{
 		Folders: []config.FolderConfiguration{defaultFolderConfig},
@@ -85,7 +85,7 @@ func TestRequest(t *testing.T) {
 
 	// device1 shares default, but device2 doesn't
 	m.AddFolder(defaultFolderConfig)
-	m.StartFolderRO("default")
+	m.StartFolder("default")
 	m.ServeBackground()
 	m.ScanFolder("default")
 
@@ -159,7 +159,7 @@ func benchmarkIndex(b *testing.B, nfiles int) {
 	db := db.OpenMemory()
 	m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
 	m.AddFolder(defaultFolderConfig)
-	m.StartFolderRO("default")
+	m.StartFolder("default")
 	m.ServeBackground()
 
 	files := genFiles(nfiles)
@@ -188,7 +188,7 @@ func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
 	db := db.OpenMemory()
 	m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
 	m.AddFolder(defaultFolderConfig)
-	m.StartFolderRO("default")
+	m.StartFolder("default")
 	m.ServeBackground()
 
 	files := genFiles(nfiles)
@@ -492,7 +492,7 @@ func TestIgnores(t *testing.T) {
 	db := db.OpenMemory()
 	m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
 	m.AddFolder(defaultFolderConfig)
-	m.StartFolderRO("default")
+	m.StartFolder("default")
 	m.ServeBackground()
 
 	expected := []string{
@@ -609,6 +609,7 @@ func TestROScanRecovery(t *testing.T) {
 	fcfg := config.FolderConfiguration{
 		ID:              "default",
 		RawPath:         "testdata/rotestfolder",
+		Type:            config.FolderTypeReadOnly,
 		RescanIntervalS: 1,
 	}
 	cfg := config.Wrap("/tmp/test", config.Configuration{
@@ -624,7 +625,7 @@ func TestROScanRecovery(t *testing.T) {
 
 	m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
 	m.AddFolder(fcfg)
-	m.StartFolderRO("default")
+	m.StartFolder("default")
 	m.ServeBackground()
 
 	waitFor := func(status string) error {
@@ -693,6 +694,7 @@ func TestRWScanRecovery(t *testing.T) {
 	fcfg := config.FolderConfiguration{
 		ID:              "default",
 		RawPath:         "testdata/rwtestfolder",
+		Type:            config.FolderTypeReadWrite,
 		RescanIntervalS: 1,
 	}
 	cfg := config.Wrap("/tmp/test", config.Configuration{
@@ -708,7 +710,7 @@ func TestRWScanRecovery(t *testing.T) {
 
 	m := NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb, nil)
 	m.AddFolder(fcfg)
-	m.StartFolderRW("default")
+	m.StartFolder("default")
 	m.ServeBackground()
 
 	waitFor := func(status string) error {
@@ -1219,7 +1221,7 @@ func TestIgnoreDelete(t *testing.T) {
 
 	m.AddFolder(cfg)
 	m.ServeBackground()
-	m.StartFolderRW("default")
+	m.StartFolder("default")
 	m.ScanFolder("default")
 
 	// Get a currently existing file

+ 6 - 1
lib/model/rofolder.go

@@ -12,13 +12,18 @@ import (
 
 	"github.com/syncthing/syncthing/lib/config"
 	"github.com/syncthing/syncthing/lib/sync"
+	"github.com/syncthing/syncthing/lib/versioner"
 )
 
+func init() {
+	folderFactories[config.FolderTypeReadOnly] = newROFolder
+}
+
 type roFolder struct {
 	folder
 }
 
-func newROFolder(model *Model, cfg config.FolderConfiguration) *roFolder {
+func newROFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner) service {
 	return &roFolder{
 		folder: folder{
 			stateTracker: stateTracker{

+ 6 - 1
lib/model/rwfolder.go

@@ -31,6 +31,10 @@ import (
 
 // TODO: Stop on errors
 
+func init() {
+	folderFactories[config.FolderTypeReadWrite] = newRWFolder
+}
+
 // A pullBlockState is passed to the puller routine for each block that needs
 // to be fetched.
 type pullBlockState struct {
@@ -98,7 +102,7 @@ type rwFolder struct {
 	errorsMut sync.Mutex
 }
 
-func newRWFolder(model *Model, cfg config.FolderConfiguration) *rwFolder {
+func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Versioner) service {
 	f := &rwFolder{
 		folder: folder{
 			stateTracker: stateTracker{
@@ -124,6 +128,7 @@ func newRWFolder(model *Model, cfg config.FolderConfiguration) *rwFolder {
 		maxConflicts:     cfg.MaxConflicts,
 		allowSparse:      !cfg.DisableSparseFiles,
 		checkFreeSpace:   cfg.MinDiskFreePct != 0,
+		versioner:        ver,
 
 		queue:       newJobQueue(),
 		pullTimer:   time.NewTimer(time.Second),