Add folder synchronization status.
authorSebastien Douheret <sebastien.douheret@iot.bzh>
Thu, 17 Aug 2017 23:04:02 +0000 (01:04 +0200)
committerSebastien Douheret <sebastien.douheret@iot.bzh>
Thu, 17 Aug 2017 23:04:02 +0000 (01:04 +0200)
Also add ability to force re-synchronization.

18 files changed:
.vscode/settings.json
lib/apiv1/apiv1.go
lib/apiv1/events.go [new file with mode: 0644]
lib/apiv1/folders.go
lib/folder/folder-interface.go
lib/folder/folder-pathmap.go
lib/model/folders.go
lib/syncthing/folder-st.go
lib/syncthing/st.go
lib/syncthing/stEvent.go [new file with mode: 0644]
lib/syncthing/stfolder.go
webapp/src/app/config/config.component.css
webapp/src/app/devel/build/build.component.html
webapp/src/app/projects/projectAddModal.component.ts
webapp/src/app/projects/projectCard.component.ts
webapp/src/app/projects/projectsListAccordion.component.ts
webapp/src/app/services/config.service.ts
webapp/src/app/services/xdsserver.service.ts

index 7ccd637..4f2a394 100644 (file)
@@ -1,59 +1,59 @@
 // Place your settings in this file to overwrite default and user settings.
 {
-    // Configure glob patterns for excluding files and folders.
-    "files.exclude": {
-        ".tmp": true,
-        ".git": true,
-        "glide.lock": true,
-        "vendor": true,
-        "debug": true,
-        "bin": true,
-        "tools": true,
-        "webapp/dist": true,
-        "webapp/node_modules": true
-    },
-    // Specify paths/files to ignore. (Supports Globs)
-    "cSpell.ignorePaths": [
-        "**/node_modules/**",
-        "**/vscode-extension/**",
-        "**/.git/**",
-        "**/vendor/**",
-        ".vscode",
-        "typings"
-    ],
-    // Words to add to dictionary for a workspace.
-    "cSpell.words": [
-        "apiv",
-        "gonic",
-        "devel",
-        "csrffound",
-        "Syncthing",
-        "STID",
-        "ISTCONFIG",
-        "socketio",
-        "ldflags",
-        "SThg",
-        "Intf",
-        "dismissible",
-        "rpath",
-        "WSID",
-        "sess",
-        "IXDS",
-        "xdsconfig",
-        "xdsserver",
-        "mfolder",
-        "inotify",
-        "Inot",
-        "pname",
-        "pkill",
-        "sdkid",
-        "CLOUDSYNC",
-        "xdsagent",
-        "gdbserver",
-        "golib",
-        "eows",
-        "mfolders",
-        "IFOLDER",
-        "flds"
-    ]
-}
+  // Configure glob patterns for excluding files and folders.
+  "files.exclude": {
+    ".tmp": true,
+    ".git": true,
+    "glide.lock": true,
+    "vendor": true,
+    "debug": true,
+    "bin": true,
+    "tools": true,
+    "webapp/dist": true,
+    "webapp/node_modules": true
+  },
+  // Specify paths/files to ignore. (Supports Globs)
+  "cSpell.ignorePaths": [
+    "**/node_modules/**",
+    "**/vscode-extension/**",
+    "**/.git/**",
+    "**/vendor/**",
+    ".vscode",
+    "typings"
+  ],
+  // Words to add to dictionary for a workspace.
+  "cSpell.words": [
+    "apiv",
+    "gonic",
+    "devel",
+    "csrffound",
+    "Syncthing",
+    "STID",
+    "ISTCONFIG",
+    "socketio",
+    "ldflags",
+    "SThg",
+    "Intf",
+    "dismissible",
+    "rpath",
+    "WSID",
+    "sess",
+    "IXDS",
+    "xdsconfig",
+    "xdsserver",
+    "mfolder",
+    "inotify",
+    "Inot",
+    "pname",
+    "pkill",
+    "sdkid",
+    "CLOUDSYNC",
+    "xdsagent",
+    "gdbserver",
+    "golib",
+    "eows",
+    "mfolders",
+    "IFOLDER",
+    "flds"
+  ]
+}
\ No newline at end of file
index f32e53b..262f513 100644 (file)
@@ -42,6 +42,7 @@ func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolders
        s.apiRouter.GET("/folders", s.getFolders)
        s.apiRouter.GET("/folder/:id", s.getFolder)
        s.apiRouter.POST("/folder", s.addFolder)
+       s.apiRouter.POST("/folder/sync/:id", s.syncFolder)
        s.apiRouter.DELETE("/folder/:id", s.delFolder)
 
        s.apiRouter.GET("/sdks", s.getSdks)
@@ -54,5 +55,9 @@ func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolders
        s.apiRouter.POST("/exec/:id", s.execCmd)
        s.apiRouter.POST("/signal", s.execSignalCmd)
 
+       s.apiRouter.GET("/events", s.eventsList)
+       s.apiRouter.POST("/events/register", s.eventsRegister)
+       s.apiRouter.POST("/events/unregister", s.eventsUnRegister)
+
        return s
 }
diff --git a/lib/apiv1/events.go b/lib/apiv1/events.go
new file mode 100644 (file)
index 0000000..da8298c
--- /dev/null
@@ -0,0 +1,147 @@
+package apiv1
+
+import (
+       "net/http"
+       "time"
+
+       "github.com/iotbzh/xds-server/lib/folder"
+
+       "github.com/gin-gonic/gin"
+       common "github.com/iotbzh/xds-common/golib"
+)
+
+// EventArgs is the parameters (json format) of /events/register command
+type EventRegisterArgs struct {
+       Name      string `json:"name"`
+       ProjectID string `json:"filterProjectID"`
+}
+
+type EventUnRegisterArgs struct {
+       Name string `json:"name"`
+       ID   int    `json:"id"`
+}
+
+// EventMsg Message send
+type EventMsg struct {
+       Time   string              `json:"time"`
+       Type   string              `json:"type"`
+       Folder folder.FolderConfig `json:"folder"`
+}
+
+// EventEvent Event send in WS when an internal event (eg. Syncthing event is received)
+const EventEventAll = "event:all"
+const EventEventType = "event:" // following by event type
+
+// eventsList Registering for events that will be send over a WS
+func (s *APIService) eventsList(c *gin.Context) {
+
+}
+
+// eventsRegister Registering for events that will be send over a WS
+func (s *APIService) eventsRegister(c *gin.Context) {
+       var args EventRegisterArgs
+
+       if c.BindJSON(&args) != nil {
+               common.APIError(c, "Invalid arguments")
+               return
+       }
+
+       sess := s.sessions.Get(c)
+       if sess == nil {
+               common.APIError(c, "Unknown sessions")
+               return
+       }
+
+       evType := "FolderStateChanged"
+       if args.Name != evType {
+               common.APIError(c, "Unsupported event name")
+               return
+       }
+
+       /* XXX - to be removed if no plan to support "generic" event
+       var cbFunc st.EventsCB
+       cbFunc = func(ev st.Event, data *st.EventsCBData) {
+
+               evid, _ := strconv.Atoi((*data)["id"].(string))
+               ssid := (*data)["sid"].(string)
+               so := s.sessions.IOSocketGet(ssid)
+               if so == nil {
+                       s.log.Infof("Event %s not emitted - sid: %s", ev.Type, ssid)
+
+                       // Consider that client disconnected, so unregister this event
+                       s.mfolders.SThg.Events.UnRegister(ev.Type, evid)
+                       return
+               }
+
+               msg := EventMsg{
+                       Time: ev.Time,
+                       Type: ev.Type,
+                       Data: ev.Data,
+               }
+
+               if err := (*so).Emit(EventEventAll, msg); err != nil {
+                       s.log.Errorf("WS Emit Event : %v", err)
+               }
+
+               if err := (*so).Emit(EventEventType+ev.Type, msg); err != nil {
+                       s.log.Errorf("WS Emit Event : %v", err)
+               }
+       }
+
+       data := make(st.EventsCBData)
+       data["sid"] = sess.ID
+
+       id, err := s.mfolders.SThg.Events.Register(args.Name, cbFunc, args.ProjectID, &data)
+       */
+
+       var cbFunc folder.EventCB
+       cbFunc = func(cfg *folder.FolderConfig, data *folder.EventCBData) {
+               ssid := (*data)["sid"].(string)
+               so := s.sessions.IOSocketGet(ssid)
+               if so == nil {
+                       //s.log.Infof("Event %s not emitted - sid: %s", ev.Type, ssid)
+
+                       // Consider that client disconnected, so unregister this event
+                       // SEB FIXMEs.mfolders.RegisterEventChange(ev.Type)
+                       return
+               }
+
+               msg := EventMsg{
+                       Time:   time.Now().String(),
+                       Type:   evType,
+                       Folder: *cfg,
+               }
+
+               if err := (*so).Emit(EventEventType+evType, msg); err != nil {
+                       s.log.Errorf("WS Emit Folder StateChanged event : %v", err)
+               }
+       }
+       data := make(folder.EventCBData)
+       data["sid"] = sess.ID
+
+       err := s.mfolders.RegisterEventChange(args.ProjectID, &cbFunc, &data)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       c.JSON(http.StatusOK, gin.H{"status": "OK"})
+}
+
+// eventsRegister Registering for events that will be send over a WS
+func (s *APIService) eventsUnRegister(c *gin.Context) {
+       var args EventUnRegisterArgs
+
+       if c.BindJSON(&args) != nil || args.Name == "" || args.ID < 0 {
+               common.APIError(c, "Invalid arguments")
+               return
+       }
+       /* TODO
+       if err := s.mfolders.SThg.Events.UnRegister(args.Name, args.ID); err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+       c.JSON(http.StatusOK, gin.H{"status": "OK"})
+       */
+       common.APIError(c, "Not implemented yet")
+}
index f957c6d..cf56c3f 100644 (file)
@@ -43,6 +43,21 @@ func (s *APIService) addFolder(c *gin.Context) {
        c.JSON(http.StatusOK, newFld)
 }
 
+// syncFolder force synchronization of folder files
+func (s *APIService) syncFolder(c *gin.Context) {
+       id := c.Param("id")
+
+       s.log.Debugln("Sync folder id: ", id)
+
+       err := s.mfolders.ForceSync(id)
+       if err != nil {
+               common.APIError(c, err.Error())
+               return
+       }
+
+       c.JSON(http.StatusOK, "")
+}
+
 // delFolder deletes folder from server config
 func (s *APIService) delFolder(c *gin.Context) {
        id := c.Param("id")
@@ -55,5 +70,4 @@ func (s *APIService) delFolder(c *gin.Context) {
                return
        }
        c.JSON(http.StatusOK, delEntry)
-
 }
index b76b3f3..c04cbd7 100644 (file)
@@ -14,16 +14,24 @@ const (
        StatusErrorConfig = "ErrorConfig"
        StatusDisable     = "Disable"
        StatusEnable      = "Enable"
+       StatusPause       = "Pause"
+       StatusSyncing     = "Syncing"
 )
 
+type EventCBData map[string]interface{}
+type EventCB func(cfg *FolderConfig, data *EventCBData)
+
 // IFOLDER Folder interface
 type IFOLDER interface {
-       Add(cfg FolderConfig) (*FolderConfig, error) // Add a new folder
-       GetConfig() FolderConfig                     // Get folder public configuration
-       GetFullPath(dir string) string               // Get folder full path
-       Remove() error                               // Remove a folder
-       Sync() error                                 // Force folder files synchronization
-       IsInSync() (bool, error)                     // Check if folder files are in-sync
+       NewUID(suffix string) string                              // Get a new folder UUID
+       Add(cfg FolderConfig) (*FolderConfig, error)              // Add a new folder
+       GetConfig() FolderConfig                                  // Get folder public configuration
+       GetFullPath(dir string) string                            // Get folder full path
+       Remove() error                                            // Remove a folder
+       RegisterEventChange(cb *EventCB, data *EventCBData) error // Request events registration (sent through WS)
+       UnRegisterEventChange() error                             // Un-register events
+       Sync() error                                              // Force folder files synchronization
+       IsInSync() (bool, error)                                  // Check if folder files are in-sync
 }
 
 // FolderConfig is the config for one folder
@@ -33,6 +41,7 @@ type FolderConfig struct {
        ClientPath string     `json:"path"`
        Type       FolderType `json:"type"`
        Status     string     `json:"status"`
+       IsInSync   bool       `json:"isInSync"`
        DefaultSdk string     `json:"defaultSdk"`
 
        // Not exported fields from REST API point of view
index 2ad8a93..f73f271 100644 (file)
@@ -8,6 +8,7 @@ import (
 
        common "github.com/iotbzh/xds-common/golib"
        "github.com/iotbzh/xds-server/lib/xdsconfig"
+       uuid "github.com/satori/go.uuid"
 )
 
 // IFOLDER interface implementation for native/path mapping folders
@@ -26,6 +27,11 @@ func NewFolderPathMap(gc *xdsconfig.Config) *PathMap {
        return &f
 }
 
+// NewUID Get a UUID
+func (f *PathMap) NewUID(suffix string) string {
+       return uuid.NewV1().String() + "_" + suffix
+}
+
 // Add a new folder
 func (f *PathMap) Add(cfg FolderConfig) (*FolderConfig, error) {
        if cfg.DataPathMap.ServerPath == "" {
@@ -63,6 +69,7 @@ func (f *PathMap) Add(cfg FolderConfig) (*FolderConfig, error) {
        f.config = cfg
        f.config.RootPath = dir
        f.config.DataPathMap.ServerPath = dir
+       f.config.IsInSync = true
        f.config.Status = StatusEnable
 
        return &f.config, nil
@@ -87,6 +94,16 @@ func (f *PathMap) Remove() error {
        return nil
 }
 
+// RegisterEventChange requests registration for folder change event
+func (f *PathMap) RegisterEventChange(cb *EventCB, data *EventCBData) error {
+       return nil
+}
+
+// UnRegisterEventChange remove registered callback
+func (f *PathMap) UnRegisterEventChange() error {
+       return nil
+}
+
 // Sync Force folder files synchronization
 func (f *PathMap) Sync() error {
        return nil
index 02c3254..ed0078e 100644 (file)
@@ -7,13 +7,13 @@ import (
        "os"
        "path/filepath"
        "strings"
+       "time"
 
        "github.com/Sirupsen/logrus"
        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 +24,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 +45,7 @@ func FoldersNew(cfg *xdsconfig.Config, st *st.SyncThing) *Folders {
                Log:        cfg.Log,
                SThg:       st,
                folders:    make(map[string]*folder.IFOLDER),
+               registerCB: []RegisteredCB{},
        }
 }
 
@@ -114,12 +121,15 @@ func (f *Folders) LoadConfig() error {
        // 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
@@ -164,11 +174,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,23 +191,7 @@ func (f *Folders) createUpdate(newF folder.FolderConfig, create bool) (*folder.F
                return nil, fmt.Errorf("ClientPath must be set")
        }
 
-       // Allocate a new UUID
-       if create {
-               newF.ID = uuid.NewV1().String()
-       }
-       if !create && newF.ID == "" {
-               return nil, fmt.Errorf("Cannot update folder with null ID")
-       }
-
-       // Set default value if needed
-       if newF.Status == "" {
-               newF.Status = folder.StatusDisable
-       }
-
-       if newF.Label == "" {
-               newF.Label = filepath.Base(newF.ClientPath) + "_" + newF.ID[0:8]
-       }
-
+       // Create a new folder object
        var fld folder.IFOLDER
        switch newF.Type {
        // SYNCTHING
@@ -213,6 +207,26 @@ func (f *Folders) createUpdate(newF folder.FolderConfig, create bool) (*folder.F
                return nil, fmt.Errorf("Unsupported folder type")
        }
 
+       // Set default value if needed
+       if newF.Status == "" {
+               newF.Status = folder.StatusDisable
+       }
+       if newF.Label == "" {
+               newF.Label = filepath.Base(newF.ClientPath) + "_" + newF.ID[0:8]
+       }
+
+       // Allocate a new UUID
+       if create {
+               i := len(newF.Label)
+               if i > 20 {
+                       i = 20
+               }
+               newF.ID = fld.NewUID(newF.Label[:i])
+       }
+       if !create && newF.ID == "" {
+               return nil, fmt.Errorf("Cannot update folder with null ID")
+       }
+
        // Normalize path (needed for Windows path including bashlashes)
        newF.ClientPath = common.PathNormalize(newF.ClientPath)
 
@@ -224,13 +238,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
+               }
+       }
+
+       // 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, err
+       return newFolder, nil
 }
 
 // Delete deletes a specific folder
@@ -260,6 +292,29 @@ func (f *Folders) Delete(id string) (folder.FolderConfig, error) {
        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)
index ffcd284..da27062 100644 (file)
@@ -6,6 +6,7 @@ import (
 
        "github.com/iotbzh/xds-server/lib/folder"
        "github.com/iotbzh/xds-server/lib/xdsconfig"
+       uuid "github.com/satori/go.uuid"
        "github.com/syncthing/syncthing/lib/config"
 )
 
@@ -13,10 +14,13 @@ import (
 
 // STFolder .
 type STFolder struct {
-       globalConfig *xdsconfig.Config
-       st           *SyncThing
-       fConfig      folder.FolderConfig
-       stfConfig    config.FolderConfiguration
+       globalConfig      *xdsconfig.Config
+       st                *SyncThing
+       fConfig           folder.FolderConfig
+       stfConfig         config.FolderConfiguration
+       eventIDs          []int
+       eventChangeCB     *folder.EventCB
+       eventChangeCBData *folder.EventCBData
 }
 
 // NewFolderST Create a new instance of STFolder
@@ -27,6 +31,15 @@ func (s *SyncThing) NewFolderST(gc *xdsconfig.Config) *STFolder {
        }
 }
 
+// NewUID Get a UUID
+func (f *STFolder) NewUID(suffix string) string {
+       i := len(f.st.MyID)
+       if i > 15 {
+               i = 15
+       }
+       return uuid.NewV1().String()[:14] + f.st.MyID[:i] + "_" + suffix
+}
+
 // Add a new folder
 func (f *STFolder) Add(cfg folder.FolderConfig) (*folder.FolderConfig, error) {
 
@@ -59,6 +72,16 @@ func (f *STFolder) Add(cfg folder.FolderConfig) (*folder.FolderConfig, error) {
                        return nil, err
                }
 
+               // Register to events to update folder status
+               for _, evName := range []string{EventStateChanged, EventFolderPaused} {
+                       evID, err := f.st.Events.Register(evName, f.cbEventState, id, nil)
+                       if err != nil {
+                               return nil, err
+                       }
+                       f.eventIDs = append(f.eventIDs, evID)
+               }
+
+               f.fConfig.IsInSync = false // will be updated later by events
                f.fConfig.Status = folder.StatusEnable
        }
 
@@ -86,6 +109,20 @@ func (f *STFolder) Remove() error {
        return f.st.FolderDelete(f.stfConfig.ID)
 }
 
+// RegisterEventChange requests registration for folder event change
+func (f *STFolder) RegisterEventChange(cb *folder.EventCB, data *folder.EventCBData) error {
+       f.eventChangeCB = cb
+       f.eventChangeCBData = data
+       return nil
+}
+
+// UnRegisterEventChange remove registered callback
+func (f *STFolder) UnRegisterEventChange() error {
+       f.eventChangeCB = nil
+       f.eventChangeCBData = nil
+       return nil
+}
+
 // Sync Force folder files synchronization
 func (f *STFolder) Sync() error {
        return f.st.FolderScan(f.stfConfig.ID, "")
@@ -93,5 +130,41 @@ func (f *STFolder) Sync() error {
 
 // IsInSync Check if folder files are in-sync
 func (f *STFolder) IsInSync() (bool, error) {
-       return f.st.IsFolderInSync(f.stfConfig.ID)
+       sts, err := f.st.IsFolderInSync(f.stfConfig.ID)
+       if err != nil {
+               return false, err
+       }
+       f.fConfig.IsInSync = sts
+       return sts, nil
+}
+
+// callback use to update IsInSync status
+func (f *STFolder) cbEventState(ev Event, data *EventsCBData) {
+       prevSync := f.fConfig.IsInSync
+       prevStatus := f.fConfig.Status
+
+       switch ev.Type {
+
+       case EventStateChanged:
+               to := ev.Data["to"]
+               switch to {
+               case "scanning", "syncing":
+                       f.fConfig.Status = folder.StatusSyncing
+               case "idle":
+                       f.fConfig.Status = folder.StatusEnable
+               }
+               f.fConfig.IsInSync = (to == "idle")
+
+       case EventFolderPaused:
+               if f.fConfig.Status == folder.StatusEnable {
+                       f.fConfig.Status = folder.StatusPause
+               }
+               f.fConfig.IsInSync = false
+       }
+
+       if f.eventChangeCB != nil &&
+               (prevSync != f.fConfig.IsInSync || prevStatus != f.fConfig.Status) {
+               cpConf := f.fConfig
+               (*f.eventChangeCB)(&cpConf, f.eventChangeCBData)
+       }
 }
index 9bdb48f..10210a4 100644 (file)
@@ -42,6 +42,7 @@ type SyncThing struct {
        conf        *xdsconfig.Config
        client      *common.HTTPClient
        log         *logrus.Logger
+       Events      *Events
 }
 
 // ExitChan Channel used for process exit
@@ -126,6 +127,9 @@ func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
                conf:    conf,
        }
 
+       // Create Events monitoring
+       s.Events = s.NewEventListener()
+
        return &s
 }
 
@@ -316,6 +320,12 @@ func (s *SyncThing) Connect() error {
        s.client.SetLogger(s.log)
 
        s.MyID, err = s.IDGet()
+       if err != nil {
+               return fmt.Errorf("ERROR: cannot retrieve ID")
+       }
+
+       // Start events monitoring
+       err = s.Events.Start()
 
        return err
 }
diff --git a/lib/syncthing/stEvent.go b/lib/syncthing/stEvent.go
new file mode 100644 (file)
index 0000000..bf2a809
--- /dev/null
@@ -0,0 +1,242 @@
+package st
+
+import (
+       "encoding/json"
+       "fmt"
+       "os"
+       "strconv"
+       "strings"
+       "time"
+
+       "github.com/Sirupsen/logrus"
+)
+
+// Events .
+type Events struct {
+       MonitorTime time.Duration
+       Debug       bool
+
+       stop  chan bool
+       st    *SyncThing
+       log   *logrus.Logger
+       cbArr map[string][]cbMap
+}
+
+type Event struct {
+       Type string            `json:"type"`
+       Time time.Time         `json:"time"`
+       Data map[string]string `json:"data"`
+}
+
+type EventsCBData map[string]interface{}
+type EventsCB func(ev Event, cbData *EventsCBData)
+
+const (
+       EventFolderCompletion string = "FolderCompletion"
+       EventFolderSummary    string = "FolderSummary"
+       EventFolderPaused     string = "FolderPaused"
+       EventFolderResumed    string = "FolderResumed"
+       EventFolderErrors     string = "FolderErrors"
+       EventStateChanged     string = "StateChanged"
+)
+
+var EventsAll string = EventFolderCompletion + "|" +
+       EventFolderSummary + "|" +
+       EventFolderPaused + "|" +
+       EventFolderResumed + "|" +
+       EventFolderErrors + "|" +
+       EventStateChanged
+
+type STEvent struct {
+       // Per-subscription sequential event ID. Named "id" for backwards compatibility with the REST API
+       SubscriptionID int `json:"id"`
+       // Global ID of the event across all subscriptions
+       GlobalID int                    `json:"globalID"`
+       Time     time.Time              `json:"time"`
+       Type     string                 `json:"type"`
+       Data     map[string]interface{} `json:"data"`
+}
+
+type cbMap struct {
+       id       int
+       cb       EventsCB
+       filterID string
+       data     *EventsCBData
+}
+
+// NewEventListener Create a new instance of Event listener
+func (s *SyncThing) NewEventListener() *Events {
+       _, dbg := os.LookupEnv("XDS_DEBUG_STEVENTS") // set to add more debug log
+       return &Events{
+               MonitorTime: 100, // in Milliseconds
+               Debug:       dbg,
+               stop:        make(chan bool, 1),
+               st:          s,
+               log:         s.log,
+               cbArr:       make(map[string][]cbMap),
+       }
+}
+
+// Start starts event monitoring loop
+func (e *Events) Start() error {
+       go e.monitorLoop()
+       return nil
+}
+
+// Stop stops event monitoring loop
+func (e *Events) Stop() {
+       e.stop <- true
+}
+
+// Register Add a listener on an event
+func (e *Events) Register(evName string, cb EventsCB, filterID string, data *EventsCBData) (int, error) {
+       if evName == "" || !strings.Contains(EventsAll, evName) {
+               return -1, fmt.Errorf("Unknown event name")
+       }
+       if data == nil {
+               data = &EventsCBData{}
+       }
+
+       cbList := []cbMap{}
+       if _, ok := e.cbArr[evName]; ok {
+               cbList = e.cbArr[evName]
+       }
+
+       id := len(cbList)
+       (*data)["id"] = strconv.Itoa(id)
+
+       e.cbArr[evName] = append(cbList, cbMap{id: id, cb: cb, filterID: filterID, data: data})
+
+       return id, nil
+}
+
+// UnRegister Remove a listener event
+func (e *Events) UnRegister(evName string, id int) error {
+       cbKey, ok := e.cbArr[evName]
+       if !ok {
+               return fmt.Errorf("No event registered to such name")
+       }
+
+       // FIXME - NOT TESTED
+       if id >= len(cbKey) {
+               return fmt.Errorf("Invalid id")
+       } else if id == len(cbKey) {
+               e.cbArr[evName] = cbKey[:id-1]
+       } else {
+               e.cbArr[evName] = cbKey[id : id+1]
+       }
+
+       return nil
+}
+
+// GetEvents returns the Syncthing events
+func (e *Events) getEvents(since int) ([]STEvent, error) {
+       var data []byte
+       ev := []STEvent{}
+       url := "events"
+       if since != -1 {
+               url += "?since=" + strconv.Itoa(since)
+       }
+       if err := e.st.client.HTTPGet(url, &data); err != nil {
+               return ev, err
+       }
+       err := json.Unmarshal(data, &ev)
+       return ev, err
+}
+
+// Loop to monitor Syncthing events
+func (e *Events) monitorLoop() {
+       e.log.Infof("Event monitoring running...")
+       since := 0
+       for {
+               select {
+               case <-e.stop:
+                       e.log.Infof("Event monitoring exited")
+                       return
+
+               case <-time.After(e.MonitorTime * time.Millisecond):
+                       stEvArr, err := e.getEvents(since)
+                       if err != nil {
+                               e.log.Errorf("Syncthing Get Events: %v", err)
+                               continue
+                       }
+                       // Process events
+                       for _, stEv := range stEvArr {
+                               since = stEv.SubscriptionID
+                               if e.Debug {
+                                       e.log.Warnf("ST EVENT: %d %s\n  %v", stEv.GlobalID, stEv.Type, stEv)
+                               }
+
+                               cbKey, ok := e.cbArr[stEv.Type]
+                               if !ok {
+                                       continue
+                               }
+
+                               evData := Event{
+                                       Type: stEv.Type,
+                                       Time: stEv.Time,
+                               }
+
+                               // Decode Events
+                               // FIXME: re-define data struct for each events
+                               // instead of map of string and use JSON marshing/unmarshing
+                               fID := ""
+                               evData.Data = make(map[string]string)
+                               switch stEv.Type {
+
+                               case EventFolderCompletion:
+                                       fID = convString(stEv.Data["folder"])
+                                       evData.Data["completion"] = convFloat64(stEv.Data["completion"])
+
+                               case EventFolderSummary:
+                                       fID = convString(stEv.Data["folder"])
+                                       evData.Data["needBytes"] = convInt64(stEv.Data["needBytes"])
+                                       evData.Data["state"] = convString(stEv.Data["state"])
+
+                               case EventFolderPaused, EventFolderResumed:
+                                       fID = convString(stEv.Data["id"])
+                                       evData.Data["label"] = convString(stEv.Data["label"])
+
+                               case EventFolderErrors:
+                                       fID = convString(stEv.Data["folder"])
+                                       // TODO decode array evData.Data["errors"] = convString(stEv.Data["errors"])
+
+                               case EventStateChanged:
+                                       fID = convString(stEv.Data["folder"])
+                                       evData.Data["from"] = convString(stEv.Data["from"])
+                                       evData.Data["to"] = convString(stEv.Data["to"])
+
+                               default:
+                                       e.log.Warnf("Unsupported event type")
+                               }
+
+                               if fID != "" {
+                                       evData.Data["id"] = fID
+                               }
+
+                               // Call all registered callbacks
+                               for _, c := range cbKey {
+                                       if e.Debug {
+                                               e.log.Warnf("EVENT CB fID=%s, filterID=%s", fID, c.filterID)
+                                       }
+                                       // Call when filterID is not set or when it matches
+                                       if c.filterID == "" || (fID != "" && fID == c.filterID) {
+                                               c.cb(evData, c.data)
+                                       }
+                               }
+                       }
+               }
+       }
+}
+
+func convString(d interface{}) string {
+       return d.(string)
+}
+
+func convFloat64(d interface{}) string {
+       return strconv.FormatFloat(d.(float64), 'f', -1, 64)
+}
+
+func convInt64(d interface{}) string {
+       return strconv.FormatInt(d.(int64), 10)
+}
index bbdcc43..70ac70a 100644 (file)
@@ -191,13 +191,11 @@ func (s *SyncThing) FolderStatus(folderID string) (*FolderStatus, error) {
 
 // IsFolderInSync Returns true when folder is in sync
 func (s *SyncThing) IsFolderInSync(folderID string) (bool, error) {
-       // FIXME better to detected FolderCompletion event (/rest/events)
-       // See https://docs.syncthing.net/dev/events.html
        sts, err := s.FolderStatus(folderID)
        if err != nil {
                return false, err
        }
-       return sts.NeedBytes == 0, nil
+       return sts.NeedBytes == 0 && sts.State == "idle", nil
 }
 
 // FolderScan Request immediate folder scan.
index 208ce6f..2bb3fea 100644 (file)
@@ -24,3 +24,7 @@ tr.info>th {
 tr.info>td {
     vertical-align: middle;
 }
+
+.panel-heading {
+    background: aliceblue;
+}
index 7f85aa6..a66231c 100644 (file)
@@ -18,7 +18,7 @@
                         </tr>
                         <tr>
                             <th>Project root path</th>
-                            <td> <input type="text" disabled style="width:99%;" [value]="curProject && curProject.path"></td>
+                            <td> <input type="text" disabled style="width:99%;" [value]="curProject && curProject.pathClient"></td>
                         </tr>
                         <tr>
                             <th>Sub-path</th>
             </div>
         </div>
     </div>
-</div>
\ No newline at end of file
+</div>
index 47e9c89..7ef5b5e 100644 (file)
@@ -62,7 +62,17 @@ export class ProjectAddModalComponent {
         this.pathCliCtrl.valueChanges
             .debounceTime(100)
             .filter(n => n)
-            .map(n => "Project_" + n.split('/')[0])
+            .map(n => {
+                let last = n.split('/');
+                let nm = n;
+                if (last.length > 0) {
+                     nm = last.pop();
+                     if (nm === "" && last.length > 0) {
+                        nm = last.pop();
+                     }
+                }
+                return "Project_" + nm;
+            })
             .subscribe(value => {
                 if (value && !this.userEditedLabel) {
                     this.addProjectForm.patchValue({ label: value });
@@ -97,10 +107,10 @@ export class ProjectAddModalComponent {
 
     onChangeLocalProject(e) {
         if e.target.files.length < 1 {
-            console.log('SEB NO files');
+            console.log('NO files');
         }
         let dir = e.target.files[0].webkitRelativePath;
-        console.log("SEB files: " + dir);
+        console.log("files: " + dir);
         let u = URL.createObjectURL(e.target.files[0]);
     }
     */
index 1b89fe7..a7ca9a3 100644 (file)
@@ -8,7 +8,9 @@ import { AlertService } from "../services/alert.service";
         <div class="row">
             <div class="col-xs-12">
                 <div class="text-right" role="group">
-                    <button class="btn btn-link" (click)="delete(project)"><span class="fa fa-trash fa-size-x2"></span></button>
+                    <button class="btn btn-link" (click)="delete(project)">
+                        <span class="fa fa-trash fa-size-x2"></span>
+                    </button>
                 </div>
             </div>
         </div>
@@ -27,16 +29,18 @@ import { AlertService } from "../services/alert.service";
                 <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Local path</span></th>
                 <td>{{ project.pathClient }}</td>
             </tr>
-            <tr *ngIf="project.pathServer != ''">
+            <tr *ngIf="project.pathServer && project.pathServer != ''">
                 <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Server path</span></th>
                 <td>{{ project.pathServer }}</td>
             </tr>
-            <!--
             <tr>
-                <th><span class="fa fa-fw fa-status"></span>&nbsp;<span>Status</span></th>
-                <td>{{ project.remotePrjDef.status }}</td>
+                <th><span class="fa fa-fw fa-flag"></span>&nbsp;<span>Status</span></th>
+                <td>{{ project.status }} - {{ project.isInSync ? "Up to Date" : "Out of Sync"}}
+                    <button *ngIf="!project.isInSync" class="btn btn-link" (click)="sync(project)">
+                        <span class="fa fa-refresh fa-size-x2"></span>
+                    </button>
+                </td>
             </tr>
-            -->
             </tbody>
         </table >
     `,
@@ -53,7 +57,6 @@ export class ProjectCardComponent {
     ) {
     }
 
-
     delete(prj: IProject) {
         this.configSvr.deleteProject(prj)
             .subscribe(res => {
@@ -62,6 +65,14 @@ export class ProjectCardComponent {
             });
     }
 
+    sync(prj: IProject) {
+        this.configSvr.syncProject(prj)
+            .subscribe(res => {
+            }, err => {
+                this.alert.error("ERROR: " + err);
+            });
+    }
+
 }
 
 // Remove APPS. prefix if translate has failed
index 1b43cea..6e697f4 100644 (file)
@@ -5,12 +5,25 @@ import { IProject } from "../services/config.service";
 @Component({
     selector: 'projects-list-accordion',
     template: `
+        <style>
+            .fa.fa-exclamation-triangle {
+                margin-right: 2em;
+                color: red;
+            }
+            .fa.fa-refresh {
+                margin-right: 10px;
+                color: darkviolet;
+            }
+        </style>
         <accordion>
             <accordion-group #group *ngFor="let prj of projects">
                 <div accordion-heading>
                     {{ prj.label }}
-                    <i class="pull-right float-xs-right fa"
-                    [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+                    <div class="pull-right">
+                        <i *ngIf="prj.status == 'Syncing'" class="fa fa-refresh faa-spin animated"></i>
+                        <i *ngIf="!prj.isInSync && prj.status != 'Syncing'" class="fa fa-exclamation-triangle"></i>
+                        <i class="fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+                    </div>
                 </div>
                 <project-card [project]="prj"></project-card>
             </accordion-group>
index 3b51768..f5e353c 100644 (file)
@@ -29,18 +29,15 @@ export var ProjectTypes = [
     { value: ProjectType.SYNCTHING, display: "Cloud Sync" }
 ];
 
-export interface INativeProject {
-    // TODO
-}
-
 export interface IProject {
     id?: string;
     label: string;
     pathClient: string;
     pathServer?: string;
     type: ProjectType;
-    remotePrjDef?: INativeProject | ISyncThingProject;
-    localPrjDef?: any;
+    status?: string;
+    isInSync?: boolean;
+    serverPrjDef?: IXDSFolderConfig;
     isExpanded?: boolean;
     visible?: boolean;
     defaultSdkID?: string;
@@ -139,6 +136,17 @@ export class ConfigService {
             );
             this.confSubject.next(Object.assign({}, this.confStore));
         });
+
+        // Update Project data
+        this.xdsServerSvr.FolderStateChange$.subscribe(prj => {
+            let i = this._getProjectIdx(prj.id);
+            if (i >= 0) {
+                // XXX for now, only isInSync and status may change
+                this.confStore.projects[i].isInSync = prj.isInSync;
+                this.confStore.projects[i].status = prj.status;
+                this.confSubject.next(Object.assign({}, this.confStore));
+            }
+        });
     }
 
     // Save config into cookie
@@ -215,17 +223,8 @@ export class ConfigService {
                 this.stSvr.getProjects().subscribe(localPrj => {
                     remotePrj.forEach(rPrj => {
                         let lPrj = localPrj.filter(item => item.id === rPrj.id);
-                        if (lPrj.length > 0) {
-                            let pp: IProject = {
-                                id: rPrj.id,
-                                label: rPrj.label,
-                                pathClient: rPrj.path,
-                                pathServer: rPrj.dataPathMap.serverPath,
-                                type: rPrj.type,
-                                remotePrjDef: Object.assign({}, rPrj),
-                                localPrjDef: Object.assign({}, lPrj[0]),
-                            };
-                            this.confStore.projects.push(pp);
+                        if (lPrj.length > 0 || rPrj.type === ProjectType.NATIVE_PATHMAP) {
+                            this._addProject(rPrj, true);
                         }
                     });
                     this.confSubject.next(Object.assign({}, this.confStore));
@@ -306,18 +305,15 @@ export class ConfigService {
         let newPrj = prj;
         return this.xdsServerSvr.addProject(xdsPrj)
             .flatMap(resStRemotePrj => {
-                newPrj.remotePrjDef = resStRemotePrj;
-                newPrj.id = resStRemotePrj.id;
-                newPrj.pathClient = resStRemotePrj.path;
-
-                if (newPrj.type === ProjectType.SYNCTHING) {
+                xdsPrj = resStRemotePrj;
+                if (xdsPrj.type === ProjectType.SYNCTHING) {
                     // FIXME REWORK local ST config
                     //  move logic to server side tunneling-back by WS
-                    let stData = resStRemotePrj.dataCloudSync;
+                    let stData = xdsPrj.dataCloudSync;
 
                     // Now setup local config
                     let stLocPrj: ISyncThingProject = {
-                        id: resStRemotePrj.id,
+                        id: xdsPrj.id,
                         label: xdsPrj.label,
                         path: xdsPrj.path,
                         serverSyncThingID: stData.builderSThgID
@@ -327,18 +323,11 @@ export class ConfigService {
                     return this.stSvr.addProject(stLocPrj);
 
                 } else {
-                    newPrj.pathServer = resStRemotePrj.dataPathMap.serverPath;
                     return Observable.of(null);
                 }
             })
             .map(resStLocalPrj => {
-                newPrj.localPrjDef = resStLocalPrj;
-
-                // FIXME: maybe reduce subject to only .project
-                //this.confSubject.next(Object.assign({}, this.confStore).project);
-                this.confStore.projects.push(Object.assign({}, newPrj));
-                this.confSubject.next(Object.assign({}, this.confStore));
-
+                this._addProject(xdsPrj);
                 return newPrj;
             });
     }
@@ -351,7 +340,10 @@ export class ConfigService {
         }
         return this.xdsServerSvr.deleteProject(prj.id)
             .flatMap(res => {
-                return this.stSvr.deleteProject(prj.id);
+                if (prj.type === ProjectType.SYNCTHING) {
+                    return this.stSvr.deleteProject(prj.id);
+                }
+                return Observable.of(null);
             })
             .map(res => {
                 this.confStore.projects.splice(idx, 1);
@@ -359,8 +351,51 @@ export class ConfigService {
             });
     }
 
+    syncProject(prj: IProject): Observable<string> {
+        let idx = this._getProjectIdx(prj.id);
+        if (idx === -1) {
+            throw new Error("Invalid project id (id=" + prj.id + ")");
+        }
+        return this.xdsServerSvr.syncProject(prj.id);
+    }
+
     private _getProjectIdx(id: string): number {
         return this.confStore.projects.findIndex((item) => item.id === id);
     }
 
+    private _addProject(rPrj: IXDSFolderConfig, noNext?: boolean) {
+
+        // Convert XDSFolderConfig to IProject
+        let pp: IProject = {
+            id: rPrj.id,
+            label: rPrj.label,
+            pathClient: rPrj.path,
+            pathServer: rPrj.dataPathMap.serverPath,
+            type: rPrj.type,
+            status: rPrj.status,
+            isInSync: rPrj.isInSync,
+            defaultSdkID: rPrj.defaultSdkID,
+            serverPrjDef: Object.assign({}, rPrj),  // do a copy
+        };
+
+        // add new project
+        this.confStore.projects.push(pp);
+
+        // sort project array
+        this.confStore.projects.sort((a, b) => {
+            if (a.label < b.label) {
+                return -1;
+            }
+            if (a.label > b.label) {
+                return 1;
+            }
+            return 0;
+        });
+
+        // FIXME: maybe reduce subject to only .project
+        //this.confSubject.next(Object.assign({}, this.confStore).project);
+        if (!noNext) {
+            this.confSubject.next(Object.assign({}, this.confStore));
+        }
+    }
 }
index b11fe9f..b69a196 100644 (file)
@@ -38,12 +38,13 @@ export interface IXDSFolderConfig {
     path: string;
     type: number;
     status?: string;
+    isInSync?: boolean;
     defaultSdkID: string;
 
     // FIXME better with union but tech pb with go code
     //data?: IXDSPathMapConfig|IXDSCloudSyncConfig;
-    dataPathMap?:IXDSPathMapConfig;
-    dataCloudSync?:IXDSCloudSyncConfig;
+    dataPathMap?: IXDSPathMapConfig;
+    dataCloudSync?: IXDSCloudSyncConfig;
 }
 
 export interface IXDSPathMapConfig {
@@ -106,8 +107,10 @@ export class XDSServerService {
 
     public CmdOutput$ = <Subject<ICmdOutput>>new Subject();
     public CmdExit$ = <Subject<ICmdExit>>new Subject();
+    public FolderStateChange$ = <Subject<IXDSFolderConfig>>new Subject();
     public Status$: Observable<IServerStatus>;
 
+
     private baseUrl: string;
     private wsUrl: string;
     private _status = { WS_connected: false };
@@ -127,6 +130,7 @@ export class XDSServerService {
         } else {
             this.wsUrl = 'ws://' + re[1];
             this._handleIoSocket();
+            this._RegisterEvents();
         }
     }
 
@@ -172,6 +176,22 @@ export class XDSServerService {
             this.CmdExit$.next(Object.assign({}, <ICmdExit>data));
         });
 
+        this.socket.on('event:FolderStateChanged', ev => {
+            if (ev && ev.folder) {
+                this.FolderStateChange$.next(Object.assign({}, ev.folder));
+            }
+        });
+    }
+
+    private _RegisterEvents() {
+        let ev = "FolderStateChanged";
+        this._post('/events/register', { "name": ev })
+            .subscribe(
+            res => { },
+            error => {
+                this.alert.error("ERROR while registering events " + ev + ": ", error);
+            }
+            );
     }
 
     getSdks(): Observable<ISdk[]> {
@@ -194,6 +214,10 @@ export class XDSServerService {
         return this._delete('/folder/' + id);
     }
 
+    syncProject(id: string): Observable<string> {
+        return this._post('/folder/sync/' + id, {});
+    }
+
     exec(prjID: string, dir: string, cmd: string, sdkid?: string, args?: string[], env?: string[]): Observable<any> {
         return this._post('/exec',
             {