2 * Copyright (C) 2017 "IoT.bzh"
3 * Author Sebastien Douheret <sebastien@iot.bzh>
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
9 * http://www.apache.org/licenses/LICENSE-2.0
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
29 "github.com/franciscocpg/reflectme"
30 common "github.com/iotbzh/xds-common/golib"
31 "github.com/iotbzh/xds-server/lib/xdsconfig"
32 "github.com/iotbzh/xds-server/lib/xsapiv1"
33 "github.com/syncthing/syncthing/lib/sync"
36 // Folders Represent a an XDS folders
40 folders map[string]*IFOLDER
41 registerCB []RegisteredCB
44 // RegisteredCB Hold registered callbacks
45 type RegisteredCB struct {
47 data *FolderEventCBData
50 // Mutex to make add/delete atomic
51 var fcMutex = sync.NewMutex()
52 var ffMutex = sync.NewMutex()
54 // FoldersNew Create a new instance of Model Folders
55 func FoldersNew(ctx *Context) *Folders {
56 file, _ := xdsconfig.FoldersConfigFilenameGet()
60 folders: make(map[string]*IFOLDER),
61 registerCB: []RegisteredCB{},
65 // LoadConfig Load folders configuration from disk
66 func (f *Folders) LoadConfig() error {
67 var flds []xsapiv1.FolderConfig
68 var stFlds []xsapiv1.FolderConfig
71 if f.Config.Options.NoFolderConfig {
72 f.Log.Infof("Don't read folder config file (-no-folderconfig option is set)")
73 } else if f.fileOnDisk != "" {
74 f.Log.Infof("Use folder config file: %s", f.fileOnDisk)
75 err := foldersConfigRead(f.fileOnDisk, &flds)
77 if strings.HasPrefix(err.Error(), "No folder config") {
78 f.Log.Warnf(err.Error())
84 f.Log.Warnf("Folders config filename not set")
87 // Retrieve initial Syncthing config (just append don't overwrite existing ones)
89 f.Log.Infof("Retrieve syncthing folder config")
90 if err := f.SThg.FolderLoadFromStConfig(&stFlds); err != nil {
91 // Don't exit on such error, just log it
92 f.Log.Errorf(err.Error())
95 f.Log.Infof("Syncthing support is disabled.")
98 // Merge syncthing folders into XDS folders
99 for _, stf := range stFlds {
101 for i, xf := range flds {
105 if xf.Type != xsapiv1.TypeCloudSync {
106 flds[i].Status = xsapiv1.StatusErrorConfig
113 flds = append(flds, stf)
117 // Detect ghost project
118 // (IOW existing in xds file config and not in syncthing database)
120 for i, xf := range flds {
121 // only for syncthing project
122 if xf.Type != xsapiv1.TypeCloudSync {
126 for _, stf := range stFlds {
133 flds[i].Status = xsapiv1.StatusErrorConfig
139 f.Log.Infof("Loading initial folders config: %d folders found", len(flds))
140 for _, fc := range flds {
141 if _, err := f.createUpdate(fc, false, true); err != nil {
146 // Save config on disk
147 err := f.SaveConfig()
152 // SaveConfig Save folders configuration to disk
153 func (f *Folders) SaveConfig() error {
154 if f.fileOnDisk == "" {
155 return fmt.Errorf("Folders config filename not set")
158 // FIXME: buffered save or avoid to write on disk each time
159 return foldersConfigWrite(f.fileOnDisk, f.getConfigArrUnsafe())
162 // ResolveID Complete a Folder ID (helper for user that can use partial ID value)
163 func (f *Folders) ResolveID(id string) (string, error) {
169 for iid := range f.folders {
170 if strings.HasPrefix(iid, id) {
171 match = append(match, iid)
177 } else if len(match) == 0 {
178 return id, fmt.Errorf("Unknown id")
180 return id, fmt.Errorf("Multiple IDs found with provided prefix: " + id)
183 // Get returns the folder config or nil if not existing
184 func (f *Folders) Get(id string) *IFOLDER {
188 fc, exist := f.folders[id]
195 // GetConfigArr returns the config of all folders as an array
196 func (f *Folders) GetConfigArr() []xsapiv1.FolderConfig {
198 defer fcMutex.Unlock()
200 return f.getConfigArrUnsafe()
203 // getConfigArrUnsafe Same as GetConfigArr without mutex protection
204 func (f *Folders) getConfigArrUnsafe() []xsapiv1.FolderConfig {
205 conf := []xsapiv1.FolderConfig{}
206 for _, v := range f.folders {
207 conf = append(conf, (*v).GetConfig())
212 // Add adds a new folder
213 func (f *Folders) Add(newF xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
214 return f.createUpdate(newF, true, false)
217 // CreateUpdate creates or update a folder
218 func (f *Folders) createUpdate(newF xsapiv1.FolderConfig, create bool, initial bool) (*xsapiv1.FolderConfig, error) {
222 defer fcMutex.Unlock()
225 if _, exist := f.folders[newF.ID]; create && exist {
226 return nil, fmt.Errorf("ID already exists")
228 if newF.ClientPath == "" {
229 return nil, fmt.Errorf("ClientPath must be set")
232 // Create a new folder object
236 case xsapiv1.TypeCloudSync:
238 fld = NewFolderST(f.Context, f.SThg)
240 f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID)
241 fld = NewFolderSTDisable(f.Context)
245 case xsapiv1.TypePathMap:
246 fld = NewFolderPathMap(f.Context)
248 return nil, fmt.Errorf("Unsupported folder type")
251 // Allocate a new UUID
253 newF.ID = fld.NewUID("")
255 if !create && newF.ID == "" {
256 return nil, fmt.Errorf("Cannot update folder with null ID")
259 // Set default value if needed
260 if newF.Status == "" {
261 newF.Status = xsapiv1.StatusDisable
263 if newF.Label == "" {
264 newF.Label = filepath.Base(newF.ClientPath)
265 if len(newF.ID) > 8 {
266 newF.Label += "_" + newF.ID[0:8]
270 // Normalize path (needed for Windows path including bashlashes)
271 newF.ClientPath = common.PathNormalize(newF.ClientPath)
273 var newFolder *xsapiv1.FolderConfig
276 if newFolder, err = fld.Add(newF); err != nil {
277 newF.Status = xsapiv1.StatusErrorConfig
278 log.Printf("ERROR Adding folder: %v\n", err)
279 return newFolder, err
282 // Just update project config
283 if newFolder, err = fld.Setup(newF); err != nil {
284 newF.Status = xsapiv1.StatusErrorConfig
285 log.Printf("ERROR Updating folder: %v\n", err)
286 return newFolder, err
290 // Add to folders list
291 f.folders[newF.ID] = &fld
293 // Save config on disk
295 if err := f.SaveConfig(); err != nil {
296 return newFolder, err
300 // Force sync after creation
301 // (need to defer to be sure that WS events will arrive after HTTP creation reply)
303 time.Sleep(time.Millisecond * 500)
307 return newFolder, nil
310 // Delete deletes a specific folder
311 func (f *Folders) Delete(id string) (xsapiv1.FolderConfig, error) {
315 defer fcMutex.Unlock()
317 fld := xsapiv1.FolderConfig{}
318 fc, exist := f.folders[id]
320 return fld, fmt.Errorf("unknown id")
323 fld = (*fc).GetConfig()
325 if err = (*fc).Remove(); err != nil {
329 delete(f.folders, id)
331 // Save config on disk
337 // Update Update a specific folder
338 func (f *Folders) Update(id string, cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
340 defer fcMutex.Unlock()
342 fc, exist := f.folders[id]
344 return nil, fmt.Errorf("unknown id")
347 // Copy current in a new object to change nothing in case of an error rises
348 newCfg := xsapiv1.FolderConfig{}
349 reflectme.Copy((*fc).GetConfig(), &newCfg)
351 // Only update some fields
353 for _, fieldName := range xsapiv1.FolderConfigUpdatableFields {
354 valNew, err := reflectme.GetField(cfg, fieldName)
356 valCur, err := reflectme.GetField(newCfg, fieldName)
357 if err == nil && valNew != valCur {
358 err = reflectme.SetField(&newCfg, fieldName, valNew)
371 fld, err := (*fc).Update(newCfg)
376 // Save config on disk
379 // Send event to notified changes
380 // TODO emit folder change event
385 // ForceSync Force the synchronization of a folder
386 func (f *Folders) ForceSync(id string) error {
389 return fmt.Errorf("Unknown id")
394 // IsFolderInSync Returns true when folder is in sync
395 func (f *Folders) IsFolderInSync(id string) (bool, error) {
398 return false, fmt.Errorf("Unknown id")
400 return (*fc).IsInSync()
403 //*** Private functions ***
405 // Use XML format and not json to be able to save/load all fields including
406 // ones that are masked in json (IOW defined with `json:"-"`)
407 type xmlFolders struct {
408 XMLName xml.Name `xml:"folders"`
409 Version string `xml:"version,attr"`
410 Folders []xsapiv1.FolderConfig `xml:"folders"`
413 // foldersConfigRead reads folders config from disk
414 func foldersConfigRead(file string, folders *[]xsapiv1.FolderConfig) error {
415 if !common.Exists(file) {
416 return fmt.Errorf("No folder config file found (%s)", file)
420 defer ffMutex.Unlock()
422 fd, err := os.Open(file)
429 err = xml.NewDecoder(fd).Decode(&data)
431 // Decode old type encoding (number) for backward compatibility
432 for i, d := range data.Folders {
435 data.Folders[i].Type = xsapiv1.TypePathMap
437 data.Folders[i].Type = xsapiv1.TypeCloudSync
439 data.Folders[i].Type = xsapiv1.TypeCifsSmb
443 *folders = data.Folders
448 // foldersConfigWrite writes folders config on disk
449 func foldersConfigWrite(file string, folders []xsapiv1.FolderConfig) error {
451 defer ffMutex.Unlock()
453 fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
464 enc := xml.NewEncoder(fd)
466 return enc.Encode(data)