1// Copyright (C) 2014 The Syncthing Authors.
2//
3// This Source Code Form is subject to the terms of the Mozilla Public
4// License, v. 2.0. If a copy of the MPL was not distributed with this file,
5// You can obtain one at https://mozilla.org/MPL/2.0/.
6
7package config
8
9import (
10	"errors"
11	"fmt"
12	"runtime"
13	"sort"
14	"strings"
15	"time"
16
17	"github.com/shirou/gopsutil/v3/disk"
18
19	"github.com/syncthing/syncthing/lib/fs"
20	"github.com/syncthing/syncthing/lib/protocol"
21	"github.com/syncthing/syncthing/lib/util"
22)
23
24var (
25	ErrPathNotDirectory = errors.New("folder path not a directory")
26	ErrPathMissing      = errors.New("folder path missing")
27	ErrMarkerMissing    = errors.New("folder marker missing (this indicates potential data loss, search docs/forum to get information about how to proceed)")
28)
29
30const (
31	DefaultMarkerName          = ".stfolder"
32	EncryptionTokenName        = "syncthing-encryption_password_token"
33	maxConcurrentWritesDefault = 2
34	maxConcurrentWritesLimit   = 64
35)
36
37func (f FolderConfiguration) Copy() FolderConfiguration {
38	c := f
39	c.Devices = make([]FolderDeviceConfiguration, len(f.Devices))
40	copy(c.Devices, f.Devices)
41	c.Versioning = f.Versioning.Copy()
42	return c
43}
44
45func (f FolderConfiguration) Filesystem() fs.Filesystem {
46	// This is intentionally not a pointer method, because things like
47	// cfg.Folders["default"].Filesystem() should be valid.
48	var opts []fs.Option
49	if f.FilesystemType == fs.FilesystemTypeBasic && f.JunctionsAsDirs {
50		opts = append(opts, new(fs.OptionJunctionsAsDirs))
51	}
52	filesystem := fs.NewFilesystem(f.FilesystemType, f.Path, opts...)
53	if !f.CaseSensitiveFS {
54		filesystem = fs.NewCaseFilesystem(filesystem)
55	}
56	return filesystem
57}
58
59func (f FolderConfiguration) ModTimeWindow() time.Duration {
60	dur := time.Duration(f.RawModTimeWindowS) * time.Second
61	if f.RawModTimeWindowS < 1 && runtime.GOOS == "android" {
62		if usage, err := disk.Usage(f.Filesystem().URI()); err != nil {
63			dur = 2 * time.Second
64			l.Debugf(`Detecting FS at "%v" on android: Setting mtime window to 2s: err == "%v"`, f.Path, err)
65		} else if usage.Fstype == "" || strings.Contains(strings.ToLower(usage.Fstype), "fat") || strings.Contains(strings.ToLower(usage.Fstype), "msdos") {
66			dur = 2 * time.Second
67			l.Debugf(`Detecting FS at "%v" on android: Setting mtime window to 2s: usage.Fstype == "%v"`, f.Path, usage.Fstype)
68		} else {
69			l.Debugf(`Detecting FS at %v on android: Leaving mtime window at 0: usage.Fstype == "%v"`, f.Path, usage.Fstype)
70		}
71	}
72	return dur
73}
74
75func (f *FolderConfiguration) CreateMarker() error {
76	if err := f.CheckPath(); err != ErrMarkerMissing {
77		return err
78	}
79	if f.MarkerName != DefaultMarkerName {
80		// Folder uses a non-default marker so we shouldn't mess with it.
81		// Pretend we created it and let the subsequent health checks sort
82		// out the actual situation.
83		return nil
84	}
85
86	permBits := fs.FileMode(0777)
87	if runtime.GOOS == "windows" {
88		// Windows has no umask so we must chose a safer set of bits to
89		// begin with.
90		permBits = 0700
91	}
92	fs := f.Filesystem()
93	err := fs.Mkdir(DefaultMarkerName, permBits)
94	if err != nil {
95		return err
96	}
97	if dir, err := fs.Open("."); err != nil {
98		l.Debugln("folder marker: open . failed:", err)
99	} else if err := dir.Sync(); err != nil {
100		l.Debugln("folder marker: fsync . failed:", err)
101	}
102	fs.Hide(DefaultMarkerName)
103
104	return nil
105}
106
107// CheckPath returns nil if the folder root exists and contains the marker file
108func (f *FolderConfiguration) CheckPath() error {
109	fi, err := f.Filesystem().Stat(".")
110	if err != nil {
111		if !fs.IsNotExist(err) {
112			return err
113		}
114		return ErrPathMissing
115	}
116
117	// Users might have the root directory as a symlink or reparse point.
118	// Furthermore, OneDrive bullcrap uses a magic reparse point to the cloudz...
119	// Yet it's impossible for this to happen, as filesystem adds a trailing
120	// path separator to the root, so even if you point the filesystem at a file
121	// Stat ends up calling stat on C:\dir\file\ which, fails with "is not a directory"
122	// in the error check above, and we don't even get to here.
123	if !fi.IsDir() && !fi.IsSymlink() {
124		return ErrPathNotDirectory
125	}
126
127	_, err = f.Filesystem().Stat(f.MarkerName)
128	if err != nil {
129		if !fs.IsNotExist(err) {
130			return err
131		}
132		return ErrMarkerMissing
133	}
134
135	return nil
136}
137
138func (f *FolderConfiguration) CreateRoot() (err error) {
139	// Directory permission bits. Will be filtered down to something
140	// sane by umask on Unixes.
141	permBits := fs.FileMode(0777)
142	if runtime.GOOS == "windows" {
143		// Windows has no umask so we must chose a safer set of bits to
144		// begin with.
145		permBits = 0700
146	}
147
148	filesystem := f.Filesystem()
149
150	if _, err = filesystem.Stat("."); fs.IsNotExist(err) {
151		err = filesystem.MkdirAll(".", permBits)
152	}
153
154	return err
155}
156
157func (f FolderConfiguration) Description() string {
158	if f.Label == "" {
159		return f.ID
160	}
161	return fmt.Sprintf("%q (%s)", f.Label, f.ID)
162}
163
164func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
165	deviceIDs := make([]protocol.DeviceID, len(f.Devices))
166	for i, n := range f.Devices {
167		deviceIDs[i] = n.DeviceID
168	}
169	return deviceIDs
170}
171
172func (f *FolderConfiguration) prepare(myID protocol.DeviceID, existingDevices map[protocol.DeviceID]bool) {
173	// Ensure that
174	// - any loose devices are not present in the wrong places
175	// - there are no duplicate devices
176	// - we are part of the devices
177	f.Devices = ensureExistingDevices(f.Devices, existingDevices)
178	f.Devices = ensureNoDuplicateFolderDevices(f.Devices)
179	f.Devices = ensureDevicePresent(f.Devices, myID)
180
181	sort.Slice(f.Devices, func(a, b int) bool {
182		return f.Devices[a].DeviceID.Compare(f.Devices[b].DeviceID) == -1
183	})
184
185	if f.RescanIntervalS > MaxRescanIntervalS {
186		f.RescanIntervalS = MaxRescanIntervalS
187	} else if f.RescanIntervalS < 0 {
188		f.RescanIntervalS = 0
189	}
190
191	if f.FSWatcherDelayS <= 0 {
192		f.FSWatcherEnabled = false
193		f.FSWatcherDelayS = 10
194	}
195
196	if f.Versioning.CleanupIntervalS > MaxRescanIntervalS {
197		f.Versioning.CleanupIntervalS = MaxRescanIntervalS
198	} else if f.Versioning.CleanupIntervalS < 0 {
199		f.Versioning.CleanupIntervalS = 0
200	}
201
202	if f.WeakHashThresholdPct == 0 {
203		f.WeakHashThresholdPct = 25
204	}
205
206	if f.MarkerName == "" {
207		f.MarkerName = DefaultMarkerName
208	}
209
210	if f.MaxConcurrentWrites <= 0 {
211		f.MaxConcurrentWrites = maxConcurrentWritesDefault
212	} else if f.MaxConcurrentWrites > maxConcurrentWritesLimit {
213		f.MaxConcurrentWrites = maxConcurrentWritesLimit
214	}
215
216	if f.Type == FolderTypeReceiveEncrypted {
217		f.DisableTempIndexes = true
218		f.IgnorePerms = true
219	}
220}
221
222// RequiresRestartOnly returns a copy with only the attributes that require
223// restart on change.
224func (f FolderConfiguration) RequiresRestartOnly() FolderConfiguration {
225	copy := f
226
227	// Manual handling for things that are not taken care of by the tag
228	// copier, yet should not cause a restart.
229
230	blank := FolderConfiguration{}
231	util.CopyMatchingTag(&blank, &copy, "restart", func(v string) bool {
232		if len(v) > 0 && v != "false" {
233			panic(fmt.Sprintf(`unexpected tag value: %s. expected untagged or "false"`, v))
234		}
235		return v == "false"
236	})
237	return copy
238}
239
240func (f *FolderConfiguration) Device(device protocol.DeviceID) (FolderDeviceConfiguration, bool) {
241	for _, dev := range f.Devices {
242		if dev.DeviceID == device {
243			return dev, true
244		}
245	}
246	return FolderDeviceConfiguration{}, false
247}
248
249func (f *FolderConfiguration) SharedWith(device protocol.DeviceID) bool {
250	_, ok := f.Device(device)
251	return ok
252}
253
254func (f *FolderConfiguration) CheckAvailableSpace(req uint64) error {
255	val := f.MinDiskFree.BaseValue()
256	if val <= 0 {
257		return nil
258	}
259	fs := f.Filesystem()
260	usage, err := fs.Usage(".")
261	if err != nil {
262		return nil
263	}
264	if !checkAvailableSpace(req, f.MinDiskFree, usage) {
265		return fmt.Errorf("insufficient space in %v %v", fs.Type(), fs.URI())
266	}
267	return nil
268}
269