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, ©, "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