config.go 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653
  1. // Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
  2. // All rights reserved. Use of this source code is governed by an MIT-style
  3. // license that can be found in the LICENSE file.
  4. // Package config implements reading and writing of the syncthing configuration file.
  5. package config
  6. import (
  7. "encoding/xml"
  8. "fmt"
  9. "os"
  10. "reflect"
  11. "sort"
  12. "strconv"
  13. "code.google.com/p/go.crypto/bcrypt"
  14. "github.com/syncthing/syncthing/internal/events"
  15. "github.com/syncthing/syncthing/internal/logger"
  16. "github.com/syncthing/syncthing/internal/osutil"
  17. "github.com/syncthing/syncthing/internal/protocol"
  18. )
  19. var l = logger.DefaultLogger
  20. type Configuration struct {
  21. Location string `xml:"-" json:"-"`
  22. Version int `xml:"version,attr" default:"3"`
  23. Folders []FolderConfiguration `xml:"folder"`
  24. Devices []DeviceConfiguration `xml:"device"`
  25. GUI GUIConfiguration `xml:"gui"`
  26. Options OptionsConfiguration `xml:"options"`
  27. XMLName xml.Name `xml:"configuration" json:"-"`
  28. Deprecated_Repositories []FolderConfiguration `xml:"repository" json:"-"`
  29. Deprecated_Nodes []DeviceConfiguration `xml:"node" json:"-"`
  30. }
  31. type FolderConfiguration struct {
  32. ID string `xml:"id,attr"`
  33. Path string `xml:"path,attr"`
  34. Devices []FolderDeviceConfiguration `xml:"device"`
  35. ReadOnly bool `xml:"ro,attr"`
  36. RescanIntervalS int `xml:"rescanIntervalS,attr" default:"60"`
  37. IgnorePerms bool `xml:"ignorePerms,attr"`
  38. Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
  39. Versioning VersioningConfiguration `xml:"versioning"`
  40. deviceIDs []protocol.DeviceID
  41. Depreceted_Directory string `xml:"directory,omitempty,attr" json:"-"`
  42. Depreceted_Nodes []FolderDeviceConfiguration `xml:"node" json:"-"`
  43. }
  44. type VersioningConfiguration struct {
  45. Type string `xml:"type,attr"`
  46. Params map[string]string
  47. }
  48. type InternalVersioningConfiguration struct {
  49. Type string `xml:"type,attr,omitempty"`
  50. Params []InternalParam `xml:"param"`
  51. }
  52. type InternalParam struct {
  53. Key string `xml:"key,attr"`
  54. Val string `xml:"val,attr"`
  55. }
  56. func (c *VersioningConfiguration) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
  57. var tmp InternalVersioningConfiguration
  58. tmp.Type = c.Type
  59. for k, v := range c.Params {
  60. tmp.Params = append(tmp.Params, InternalParam{k, v})
  61. }
  62. return e.EncodeElement(tmp, start)
  63. }
  64. func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
  65. var tmp InternalVersioningConfiguration
  66. err := d.DecodeElement(&tmp, &start)
  67. if err != nil {
  68. return err
  69. }
  70. c.Type = tmp.Type
  71. c.Params = make(map[string]string, len(tmp.Params))
  72. for _, p := range tmp.Params {
  73. c.Params[p.Key] = p.Val
  74. }
  75. return nil
  76. }
  77. func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
  78. if r.deviceIDs == nil {
  79. for _, n := range r.Devices {
  80. r.deviceIDs = append(r.deviceIDs, n.DeviceID)
  81. }
  82. }
  83. return r.deviceIDs
  84. }
  85. type DeviceConfiguration struct {
  86. DeviceID protocol.DeviceID `xml:"id,attr"`
  87. Name string `xml:"name,attr,omitempty"`
  88. Addresses []string `xml:"address,omitempty"`
  89. Compression bool `xml:"compression,attr"`
  90. CertName string `xml:"certName,attr,omitempty"`
  91. Introducer bool `xml:"introducer,attr"`
  92. }
  93. type FolderDeviceConfiguration struct {
  94. DeviceID protocol.DeviceID `xml:"id,attr"`
  95. Deprecated_Name string `xml:"name,attr,omitempty" json:"-"`
  96. Deprecated_Addresses []string `xml:"address,omitempty" json:"-"`
  97. }
  98. type OptionsConfiguration struct {
  99. ListenAddress []string `xml:"listenAddress" default:"0.0.0.0:22000"`
  100. GlobalAnnServer string `xml:"globalAnnounceServer" default:"announce.syncthing.net:22026"`
  101. GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"`
  102. LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"`
  103. LocalAnnPort int `xml:"localAnnouncePort" default:"21025"`
  104. LocalAnnMCAddr string `xml:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
  105. MaxSendKbps int `xml:"maxSendKbps"`
  106. MaxRecvKbps int `xml:"maxRecvKbps"`
  107. ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60"`
  108. StartBrowser bool `xml:"startBrowser" default:"true"`
  109. UPnPEnabled bool `xml:"upnpEnabled" default:"true"`
  110. UPnPLease int `xml:"upnpLeaseMinutes" default:"0"`
  111. UPnPRenewal int `xml:"upnpRenewalMinutes" default:"30"`
  112. URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
  113. RestartOnWakeup bool `xml:"restartOnWakeup" default:"true"`
  114. Deprecated_RescanIntervalS int `xml:"rescanIntervalS,omitempty" json:"-"`
  115. Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"`
  116. Deprecated_URDeclined bool `xml:"urDeclined,omitempty" json:"-"`
  117. Deprecated_ReadOnly bool `xml:"readOnly,omitempty" json:"-"`
  118. Deprecated_GUIEnabled bool `xml:"guiEnabled,omitempty" json:"-"`
  119. Deprecated_GUIAddress string `xml:"guiAddress,omitempty" json:"-"`
  120. }
  121. type GUIConfiguration struct {
  122. Enabled bool `xml:"enabled,attr" default:"true"`
  123. Address string `xml:"address" default:"127.0.0.1:8080"`
  124. User string `xml:"user,omitempty"`
  125. Password string `xml:"password,omitempty"`
  126. UseTLS bool `xml:"tls,attr"`
  127. APIKey string `xml:"apikey,omitempty"`
  128. }
  129. func (cfg *Configuration) DeviceMap() map[protocol.DeviceID]DeviceConfiguration {
  130. m := make(map[protocol.DeviceID]DeviceConfiguration, len(cfg.Devices))
  131. for _, n := range cfg.Devices {
  132. m[n.DeviceID] = n
  133. }
  134. return m
  135. }
  136. func (cfg *Configuration) GetDeviceConfiguration(deviceID protocol.DeviceID) *DeviceConfiguration {
  137. for i, device := range cfg.Devices {
  138. if device.DeviceID == deviceID {
  139. return &cfg.Devices[i]
  140. }
  141. }
  142. return nil
  143. }
  144. func (cfg *Configuration) GetFolderConfiguration(folderID string) *FolderConfiguration {
  145. for i, folder := range cfg.Folders {
  146. if folder.ID == folderID {
  147. return &cfg.Folders[i]
  148. }
  149. }
  150. return nil
  151. }
  152. func (cfg *Configuration) FolderMap() map[string]FolderConfiguration {
  153. m := make(map[string]FolderConfiguration, len(cfg.Folders))
  154. for _, r := range cfg.Folders {
  155. m[r.ID] = r
  156. }
  157. return m
  158. }
  159. func setDefaults(data interface{}) error {
  160. s := reflect.ValueOf(data).Elem()
  161. t := s.Type()
  162. for i := 0; i < s.NumField(); i++ {
  163. f := s.Field(i)
  164. tag := t.Field(i).Tag
  165. v := tag.Get("default")
  166. if len(v) > 0 {
  167. switch f.Interface().(type) {
  168. case string:
  169. f.SetString(v)
  170. case int:
  171. i, err := strconv.ParseInt(v, 10, 64)
  172. if err != nil {
  173. return err
  174. }
  175. f.SetInt(i)
  176. case bool:
  177. f.SetBool(v == "true")
  178. case []string:
  179. // We don't do anything with string slices here. Any default
  180. // we set will be appended to by the XML decoder, so we fill
  181. // those after decoding.
  182. default:
  183. panic(f.Type())
  184. }
  185. }
  186. }
  187. return nil
  188. }
  189. // fillNilSlices sets default value on slices that are still nil.
  190. func fillNilSlices(data interface{}) error {
  191. s := reflect.ValueOf(data).Elem()
  192. t := s.Type()
  193. for i := 0; i < s.NumField(); i++ {
  194. f := s.Field(i)
  195. tag := t.Field(i).Tag
  196. v := tag.Get("default")
  197. if len(v) > 0 {
  198. switch f.Interface().(type) {
  199. case []string:
  200. if f.IsNil() {
  201. rv := reflect.MakeSlice(reflect.TypeOf([]string{}), 1, 1)
  202. rv.Index(0).SetString(v)
  203. f.Set(rv)
  204. }
  205. }
  206. }
  207. }
  208. return nil
  209. }
  210. func (cfg *Configuration) Save() error {
  211. fd, err := os.Create(cfg.Location + ".tmp")
  212. if err != nil {
  213. l.Warnln("Saving config:", err)
  214. return err
  215. }
  216. e := xml.NewEncoder(fd)
  217. e.Indent("", " ")
  218. err = e.Encode(cfg)
  219. if err != nil {
  220. fd.Close()
  221. return err
  222. }
  223. _, err = fd.Write([]byte("\n"))
  224. if err != nil {
  225. l.Warnln("Saving config:", err)
  226. fd.Close()
  227. return err
  228. }
  229. err = fd.Close()
  230. if err != nil {
  231. l.Warnln("Saving config:", err)
  232. return err
  233. }
  234. err = osutil.Rename(cfg.Location+".tmp", cfg.Location)
  235. if err != nil {
  236. l.Warnln("Saving config:", err)
  237. }
  238. events.Default.Log(events.ConfigSaved, cfg)
  239. return err
  240. }
  241. func uniqueStrings(ss []string) []string {
  242. var m = make(map[string]bool, len(ss))
  243. for _, s := range ss {
  244. m[s] = true
  245. }
  246. var us = make([]string, 0, len(m))
  247. for k := range m {
  248. us = append(us, k)
  249. }
  250. return us
  251. }
  252. func (cfg *Configuration) prepare(myID protocol.DeviceID) {
  253. fillNilSlices(&cfg.Options)
  254. cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
  255. // Initialize an empty slice for folders if the config has none
  256. if cfg.Folders == nil {
  257. cfg.Folders = []FolderConfiguration{}
  258. }
  259. // Check for missing, bad or duplicate folder ID:s
  260. var seenFolders = map[string]*FolderConfiguration{}
  261. var uniqueCounter int
  262. for i := range cfg.Folders {
  263. folder := &cfg.Folders[i]
  264. if len(folder.Path) == 0 {
  265. folder.Invalid = "no directory configured"
  266. continue
  267. }
  268. if folder.ID == "" {
  269. folder.ID = "default"
  270. }
  271. if seen, ok := seenFolders[folder.ID]; ok {
  272. l.Warnf("Multiple folders with ID %q; disabling", folder.ID)
  273. seen.Invalid = "duplicate folder ID"
  274. if seen.ID == folder.ID {
  275. uniqueCounter++
  276. seen.ID = fmt.Sprintf("%s~%d", folder.ID, uniqueCounter)
  277. }
  278. folder.Invalid = "duplicate folder ID"
  279. uniqueCounter++
  280. folder.ID = fmt.Sprintf("%s~%d", folder.ID, uniqueCounter)
  281. } else {
  282. seenFolders[folder.ID] = folder
  283. }
  284. }
  285. if cfg.Options.Deprecated_URDeclined {
  286. cfg.Options.URAccepted = -1
  287. }
  288. cfg.Options.Deprecated_URDeclined = false
  289. cfg.Options.Deprecated_UREnabled = false
  290. // Upgrade to v2 configuration if appropriate
  291. if cfg.Version == 1 {
  292. convertV1V2(cfg)
  293. }
  294. // Upgrade to v3 configuration if appropriate
  295. if cfg.Version == 2 {
  296. convertV2V3(cfg)
  297. }
  298. // Upgrade to v4 configuration if appropriate
  299. if cfg.Version == 3 {
  300. convertV3V4(cfg)
  301. }
  302. // Upgrade to v5 configuration if appropriate
  303. if cfg.Version == 4 {
  304. convertV4V5(cfg)
  305. }
  306. // Hash old cleartext passwords
  307. if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
  308. hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
  309. if err != nil {
  310. l.Warnln("bcrypting password:", err)
  311. } else {
  312. cfg.GUI.Password = string(hash)
  313. }
  314. }
  315. // Build a list of available devices
  316. existingDevices := make(map[protocol.DeviceID]bool)
  317. existingDevices[myID] = true
  318. for _, device := range cfg.Devices {
  319. existingDevices[device.DeviceID] = true
  320. }
  321. // Ensure this device is present in all relevant places
  322. me := cfg.GetDeviceConfiguration(myID)
  323. if me == nil {
  324. myName, _ := os.Hostname()
  325. cfg.Devices = append(cfg.Devices, DeviceConfiguration{
  326. DeviceID: myID,
  327. Name: myName,
  328. })
  329. }
  330. sort.Sort(DeviceConfigurationList(cfg.Devices))
  331. // Ensure that any loose devices are not present in the wrong places
  332. // Ensure that there are no duplicate devices
  333. for i := range cfg.Folders {
  334. cfg.Folders[i].Devices = ensureDevicePresent(cfg.Folders[i].Devices, myID)
  335. cfg.Folders[i].Devices = ensureExistingDevices(cfg.Folders[i].Devices, existingDevices)
  336. cfg.Folders[i].Devices = ensureNoDuplicates(cfg.Folders[i].Devices)
  337. sort.Sort(FolderDeviceConfigurationList(cfg.Folders[i].Devices))
  338. }
  339. // An empty address list is equivalent to a single "dynamic" entry
  340. for i := range cfg.Devices {
  341. n := &cfg.Devices[i]
  342. if len(n.Addresses) == 0 || len(n.Addresses) == 1 && n.Addresses[0] == "" {
  343. n.Addresses = []string{"dynamic"}
  344. }
  345. }
  346. }
  347. func New(location string, myID protocol.DeviceID) Configuration {
  348. var cfg Configuration
  349. cfg.Location = location
  350. setDefaults(&cfg)
  351. setDefaults(&cfg.Options)
  352. setDefaults(&cfg.GUI)
  353. cfg.prepare(myID)
  354. return cfg
  355. }
  356. func Load(location string, myID protocol.DeviceID) (Configuration, error) {
  357. var cfg Configuration
  358. cfg.Location = location
  359. setDefaults(&cfg)
  360. setDefaults(&cfg.Options)
  361. setDefaults(&cfg.GUI)
  362. fd, err := os.Open(location)
  363. if err != nil {
  364. return Configuration{}, err
  365. }
  366. err = xml.NewDecoder(fd).Decode(&cfg)
  367. fd.Close()
  368. cfg.prepare(myID)
  369. return cfg, err
  370. }
  371. // ChangeRequiresRestart returns true if updating the configuration requires a
  372. // complete restart.
  373. func ChangeRequiresRestart(from, to Configuration) bool {
  374. // Adding, removing or changing folders requires restart
  375. if len(from.Folders) != len(to.Folders) {
  376. return true
  377. }
  378. fromFolders := from.FolderMap()
  379. toFolders := to.FolderMap()
  380. for id := range fromFolders {
  381. if !reflect.DeepEqual(fromFolders[id], toFolders[id]) {
  382. return true
  383. }
  384. }
  385. // Removing a device requires a restart. Adding one does not. Changing
  386. // address or name does not.
  387. fromDevices := from.DeviceMap()
  388. toDevices := to.DeviceMap()
  389. for deviceID := range fromDevices {
  390. if _, ok := toDevices[deviceID]; !ok {
  391. return true
  392. }
  393. }
  394. // All of the generic options require restart
  395. if !reflect.DeepEqual(from.Options, to.Options) || !reflect.DeepEqual(from.GUI, to.GUI) {
  396. return true
  397. }
  398. return false
  399. }
  400. func convertV4V5(cfg *Configuration) {
  401. // Renamed a bunch of fields in the structs.
  402. if cfg.Deprecated_Nodes == nil {
  403. cfg.Deprecated_Nodes = []DeviceConfiguration{}
  404. }
  405. if cfg.Deprecated_Repositories == nil {
  406. cfg.Deprecated_Repositories = []FolderConfiguration{}
  407. }
  408. cfg.Devices = cfg.Deprecated_Nodes
  409. cfg.Folders = cfg.Deprecated_Repositories
  410. for i := range cfg.Folders {
  411. cfg.Folders[i].Path = cfg.Folders[i].Depreceted_Directory
  412. cfg.Folders[i].Depreceted_Directory = ""
  413. cfg.Folders[i].Devices = cfg.Folders[i].Depreceted_Nodes
  414. cfg.Folders[i].Depreceted_Nodes = nil
  415. }
  416. cfg.Deprecated_Nodes = nil
  417. cfg.Deprecated_Repositories = nil
  418. cfg.Version = 5
  419. }
  420. func convertV3V4(cfg *Configuration) {
  421. // In previous versions, rescan interval was common for each folder.
  422. // From now, it can be set independently. We have to make sure, that after upgrade
  423. // the individual rescan interval will be defined for every existing folder.
  424. for i := range cfg.Deprecated_Repositories {
  425. cfg.Deprecated_Repositories[i].RescanIntervalS = cfg.Options.Deprecated_RescanIntervalS
  426. }
  427. cfg.Options.Deprecated_RescanIntervalS = 0
  428. // In previous versions, folders held full device configurations.
  429. // Since that's the only place where device configs were in V1, we still have
  430. // to define the deprecated fields to be able to upgrade from V1 to V4.
  431. for i, folder := range cfg.Deprecated_Repositories {
  432. for j := range folder.Depreceted_Nodes {
  433. rncfg := cfg.Deprecated_Repositories[i].Depreceted_Nodes[j]
  434. rncfg.Deprecated_Name = ""
  435. rncfg.Deprecated_Addresses = nil
  436. }
  437. }
  438. cfg.Version = 4
  439. }
  440. func convertV2V3(cfg *Configuration) {
  441. // In previous versions, compression was always on. When upgrading, enable
  442. // compression on all existing new. New devices will get compression on by
  443. // default by the GUI.
  444. for i := range cfg.Deprecated_Nodes {
  445. cfg.Deprecated_Nodes[i].Compression = true
  446. }
  447. // The global discovery format and port number changed in v0.9. Having the
  448. // default announce server but old port number is guaranteed to be legacy.
  449. if cfg.Options.GlobalAnnServer == "announce.syncthing.net:22025" {
  450. cfg.Options.GlobalAnnServer = "announce.syncthing.net:22026"
  451. }
  452. cfg.Version = 3
  453. }
  454. func convertV1V2(cfg *Configuration) {
  455. // Collect the list of devices.
  456. // Replace device configs inside folders with only a reference to the
  457. // device ID. Set all folders to read only if the global read only flag is
  458. // set.
  459. var devices = map[string]FolderDeviceConfiguration{}
  460. for i, folder := range cfg.Deprecated_Repositories {
  461. cfg.Deprecated_Repositories[i].ReadOnly = cfg.Options.Deprecated_ReadOnly
  462. for j, device := range folder.Depreceted_Nodes {
  463. id := device.DeviceID.String()
  464. if _, ok := devices[id]; !ok {
  465. devices[id] = device
  466. }
  467. cfg.Deprecated_Repositories[i].Depreceted_Nodes[j] = FolderDeviceConfiguration{DeviceID: device.DeviceID}
  468. }
  469. }
  470. cfg.Options.Deprecated_ReadOnly = false
  471. // Set and sort the list of devices.
  472. for _, device := range devices {
  473. cfg.Deprecated_Nodes = append(cfg.Deprecated_Nodes, DeviceConfiguration{
  474. DeviceID: device.DeviceID,
  475. Name: device.Deprecated_Name,
  476. Addresses: device.Deprecated_Addresses,
  477. })
  478. }
  479. sort.Sort(DeviceConfigurationList(cfg.Deprecated_Nodes))
  480. // GUI
  481. cfg.GUI.Address = cfg.Options.Deprecated_GUIAddress
  482. cfg.GUI.Enabled = cfg.Options.Deprecated_GUIEnabled
  483. cfg.Options.Deprecated_GUIEnabled = false
  484. cfg.Options.Deprecated_GUIAddress = ""
  485. cfg.Version = 2
  486. }
  487. type DeviceConfigurationList []DeviceConfiguration
  488. func (l DeviceConfigurationList) Less(a, b int) bool {
  489. return l[a].DeviceID.Compare(l[b].DeviceID) == -1
  490. }
  491. func (l DeviceConfigurationList) Swap(a, b int) {
  492. l[a], l[b] = l[b], l[a]
  493. }
  494. func (l DeviceConfigurationList) Len() int {
  495. return len(l)
  496. }
  497. type FolderDeviceConfigurationList []FolderDeviceConfiguration
  498. func (l FolderDeviceConfigurationList) Less(a, b int) bool {
  499. return l[a].DeviceID.Compare(l[b].DeviceID) == -1
  500. }
  501. func (l FolderDeviceConfigurationList) Swap(a, b int) {
  502. l[a], l[b] = l[b], l[a]
  503. }
  504. func (l FolderDeviceConfigurationList) Len() int {
  505. return len(l)
  506. }
  507. func ensureDevicePresent(devices []FolderDeviceConfiguration, myID protocol.DeviceID) []FolderDeviceConfiguration {
  508. for _, device := range devices {
  509. if device.DeviceID.Equals(myID) {
  510. return devices
  511. }
  512. }
  513. devices = append(devices, FolderDeviceConfiguration{
  514. DeviceID: myID,
  515. })
  516. return devices
  517. }
  518. func ensureExistingDevices(devices []FolderDeviceConfiguration, existingDevices map[protocol.DeviceID]bool) []FolderDeviceConfiguration {
  519. count := len(devices)
  520. i := 0
  521. loop:
  522. for i < count {
  523. if _, ok := existingDevices[devices[i].DeviceID]; !ok {
  524. devices[i] = devices[count-1]
  525. count--
  526. continue loop
  527. }
  528. i++
  529. }
  530. return devices[0:count]
  531. }
  532. func ensureNoDuplicates(devices []FolderDeviceConfiguration) []FolderDeviceConfiguration {
  533. count := len(devices)
  534. i := 0
  535. seenDevices := make(map[protocol.DeviceID]bool)
  536. loop:
  537. for i < count {
  538. id := devices[i].DeviceID
  539. if _, ok := seenDevices[id]; ok {
  540. devices[i] = devices[count-1]
  541. count--
  542. continue loop
  543. }
  544. seenDevices[id] = true
  545. i++
  546. }
  547. return devices[0:count]
  548. }