Add folder update support and ClientData field.
[src/xds/xds-server.git] / lib / model / folders.go
index 3c2457c..0e28538 100644 (file)
@@ -7,13 +7,14 @@ import (
        "os"
        "path/filepath"
        "strings"
+       "time"
 
        "github.com/Sirupsen/logrus"
+       "github.com/franciscocpg/reflectme"
        common "github.com/iotbzh/xds-common/golib"
        "github.com/iotbzh/xds-server/lib/folder"
        "github.com/iotbzh/xds-server/lib/syncthing"
        "github.com/iotbzh/xds-server/lib/xdsconfig"
-       uuid "github.com/satori/go.uuid"
        "github.com/syncthing/syncthing/lib/sync"
 )
 
@@ -24,6 +25,12 @@ type Folders struct {
        Log        *logrus.Logger
        SThg       *st.SyncThing
        folders    map[string]*folder.IFOLDER
+       registerCB []RegisteredCB
+}
+
+type RegisteredCB struct {
+       cb   *folder.EventCB
+       data *folder.EventCBData
 }
 
 // Mutex to make add/delete atomic
@@ -39,6 +46,7 @@ func FoldersNew(cfg *xdsconfig.Config, st *st.SyncThing) *Folders {
                Log:        cfg.Log,
                SThg:       st,
                folders:    make(map[string]*folder.IFOLDER),
+               registerCB: []RegisteredCB{},
        }
 }
 
@@ -71,6 +79,8 @@ func (f *Folders) LoadConfig() error {
                        // Don't exit on such error, just log it
                        f.Log.Errorf(err.Error())
                }
+       } else {
+               f.Log.Infof("Syncthing support is disabled.")
        }
 
        // Merge syncthing folders into XDS folders
@@ -94,32 +104,37 @@ func (f *Folders) LoadConfig() error {
 
        // Detect ghost project
        // (IOW existing in xds file config and not in syncthing database)
-       for i, xf := range flds {
-               // only for syncthing project
-               if xf.Type != folder.TypeCloudSync {
-                       continue
-               }
-               found := false
-               for _, stf := range stFlds {
-                       if stf.ID == xf.ID {
-                               found = true
-                               break
+       if f.SThg != nil {
+               for i, xf := range flds {
+                       // only for syncthing project
+                       if xf.Type != folder.TypeCloudSync {
+                               continue
+                       }
+                       found := false
+                       for _, stf := range stFlds {
+                               if stf.ID == xf.ID {
+                                       found = true
+                                       break
+                               }
+                       }
+                       if !found {
+                               flds[i].Status = folder.StatusErrorConfig
                        }
-               }
-               if !found {
-                       flds[i].Status = folder.StatusErrorConfig
                }
        }
 
        // Update folders
        f.Log.Infof("Loading initial folders config: %d folders found", len(flds))
        for _, fc := range flds {
-               if _, err := f.createUpdate(fc, false); err != nil {
+               if _, err := f.createUpdate(fc, false, true); err != nil {
                        return err
                }
        }
 
-       return nil
+       // Save config on disk
+       err := f.SaveConfig()
+
+       return err
 }
 
 // SaveConfig Save folders configuration to disk
@@ -132,6 +147,27 @@ func (f *Folders) SaveConfig() error {
        return foldersConfigWrite(f.fileOnDisk, f.getConfigArrUnsafe())
 }
 
+// ResolveID Complete a Folder ID (helper for user that can use partial ID value)
+func (f *Folders) ResolveID(id string) (string, error) {
+       if id == "" {
+               return "", nil
+       }
+
+       match := []string{}
+       for iid := range f.folders {
+               if strings.HasPrefix(iid, id) {
+                       match = append(match, iid)
+               }
+       }
+
+       if len(match) == 1 {
+               return match[0], nil
+       } else if len(match) == 0 {
+               return id, fmt.Errorf("Unknown id")
+       }
+       return id, fmt.Errorf("Multiple IDs found with provided prefix: " + id)
+}
+
 // Get returns the folder config or nil if not existing
 func (f *Folders) Get(id string) *folder.IFOLDER {
        if id == "" {
@@ -154,8 +190,7 @@ func (f *Folders) GetConfigArr() []folder.FolderConfig {
 
 // getConfigArrUnsafe Same as GetConfigArr without mutex protection
 func (f *Folders) getConfigArrUnsafe() []folder.FolderConfig {
-       var conf []folder.FolderConfig
-
+       conf := []folder.FolderConfig{}
        for _, v := range f.folders {
                conf = append(conf, (*v).GetConfig())
        }
@@ -164,11 +199,11 @@ func (f *Folders) getConfigArrUnsafe() []folder.FolderConfig {
 
 // Add adds a new folder
 func (f *Folders) Add(newF folder.FolderConfig) (*folder.FolderConfig, error) {
-       return f.createUpdate(newF, true)
+       return f.createUpdate(newF, true, false)
 }
 
 // CreateUpdate creates or update a folder
-func (f *Folders) createUpdate(newF folder.FolderConfig, create bool) (*folder.FolderConfig, error) {
+func (f *Folders) createUpdate(newF folder.FolderConfig, create bool, initial bool) (*folder.FolderConfig, error) {
 
        fcMutex.Lock()
        defer fcMutex.Unlock()
@@ -181,9 +216,28 @@ func (f *Folders) createUpdate(newF folder.FolderConfig, create bool) (*folder.F
                return nil, fmt.Errorf("ClientPath must be set")
        }
 
+       // Create a new folder object
+       var fld folder.IFOLDER
+       switch newF.Type {
+       // SYNCTHING
+       case folder.TypeCloudSync:
+               if f.SThg != nil {
+                       fld = f.SThg.NewFolderST(f.Conf)
+               } else {
+                       f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID)
+                       fld = folder.NewFolderSTDisable(f.Conf)
+               }
+
+       // PATH MAP
+       case folder.TypePathMap:
+               fld = folder.NewFolderPathMap(f.Conf)
+       default:
+               return nil, fmt.Errorf("Unsupported folder type")
+       }
+
        // Allocate a new UUID
        if create {
-               newF.ID = uuid.NewV1().String()
+               newF.ID = fld.NewUID("")
        }
        if !create && newF.ID == "" {
                return nil, fmt.Errorf("Cannot update folder with null ID")
@@ -193,24 +247,11 @@ func (f *Folders) createUpdate(newF folder.FolderConfig, create bool) (*folder.F
        if newF.Status == "" {
                newF.Status = folder.StatusDisable
        }
-
        if newF.Label == "" {
-               newF.Label = filepath.Base(newF.ClientPath) + "_" + newF.ID[0:8]
-       }
-
-       var fld folder.IFOLDER
-       switch newF.Type {
-       // SYNCTHING
-       case folder.TypeCloudSync:
-               if f.SThg == nil {
-                       return nil, fmt.Errorf("ClownSync type not supported (syncthing not initialized)")
+               newF.Label = filepath.Base(newF.ClientPath)
+               if len(newF.ID) > 8 {
+                       newF.Label += "_" + newF.ID[0:8]
                }
-               fld = f.SThg.NewFolderST(f.Conf)
-       // PATH MAP
-       case folder.TypePathMap:
-               fld = folder.NewFolderPathMap()
-       default:
-               return nil, fmt.Errorf("Unsupported folder type")
        }
 
        // Normalize path (needed for Windows path including bashlashes)
@@ -224,13 +265,31 @@ func (f *Folders) createUpdate(newF folder.FolderConfig, create bool) (*folder.F
                return newFolder, err
        }
 
-       // Register folder object
+       // Add to folders list
        f.folders[newF.ID] = &fld
 
        // Save config on disk
-       err = f.SaveConfig()
+       if !initial {
+               if err := f.SaveConfig(); err != nil {
+                       return newFolder, err
+               }
+       }
 
-       return newFolder, err
+       // Register event change callback
+       for _, rcb := range f.registerCB {
+               if err := fld.RegisterEventChange(rcb.cb, rcb.data); err != nil {
+                       return newFolder, err
+               }
+       }
+
+       // Force sync after creation
+       // (need to defer to be sure that WS events will arrive after HTTP creation reply)
+       go func() {
+               time.Sleep(time.Millisecond * 500)
+               fld.Sync()
+       }()
+
+       return newFolder, nil
 }
 
 // Delete deletes a specific folder
@@ -260,6 +319,77 @@ func (f *Folders) Delete(id string) (folder.FolderConfig, error) {
        return fld, err
 }
 
+// Update Update a specific folder
+func (f *Folders) Update(id string, cfg folder.FolderConfig) (*folder.FolderConfig, error) {
+       fcMutex.Lock()
+       defer fcMutex.Unlock()
+
+       fc, exist := f.folders[id]
+       if !exist {
+               return nil, fmt.Errorf("unknown id")
+       }
+
+       // Copy current in a new object to change nothing in case of an error rises
+       newCfg := folder.FolderConfig{}
+       reflectme.Copy((*fc).GetConfig(), &newCfg)
+
+       // Only update some fields
+       dirty := false
+       for _, fieldName := range folder.FolderConfigUpdatableFields {
+               valNew, err := reflectme.GetField(cfg, fieldName)
+               if err == nil {
+                       valCur, err := reflectme.GetField(newCfg, fieldName)
+                       if err == nil && valNew != valCur {
+                               err = reflectme.SetField(&newCfg, fieldName, valNew)
+                               if err != nil {
+                                       return nil, err
+                               }
+                               dirty = true
+                       }
+               }
+       }
+
+       if !dirty {
+               return &newCfg, nil
+       }
+
+       fld, err := (*fc).Update(newCfg)
+       if err != nil {
+               return fld, err
+       }
+
+       // Save config on disk
+       err = f.SaveConfig()
+
+       // Send event to notified changes
+       // TODO emit folder change event
+
+       return fld, err
+}
+
+// RegisterEventChange requests registration for folder event change
+func (f *Folders) RegisterEventChange(id string, cb *folder.EventCB, data *folder.EventCBData) error {
+
+       flds := make(map[string]*folder.IFOLDER)
+       if id != "" {
+               // Register to a specific folder
+               flds[id] = f.Get(id)
+       } else {
+               // Register to all folders
+               flds = f.folders
+               f.registerCB = append(f.registerCB, RegisteredCB{cb: cb, data: data})
+       }
+
+       for _, fld := range flds {
+               err := (*fld).RegisterEventChange(cb, data)
+               if err != nil {
+                       return err
+               }
+       }
+
+       return nil
+}
+
 // ForceSync Force the synchronization of a folder
 func (f *Folders) ForceSync(id string) error {
        fc := f.Get(id)