aboutsummaryrefslogtreecommitdiffstats
path: root/lib/xdsserver
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2017-11-29 08:54:00 +0100
committerSebastien Douheret <sebastien.douheret@iot.bzh>2017-11-29 11:10:30 +0100
commit2f7828d01f4c4ca2909f95f098627cd5475ed225 (patch)
treeb5e71920b813b95cae3e32044be08b99223348ec /lib/xdsserver
parent5caebfb4b7c3b73988f067082b219ce5b7f409ba (diff)
Refit source files to have a public xs-apiv1 lib package.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
Diffstat (limited to 'lib/xdsserver')
-rw-r--r--lib/xdsserver/apiv1-config.go40
-rw-r--r--lib/xdsserver/apiv1-events.go132
-rw-r--r--lib/xdsserver/apiv1-exec.go342
-rw-r--r--lib/xdsserver/apiv1-folders.go131
-rw-r--r--lib/xdsserver/apiv1-make.go214
-rw-r--r--lib/xdsserver/apiv1-sdks.go29
-rw-r--r--lib/xdsserver/apiv1-version.go20
-rw-r--r--lib/xdsserver/apiv1.go47
-rw-r--r--lib/xdsserver/folder-interface.go22
-rw-r--r--lib/xdsserver/folder-pathmap.go175
-rw-r--r--lib/xdsserver/folder-st-disable.go91
-rw-r--r--lib/xdsserver/folder-st.go213
-rw-r--r--lib/xdsserver/folders.go458
-rw-r--r--lib/xdsserver/sdk.go56
-rw-r--r--lib/xdsserver/sdks.go127
-rw-r--r--lib/xdsserver/sessions.go225
-rw-r--r--lib/xdsserver/webserver.go181
-rw-r--r--lib/xdsserver/xdsserver.go202
18 files changed, 2705 insertions, 0 deletions
diff --git a/lib/xdsserver/apiv1-config.go b/lib/xdsserver/apiv1-config.go
new file mode 100644
index 0000000..5a5bb6e
--- /dev/null
+++ b/lib/xdsserver/apiv1-config.go
@@ -0,0 +1,40 @@
+package xdsserver
+
+import (
+ "net/http"
+ "sync"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+)
+
+var confMut sync.Mutex
+
+// GetConfig returns server configuration
+func (s *APIService) getConfig(c *gin.Context) {
+ confMut.Lock()
+ defer confMut.Unlock()
+
+ c.JSON(http.StatusOK, s.Config)
+}
+
+// SetConfig sets server configuration
+func (s *APIService) setConfig(c *gin.Context) {
+ // FIXME - must be tested
+ c.JSON(http.StatusNotImplemented, "Not implemented")
+
+ var cfgArg xsapiv1.APIConfig
+
+ if c.BindJSON(&cfgArg) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ confMut.Lock()
+ defer confMut.Unlock()
+
+ s.Log.Debugln("SET config: ", cfgArg)
+
+ common.APIError(c, "Not Supported")
+}
diff --git a/lib/xdsserver/apiv1-events.go b/lib/xdsserver/apiv1-events.go
new file mode 100644
index 0000000..3823f9e
--- /dev/null
+++ b/lib/xdsserver/apiv1-events.go
@@ -0,0 +1,132 @@
+package xdsserver
+
+import (
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+)
+
+// 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 xsapiv1.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 := strings.TrimPrefix(xsapiv1.EVTFolderStateChange, xsapiv1.EventTypePrefix)
+ 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(EVTAll, msg); err != nil {
+ s.log.Errorf("WS Emit Event : %v", err)
+ }
+
+ if err := (*so).Emit(EventTypePrefix+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 FolderEventCB
+ cbFunc = func(cfg *xsapiv1.FolderConfig, data *FolderEventCBData) {
+ 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 := xsapiv1.EventMsg{
+ Time: time.Now().String(),
+ Type: evType,
+ Folder: *cfg,
+ }
+
+ s.Log.Debugf("WS Emit %s - Status=%10s, IsInSync=%6v, ID=%s",
+ xsapiv1.EventTypePrefix+evType, cfg.Status, cfg.IsInSync, cfg.ID)
+
+ if err := (*so).Emit(xsapiv1.EventTypePrefix+evType, msg); err != nil {
+ s.Log.Errorf("WS Emit Folder StateChanged event : %v", err)
+ }
+ }
+ data := make(FolderEventCBData)
+ data["sid"] = sess.ID
+
+ prjID, err := s.mfolders.ResolveID(args.ProjectID)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ if err = s.mfolders.RegisterEventChange(prjID, &cbFunc, &data); 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 xsapiv1.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/xdsserver/apiv1-exec.go b/lib/xdsserver/apiv1-exec.go
new file mode 100644
index 0000000..ce5e7b7
--- /dev/null
+++ b/lib/xdsserver/apiv1-exec.go
@@ -0,0 +1,342 @@
+package xdsserver
+
+import (
+ "fmt"
+ "net/http"
+ "os"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+ "github.com/iotbzh/xds-common/golib/eows"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+ "github.com/kr/pty"
+)
+
+var execCommandID = 1
+
+// ExecCmd executes remotely a command
+func (s *APIService) execCmd(c *gin.Context) {
+ var gdbPty, gdbTty *os.File
+ var err error
+ var args xsapiv1.ExecArgs
+ if c.BindJSON(&args) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ // TODO: add permission ?
+
+ // Retrieve session info
+ sess := s.sessions.Get(c)
+ if sess == nil {
+ common.APIError(c, "Unknown sessions")
+ return
+ }
+ sop := sess.IOSocket
+ if sop == nil {
+ common.APIError(c, "Websocket not established")
+ return
+ }
+
+ // Allow to pass id in url (/exec/:id) or as JSON argument
+ idArg := c.Param("id")
+ if idArg == "" {
+ idArg = args.ID
+ }
+ if idArg == "" {
+ common.APIError(c, "Invalid id")
+ return
+ }
+ id, err := s.mfolders.ResolveID(idArg)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ f := s.mfolders.Get(id)
+ if f == nil {
+ common.APIError(c, "Unknown id")
+ return
+ }
+ fld := *f
+ prj := fld.GetConfig()
+
+ // Build command line
+ cmd := []string{}
+ // Setup env var regarding Sdk ID (used for example to setup cross toolchain)
+ if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
+ cmd = append(cmd, envCmd...)
+ cmd = append(cmd, "&&")
+ } else {
+ // It's an error if no envcmd found while a sdkid has been provided
+ if args.SdkID != "" {
+ common.APIError(c, "Unknown sdkid")
+ return
+ }
+ }
+
+ cmd = append(cmd, "cd", "\""+fld.GetFullPath(args.RPath)+"\"")
+ // FIXME - add 'exec' prevents to use syntax:
+ // xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
+ // but exec is mandatory to allow to pass correctly signals
+ // As workaround, exec is set for now on client side (eg. in xds-gdb)
+ //cmd = append(cmd, "&&", "exec", args.Cmd)
+ cmd = append(cmd, "&&", args.Cmd)
+
+ // Process command arguments
+ cmdArgs := make([]string, len(args.Args)+1)
+
+ // Copy and Translate path from client to server
+ for _, aa := range args.Args {
+ if strings.Contains(aa, prj.ClientPath) {
+ cmdArgs = append(cmdArgs, fld.ConvPathCli2Svr(aa))
+ } else {
+ cmdArgs = append(cmdArgs, aa)
+ }
+ }
+
+ // Allocate pts if tty if used
+ if args.TTY {
+ gdbPty, gdbTty, err = pty.Open()
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ s.Log.Debugf("Client command tty: %v %v\n", gdbTty.Name(), gdbTty.Name())
+ cmdArgs = append(cmdArgs, "--tty="+gdbTty.Name())
+ }
+
+ // Unique ID for each commands
+ if args.CmdID == "" {
+ args.CmdID = s.Config.ServerUID[:18] + "_" + strconv.Itoa(execCommandID)
+ execCommandID++
+ }
+
+ // Create new execution over WS context
+ execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, args.CmdID)
+ execWS.Log = s.Log
+
+ // Append client project dir to environment
+ execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
+
+ // Set command execution timeout
+ if args.CmdTimeout == 0 {
+ // 0 : default timeout
+ // TODO get default timeout from config.json file
+ execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
+ } else {
+ execWS.CmdExecTimeout = args.CmdTimeout
+ }
+
+ // Define callback for input (stdin)
+ execWS.InputEvent = xsapiv1.ExecInEvent
+ execWS.InputCB = func(e *eows.ExecOverWS, stdin string) (string, error) {
+ s.Log.Debugf("STDIN <<%v>>", strings.Replace(stdin, "\n", "\\n", -1))
+
+ // Handle Ctrl-D
+ if len(stdin) == 1 && stdin == "\x04" {
+ // Close stdin
+ errMsg := fmt.Errorf("close stdin: %v", stdin)
+ return "", errMsg
+ }
+
+ // Set correct path
+ data := e.UserData
+ prjID := (*data)["ID"].(string)
+ f := s.mfolders.Get(prjID)
+ if f == nil {
+ s.Log.Errorf("InputCB: Cannot get folder ID %s", prjID)
+ } else {
+ // Translate paths from client to server
+ stdin = (*f).ConvPathCli2Svr(stdin)
+ }
+
+ return stdin, nil
+ }
+
+ // Define callback for output (stdout+stderr)
+ execWS.OutputCB = func(e *eows.ExecOverWS, stdout, stderr string) {
+ // IO socket can be nil when disconnected
+ so := s.sessions.IOSocketGet(e.Sid)
+ if so == nil {
+ s.Log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", xsapiv1.ExecOutEvent, e.Sid, e.CmdID)
+ return
+ }
+
+ // Retrieve project ID and RootPath
+ data := e.UserData
+ prjID := (*data)["ID"].(string)
+ gdbServerTTY := (*data)["gdbServerTTY"].(string)
+
+ f := s.mfolders.Get(prjID)
+ if f == nil {
+ s.Log.Errorf("OutputCB: Cannot get folder ID %s", prjID)
+ } else {
+ // Translate paths from server to client
+ stdout = (*f).ConvPathSvr2Cli(stdout)
+ stderr = (*f).ConvPathSvr2Cli(stderr)
+ }
+
+ s.Log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", xsapiv1.ExecOutEvent, e.Sid[4:], e.CmdID, prjID)
+ if stdout != "" {
+ s.Log.Debugf("STDOUT <<%v>>", strings.Replace(stdout, "\n", "\\n", -1))
+ }
+ if stderr != "" {
+ s.Log.Debugf("STDERR <<%v>>", strings.Replace(stderr, "\n", "\\n", -1))
+ }
+
+ // FIXME replace by .BroadcastTo a room
+ err := (*so).Emit(xsapiv1.ExecOutEvent, xsapiv1.ExecOutMsg{
+ CmdID: e.CmdID,
+ Timestamp: time.Now().String(),
+ Stdout: stdout,
+ Stderr: stderr,
+ })
+ if err != nil {
+ s.Log.Errorf("WS Emit : %v", err)
+ }
+
+ // XXX - Workaround due to gdbserver bug that doesn't redirect
+ // inferior output (https://bugs.eclipse.org/bugs/show_bug.cgi?id=437532#c13)
+ if gdbServerTTY == "workaround" && len(stdout) > 1 && stdout[0] == '&' {
+
+ // Extract and cleanup string like &"bla bla\n"
+ re := regexp.MustCompile("&\"(.*)\"")
+ rer := re.FindAllStringSubmatch(stdout, -1)
+ out := ""
+ if rer != nil && len(rer) > 0 {
+ for _, o := range rer {
+ if len(o) >= 1 {
+ out = strings.Replace(o[1], "\\n", "\n", -1)
+ out = strings.Replace(out, "\\r", "\r", -1)
+ out = strings.Replace(out, "\\t", "\t", -1)
+
+ s.Log.Debugf("STDOUT INFERIOR: <<%v>>", out)
+ err := (*so).Emit(xsapiv1.ExecInferiorOutEvent, xsapiv1.ExecOutMsg{
+ CmdID: e.CmdID,
+ Timestamp: time.Now().String(),
+ Stdout: out,
+ Stderr: "",
+ })
+ if err != nil {
+ s.Log.Errorf("WS Emit : %v", err)
+ }
+ }
+ }
+ } else {
+ s.Log.Errorf("INFERIOR out parsing error: stdout=<%v>", stdout)
+ }
+ }
+ }
+
+ // Define callback for output
+ execWS.ExitCB = func(e *eows.ExecOverWS, code int, err error) {
+ s.Log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", e.CmdID, code, err)
+
+ // Close client tty
+ defer func() {
+ if gdbPty != nil {
+ gdbPty.Close()
+ }
+ if gdbTty != nil {
+ gdbTty.Close()
+ }
+ }()
+
+ // IO socket can be nil when disconnected
+ so := s.sessions.IOSocketGet(e.Sid)
+ if so == nil {
+ s.Log.Infof("%s not emitted - WS closed (id:%s)", xsapiv1.ExecExitEvent, e.CmdID)
+ return
+ }
+
+ // Retrieve project ID and RootPath
+ data := e.UserData
+ prjID := (*data)["ID"].(string)
+ exitImm := (*data)["ExitImmediate"].(bool)
+
+ // XXX - workaround to be sure that Syncthing detected all changes
+ if err := s.mfolders.ForceSync(prjID); err != nil {
+ s.Log.Errorf("Error while syncing folder %s: %v", prjID, err)
+ }
+ if !exitImm {
+ // Wait end of file sync
+ // FIXME pass as argument
+ tmo := 60
+ for t := tmo; t > 0; t-- {
+ s.Log.Debugf("Wait file in-sync for %s (%d/%d)", prjID, t, tmo)
+ if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
+ if err != nil {
+ s.Log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
+ }
+ break
+ }
+ time.Sleep(time.Second)
+ }
+ s.Log.Debugf("OK file are synchronized.")
+ }
+
+ // FIXME replace by .BroadcastTo a room
+ errSoEmit := (*so).Emit(xsapiv1.ExecExitEvent, xsapiv1.ExecExitMsg{
+ CmdID: e.CmdID,
+ Timestamp: time.Now().String(),
+ Code: code,
+ Error: err,
+ })
+ if errSoEmit != nil {
+ s.Log.Errorf("WS Emit : %v", errSoEmit)
+ }
+ }
+
+ // User data (used within callbacks)
+ data := make(map[string]interface{})
+ data["ID"] = prj.ID
+ data["ExitImmediate"] = args.ExitImmediate
+ if args.TTY && args.TTYGdbserverFix {
+ data["gdbServerTTY"] = "workaround"
+ } else {
+ data["gdbServerTTY"] = ""
+ }
+ execWS.UserData = &data
+
+ // Start command execution
+ s.Log.Infof("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
+
+ err = execWS.Start()
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK, xsapiv1.ExecResult{Status: "OK", CmdID: execWS.CmdID})
+}
+
+// ExecCmd executes remotely a command
+func (s *APIService) execSignalCmd(c *gin.Context) {
+ var args xsapiv1.ExecSignalArgs
+
+ if c.BindJSON(&args) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ s.Log.Debugf("Signal %s for command ID %s", args.Signal, args.CmdID)
+
+ e := eows.GetEows(args.CmdID)
+ if e == nil {
+ common.APIError(c, "unknown cmdID")
+ return
+ }
+
+ err := e.Signal(args.Signal)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK, xsapiv1.ExecSigResult{Status: "OK", CmdID: args.CmdID})
+}
diff --git a/lib/xdsserver/apiv1-folders.go b/lib/xdsserver/apiv1-folders.go
new file mode 100644
index 0000000..fe11e52
--- /dev/null
+++ b/lib/xdsserver/apiv1-folders.go
@@ -0,0 +1,131 @@
+package xdsserver
+
+import (
+ "net/http"
+ "os"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+)
+
+// getFolders returns all folders configuration
+func (s *APIService) getFolders(c *gin.Context) {
+ c.JSON(http.StatusOK, s.mfolders.GetConfigArr())
+}
+
+// getFolder returns a specific folder configuration
+func (s *APIService) getFolder(c *gin.Context) {
+ id, err := s.mfolders.ResolveID(c.Param("id"))
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ f := s.mfolders.Get(id)
+ if f == nil {
+ common.APIError(c, "Invalid id")
+ return
+ }
+
+ c.JSON(http.StatusOK, (*f).GetConfig())
+}
+
+// addFolder adds a new folder to server config
+func (s *APIService) addFolder(c *gin.Context) {
+ var cfgArg xsapiv1.FolderConfig
+ if c.BindJSON(&cfgArg) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ s.Log.Debugln("Add folder config: ", cfgArg)
+
+ newFld, err := s.mfolders.Add(cfgArg)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ // Create xds-project.conf file
+ // FIXME: move to folders.createUpdate func (but gin context needed)
+ fld := s.mfolders.Get(newFld.ID)
+ prjConfFile := (*fld).GetFullPath("xds-project.conf")
+ if !common.Exists(prjConfFile) {
+ fd, err := os.OpenFile(prjConfFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ fd.WriteString("# XDS project settings\n")
+ fd.WriteString("export XDS_SERVER_URL=" + c.Request.Host + "\n")
+ fd.WriteString("export XDS_PROJECT_ID=" + newFld.ID + "\n")
+ if newFld.DefaultSdk == "" {
+ sdks := s.sdks.GetAll()
+ newFld.DefaultSdk = sdks[0].ID
+ }
+ fd.WriteString("export XDS_SDK_ID=" + newFld.DefaultSdk + "\n")
+ fd.Close()
+ }
+
+ c.JSON(http.StatusOK, newFld)
+}
+
+// syncFolder force synchronization of folder files
+func (s *APIService) syncFolder(c *gin.Context) {
+ id, err := s.mfolders.ResolveID(c.Param("id"))
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ 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, err := s.mfolders.ResolveID(c.Param("id"))
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ s.Log.Debugln("Delete folder id ", id)
+
+ delEntry, err := s.mfolders.Delete(id)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ c.JSON(http.StatusOK, delEntry)
+}
+
+// updateFolder update some field of a folder
+func (s *APIService) updateFolder(c *gin.Context) {
+ id, err := s.mfolders.ResolveID(c.Param("id"))
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ s.Log.Debugln("Update folder id ", id)
+
+ var cfgArg xsapiv1.FolderConfig
+ if c.BindJSON(&cfgArg) != nil {
+ common.APIError(c, "Invalid arguments")
+ return
+ }
+
+ upFld, err := s.mfolders.Update(id, cfgArg)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ c.JSON(http.StatusOK, upFld)
+}
diff --git a/lib/xdsserver/apiv1-make.go b/lib/xdsserver/apiv1-make.go
new file mode 100644
index 0000000..bcb4d95
--- /dev/null
+++ b/lib/xdsserver/apiv1-make.go
@@ -0,0 +1,214 @@
+package xdsserver
+
+import (
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+)
+
+/* TODO: Deprecated - should be removed
+// MakeArgs is the parameters (json format) of /make command
+type MakeArgs struct {
+ ID string `json:"id"`
+ SdkID string `json:"sdkID"` // sdk ID to use for setting env
+ CmdID string `json:"cmdID"` // command unique ID
+ Args []string `json:"args"` // args to pass to make command
+ Env []string `json:"env"`
+ RPath string `json:"rpath"` // relative path into project
+ ExitImmediate bool `json:"exitImmediate"` // when true, exit event sent immediately when command exited (IOW, don't wait file synchronization)
+ CmdTimeout int `json:"timeout"` // command completion timeout in Second
+}
+
+// MakeOutMsg Message send on each output (stdout+stderr) of make command
+type MakeOutMsg struct {
+ CmdID string `json:"cmdID"`
+ Timestamp string `json:"timestamp"`
+ Stdout string `json:"stdout"`
+ Stderr string `json:"stderr"`
+}
+
+// MakeExitMsg Message send on make command exit
+type MakeExitMsg struct {
+ CmdID string `json:"cmdID"`
+ Timestamp string `json:"timestamp"`
+ Code int `json:"code"`
+ Error error `json:"error"`
+}
+
+// MakeOutEvent Event send in WS when characters are received on stdout/stderr
+const MakeOutEvent = "make:output"
+
+// MakeExitEvent Event send in WS when command exited
+const MakeExitEvent = "make:exit"
+
+var makeCommandID = 1
+*/
+
+func (s *APIService) buildMake(c *gin.Context) {
+ common.APIError(c, "/make route is not longer supported, use /exec instead")
+
+ /*
+ var args MakeArgs
+
+ 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
+ }
+ sop := sess.IOSocket
+ if sop == nil {
+ common.APIError(c, "Websocket not established")
+ return
+ }
+
+ // Allow to pass id in url (/make/:id) or as JSON argument
+ idArg := c.Param("id")
+ if idArg == "" {
+ idArg = args.ID
+ }
+ if idArg == "" {
+ common.APIError(c, "Invalid id")
+ return
+ }
+ id, err := s.mfolders.ResolveID(idArg)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ pf := s.mfolders.Get(id)
+ if pf == nil {
+ common.APIError(c, "Unknown id")
+ return
+ }
+ folder := *pf
+ prj := folder.GetConfig()
+
+ execTmo := args.CmdTimeout
+ if execTmo == 0 {
+ // TODO get default timeout from config.json file
+ execTmo = 24 * 60 * 60 // 1 day
+ }
+
+ // TODO merge all code below with exec.go
+
+ // Define callback for output
+ var oCB common.EmitOutputCB
+ oCB = func(sid string, cmdID string, stdout, stderr string, data *map[string]interface{}) {
+ // IO socket can be nil when disconnected
+ so := s.sessions.IOSocketGet(sid)
+ if so == nil {
+ s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%s", MakeOutEvent, sid, cmdID)
+ return
+ }
+
+ // Retrieve project ID and RootPath
+ prjID := (*data)["ID"].(string)
+ prjRootPath := (*data)["RootPath"].(string)
+
+ // Cleanup any references to internal rootpath in stdout & stderr
+ stdout = strings.Replace(stdout, prjRootPath, "", -1)
+ stderr = strings.Replace(stderr, prjRootPath, "", -1)
+
+ s.log.Debugf("%s emitted - WS sid %s - id:%d - prjID:%s", MakeOutEvent, sid, id, prjID)
+
+ // FIXME replace by .BroadcastTo a room
+ err := (*so).Emit(MakeOutEvent, MakeOutMsg{
+ CmdID: cmdID,
+ Timestamp: time.Now().String(),
+ Stdout: stdout,
+ Stderr: stderr,
+ })
+ if err != nil {
+ s.log.Errorf("WS Emit : %v", err)
+ }
+ }
+
+ // Define callback for output
+ eCB := func(sid string, cmdID string, code int, err error, data *map[string]interface{}) {
+ s.log.Debugf("Command [Cmd ID %s] exited: code %d, error: %v", cmdID, code, err)
+
+ // IO socket can be nil when disconnected
+ so := s.sessions.IOSocketGet(sid)
+ if so == nil {
+ s.log.Infof("%s not emitted - WS closed (id:%s", MakeExitEvent, cmdID)
+ return
+ }
+
+ // Retrieve project ID and RootPath
+ prjID := (*data)["ID"].(string)
+ exitImm := (*data)["ExitImmediate"].(bool)
+
+ // XXX - workaround to be sure that Syncthing detected all changes
+ if err := s.mfolders.ForceSync(prjID); err != nil {
+ s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
+ }
+ if !exitImm {
+ // Wait end of file sync
+ // FIXME pass as argument
+ tmo := 60
+ for t := tmo; t > 0; t-- {
+ s.log.Debugf("Wait file insync for %s (%d/%d)", prjID, t, tmo)
+ if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
+ if err != nil {
+ s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
+ }
+ break
+ }
+ time.Sleep(time.Second)
+ }
+ }
+
+ // FIXME replace by .BroadcastTo a room
+ e := (*so).Emit(MakeExitEvent, MakeExitMsg{
+ CmdID: id,
+ Timestamp: time.Now().String(),
+ Code: code,
+ Error: err,
+ })
+ if e != nil {
+ s.log.Errorf("WS Emit : %v", e)
+ }
+ }
+
+ // Unique ID for each commands
+ if args.CmdID == "" {
+ args.CmdID = s.cfg.ServerUID[:18] + "_" + strconv.Itoa(makeCommandID)
+ makeCommandID++
+ }
+ cmd := []string{}
+
+ // Retrieve env command regarding Sdk ID
+ if envCmd := s.sdks.GetEnvCmd(args.SdkID, prj.DefaultSdk); len(envCmd) > 0 {
+ cmd = append(cmd, envCmd...)
+ cmd = append(cmd, "&&")
+ }
+
+ cmd = append(cmd, "cd", folder.GetFullPath(args.RPath), "&&", "make")
+ if len(args.Args) > 0 {
+ cmd = append(cmd, args.Args...)
+ }
+
+ s.log.Debugf("Execute [Cmd ID %d]: %v", args.CmdID, cmd)
+
+ data := make(map[string]interface{})
+ data["ID"] = prj.ID
+ data["RootPath"] = prj.RootPath
+ data["ExitImmediate"] = args.ExitImmediate
+
+ err = common.ExecPipeWs(cmd, args.Env, sop, sess.ID, args.CmdID, execTmo, s.log, oCB, eCB, &data)
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+
+ c.JSON(http.StatusOK,
+ gin.H{
+ "status": "OK",
+ "cmdID": args.CmdID,
+ })
+ */
+}
diff --git a/lib/xdsserver/apiv1-sdks.go b/lib/xdsserver/apiv1-sdks.go
new file mode 100644
index 0000000..be9fcf7
--- /dev/null
+++ b/lib/xdsserver/apiv1-sdks.go
@@ -0,0 +1,29 @@
+package xdsserver
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ common "github.com/iotbzh/xds-common/golib"
+)
+
+// getSdks returns all SDKs configuration
+func (s *APIService) getSdks(c *gin.Context) {
+ c.JSON(http.StatusOK, s.sdks.GetAll())
+}
+
+// getSdk returns a specific Sdk configuration
+func (s *APIService) getSdk(c *gin.Context) {
+ id, err := s.sdks.ResolveID(c.Param("id"))
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
+ }
+ sdk := s.sdks.Get(id)
+ if sdk.Profile == "" {
+ common.APIError(c, "Invalid id")
+ return
+ }
+
+ c.JSON(http.StatusOK, sdk)
+}
diff --git a/lib/xdsserver/apiv1-version.go b/lib/xdsserver/apiv1-version.go
new file mode 100644
index 0000000..2c2547c
--- /dev/null
+++ b/lib/xdsserver/apiv1-version.go
@@ -0,0 +1,20 @@
+package xdsserver
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+)
+
+// getInfo : return various information about server
+func (s *APIService) getVersion(c *gin.Context) {
+ response := xsapiv1.Version{
+ ID: s.Config.ServerUID,
+ Version: s.Config.Version,
+ APIVersion: s.Config.APIVersion,
+ VersionGitTag: s.Config.VersionGitTag,
+ }
+
+ c.JSON(http.StatusOK, response)
+}
diff --git a/lib/xdsserver/apiv1.go b/lib/xdsserver/apiv1.go
new file mode 100644
index 0000000..1f6df9e
--- /dev/null
+++ b/lib/xdsserver/apiv1.go
@@ -0,0 +1,47 @@
+package xdsserver
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// APIService .
+type APIService struct {
+ *Context
+ apiRouter *gin.RouterGroup
+}
+
+// NewAPIV1 creates a new instance of API service
+func NewAPIV1(ctx *Context) *APIService {
+ s := &APIService{
+ Context: ctx,
+ apiRouter: ctx.WWWServer.router.Group("/api/v1"),
+ }
+
+ s.apiRouter.GET("/version", s.getVersion)
+
+ s.apiRouter.GET("/config", s.getConfig)
+ s.apiRouter.POST("/config", s.setConfig)
+
+ s.apiRouter.GET("/folders", s.getFolders)
+ s.apiRouter.GET("/folders/:id", s.getFolder)
+ s.apiRouter.PUT("/folders/:id", s.updateFolder)
+ s.apiRouter.POST("/folders", s.addFolder)
+ s.apiRouter.POST("/folders/sync/:id", s.syncFolder)
+ s.apiRouter.DELETE("/folders/:id", s.delFolder)
+
+ s.apiRouter.GET("/sdks", s.getSdks)
+ s.apiRouter.GET("/sdks/:id", s.getSdk)
+
+ s.apiRouter.POST("/make", s.buildMake)
+ s.apiRouter.POST("/make/:id", s.buildMake)
+
+ s.apiRouter.POST("/exec", s.execCmd)
+ 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/xdsserver/folder-interface.go b/lib/xdsserver/folder-interface.go
new file mode 100644
index 0000000..c2b2ada
--- /dev/null
+++ b/lib/xdsserver/folder-interface.go
@@ -0,0 +1,22 @@
+package xdsserver
+
+import "github.com/iotbzh/xds-server/lib/xsapiv1"
+
+type FolderEventCBData map[string]interface{}
+type FolderEventCB func(cfg *xsapiv1.FolderConfig, data *FolderEventCBData)
+
+// IFOLDER Folder interface
+type IFOLDER interface {
+ NewUID(suffix string) string // Get a new folder UUID
+ Add(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) // Add a new folder
+ GetConfig() xsapiv1.FolderConfig // Get folder public configuration
+ GetFullPath(dir string) string // Get folder full path
+ ConvPathCli2Svr(s string) string // Convert path from Client to Server
+ ConvPathSvr2Cli(s string) string // Convert path from Server to Client
+ Remove() error // Remove a folder
+ Update(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) // Update a new folder
+ RegisterEventChange(cb *FolderEventCB, data *FolderEventCBData) 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
+}
diff --git a/lib/xdsserver/folder-pathmap.go b/lib/xdsserver/folder-pathmap.go
new file mode 100644
index 0000000..c5318de
--- /dev/null
+++ b/lib/xdsserver/folder-pathmap.go
@@ -0,0 +1,175 @@
+package xdsserver
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ common "github.com/iotbzh/xds-common/golib"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+ uuid "github.com/satori/go.uuid"
+)
+
+// IFOLDER interface implementation for native/path mapping folders
+
+// PathMap .
+type PathMap struct {
+ *Context
+ config xsapiv1.FolderConfig
+}
+
+// NewFolderPathMap Create a new instance of PathMap
+func NewFolderPathMap(ctx *Context) *PathMap {
+ f := PathMap{
+ Context: ctx,
+ config: xsapiv1.FolderConfig{
+ Status: xsapiv1.StatusDisable,
+ },
+ }
+ return &f
+}
+
+// NewUID Get a UUID
+func (f *PathMap) NewUID(suffix string) string {
+ uuid := uuid.NewV1().String()
+ if len(suffix) > 0 {
+ uuid += "_" + suffix
+ }
+ return uuid
+}
+
+// Add a new folder
+func (f *PathMap) Add(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
+ if cfg.DataPathMap.ServerPath == "" {
+ return nil, fmt.Errorf("ServerPath must be set")
+ }
+
+ // Use shareRootDir if ServerPath is a relative path
+ dir := cfg.DataPathMap.ServerPath
+ if !filepath.IsAbs(dir) {
+ dir = filepath.Join(f.Config.FileConf.ShareRootDir, dir)
+ }
+
+ // Sanity check
+ if !common.Exists(dir) {
+ // try to create if not existing
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return nil, fmt.Errorf("Cannot create ServerPath directory: %s", dir)
+ }
+ }
+ if !common.Exists(dir) {
+ return nil, fmt.Errorf("ServerPath directory is not accessible: %s", dir)
+ }
+
+ f.config = cfg
+ f.config.RootPath = dir
+ f.config.DataPathMap.ServerPath = dir
+ f.config.IsInSync = true
+
+ // Verify file created by XDS agent when needed
+ if cfg.DataPathMap.CheckFile != "" {
+ errMsg := "ServerPath sanity check error (%d): %v"
+ ckFile := f.ConvPathCli2Svr(cfg.DataPathMap.CheckFile)
+ if !common.Exists(ckFile) {
+ return nil, fmt.Errorf(errMsg, 1, "file not present")
+ }
+ if cfg.DataPathMap.CheckContent != "" {
+ fd, err := os.OpenFile(ckFile, os.O_APPEND|os.O_RDWR, 0600)
+ if err != nil {
+ return nil, fmt.Errorf(errMsg, 2, err)
+ }
+ defer fd.Close()
+
+ // Check specific message written by agent
+ content, err := ioutil.ReadAll(fd)
+ if err != nil {
+ return nil, fmt.Errorf(errMsg, 3, err)
+ }
+ if string(content) != cfg.DataPathMap.CheckContent {
+ return nil, fmt.Errorf(errMsg, 4, "file content differ")
+ }
+
+ // Write a specific message that will be check back on agent side
+ msg := "Pathmap checked message written by xds-server ID: " + f.Config.ServerUID + "\n"
+ if n, err := fd.WriteString(msg); n != len(msg) || err != nil {
+ return nil, fmt.Errorf(errMsg, 5, err)
+ }
+ }
+ }
+
+ f.config.Status = xsapiv1.StatusEnable
+
+ return &f.config, nil
+}
+
+// GetConfig Get public part of folder config
+func (f *PathMap) GetConfig() xsapiv1.FolderConfig {
+ return f.config
+}
+
+// GetFullPath returns the full path of a directory (from server POV)
+func (f *PathMap) GetFullPath(dir string) string {
+ if &dir == nil {
+ return f.config.DataPathMap.ServerPath
+ }
+ return filepath.Join(f.config.DataPathMap.ServerPath, dir)
+}
+
+// ConvPathCli2Svr Convert path from Client to Server
+func (f *PathMap) ConvPathCli2Svr(s string) string {
+ if f.config.ClientPath != "" && f.config.DataPathMap.ServerPath != "" {
+ return strings.Replace(s,
+ f.config.ClientPath,
+ f.config.DataPathMap.ServerPath,
+ -1)
+ }
+ return s
+}
+
+// ConvPathSvr2Cli Convert path from Server to Client
+func (f *PathMap) ConvPathSvr2Cli(s string) string {
+ if f.config.ClientPath != "" && f.config.DataPathMap.ServerPath != "" {
+ return strings.Replace(s,
+ f.config.DataPathMap.ServerPath,
+ f.config.ClientPath,
+ -1)
+ }
+ return s
+}
+
+// Remove a folder
+func (f *PathMap) Remove() error {
+ // nothing to do
+ return nil
+}
+
+// Update update some fields of a folder
+func (f *PathMap) Update(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
+ if f.config.ID != cfg.ID {
+ return nil, fmt.Errorf("Invalid id")
+ }
+ f.config = cfg
+ return &f.config, nil
+}
+
+// RegisterEventChange requests registration for folder change event
+func (f *PathMap) RegisterEventChange(cb *FolderEventCB, data *FolderEventCBData) 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
+}
+
+// IsInSync Check if folder files are in-sync
+func (f *PathMap) IsInSync() (bool, error) {
+ return true, nil
+}
diff --git a/lib/xdsserver/folder-st-disable.go b/lib/xdsserver/folder-st-disable.go
new file mode 100644
index 0000000..64b1efc
--- /dev/null
+++ b/lib/xdsserver/folder-st-disable.go
@@ -0,0 +1,91 @@
+package xdsserver
+
+import (
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+ uuid "github.com/satori/go.uuid"
+)
+
+// IFOLDER interface implementation for disabled Syncthing folders
+// It's a "fallback" interface used to keep syncthing folders config even
+// when syncthing is not running.
+
+// STFolderDisable .
+type STFolderDisable struct {
+ *Context
+ config xsapiv1.FolderConfig
+}
+
+// NewFolderSTDisable Create a new instance of STFolderDisable
+func NewFolderSTDisable(ctx *Context) *STFolderDisable {
+ f := STFolderDisable{
+ Context: ctx,
+ }
+ return &f
+}
+
+// NewUID Get a UUID
+func (f *STFolderDisable) NewUID(suffix string) string {
+ uuid := uuid.NewV1().String()
+ if len(suffix) > 0 {
+ uuid += "_" + suffix
+ }
+ return uuid
+}
+
+// Add a new folder
+func (f *STFolderDisable) Add(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
+ f.config = cfg
+ f.config.Status = xsapiv1.StatusDisable
+ f.config.IsInSync = false
+ return &f.config, nil
+}
+
+// GetConfig Get public part of folder config
+func (f *STFolderDisable) GetConfig() xsapiv1.FolderConfig {
+ return f.config
+}
+
+// GetFullPath returns the full path of a directory (from server POV)
+func (f *STFolderDisable) GetFullPath(dir string) string {
+ return ""
+}
+
+// ConvPathCli2Svr Convert path from Client to Server
+func (f *STFolderDisable) ConvPathCli2Svr(s string) string {
+ return ""
+}
+
+// ConvPathSvr2Cli Convert path from Server to Client
+func (f *STFolderDisable) ConvPathSvr2Cli(s string) string {
+ return ""
+}
+
+// Remove a folder
+func (f *STFolderDisable) Remove() error {
+ return nil
+}
+
+// Update update some fields of a folder
+func (f *STFolderDisable) Update(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
+ return nil, nil
+}
+
+// RegisterEventChange requests registration for folder change event
+func (f *STFolderDisable) RegisterEventChange(cb *FolderEventCB, data *FolderEventCBData) error {
+ return nil
+}
+
+// UnRegisterEventChange remove registered callback
+func (f *STFolderDisable) UnRegisterEventChange() error {
+ return nil
+}
+
+// Sync Force folder files synchronization
+func (f *STFolderDisable) Sync() error {
+ return nil
+}
+
+// IsInSync Check if folder files are in-sync
+func (f *STFolderDisable) IsInSync() (bool, error) {
+ return false, nil
+}
diff --git a/lib/xdsserver/folder-st.go b/lib/xdsserver/folder-st.go
new file mode 100644
index 0000000..04bbf76
--- /dev/null
+++ b/lib/xdsserver/folder-st.go
@@ -0,0 +1,213 @@
+package xdsserver
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+ st "github.com/iotbzh/xds-server/lib/syncthing"
+ uuid "github.com/satori/go.uuid"
+ "github.com/syncthing/syncthing/lib/config"
+)
+
+// IFOLDER interface implementation for syncthing
+
+// STFolder .
+type STFolder struct {
+ *Context
+ st *st.SyncThing
+ fConfig xsapiv1.FolderConfig
+ stfConfig config.FolderConfiguration
+ eventIDs []int
+ eventChangeCB *FolderEventCB
+ eventChangeCBData *FolderEventCBData
+}
+
+// NewFolderST Create a new instance of STFolder
+func NewFolderST(ctx *Context, sthg *st.SyncThing) *STFolder {
+ return &STFolder{
+ Context: ctx,
+ st: sthg,
+ }
+}
+
+// NewUID Get a UUID
+func (f *STFolder) NewUID(suffix string) string {
+ i := len(f.st.MyID)
+ if i > 15 {
+ i = 15
+ }
+ uuid := uuid.NewV1().String()[:14] + f.st.MyID[:i]
+ if len(suffix) > 0 {
+ uuid += "_" + suffix
+ }
+ return uuid
+}
+
+// Add a new folder
+func (f *STFolder) Add(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
+
+ // Sanity check
+ if cfg.DataCloudSync.SyncThingID == "" {
+ return nil, fmt.Errorf("device id not set (SyncThingID field)")
+ }
+
+ // rootPath should not be empty
+ if cfg.RootPath == "" {
+ cfg.RootPath = f.Config.FileConf.ShareRootDir
+ }
+
+ f.fConfig = cfg
+
+ // Update Syncthing folder
+ // (except if status is ErrorConfig)
+ // TODO: add cache to avoid multiple requests on startup
+ if f.fConfig.Status != xsapiv1.StatusErrorConfig {
+ id, err := f.st.FolderChange(f.fConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ f.stfConfig, err = f.st.FolderConfigGet(id)
+ if err != nil {
+ f.fConfig.Status = xsapiv1.StatusErrorConfig
+ return nil, err
+ }
+
+ // Register to events to update folder status
+ for _, evName := range []string{st.EventStateChanged, st.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 = xsapiv1.StatusEnable
+ }
+
+ return &f.fConfig, nil
+}
+
+// GetConfig Get public part of folder config
+func (f *STFolder) GetConfig() xsapiv1.FolderConfig {
+ return f.fConfig
+}
+
+// GetFullPath returns the full path of a directory (from server POV)
+func (f *STFolder) GetFullPath(dir string) string {
+ if &dir == nil {
+ dir = ""
+ }
+ if filepath.IsAbs(dir) {
+ return filepath.Join(f.fConfig.RootPath, dir)
+ }
+ return filepath.Join(f.fConfig.RootPath, f.fConfig.ClientPath, dir)
+}
+
+// ConvPathCli2Svr Convert path from Client to Server
+func (f *STFolder) ConvPathCli2Svr(s string) string {
+ if f.fConfig.ClientPath != "" && f.fConfig.RootPath != "" {
+ return strings.Replace(s,
+ f.fConfig.ClientPath,
+ f.fConfig.RootPath+"/"+f.fConfig.ClientPath,
+ -1)
+ }
+ return s
+}
+
+// ConvPathSvr2Cli Convert path from Server to Client
+func (f *STFolder) ConvPathSvr2Cli(s string) string {
+ if f.fConfig.ClientPath != "" && f.fConfig.RootPath != "" {
+ return strings.Replace(s,
+ f.fConfig.RootPath+"/"+f.fConfig.ClientPath,
+ f.fConfig.ClientPath,
+ -1)
+ }
+ return s
+}
+
+// Remove a folder
+func (f *STFolder) Remove() error {
+ err := f.st.FolderDelete(f.stfConfig.ID)
+
+ // Delete folder on server side
+ err2 := os.RemoveAll(f.GetFullPath(""))
+
+ if err != nil {
+ return err
+ }
+ return err2
+}
+
+// Update update some fields of a folder
+func (f *STFolder) Update(cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
+ if f.fConfig.ID != cfg.ID {
+ return nil, fmt.Errorf("Invalid id")
+ }
+ f.fConfig = cfg
+ return &f.fConfig, nil
+}
+
+// RegisterEventChange requests registration for folder event change
+func (f *STFolder) RegisterEventChange(cb *FolderEventCB, data *FolderEventCBData) 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, "")
+}
+
+// IsInSync Check if folder files are in-sync
+func (f *STFolder) IsInSync() (bool, error) {
+ 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 st.Event, data *st.EventsCBData) {
+ prevSync := f.fConfig.IsInSync
+ prevStatus := f.fConfig.Status
+
+ switch ev.Type {
+
+ case st.EventStateChanged:
+ to := ev.Data["to"]
+ switch to {
+ case "scanning", "syncing":
+ f.fConfig.Status = xsapiv1.StatusSyncing
+ case "idle":
+ f.fConfig.Status = xsapiv1.StatusEnable
+ }
+ f.fConfig.IsInSync = (to == "idle")
+
+ case st.EventFolderPaused:
+ if f.fConfig.Status == xsapiv1.StatusEnable {
+ f.fConfig.Status = xsapiv1.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/xdsserver/folders.go b/lib/xdsserver/folders.go
new file mode 100644
index 0000000..e36f84c
--- /dev/null
+++ b/lib/xdsserver/folders.go
@@ -0,0 +1,458 @@
+package xdsserver
+
+import (
+ "encoding/xml"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/franciscocpg/reflectme"
+ common "github.com/iotbzh/xds-common/golib"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+ "github.com/iotbzh/xds-server/lib/xdsconfig"
+ "github.com/syncthing/syncthing/lib/sync"
+)
+
+// Folders Represent a an XDS folders
+type Folders struct {
+ *Context
+ fileOnDisk string
+ folders map[string]*IFOLDER
+ registerCB []RegisteredCB
+}
+
+// RegisteredCB Hold registered callbacks
+type RegisteredCB struct {
+ cb *FolderEventCB
+ data *FolderEventCBData
+}
+
+// Mutex to make add/delete atomic
+var fcMutex = sync.NewMutex()
+var ffMutex = sync.NewMutex()
+
+// FoldersNew Create a new instance of Model Folders
+func FoldersNew(ctx *Context) *Folders {
+ file, _ := xdsconfig.FoldersConfigFilenameGet()
+ return &Folders{
+ Context: ctx,
+ fileOnDisk: file,
+ folders: make(map[string]*IFOLDER),
+ registerCB: []RegisteredCB{},
+ }
+}
+
+// LoadConfig Load folders configuration from disk
+func (f *Folders) LoadConfig() error {
+ var flds []xsapiv1.FolderConfig
+ var stFlds []xsapiv1.FolderConfig
+
+ // load from disk
+ if f.Config.Options.NoFolderConfig {
+ f.Log.Infof("Don't read folder config file (-no-folderconfig option is set)")
+ } else if f.fileOnDisk != "" {
+ f.Log.Infof("Use folder config file: %s", f.fileOnDisk)
+ err := foldersConfigRead(f.fileOnDisk, &flds)
+ if err != nil {
+ if strings.HasPrefix(err.Error(), "No folder config") {
+ f.Log.Warnf(err.Error())
+ } else {
+ return err
+ }
+ }
+ } else {
+ f.Log.Warnf("Folders config filename not set")
+ }
+
+ // Retrieve initial Syncthing config (just append don't overwrite existing ones)
+ if f.SThg != nil {
+ f.Log.Infof("Retrieve syncthing folder config")
+ if err := f.SThg.FolderLoadFromStConfig(&stFlds); err != nil {
+ // 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
+ for _, stf := range stFlds {
+ found := false
+ for i, xf := range flds {
+ if xf.ID == stf.ID {
+ found = true
+ // sanity check
+ if xf.Type != xsapiv1.TypeCloudSync {
+ flds[i].Status = xsapiv1.StatusErrorConfig
+ }
+ break
+ }
+ }
+ // add it
+ if !found {
+ flds = append(flds, stf)
+ }
+ }
+
+ // Detect ghost project
+ // (IOW existing in xds file config and not in syncthing database)
+ if f.SThg != nil {
+ for i, xf := range flds {
+ // only for syncthing project
+ if xf.Type != xsapiv1.TypeCloudSync {
+ continue
+ }
+ found := false
+ for _, stf := range stFlds {
+ if stf.ID == xf.ID {
+ found = true
+ break
+ }
+ }
+ if !found {
+ flds[i].Status = xsapiv1.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, true); err != nil {
+ return err
+ }
+ }
+
+ // Save config on disk
+ err := f.SaveConfig()
+
+ return err
+}
+
+// SaveConfig Save folders configuration to disk
+func (f *Folders) SaveConfig() error {
+ if f.fileOnDisk == "" {
+ return fmt.Errorf("Folders config filename not set")
+ }
+
+ // FIXME: buffered save or avoid to write on disk each time
+ 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) *IFOLDER {
+ if id == "" {
+ return nil
+ }
+ fc, exist := f.folders[id]
+ if !exist {
+ return nil
+ }
+ return fc
+}
+
+// GetConfigArr returns the config of all folders as an array
+func (f *Folders) GetConfigArr() []xsapiv1.FolderConfig {
+ fcMutex.Lock()
+ defer fcMutex.Unlock()
+
+ return f.getConfigArrUnsafe()
+}
+
+// getConfigArrUnsafe Same as GetConfigArr without mutex protection
+func (f *Folders) getConfigArrUnsafe() []xsapiv1.FolderConfig {
+ conf := []xsapiv1.FolderConfig{}
+ for _, v := range f.folders {
+ conf = append(conf, (*v).GetConfig())
+ }
+ return conf
+}
+
+// Add adds a new folder
+func (f *Folders) Add(newF xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
+ return f.createUpdate(newF, true, false)
+}
+
+// CreateUpdate creates or update a folder
+func (f *Folders) createUpdate(newF xsapiv1.FolderConfig, create bool, initial bool) (*xsapiv1.FolderConfig, error) {
+
+ fcMutex.Lock()
+ defer fcMutex.Unlock()
+
+ // Sanity check
+ if _, exist := f.folders[newF.ID]; create && exist {
+ return nil, fmt.Errorf("ID already exists")
+ }
+ if newF.ClientPath == "" {
+ return nil, fmt.Errorf("ClientPath must be set")
+ }
+
+ // Create a new folder object
+ var fld IFOLDER
+ switch newF.Type {
+ // SYNCTHING
+ case xsapiv1.TypeCloudSync:
+ if f.SThg != nil {
+ fld = NewFolderST(f.Context, f.SThg)
+ } else {
+ f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID)
+ fld = NewFolderSTDisable(f.Context)
+ }
+
+ // PATH MAP
+ case xsapiv1.TypePathMap:
+ fld = NewFolderPathMap(f.Context)
+ default:
+ return nil, fmt.Errorf("Unsupported folder type")
+ }
+
+ // Allocate a new UUID
+ if create {
+ newF.ID = fld.NewUID("")
+ }
+ if !create && newF.ID == "" {
+ return nil, fmt.Errorf("Cannot update folder with null ID")
+ }
+
+ // Set default value if needed
+ if newF.Status == "" {
+ newF.Status = xsapiv1.StatusDisable
+ }
+ if newF.Label == "" {
+ newF.Label = filepath.Base(newF.ClientPath)
+ if len(newF.ID) > 8 {
+ newF.Label += "_" + newF.ID[0:8]
+ }
+ }
+
+ // Normalize path (needed for Windows path including bashlashes)
+ newF.ClientPath = common.PathNormalize(newF.ClientPath)
+
+ // Add new folder
+ newFolder, err := fld.Add(newF)
+ if err != nil {
+ newF.Status = xsapiv1.StatusErrorConfig
+ log.Printf("ERROR Adding folder: %v\n", err)
+ return newFolder, err
+ }
+
+ // Add to folders list
+ f.folders[newF.ID] = &fld
+
+ // Save config on disk
+ 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, nil
+}
+
+// Delete deletes a specific folder
+func (f *Folders) Delete(id string) (xsapiv1.FolderConfig, error) {
+ var err error
+
+ fcMutex.Lock()
+ defer fcMutex.Unlock()
+
+ fld := xsapiv1.FolderConfig{}
+ fc, exist := f.folders[id]
+ if !exist {
+ return fld, fmt.Errorf("unknown id")
+ }
+
+ fld = (*fc).GetConfig()
+
+ if err = (*fc).Remove(); err != nil {
+ return fld, err
+ }
+
+ delete(f.folders, id)
+
+ // Save config on disk
+ err = f.SaveConfig()
+
+ return fld, err
+}
+
+// Update Update a specific folder
+func (f *Folders) Update(id string, cfg xsapiv1.FolderConfig) (*xsapiv1.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 := xsapiv1.FolderConfig{}
+ reflectme.Copy((*fc).GetConfig(), &newCfg)
+
+ // Only update some fields
+ dirty := false
+ for _, fieldName := range xsapiv1.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 *FolderEventCB, data *FolderEventCBData) error {
+
+ flds := make(map[string]*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)
+ if fc == nil {
+ return fmt.Errorf("Unknown id")
+ }
+ return (*fc).Sync()
+}
+
+// IsFolderInSync Returns true when folder is in sync
+func (f *Folders) IsFolderInSync(id string) (bool, error) {
+ fc := f.Get(id)
+ if fc == nil {
+ return false, fmt.Errorf("Unknown id")
+ }
+ return (*fc).IsInSync()
+}
+
+//*** Private functions ***
+
+// Use XML format and not json to be able to save/load all fields including
+// ones that are masked in json (IOW defined with `json:"-"`)
+type xmlFolders struct {
+ XMLName xml.Name `xml:"folders"`
+ Version string `xml:"version,attr"`
+ Folders []xsapiv1.FolderConfig `xml:"folders"`
+}
+
+// foldersConfigRead reads folders config from disk
+func foldersConfigRead(file string, folders *[]xsapiv1.FolderConfig) error {
+ if !common.Exists(file) {
+ return fmt.Errorf("No folder config file found (%s)", file)
+ }
+
+ ffMutex.Lock()
+ defer ffMutex.Unlock()
+
+ fd, err := os.Open(file)
+ defer fd.Close()
+ if err != nil {
+ return err
+ }
+
+ data := xmlFolders{}
+ err = xml.NewDecoder(fd).Decode(&data)
+ if err == nil {
+ *folders = data.Folders
+ }
+ return err
+}
+
+// foldersConfigWrite writes folders config on disk
+func foldersConfigWrite(file string, folders []xsapiv1.FolderConfig) error {
+ ffMutex.Lock()
+ defer ffMutex.Unlock()
+
+ fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
+ defer fd.Close()
+ if err != nil {
+ return err
+ }
+
+ data := &xmlFolders{
+ Version: "1",
+ Folders: folders,
+ }
+
+ enc := xml.NewEncoder(fd)
+ enc.Indent("", " ")
+ return enc.Encode(data)
+}
diff --git a/lib/xdsserver/sdk.go b/lib/xdsserver/sdk.go
new file mode 100644
index 0000000..c0acb24
--- /dev/null
+++ b/lib/xdsserver/sdk.go
@@ -0,0 +1,56 @@
+package xdsserver
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+ uuid "github.com/satori/go.uuid"
+)
+
+// CrossSDK Hold SDK config
+type CrossSDK struct {
+ sdk xsapiv1.SDK
+}
+
+// NewCrossSDK creates a new instance of Syncthing
+func NewCrossSDK(path string) (*CrossSDK, error) {
+ // Assume that we have .../<profile>/<version>/<arch>
+ s := CrossSDK{
+ sdk: xsapiv1.SDK{Path: path},
+ }
+
+ s.sdk.Arch = filepath.Base(path)
+
+ d := filepath.Dir(path)
+ s.sdk.Version = filepath.Base(d)
+
+ d = filepath.Dir(d)
+ s.sdk.Profile = filepath.Base(d)
+
+ // Use V3 to ensure that we get same uuid on restart
+ s.sdk.ID = uuid.NewV3(uuid.FromStringOrNil("sdks"), s.sdk.Profile+"_"+s.sdk.Arch+"_"+s.sdk.Version).String()
+ s.sdk.Name = s.sdk.Arch + " (" + s.sdk.Version + ")"
+
+ envFile := filepath.Join(path, "environment-setup*")
+ ef, err := filepath.Glob(envFile)
+ if err != nil {
+ return nil, fmt.Errorf("Cannot retrieve environment setup file: %v", err)
+ }
+ if len(ef) != 1 {
+ return nil, fmt.Errorf("No environment setup file found match %s", envFile)
+ }
+ s.sdk.EnvFile = ef[0]
+
+ return &s, nil
+}
+
+// Get Return SDK definition
+func (s *CrossSDK) Get() *xsapiv1.SDK {
+ return &s.sdk
+}
+
+// GetEnvCmd returns the command used to initialized the environment
+func (s *CrossSDK) GetEnvCmd() []string {
+ return []string{"source", s.sdk.EnvFile}
+}
diff --git a/lib/xdsserver/sdks.go b/lib/xdsserver/sdks.go
new file mode 100644
index 0000000..1a40ab5
--- /dev/null
+++ b/lib/xdsserver/sdks.go
@@ -0,0 +1,127 @@
+package xdsserver
+
+import (
+ "fmt"
+ "path"
+ "path/filepath"
+ "strings"
+ "sync"
+
+ common "github.com/iotbzh/xds-common/golib"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+)
+
+// SDKs List of installed SDK
+type SDKs struct {
+ *Context
+ Sdks map[string]*CrossSDK
+
+ mutex sync.Mutex
+}
+
+// NewSDKs creates a new instance of SDKs
+func NewSDKs(ctx *Context) (*SDKs, error) {
+ s := SDKs{
+ Context: ctx,
+ Sdks: make(map[string]*CrossSDK),
+ }
+
+ // Retrieve installed sdks
+ sdkRD := ctx.Config.FileConf.SdkRootDir
+
+ if common.Exists(sdkRD) {
+
+ // Assume that SDK install tree is <rootdir>/<profile>/<version>/<arch>
+ dirs, err := filepath.Glob(path.Join(sdkRD, "*", "*", "*"))
+ if err != nil {
+ ctx.Log.Debugf("Error while retrieving SDKs: dir=%s, error=%s", sdkRD, err.Error())
+ return &s, err
+ }
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ for _, d := range dirs {
+ if !common.IsDir(d) {
+ continue
+ }
+ cSdk, err := NewCrossSDK(d)
+ if err != nil {
+ ctx.Log.Debugf("Error while processing SDK dir=%s, err=%s", d, err.Error())
+ continue
+ }
+ s.Sdks[cSdk.sdk.ID] = cSdk
+ }
+ }
+
+ ctx.Log.Debugf("SDKs: %d cross sdks found", len(s.Sdks))
+
+ return &s, nil
+}
+
+// ResolveID Complete an SDK ID (helper for user that can use partial ID value)
+func (s *SDKs) ResolveID(id string) (string, error) {
+ if id == "" {
+ return "", nil
+ }
+
+ match := []string{}
+ for iid := range s.Sdks {
+ 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 sdk id")
+ }
+ return id, fmt.Errorf("Multiple sdk IDs found with provided prefix: " + id)
+}
+
+// Get returns an SDK from id
+func (s *SDKs) Get(id string) *xsapiv1.SDK {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ sc, exist := s.Sdks[id]
+ if !exist {
+ return nil
+ }
+ return (*sc).Get()
+}
+
+// GetAll returns all existing SDKs
+func (s *SDKs) GetAll() []xsapiv1.SDK {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ res := []xsapiv1.SDK{}
+ for _, v := range s.Sdks {
+ res = append(res, *(*v).Get())
+ }
+ return res
+}
+
+// GetEnvCmd returns the command used to initialized the environment for an SDK
+func (s *SDKs) GetEnvCmd(id string, defaultID string) []string {
+ if id == "" && defaultID == "" {
+ // no env cmd
+ return []string{}
+ }
+
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ if iid, err := s.ResolveID(id); err == nil {
+ if sdk, exist := s.Sdks[iid]; exist {
+ return sdk.GetEnvCmd()
+ }
+ }
+
+ if sdk, exist := s.Sdks[defaultID]; defaultID != "" && exist {
+ return sdk.GetEnvCmd()
+ }
+
+ // Return default env that may be empty
+ return []string{}
+}
diff --git a/lib/xdsserver/sessions.go b/lib/xdsserver/sessions.go
new file mode 100644
index 0000000..6da9fd8
--- /dev/null
+++ b/lib/xdsserver/sessions.go
@@ -0,0 +1,225 @@
+package xdsserver
+
+import (
+ "encoding/base64"
+ "strconv"
+ "time"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/gin-gonic/gin"
+ "github.com/googollee/go-socket.io"
+ uuid "github.com/satori/go.uuid"
+ "github.com/syncthing/syncthing/lib/sync"
+)
+
+const sessionCookieName = "xds-sid"
+const sessionHeaderName = "XDS-SID"
+
+const sessionMonitorTime = 10 // Time (in seconds) to schedule monitoring session tasks
+
+const initSessionMaxAge = 10 // Initial session max age in seconds
+const maxSessions = 100000 // Maximum number of sessions in sessMap map
+
+const secureCookie = false // TODO: see https://github.com/astaxie/beego/blob/master/session/session.go#L218
+
+// ClientSession contains the info of a user/client session
+type ClientSession struct {
+ ID string
+ WSID string // only one WebSocket per client/session
+ MaxAge int64
+ IOSocket *socketio.Socket
+
+ // private
+ expireAt time.Time
+ useCount int64
+}
+
+// Sessions holds client sessions
+type Sessions struct {
+ *Context
+ cookieMaxAge int64
+ sessMap map[string]ClientSession
+ mutex sync.Mutex
+ log *logrus.Logger
+ LogLevelSilly bool
+ stop chan struct{} // signals intentional stop
+}
+
+// NewClientSessions .
+func NewClientSessions(ctx *Context, cookieMaxAge string) *Sessions {
+ ckMaxAge, err := strconv.ParseInt(cookieMaxAge, 10, 0)
+ if err != nil {
+ ckMaxAge = 0
+ }
+ s := Sessions{
+ Context: ctx,
+ cookieMaxAge: ckMaxAge,
+ sessMap: make(map[string]ClientSession),
+ mutex: sync.NewMutex(),
+ stop: make(chan struct{}),
+ }
+ s.WWWServer.router.Use(s.Middleware())
+
+ // Start monitoring of sessions Map (use to manage expiration and cleanup)
+ go s.monitorSessMap()
+
+ return &s
+}
+
+// Stop sessions management
+func (s *Sessions) Stop() {
+ close(s.stop)
+}
+
+// Middleware is used to managed session
+func (s *Sessions) Middleware() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // FIXME Add CSRF management
+
+ // Get session
+ sess := s.Get(c)
+ if sess == nil {
+ // Allocate a new session key and put in cookie
+ sess = s.newSession("")
+ } else {
+ s.refresh(sess.ID)
+ }
+
+ // Set session in cookie and in header
+ // Do not set Domain to localhost (http://stackoverflow.com/questions/1134290/cookies-on-localhost-with-explicit-domain)
+ c.SetCookie(sessionCookieName, sess.ID, int(sess.MaxAge), "/", "",
+ secureCookie, false)
+ c.Header(sessionHeaderName, sess.ID)
+
+ // Save session id in gin metadata
+ c.Set(sessionCookieName, sess.ID)
+
+ c.Next()
+ }
+}
+
+// Get returns the client session for a specific ID
+func (s *Sessions) Get(c *gin.Context) *ClientSession {
+ var sid string
+
+ // First get from gin metadata
+ v, exist := c.Get(sessionCookieName)
+ if v != nil {
+ sid = v.(string)
+ }
+
+ // Then look in cookie
+ if !exist || sid == "" {
+ sid, _ = c.Cookie(sessionCookieName)
+ }
+
+ // Then look in Header
+ if sid == "" {
+ sid = c.Request.Header.Get(sessionCookieName)
+ }
+ if sid != "" {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ if key, ok := s.sessMap[sid]; ok {
+ // TODO: return a copy ???
+ return &key
+ }
+ }
+ return nil
+}
+
+// IOSocketGet Get socketio definition from sid
+func (s *Sessions) IOSocketGet(sid string) *socketio.Socket {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ sess, ok := s.sessMap[sid]
+ if ok {
+ return sess.IOSocket
+ }
+ return nil
+}
+
+// UpdateIOSocket updates the IO Socket definition for of a session
+func (s *Sessions) UpdateIOSocket(sid string, so *socketio.Socket) error {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+ if _, ok := s.sessMap[sid]; ok {
+ sess := s.sessMap[sid]
+ if so == nil {
+ // Could be the case when socketio is closed/disconnected
+ sess.WSID = ""
+ } else {
+ sess.WSID = (*so).Id()
+ }
+ sess.IOSocket = so
+ s.sessMap[sid] = sess
+ }
+ return nil
+}
+
+// nesSession Allocate a new client session
+func (s *Sessions) newSession(prefix string) *ClientSession {
+ uuid := prefix + uuid.NewV4().String()
+ id := base64.URLEncoding.EncodeToString([]byte(uuid))
+ se := ClientSession{
+ ID: id,
+ WSID: "",
+ MaxAge: initSessionMaxAge,
+ IOSocket: nil,
+ expireAt: time.Now().Add(time.Duration(initSessionMaxAge) * time.Second),
+ useCount: 0,
+ }
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ s.sessMap[se.ID] = se
+
+ s.log.Debugf("NEW session (%d): %s", len(s.sessMap), id)
+ return &se
+}
+
+// refresh Move this session ID to the head of the list
+func (s *Sessions) refresh(sid string) {
+ s.mutex.Lock()
+ defer s.mutex.Unlock()
+
+ sess := s.sessMap[sid]
+ sess.useCount++
+ if sess.MaxAge < s.cookieMaxAge && sess.useCount > 1 {
+ sess.MaxAge = s.cookieMaxAge
+ sess.expireAt = time.Now().Add(time.Duration(sess.MaxAge) * time.Second)
+ }
+
+ // TODO - Add flood detection (like limit_req of nginx)
+ // (delayed request when to much requests in a short period of time)
+
+ s.sessMap[sid] = sess
+}
+
+func (s *Sessions) monitorSessMap() {
+ for {
+ select {
+ case <-s.stop:
+ s.log.Debugln("Stop monitorSessMap")
+ return
+ case <-time.After(sessionMonitorTime * time.Second):
+ if s.LogLevelSilly {
+ s.log.Debugf("Sessions Map size: %d", len(s.sessMap))
+ s.log.Debugf("Sessions Map : %v", s.sessMap)
+ }
+
+ if len(s.sessMap) > maxSessions {
+ s.log.Errorln("TOO MUCH sessions, cleanup old ones !")
+ }
+
+ s.mutex.Lock()
+ for _, ss := range s.sessMap {
+ if ss.expireAt.Sub(time.Now()) < 0 {
+ s.log.Debugf("Delete expired session id: %s", ss.ID)
+ delete(s.sessMap, ss.ID)
+ }
+ }
+ s.mutex.Unlock()
+ }
+ }
+}
diff --git a/lib/xdsserver/webserver.go b/lib/xdsserver/webserver.go
new file mode 100644
index 0000000..0e1676a
--- /dev/null
+++ b/lib/xdsserver/webserver.go
@@ -0,0 +1,181 @@
+package xdsserver
+
+import (
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+
+ "path"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/gin-contrib/static"
+ "github.com/gin-gonic/gin"
+ "github.com/googollee/go-socket.io"
+)
+
+// WebServer .
+type WebServer struct {
+ *Context
+ router *gin.Engine
+ api *APIService
+ sIOServer *socketio.Server
+ webApp *gin.RouterGroup
+ stop chan struct{} // signals intentional stop
+}
+
+const indexFilename = "index.html"
+
+// NewWebServer creates an instance of WebServer
+func NewWebServer(ctx *Context) *WebServer {
+
+ // Setup logging for gin router
+ if ctx.Log.Level == logrus.DebugLevel {
+ gin.SetMode(gin.DebugMode)
+ } else {
+ gin.SetMode(gin.ReleaseMode)
+ }
+
+ // Redirect gin logs into another logger (LogVerboseOut may be stderr or a file)
+ gin.DefaultWriter = ctx.Config.LogVerboseOut
+ gin.DefaultErrorWriter = ctx.Config.LogVerboseOut
+ log.SetOutput(ctx.Config.LogVerboseOut)
+
+ // FIXME - fix pb about isTerminal=false when out is in VSC Debug Console
+
+ // Creates gin router
+ r := gin.New()
+
+ svr := &WebServer{
+ router: r,
+ api: nil,
+ sIOServer: nil,
+ webApp: nil,
+ stop: make(chan struct{}),
+ }
+
+ return svr
+}
+
+// Serve starts a new instance of the Web Server
+func (s *WebServer) Serve() error {
+ var err error
+
+ // Setup middlewares
+ s.router.Use(gin.Logger())
+ s.router.Use(gin.Recovery())
+ s.router.Use(s.middlewareXDSDetails())
+ s.router.Use(s.middlewareCORS())
+
+ // Create REST API
+ s.api = NewAPIV1(s.Context)
+
+ // Websocket routes
+ s.sIOServer, err = socketio.NewServer(nil)
+ if err != nil {
+ s.Log.Fatalln(err)
+ }
+
+ s.router.GET("/socket.io/", s.socketHandler)
+ s.router.POST("/socket.io/", s.socketHandler)
+ /* TODO: do we want to support ws://... ?
+ s.router.Handle("WS", "/socket.io/", s.socketHandler)
+ s.router.Handle("WSS", "/socket.io/", s.socketHandler)
+ */
+
+ // Web Application (serve on / )
+ idxFile := path.Join(s.Config.FileConf.WebAppDir, indexFilename)
+ if _, err := os.Stat(idxFile); err != nil {
+ s.Log.Fatalln("Web app directory not found, check/use webAppDir setting in config file: ", idxFile)
+ }
+ s.Log.Infof("Serve WEB app dir: %s", s.Config.FileConf.WebAppDir)
+ s.router.Use(static.Serve("/", static.LocalFile(s.Config.FileConf.WebAppDir, true)))
+ s.webApp = s.router.Group("/", s.serveIndexFile)
+ {
+ s.webApp.GET("/")
+ }
+
+ // Serve in the background
+ serveError := make(chan error, 1)
+ go func() {
+ msg := fmt.Sprintf("Web Server running on localhost:%s ...\n", s.Config.FileConf.HTTPPort)
+ s.Log.Infof(msg)
+ fmt.Printf(msg)
+ serveError <- http.ListenAndServe(":"+s.Config.FileConf.HTTPPort, s.router)
+ }()
+
+ // Wait for stop, restart or error signals
+ select {
+ case <-s.stop:
+ // Shutting down permanently
+ s.sessions.Stop()
+ s.Log.Infoln("shutting down (stop)")
+ case err = <-serveError:
+ // Error due to listen/serve failure
+ s.Log.Errorln(err)
+ }
+
+ return nil
+}
+
+// Stop web server
+func (s *WebServer) Stop() {
+ close(s.stop)
+}
+
+// serveIndexFile provides initial file (eg. index.html) of webapp
+func (s *WebServer) serveIndexFile(c *gin.Context) {
+ c.HTML(200, indexFilename, gin.H{})
+}
+
+// Add details in Header
+func (s *WebServer) middlewareXDSDetails() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Header("XDS-Version", s.Config.Version)
+ c.Header("XDS-API-Version", s.Config.APIVersion)
+ c.Next()
+ }
+}
+
+// CORS middleware
+func (s *WebServer) middlewareCORS() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ if c.Request.Method == "OPTIONS" {
+ c.Header("Access-Control-Allow-Origin", "*")
+ c.Header("Access-Control-Allow-Headers", "Content-Type")
+ c.Header("Access-Control-Allow-Methods", "GET, POST, DELETE")
+ c.Header("Access-Control-Max-Age", cookieMaxAge)
+ c.AbortWithStatus(204)
+ return
+ }
+
+ c.Next()
+ }
+}
+
+// socketHandler is the handler for the "main" websocket connection
+func (s *WebServer) socketHandler(c *gin.Context) {
+
+ // Retrieve user session
+ sess := s.sessions.Get(c)
+ if sess == nil {
+ c.JSON(500, gin.H{"error": "Cannot retrieve session"})
+ return
+ }
+
+ s.sIOServer.On("connection", func(so socketio.Socket) {
+ s.Log.Debugf("WS Connected (SID=%v)", so.Id())
+ s.sessions.UpdateIOSocket(sess.ID, &so)
+
+ so.On("disconnection", func() {
+ s.Log.Debugf("WS disconnected (SID=%v)", so.Id())
+ s.sessions.UpdateIOSocket(sess.ID, nil)
+ })
+ })
+
+ s.sIOServer.On("error", func(so socketio.Socket, err error) {
+ s.Log.Errorf("WS SID=%v Error : %v", so.Id(), err.Error())
+ })
+
+ s.sIOServer.ServeHTTP(c.Writer, c.Request)
+}
diff --git a/lib/xdsserver/xdsserver.go b/lib/xdsserver/xdsserver.go
new file mode 100644
index 0000000..dcdedc4
--- /dev/null
+++ b/lib/xdsserver/xdsserver.go
@@ -0,0 +1,202 @@
+package xdsserver
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "os/signal"
+ "path/filepath"
+ "syscall"
+ "time"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/codegangsta/cli"
+ "github.com/iotbzh/xds-server/lib/xsapiv1"
+
+ "github.com/iotbzh/xds-server/lib/syncthing"
+ "github.com/iotbzh/xds-server/lib/xdsconfig"
+)
+
+const cookieMaxAge = "3600"
+
+// Context holds the XDS server context
+type Context struct {
+ ProgName string
+ Cli *cli.Context
+ Config *xdsconfig.Config
+ Log *logrus.Logger
+ LogLevelSilly bool
+ SThg *st.SyncThing
+ SThgCmd *exec.Cmd
+ SThgInotCmd *exec.Cmd
+ mfolders *Folders
+ sdks *SDKs
+ WWWServer *WebServer
+ sessions *Sessions
+ Exit chan os.Signal
+}
+
+// NewXdsServer Create a new instance of XDS server
+func NewXdsServer(cliCtx *cli.Context) *Context {
+ var err error
+
+ // Set logger level and formatter
+ log := cliCtx.App.Metadata["logger"].(*logrus.Logger)
+
+ logLevel := cliCtx.GlobalString("log")
+ if logLevel == "" {
+ logLevel = "error" // FIXME get from Config DefaultLogLevel
+ }
+ if log.Level, err = logrus.ParseLevel(logLevel); err != nil {
+ fmt.Printf("Invalid log level : \"%v\"\n", logLevel)
+ os.Exit(1)
+ }
+ log.Formatter = &logrus.TextFormatter{}
+
+ sillyVal, sillyLog := os.LookupEnv("XDS_LOG_SILLY")
+
+ // Define default configuration
+ ctx := Context{
+ ProgName: cliCtx.App.Name,
+ Cli: cliCtx,
+ Log: log,
+ LogLevelSilly: (sillyLog && sillyVal == "1"),
+ Exit: make(chan os.Signal, 1),
+ }
+
+ // register handler on SIGTERM / exit
+ signal.Notify(ctx.Exit, os.Interrupt, syscall.SIGTERM)
+ go handlerSigTerm(&ctx)
+
+ return &ctx
+}
+
+// Run Main function called to run XDS Server
+func (ctx *Context) Run() (int, error) {
+ var err error
+
+ // Logs redirected into a file when logfile option or logsDir config is set
+ ctx.Config.LogVerboseOut = os.Stderr
+ if ctx.Config.FileConf.LogsDir != "" {
+ if ctx.Config.Options.LogFile != "stdout" {
+ logFile := ctx.Config.Options.LogFile
+
+ fdL, err := os.OpenFile(logFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
+ if err != nil {
+ msgErr := fmt.Sprintf("Cannot create log file %s", logFile)
+ return int(syscall.EPERM), fmt.Errorf(msgErr)
+ }
+ ctx.Log.Out = fdL
+
+ ctx._logPrint("Logging file: %s\n", logFile)
+ }
+
+ logFileHTTPReq := filepath.Join(ctx.Config.FileConf.LogsDir, "xds-server-verbose.log")
+ fdLH, err := os.OpenFile(logFileHTTPReq, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
+ if err != nil {
+ msgErr := fmt.Sprintf("Cannot create log file %s", logFileHTTPReq)
+ return int(syscall.EPERM), fmt.Errorf(msgErr)
+ }
+ ctx.Config.LogVerboseOut = fdLH
+
+ ctx._logPrint("Logging file for HTTP requests: %s\n", logFileHTTPReq)
+ }
+
+ // Create syncthing instance when section "syncthing" is present in config.json
+ if ctx.Config.FileConf.SThgConf != nil {
+ ctx.SThg = st.NewSyncThing(ctx.Config, ctx.Log)
+ }
+
+ // Start local instance of Syncthing and Syncthing-notify
+ if ctx.SThg != nil {
+ ctx.Log.Infof("Starting Syncthing...")
+ ctx.SThgCmd, err = ctx.SThg.Start()
+ if err != nil {
+ return -4, err
+ }
+ ctx._logPrint("Syncthing started (PID %d)\n", ctx.SThgCmd.Process.Pid)
+
+ ctx.Log.Infof("Starting Syncthing-inotify...")
+ ctx.SThgInotCmd, err = ctx.SThg.StartInotify()
+ if err != nil {
+ return -4, err
+ }
+ ctx._logPrint("Syncthing-inotify started (PID %d)\n", ctx.SThgInotCmd.Process.Pid)
+
+ // Establish connection with local Syncthing (retry if connection fail)
+ ctx._logPrint("Establishing connection with Syncthing...\n")
+ time.Sleep(2 * time.Second)
+ maxRetry := 30
+ retry := maxRetry
+ err = nil
+ for retry > 0 {
+ if err = ctx.SThg.Connect(); err == nil {
+ break
+ }
+ ctx.Log.Warningf("Establishing connection to Syncthing (retry %d/%d)", retry, maxRetry)
+ time.Sleep(time.Second)
+ retry--
+ }
+ if err != nil || retry == 0 {
+ return -4, err
+ }
+
+ // FIXME: do we still need Builder notion ? if no cleanup
+ if ctx.Config.Builder, err = xdsconfig.NewBuilderConfig(ctx.SThg.MyID); err != nil {
+ return -4, err
+ }
+ ctx.Config.SupportedSharing[xsapiv1.TypeCloudSync] = true
+ }
+
+ // Init model folder
+ ctx.mfolders = FoldersNew(ctx)
+
+ // Load initial folders config from disk
+ if err := ctx.mfolders.LoadConfig(); err != nil {
+ return -5, err
+ }
+
+ // Init cross SDKs
+ ctx.sdks, err = NewSDKs(ctx)
+ if err != nil {
+ return -6, err
+ }
+
+ // Create Web Server
+ ctx.WWWServer = NewWebServer(ctx)
+
+ // Sessions manager
+ ctx.sessions = NewClientSessions(ctx, cookieMaxAge)
+
+ // Run Web Server until exit requested (blocking call)
+ if err = ctx.WWWServer.Serve(); err != nil {
+ ctx.Log.Println(err)
+ return -7, err
+ }
+
+ return -99, fmt.Errorf("Program exited ")
+}
+
+// Helper function to log message on both stdout and logger
+func (ctx *Context) _logPrint(format string, args ...interface{}) {
+ fmt.Printf(format, args...)
+ if ctx.Log.Out != os.Stdout {
+ ctx.Log.Infof(format, args...)
+ }
+}
+
+// Handle exit and properly stop/close all stuff
+func handlerSigTerm(ctx *Context) {
+ <-ctx.Exit
+ if ctx.SThg != nil {
+ ctx.Log.Infof("Stoping Syncthing... (PID %d)", ctx.SThgCmd.Process.Pid)
+ ctx.SThg.Stop()
+ ctx.Log.Infof("Stoping Syncthing-inotify... (PID %d)", ctx.SThgInotCmd.Process.Pid)
+ ctx.SThg.StopInotify()
+ }
+ if ctx.WWWServer != nil {
+ ctx.Log.Infof("Stoping Web server...")
+ ctx.WWWServer.Stop()
+ }
+ os.Exit(0)
+}