summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2017-08-18 01:04:02 +0200
committerSebastien Douheret <sebastien.douheret@iot.bzh>2017-08-18 01:04:02 +0200
commit8f44cc7217ce48f3f94c8ea3f037cdf011c4493b (patch)
treea93c483370e6ad64a7440241cfb7c21beb6021ba
parent4feef5296bf3aea331fdde4cd7b94ee2322a907e (diff)
Add folder synchronization status.
Also add ability to force re-synchronization.
-rw-r--r--.vscode/settings.json114
-rw-r--r--lib/apiv1/apiv1.go5
-rw-r--r--lib/apiv1/events.go147
-rw-r--r--lib/apiv1/folders.go16
-rw-r--r--lib/folder/folder-interface.go21
-rw-r--r--lib/folder/folder-pathmap.go17
-rw-r--r--lib/model/folders.go105
-rw-r--r--lib/syncthing/folder-st.go83
-rw-r--r--lib/syncthing/st.go10
-rw-r--r--lib/syncthing/stEvent.go242
-rw-r--r--lib/syncthing/stfolder.go4
-rw-r--r--webapp/src/app/config/config.component.css4
-rw-r--r--webapp/src/app/devel/build/build.component.html4
-rw-r--r--webapp/src/app/projects/projectAddModal.component.ts16
-rw-r--r--webapp/src/app/projects/projectCard.component.ts25
-rw-r--r--webapp/src/app/projects/projectsListAccordion.component.ts17
-rw-r--r--webapp/src/app/services/config.service.ts101
-rw-r--r--webapp/src/app/services/xdsserver.service.ts28
18 files changed, 813 insertions, 146 deletions
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 7ccd637..4f2a394 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -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
diff --git a/lib/apiv1/apiv1.go b/lib/apiv1/apiv1.go
index f32e53b..262f513 100644
--- a/lib/apiv1/apiv1.go
+++ b/lib/apiv1/apiv1.go
@@ -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
index 0000000..da8298c
--- /dev/null
+++ b/lib/apiv1/events.go
@@ -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")
+}
diff --git a/lib/apiv1/folders.go b/lib/apiv1/folders.go
index f957c6d..cf56c3f 100644
--- a/lib/apiv1/folders.go
+++ b/lib/apiv1/folders.go
@@ -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)
-
}
diff --git a/lib/folder/folder-interface.go b/lib/folder/folder-interface.go
index b76b3f3..c04cbd7 100644
--- a/lib/folder/folder-interface.go
+++ b/lib/folder/folder-interface.go
@@ -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
diff --git a/lib/folder/folder-pathmap.go b/lib/folder/folder-pathmap.go
index 2ad8a93..f73f271 100644
--- a/lib/folder/folder-pathmap.go
+++ b/lib/folder/folder-pathmap.go
@@ -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
diff --git a/lib/model/folders.go b/lib/model/folders.go
index 02c3254..ed0078e 100644
--- a/lib/model/folders.go
+++ b/lib/model/folders.go
@@ -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)
diff --git a/lib/syncthing/folder-st.go b/lib/syncthing/folder-st.go
index ffcd284..da27062 100644
--- a/lib/syncthing/folder-st.go
+++ b/lib/syncthing/folder-st.go
@@ -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)
+ }
}
diff --git a/lib/syncthing/st.go b/lib/syncthing/st.go
index 9bdb48f..10210a4 100644
--- a/lib/syncthing/st.go
+++ b/lib/syncthing/st.go
@@ -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
index 0000000..bf2a809
--- /dev/null
+++ b/lib/syncthing/stEvent.go
@@ -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)
+}
diff --git a/lib/syncthing/stfolder.go b/lib/syncthing/stfolder.go
index bbdcc43..70ac70a 100644
--- a/lib/syncthing/stfolder.go
+++ b/lib/syncthing/stfolder.go
@@ -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.
diff --git a/webapp/src/app/config/config.component.css b/webapp/src/app/config/config.component.css
index 208ce6f..2bb3fea 100644
--- a/webapp/src/app/config/config.component.css
+++ b/webapp/src/app/config/config.component.css
@@ -24,3 +24,7 @@ tr.info>th {
tr.info>td {
vertical-align: middle;
}
+
+.panel-heading {
+ background: aliceblue;
+}
diff --git a/webapp/src/app/devel/build/build.component.html b/webapp/src/app/devel/build/build.component.html
index 7f85aa6..a66231c 100644
--- a/webapp/src/app/devel/build/build.component.html
+++ b/webapp/src/app/devel/build/build.component.html
@@ -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>
@@ -105,4 +105,4 @@
</div>
</div>
</div>
-</div> \ No newline at end of file
+</div>
diff --git a/webapp/src/app/projects/projectAddModal.component.ts b/webapp/src/app/projects/projectAddModal.component.ts
index 47e9c89..7ef5b5e 100644
--- a/webapp/src/app/projects/projectAddModal.component.ts
+++ b/webapp/src/app/projects/projectAddModal.component.ts
@@ -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]);
}
*/
diff --git a/webapp/src/app/projects/projectCard.component.ts b/webapp/src/app/projects/projectCard.component.ts
index 1b89fe7..a7ca9a3 100644
--- a/webapp/src/app/projects/projectCard.component.ts
+++ b/webapp/src/app/projects/projectCard.component.ts
@@ -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
diff --git a/webapp/src/app/projects/projectsListAccordion.component.ts b/webapp/src/app/projects/projectsListAccordion.component.ts
index 1b43cea..6e697f4 100644
--- a/webapp/src/app/projects/projectsListAccordion.component.ts
+++ b/webapp/src/app/projects/projectsListAccordion.component.ts
@@ -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>
diff --git a/webapp/src/app/services/config.service.ts b/webapp/src/app/services/config.service.ts
index 3b51768..f5e353c 100644
--- a/webapp/src/app/services/config.service.ts
+++ b/webapp/src/app/services/config.service.ts
@@ -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));
+ }
+ }
}
diff --git a/webapp/src/app/services/xdsserver.service.ts b/webapp/src/app/services/xdsserver.service.ts
index b11fe9f..b69a196 100644
--- a/webapp/src/app/services/xdsserver.service.ts
+++ b/webapp/src/app/services/xdsserver.service.ts
@@ -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',
{