summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.vscode/launch.json17
-rw-r--r--.vscode/settings.json98
-rw-r--r--Makefile6
-rw-r--r--config.json.in3
-rw-r--r--glide.yaml7
-rw-r--r--lib/apiv1/agent.go3
-rw-r--r--lib/apiv1/apiv1.go12
-rw-r--r--lib/apiv1/config.go7
-rw-r--r--lib/apiv1/events.go147
-rw-r--r--lib/apiv1/exec.go322
-rw-r--r--lib/apiv1/folders.go50
-rw-r--r--lib/apiv1/make.go28
-rw-r--r--lib/crosssdk/sdks.go3
-rw-r--r--lib/folder/folder-interface.go68
-rw-r--r--lib/folder/folder-pathmap.go115
-rw-r--r--lib/model/folder.go110
-rw-r--r--lib/model/folders.go388
-rw-r--r--lib/syncthing/folder-st.go170
-rw-r--r--lib/syncthing/st.go79
-rw-r--r--lib/syncthing/stEvent.go265
-rw-r--r--lib/syncthing/stfolder.go121
-rw-r--r--lib/webserver/server.go24
-rw-r--r--lib/xdsconfig/config.go27
-rw-r--r--lib/xdsconfig/fileconfig.go40
-rw-r--r--lib/xdsconfig/folderconfig.go85
-rw-r--r--lib/xdsconfig/foldersconfig.go47
-rw-r--r--main.go121
-rwxr-xr-xscripts/xds-server-start.sh2
-rwxr-xr-xscripts/xds-server-stop.sh1
-rwxr-xr-xscripts/xds-utils/install-agl-sdks.sh10
-rw-r--r--webapp/assets/images/iot-bzh-logo-small.pngbin0 -> 14449 bytes
-rw-r--r--webapp/src/app/app.component.css22
-rw-r--r--webapp/src/app/app.component.html7
-rw-r--r--webapp/src/app/app.module.ts8
-rw-r--r--webapp/src/app/config/config.component.css11
-rw-r--r--webapp/src/app/config/config.component.html66
-rw-r--r--webapp/src/app/config/config.component.ts62
-rw-r--r--webapp/src/app/devel/build/build.component.css9
-rw-r--r--webapp/src/app/devel/build/build.component.html87
-rw-r--r--webapp/src/app/devel/build/build.component.ts13
-rw-r--r--webapp/src/app/devel/deploy/deploy.component.ts6
-rw-r--r--webapp/src/app/devel/devel.component.css5
-rw-r--r--webapp/src/app/devel/devel.component.html11
-rw-r--r--webapp/src/app/devel/devel.component.ts7
-rw-r--r--webapp/src/app/projects/projectAddModal.component.css24
-rw-r--r--webapp/src/app/projects/projectAddModal.component.html54
-rw-r--r--webapp/src/app/projects/projectAddModal.component.ts152
-rw-r--r--webapp/src/app/projects/projectCard.component.ts60
-rw-r--r--webapp/src/app/projects/projectsListAccordion.component.ts17
-rw-r--r--webapp/src/app/sdks/sdkAddModal.component.html23
-rw-r--r--webapp/src/app/sdks/sdkAddModal.component.ts24
-rw-r--r--webapp/src/app/services/alert.service.ts6
-rw-r--r--webapp/src/app/services/config.service.ts235
-rw-r--r--webapp/src/app/services/syncthing.service.ts4
-rw-r--r--webapp/src/app/services/xdsserver.service.ts68
-rw-r--r--webapp/src/index.html5
-rw-r--r--webapp/src/systemjs.config.js3
57 files changed, 2529 insertions, 836 deletions
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 8bdde69..5583251 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -1,7 +1,7 @@
{
"version": "0.2.0",
"configurations": [{
- "name": "XDS-Server local",
+ "name": "XDS-Server",
"type": "go",
"request": "launch",
"mode": "debug",
@@ -17,21 +17,20 @@
"showLog": false
},
{
- "name": "XDS-Server IN DOCKER",
+ "name": "XDS-Server local dev",
"type": "go",
"request": "launch",
"mode": "debug",
- "port": 22000,
- "host": "172.17.0.2",
- "remotePath": "/xds/src/github.com/iotbzh/xds-server/bin/xds-server",
+ "remotePath": "",
+ "port": 2345,
+ "host": "127.0.0.1",
"program": "${workspaceRoot}",
"env": {
"GOPATH": "${workspaceRoot}/../../../..:${env:GOPATH}",
"ROOT_DIR": "${workspaceRoot}/../../../.."
},
- "args": [],
- "showLog": true
+ "args": ["-log", "debug", "-c", "__config_local_dev.json"],
+ "showLog": false
}
-
]
-} \ No newline at end of file
+}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index bb7040e..4f2a394 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,45 +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
- },
-
- // 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"
- ]
+ // 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/Makefile b/Makefile
index d088c5d..d839539 100644
--- a/Makefile
+++ b/Makefile
@@ -84,7 +84,7 @@ all: tools/syncthing build
build: vendor xds webapp
xds: scripts tools/syncthing/copytobin
- @echo "### Build XDS server (version $(VERSION), subversion $(SUB_VERSION))";
+ @echo "### Build XDS server (version $(VERSION), subversion $(SUB_VERSION), $(BUILD_MODE))";
@cd $(ROOT_SRCDIR); $(BUILD_ENV_FLAGS) go build $(VERBOSE_$(V)) -i -o $(LOCAL_BINDIR)/xds-server$(EXT) -ldflags "$(GORELEASE) -X main.AppVersion=$(VERSION) -X main.AppSubVersion=$(SUB_VERSION)" .
test: tools/glide
@@ -157,6 +157,10 @@ package-all:
vendor: tools/glide glide.yaml
$(LOCAL_TOOLSDIR)/glide install --strip-vendor
+vendor/debug: vendor
+ (cd vendor/github.com/iotbzh && \
+ rm -rf xds-common && ln -s ../../../../xds-common )
+
.PHONY: tools/glide
tools/glide:
@test -f $(LOCAL_TOOLSDIR)/glide || { \
diff --git a/config.json.in b/config.json.in
index 751bb29..1668e05 100644
--- a/config.json.in
+++ b/config.json.in
@@ -1,5 +1,6 @@
{
"webAppDir": "webapp/dist",
+ "httpPort": "8000",
"shareRootDir": "${ROOT_DIR}/tmp/builder_dev_host/share",
"logsDir": "/tmp/xds-server/logs",
"sdkRootDir": "/xdt/sdk",
@@ -8,4 +9,4 @@
"home": "${ROOT_DIR}/tmp/builder_dev_host/syncthing-config",
"gui-address": "http://localhost:8384"
}
-} \ No newline at end of file
+}
diff --git a/glide.yaml b/glide.yaml
index aecb56c..e017281 100644
--- a/glide.yaml
+++ b/glide.yaml
@@ -7,6 +7,7 @@ import:
- package: github.com/gin-gonic/gin
version: ^1.1.4
- package: github.com/gin-contrib/static
+ version: master
- package: github.com/syncthing/syncthing
version: =0.14.28
subpackages:
@@ -18,9 +19,15 @@ import:
- package: github.com/Sirupsen/logrus
version: ^0.11.5
- package: github.com/googollee/go-socket.io
+ version: 5447e71f36d3947
- package: github.com/zhouhui8915/go-socket.io-client
+ version: master
- package: github.com/satori/go.uuid
version: ^1.1.0
- package: github.com/iotbzh/xds-common
+ version: 4b8e35b6786b
subpackages:
- golib/common
+ - golib/eows
+- package: github.com/kr/pty
+ version: ^1.0.0
diff --git a/lib/apiv1/agent.go b/lib/apiv1/agent.go
index 651f246..925f12b 100644
--- a/lib/apiv1/agent.go
+++ b/lib/apiv1/agent.go
@@ -11,6 +11,7 @@ import (
common "github.com/iotbzh/xds-common/golib"
)
+// XDSAgentTarball .
type XDSAgentTarball struct {
OS string `json:"os"`
Arch string `json:"arch"`
@@ -18,6 +19,8 @@ type XDSAgentTarball struct {
RawVersion string `json:"raw-version"`
FileURL string `json:"fileUrl"`
}
+
+// XDSAgentInfo .
type XDSAgentInfo struct {
Tarballs []XDSAgentTarball `json:"tarballs"`
}
diff --git a/lib/apiv1/apiv1.go b/lib/apiv1/apiv1.go
index 7fa69e9..262f513 100644
--- a/lib/apiv1/apiv1.go
+++ b/lib/apiv1/apiv1.go
@@ -16,19 +16,19 @@ type APIService struct {
apiRouter *gin.RouterGroup
sessions *session.Sessions
cfg *xdsconfig.Config
- mfolder *model.Folder
+ mfolders *model.Folders
sdks *crosssdk.SDKs
log *logrus.Logger
}
// New creates a new instance of API service
-func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolder *model.Folder, sdks *crosssdk.SDKs) *APIService {
+func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolders *model.Folders, sdks *crosssdk.SDKs) *APIService {
s := &APIService{
router: r,
sessions: sess,
apiRouter: r.Group("/api/v1"),
cfg: cfg,
- mfolder: mfolder,
+ mfolders: mfolders,
sdks: sdks,
log: cfg.Log,
}
@@ -42,6 +42,7 @@ func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolder *
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)
@@ -52,6 +53,11 @@ func New(r *gin.Engine, sess *session.Sessions, cfg *xdsconfig.Config, mfolder *
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/apiv1/config.go b/lib/apiv1/config.go
index 662ec8e..4b53217 100644
--- a/lib/apiv1/config.go
+++ b/lib/apiv1/config.go
@@ -36,10 +36,5 @@ func (s *APIService) setConfig(c *gin.Context) {
s.log.Debugln("SET config: ", cfgArg)
- if err := s.mfolder.UpdateAll(cfgArg); err != nil {
- common.APIError(c, err.Error())
- return
- }
-
- c.JSON(http.StatusOK, s.cfg)
+ common.APIError(c, "Not Supported")
}
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/exec.go b/lib/apiv1/exec.go
index 654ff64..6300dba 100644
--- a/lib/apiv1/exec.go
+++ b/lib/apiv1/exec.go
@@ -1,53 +1,88 @@
package apiv1
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/kr/pty"
)
-// ExecArgs JSON parameters of /exec command
-type ExecArgs struct {
- ID string `json:"id" binding:"required"`
- SdkID string `json:"sdkid"` // sdk ID to use for setting env
- Cmd string `json:"cmd" binding:"required"`
- Args []string `json:"args"`
- 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
-}
+type (
+ // ExecArgs JSON parameters of /exec command
+ ExecArgs struct {
+ ID string `json:"id" binding:"required"`
+ SdkID string `json:"sdkid"` // sdk ID to use for setting env
+ Cmd string `json:"cmd" binding:"required"`
+ Args []string `json:"args"`
+ Env []string `json:"env"`
+ RPath string `json:"rpath"` // relative path into project
+ TTY bool `json:"tty"` // Use a tty, specific to gdb --tty option
+ TTYGdbserverFix bool `json:"ttyGdbserverFix"` // Set to true to activate gdbserver workaround about inferior output
+ 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
+ }
-// ExecOutMsg Message send on each output (stdout+stderr) of executed command
-type ExecOutMsg struct {
- CmdID string `json:"cmdID"`
- Timestamp string `json:"timestamp"`
- Stdout string `json:"stdout"`
- Stderr string `json:"stderr"`
-}
+ // ExecInMsg Message used to received input characters (stdin)
+ ExecInMsg struct {
+ CmdID string `json:"cmdID"`
+ Timestamp string `json:"timestamp"`
+ Stdin string `json:"stdin"`
+ }
-// ExecExitMsg Message send when executed command exited
-type ExecExitMsg struct {
- CmdID string `json:"cmdID"`
- Timestamp string `json:"timestamp"`
- Code int `json:"code"`
- Error error `json:"error"`
-}
+ // ExecOutMsg Message used to send output characters (stdout+stderr)
+ ExecOutMsg struct {
+ CmdID string `json:"cmdID"`
+ Timestamp string `json:"timestamp"`
+ Stdout string `json:"stdout"`
+ Stderr string `json:"stderr"`
+ }
+
+ // ExecExitMsg Message sent when executed command exited
+ ExecExitMsg struct {
+ CmdID string `json:"cmdID"`
+ Timestamp string `json:"timestamp"`
+ Code int `json:"code"`
+ Error error `json:"error"`
+ }
+
+ // ExecSignalArgs JSON parameters of /exec/signal command
+ ExecSignalArgs struct {
+ CmdID string `json:"cmdID" binding:"required"` // command id
+ Signal string `json:"signal" binding:"required"` // signal number
+ }
+)
-// ExecOutEvent Event send in WS when characters are received
-const ExecOutEvent = "exec:output"
+const (
+ // ExecInEvent Event send in WS when characters are sent (stdin)
+ ExecInEvent = "exec:input"
-// ExecExitEvent Event send in WS when program exited
-const ExecExitEvent = "exec:exit"
+ // ExecOutEvent Event send in WS when characters are received (stdout or stderr)
+ ExecOutEvent = "exec:output"
+
+ // ExecExitEvent Event send in WS when program exited
+ ExecExitEvent = "exec:exit"
+
+ // ExecInferiorInEvent Event send in WS when characters are sent to an inferior (used by gdb inferior/tty)
+ ExecInferiorInEvent = "exec:inferior-input"
+
+ // ExecInferiorOutEvent Event send in WS when characters are received by an inferior
+ ExecInferiorOutEvent = "exec:inferior-output"
+)
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 ExecArgs
if c.BindJSON(&args) != nil {
common.APIError(c, "Invalid arguments")
@@ -78,41 +113,123 @@ func (s *APIService) execCmd(c *gin.Context) {
return
}
- prj := s.mfolder.GetFolderFromID(id)
- if prj == nil {
+ f := s.mfolders.Get(id)
+ if f == nil {
common.APIError(c, "Unknown id")
return
}
+ folder := *f
+ prj := folder.GetConfig()
- execTmo := args.CmdTimeout
- if execTmo == 0 {
+ // 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", folder.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(cmdArgs, args.Args)
+
+ // 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
+ cmdID := strconv.Itoa(execCommandID)
+ execCommandID++
+
+ // Create new execution over WS context
+ execWS := eows.New(strings.Join(cmd, " "), cmdArgs, sop, sess.ID, 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
- execTmo = 24 * 60 * 60 // 1 day
+ execWS.CmdExecTimeout = 24 * 60 * 60 // 1 day
+ } else {
+ execWS.CmdExecTimeout = args.CmdTimeout
}
- // Define callback for output
- var oCB common.EmitOutputCB
- oCB = func(sid string, id int, stdout, stderr string, data *map[string]interface{}) {
+ // Define callback for input (stdin)
+ execWS.InputEvent = 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
+ rootPath := (*data)["RootPath"].(string)
+ clientPath := (*data)["ClientPath"].(string)
+ stdin = strings.Replace(stdin, clientPath, rootPath+"/"+clientPath, -1)
+
+ 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(sid)
+ so := s.sessions.IOSocketGet(e.Sid)
if so == nil {
- s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%d", ExecOutEvent, sid, id)
+ s.log.Infof("%s not emitted: WS closed (sid:%s, msgid:%s)", ExecOutEvent, e.Sid, e.CmdID)
return
}
// Retrieve project ID and RootPath
+ data := e.UserData
prjID := (*data)["ID"].(string)
prjRootPath := (*data)["RootPath"].(string)
+ gdbServerTTY := (*data)["gdbServerTTY"].(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", ExecOutEvent, sid, id, prjID)
+ s.log.Debugf("%s emitted - WS sid[4:] %s - id:%s - prjID:%s", 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(ExecOutEvent, ExecOutMsg{
- CmdID: strconv.Itoa(id),
+ CmdID: e.CmdID,
Timestamp: time.Now().String(),
Stdout: stdout,
Stderr: stderr,
@@ -120,25 +237,58 @@ func (s *APIService) execCmd(c *gin.Context) {
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(ExecInferiorOutEvent, 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
- eCB := func(sid string, id int, code int, err error, data *map[string]interface{}) {
- s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err)
+ 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)
// IO socket can be nil when disconnected
- so := s.sessions.IOSocketGet(sid)
+ so := s.sessions.IOSocketGet(e.Sid)
if so == nil {
- s.log.Infof("%s not emitted - WS closed (id:%d", ExecExitEvent, id)
+ s.log.Infof("%s not emitted - WS closed (id:%s)", 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.mfolder.ForceSync(prjID); err != nil {
+ if err := s.mfolders.ForceSync(prjID); err != nil {
s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
}
if !exitImm {
@@ -146,8 +296,8 @@ func (s *APIService) execCmd(c *gin.Context) {
// 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.mfolder.IsFolderInSync(prjID); sync || err != nil {
+ 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)
}
@@ -157,44 +307,73 @@ func (s *APIService) execCmd(c *gin.Context) {
}
}
+ // Close client tty
+ if gdbPty != nil {
+ gdbPty.Close()
+ }
+ if gdbTty != nil {
+ gdbTty.Close()
+ }
+
// FIXME replace by .BroadcastTo a room
- e := (*so).Emit(ExecExitEvent, ExecExitMsg{
- CmdID: strconv.Itoa(id),
+ errSoEmit := (*so).Emit(ExecExitEvent, ExecExitMsg{
+ CmdID: e.CmdID,
Timestamp: time.Now().String(),
Code: code,
Error: err,
})
- if e != nil {
- s.log.Errorf("WS Emit : %v", e)
+ if errSoEmit != nil {
+ s.log.Errorf("WS Emit : %v", errSoEmit)
}
}
- cmdID := execCommandID
- execCommandID++
- 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, "&&")
+ // User data (used within callbacks)
+ data := make(map[string]interface{})
+ data["ID"] = prj.ID
+ data["RootPath"] = prj.RootPath
+ data["ClientPath"] = prj.ClientPath
+ data["ExitImmediate"] = args.ExitImmediate
+ if args.TTY && args.TTYGdbserverFix {
+ data["gdbServerTTY"] = "workaround"
+ } else {
+ data["gdbServerTTY"] = ""
}
+ execWS.UserData = &data
- cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", args.Cmd)
- if len(args.Args) > 0 {
- cmd = append(cmd, args.Args...)
+ // Start command execution
+ s.log.Debugf("Execute [Cmd ID %s]: %v %v", execWS.CmdID, execWS.Cmd, execWS.Args)
+
+ err = execWS.Start()
+ if err != nil {
+ common.APIError(c, err.Error())
+ return
}
- // Append client project dir to environment
- args.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.RelativePath)
+ c.JSON(http.StatusOK,
+ gin.H{
+ "status": "OK",
+ "cmdID": execWS.CmdID,
+ })
+}
- s.log.Debugf("Execute [Cmd ID %d]: %v", cmdID, cmd)
+// ExecCmd executes remotely a command
+func (s *APIService) execSignalCmd(c *gin.Context) {
+ var args ExecSignalArgs
- data := make(map[string]interface{})
- data["ID"] = prj.ID
- data["RootPath"] = prj.RootPath
- data["ExitImmediate"] = args.ExitImmediate
+ 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 := common.ExecPipeWs(cmd, args.Env, sop, sess.ID, cmdID, execTmo, s.log, oCB, eCB, &data)
+ err := e.Signal(args.Signal)
if err != nil {
common.APIError(c, err.Error())
return
@@ -203,6 +382,5 @@ func (s *APIService) execCmd(c *gin.Context) {
c.JSON(http.StatusOK,
gin.H{
"status": "OK",
- "cmdID": cmdID,
})
}
diff --git a/lib/apiv1/folders.go b/lib/apiv1/folders.go
index 44bda24..cf56c3f 100644
--- a/lib/apiv1/folders.go
+++ b/lib/apiv1/folders.go
@@ -2,49 +2,39 @@ package apiv1
import (
"net/http"
- "strconv"
"github.com/gin-gonic/gin"
common "github.com/iotbzh/xds-common/golib"
- "github.com/iotbzh/xds-server/lib/xdsconfig"
+ "github.com/iotbzh/xds-server/lib/folder"
)
// getFolders returns all folders configuration
func (s *APIService) getFolders(c *gin.Context) {
- confMut.Lock()
- defer confMut.Unlock()
-
- c.JSON(http.StatusOK, s.cfg.Folders)
+ c.JSON(http.StatusOK, s.mfolders.GetConfigArr())
}
// getFolder returns a specific folder configuration
func (s *APIService) getFolder(c *gin.Context) {
- id, err := strconv.Atoi(c.Param("id"))
- if err != nil || id < 0 || id > len(s.cfg.Folders) {
+ f := s.mfolders.Get(c.Param("id"))
+ if f == nil {
common.APIError(c, "Invalid id")
return
}
- confMut.Lock()
- defer confMut.Unlock()
-
- c.JSON(http.StatusOK, s.cfg.Folders[id])
+ c.JSON(http.StatusOK, (*f).GetConfig())
}
// addFolder adds a new folder to server config
func (s *APIService) addFolder(c *gin.Context) {
- var cfgArg xdsconfig.FolderConfig
+ var cfgArg folder.FolderConfig
if c.BindJSON(&cfgArg) != nil {
common.APIError(c, "Invalid arguments")
return
}
- confMut.Lock()
- defer confMut.Unlock()
-
s.log.Debugln("Add folder config: ", cfgArg)
- newFld, err := s.mfolder.UpdateFolder(cfgArg)
+ newFld, err := s.mfolders.Add(cfgArg)
if err != nil {
common.APIError(c, err.Error())
return
@@ -53,25 +43,31 @@ func (s *APIService) addFolder(c *gin.Context) {
c.JSON(http.StatusOK, newFld)
}
-// delFolder deletes folder from server config
-func (s *APIService) delFolder(c *gin.Context) {
+// syncFolder force synchronization of folder files
+func (s *APIService) syncFolder(c *gin.Context) {
id := c.Param("id")
- if id == "" {
- common.APIError(c, "Invalid id")
+
+ s.log.Debugln("Sync folder id: ", id)
+
+ err := s.mfolders.ForceSync(id)
+ if err != nil {
+ common.APIError(c, err.Error())
return
}
- confMut.Lock()
- defer confMut.Unlock()
+ c.JSON(http.StatusOK, "")
+}
+
+// delFolder deletes folder from server config
+func (s *APIService) delFolder(c *gin.Context) {
+ id := c.Param("id")
s.log.Debugln("Delete folder id ", id)
- var delEntry xdsconfig.FolderConfig
- var err error
- if delEntry, err = s.mfolder.DeleteFolder(id); err != nil {
+ delEntry, err := s.mfolders.Delete(id)
+ if err != nil {
common.APIError(c, err.Error())
return
}
c.JSON(http.StatusOK, delEntry)
-
}
diff --git a/lib/apiv1/make.go b/lib/apiv1/make.go
index 5cd98c6..cf76476 100644
--- a/lib/apiv1/make.go
+++ b/lib/apiv1/make.go
@@ -76,11 +76,13 @@ func (s *APIService) buildMake(c *gin.Context) {
return
}
- prj := s.mfolder.GetFolderFromID(id)
- if prj == nil {
+ 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 {
@@ -92,11 +94,11 @@ func (s *APIService) buildMake(c *gin.Context) {
// Define callback for output
var oCB common.EmitOutputCB
- oCB = func(sid string, id int, stdout, stderr string, data *map[string]interface{}) {
+ 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:%d", MakeOutEvent, sid, id)
+ s.log.Infof("%s not emitted: WS closed - sid: %s - msg id:%s", MakeOutEvent, sid, cmdID)
return
}
@@ -112,7 +114,7 @@ func (s *APIService) buildMake(c *gin.Context) {
// FIXME replace by .BroadcastTo a room
err := (*so).Emit(MakeOutEvent, MakeOutMsg{
- CmdID: strconv.Itoa(id),
+ CmdID: cmdID,
Timestamp: time.Now().String(),
Stdout: stdout,
Stderr: stderr,
@@ -123,13 +125,13 @@ func (s *APIService) buildMake(c *gin.Context) {
}
// Define callback for output
- eCB := func(sid string, id int, code int, err error, data *map[string]interface{}) {
- s.log.Debugf("Command [Cmd ID %d] exited: code %d, error: %v", id, code, err)
+ 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:%d", MakeExitEvent, id)
+ s.log.Infof("%s not emitted - WS closed (id:%s", MakeExitEvent, cmdID)
return
}
@@ -138,7 +140,7 @@ func (s *APIService) buildMake(c *gin.Context) {
exitImm := (*data)["ExitImmediate"].(bool)
// XXX - workaround to be sure that Syncthing detected all changes
- if err := s.mfolder.ForceSync(prjID); err != nil {
+ if err := s.mfolders.ForceSync(prjID); err != nil {
s.log.Errorf("Error while syncing folder %s: %v", prjID, err)
}
if !exitImm {
@@ -147,7 +149,7 @@ func (s *APIService) buildMake(c *gin.Context) {
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.mfolder.IsFolderInSync(prjID); sync || err != nil {
+ if sync, err := s.mfolders.IsFolderInSync(prjID); sync || err != nil {
if err != nil {
s.log.Errorf("ERROR IsFolderInSync (%s): %v", prjID, err)
}
@@ -159,7 +161,7 @@ func (s *APIService) buildMake(c *gin.Context) {
// FIXME replace by .BroadcastTo a room
e := (*so).Emit(MakeExitEvent, MakeExitMsg{
- CmdID: strconv.Itoa(id),
+ CmdID: id,
Timestamp: time.Now().String(),
Code: code,
Error: err,
@@ -169,7 +171,7 @@ func (s *APIService) buildMake(c *gin.Context) {
}
}
- cmdID := makeCommandID
+ cmdID := strconv.Itoa(makeCommandID)
makeCommandID++
cmd := []string{}
@@ -179,7 +181,7 @@ func (s *APIService) buildMake(c *gin.Context) {
cmd = append(cmd, "&&")
}
- cmd = append(cmd, "cd", prj.GetFullPath(args.RPath), "&&", "make")
+ cmd = append(cmd, "cd", folder.GetFullPath(args.RPath), "&&", "make")
if len(args.Args) > 0 {
cmd = append(cmd, args.Args...)
}
diff --git a/lib/crosssdk/sdks.go b/lib/crosssdk/sdks.go
index 35a9998..0da0d1b 100644
--- a/lib/crosssdk/sdks.go
+++ b/lib/crosssdk/sdks.go
@@ -36,6 +36,9 @@ func Init(cfg *xdsconfig.Config, log *logrus.Logger) (*SDKs, error) {
defer s.mutex.Unlock()
for _, d := range dirs {
+ if !common.IsDir(d) {
+ continue
+ }
sdk, err := NewCrossSDK(d)
if err != nil {
log.Debugf("Error while processing SDK dir=%s, err=%s", d, err.Error())
diff --git a/lib/folder/folder-interface.go b/lib/folder/folder-interface.go
new file mode 100644
index 0000000..c04cbd7
--- /dev/null
+++ b/lib/folder/folder-interface.go
@@ -0,0 +1,68 @@
+package folder
+
+// FolderType definition
+type FolderType int
+
+const (
+ TypePathMap = 1
+ TypeCloudSync = 2
+ TypeCifsSmb = 3
+)
+
+// Folder Status definition
+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 {
+ 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
+type FolderConfig struct {
+ ID string `json:"id"`
+ Label string `json:"label"`
+ 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
+ RootPath string `json:"-"`
+
+ // FIXME: better to define an equivalent to union data and then implement
+ // UnmarshalJSON/MarshalJSON to decode/encode according to Type value
+ // Data interface{} `json:"data"`
+
+ // Specific data depending on which Type is used
+ DataPathMap PathMapConfig `json:"dataPathMap,omitempty"`
+ DataCloudSync CloudSyncConfig `json:"dataCloudSync,omitempty"`
+}
+
+// PathMapConfig Path mapping specific data
+type PathMapConfig struct {
+ ServerPath string `json:"serverPath"`
+}
+
+// CloudSyncConfig CloudSync (AKA Syncthing) specific data
+type CloudSyncConfig struct {
+ SyncThingID string `json:"syncThingID"`
+ BuilderSThgID string `json:"builderSThgID"`
+}
diff --git a/lib/folder/folder-pathmap.go b/lib/folder/folder-pathmap.go
new file mode 100644
index 0000000..f73f271
--- /dev/null
+++ b/lib/folder/folder-pathmap.go
@@ -0,0 +1,115 @@
+package folder
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ 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
+
+// PathMap .
+type PathMap struct {
+ globalConfig *xdsconfig.Config
+ config FolderConfig
+}
+
+// NewFolderPathMap Create a new instance of PathMap
+func NewFolderPathMap(gc *xdsconfig.Config) *PathMap {
+ f := PathMap{
+ globalConfig: gc,
+ }
+ 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 == "" {
+ 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.globalConfig.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)
+ }
+ file, err := ioutil.TempFile(dir, "xds_pathmap_check")
+ if err != nil {
+ return nil, fmt.Errorf("ServerPath sanity check error: %s", err.Error())
+ }
+ defer os.Remove(file.Name())
+
+ msg := "sanity check PathMap Add folder"
+ n, err := file.Write([]byte(msg))
+ if err != nil || n != len(msg) {
+ return nil, fmt.Errorf("ServerPath sanity check error: %s", err.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
+}
+
+// GetConfig Get public part of folder config
+func (f *PathMap) GetConfig() FolderConfig {
+ return f.config
+}
+
+// GetFullPath returns the full path
+func (f *PathMap) GetFullPath(dir string) string {
+ if &dir == nil {
+ return f.config.DataPathMap.ServerPath
+ }
+ return filepath.Join(f.config.DataPathMap.ServerPath, dir)
+}
+
+// Remove a folder
+func (f *PathMap) Remove() error {
+ // nothing to do
+ 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
+}
+
+// IsInSync Check if folder files are in-sync
+func (f *PathMap) IsInSync() (bool, error) {
+ return true, nil
+}
diff --git a/lib/model/folder.go b/lib/model/folder.go
deleted file mode 100644
index 56a46b1..0000000
--- a/lib/model/folder.go
+++ /dev/null
@@ -1,110 +0,0 @@
-package model
-
-import (
- "fmt"
-
- common "github.com/iotbzh/xds-common/golib"
- "github.com/iotbzh/xds-server/lib/syncthing"
- "github.com/iotbzh/xds-server/lib/xdsconfig"
-)
-
-// Folder Represent a an XDS folder
-type Folder struct {
- Conf *xdsconfig.Config
- SThg *st.SyncThing
-}
-
-// NewFolder Create a new instance of Model Folder
-func NewFolder(cfg *xdsconfig.Config, st *st.SyncThing) *Folder {
- return &Folder{
- Conf: cfg,
- SThg: st,
- }
-}
-
-// GetFolderFromID retrieves the Folder config from id
-func (c *Folder) GetFolderFromID(id string) *xdsconfig.FolderConfig {
- if idx := c.Conf.Folders.GetIdx(id); idx != -1 {
- return &c.Conf.Folders[idx]
- }
- return nil
-}
-
-// UpdateAll updates all the current configuration
-func (c *Folder) UpdateAll(newCfg xdsconfig.Config) error {
- return fmt.Errorf("Not Supported")
- /*
- if err := VerifyConfig(newCfg); err != nil {
- return err
- }
-
- // TODO: c.Builder = c.Builder.Update(newCfg.Builder)
- c.Folders = c.Folders.Update(newCfg.Folders)
-
- // FIXME To be tested & improved error handling
- for _, f := range c.Folders {
- if err := c.SThg.FolderChange(st.FolderChangeArg{
- ID: f.ID,
- Label: f.Label,
- RelativePath: f.RelativePath,
- SyncThingID: f.SyncThingID,
- ShareRootDir: c.FileConf.ShareRootDir,
- }); err != nil {
- return err
- }
- }
-
- return nil
- */
-}
-
-// UpdateFolder updates a specific folder into the current configuration
-func (c *Folder) UpdateFolder(newFolder xdsconfig.FolderConfig) (xdsconfig.FolderConfig, error) {
- // rootPath should not be empty
- if newFolder.RootPath == "" {
- newFolder.RootPath = c.Conf.FileConf.ShareRootDir
- }
-
- // Sanity check of folder settings
- if err := newFolder.Verify(); err != nil {
- return xdsconfig.FolderConfig{}, err
- }
-
- // Normalize path (needed for Windows path including bashlashes)
- newFolder.RelativePath = common.PathNormalize(newFolder.RelativePath)
-
- // Update config folder
- c.Conf.Folders = c.Conf.Folders.Update(xdsconfig.FoldersConfig{newFolder})
-
- // Update Syncthing folder
- err := c.SThg.FolderChange(newFolder)
-
- newFolder.BuilderSThgID = c.Conf.Builder.SyncThingID // FIXME - should be removed after local ST config rework
- newFolder.Status = xdsconfig.FolderStatusEnable
-
- return newFolder, err
-}
-
-// DeleteFolder deletes a specific folder
-func (c *Folder) DeleteFolder(id string) (xdsconfig.FolderConfig, error) {
- var fld xdsconfig.FolderConfig
- var err error
-
- if err = c.SThg.FolderDelete(id); err != nil {
- return fld, err
- }
-
- c.Conf.Folders, fld, err = c.Conf.Folders.Delete(id)
-
- return fld, err
-}
-
-// ForceSync Force the synchronization of a folder
-func (c *Folder) ForceSync(id string) error {
- return c.SThg.FolderScan(id, "")
-}
-
-// IsFolderInSync Returns true when folder is in sync
-func (c *Folder) IsFolderInSync(id string) (bool, error) {
- return c.SThg.IsFolderInSync(id)
-}
diff --git a/lib/model/folders.go b/lib/model/folders.go
new file mode 100644
index 0000000..ed0078e
--- /dev/null
+++ b/lib/model/folders.go
@@ -0,0 +1,388 @@
+package model
+
+import (
+ "encoding/xml"
+ "fmt"
+ "log"
+ "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"
+ "github.com/syncthing/syncthing/lib/sync"
+)
+
+// Folders Represent a an XDS folders
+type Folders struct {
+ fileOnDisk string
+ Conf *xdsconfig.Config
+ 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
+var fcMutex = sync.NewMutex()
+var ffMutex = sync.NewMutex()
+
+// FoldersNew Create a new instance of Model Folders
+func FoldersNew(cfg *xdsconfig.Config, st *st.SyncThing) *Folders {
+ file, _ := xdsconfig.FoldersConfigFilenameGet()
+ return &Folders{
+ fileOnDisk: file,
+ Conf: cfg,
+ Log: cfg.Log,
+ SThg: st,
+ folders: make(map[string]*folder.IFOLDER),
+ registerCB: []RegisteredCB{},
+ }
+}
+
+// LoadConfig Load folders configuration from disk
+func (f *Folders) LoadConfig() error {
+ var flds []folder.FolderConfig
+ var stFlds []folder.FolderConfig
+
+ // load from disk
+ if f.Conf.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())
+ }
+ }
+
+ // 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 != folder.TypeCloudSync {
+ flds[i].Status = folder.StatusErrorConfig
+ }
+ break
+ }
+ }
+ // add it
+ if !found {
+ flds = append(flds, stf)
+ }
+ }
+
+ // Detect ghost project
+ // (IOW existing in xds file config and not in syncthing database)
+ for i, xf := range flds {
+ // only for syncthing project
+ if xf.Type != folder.TypeCloudSync {
+ continue
+ }
+ found := false
+ for _, stf := range stFlds {
+ if stf.ID == xf.ID {
+ found = true
+ break
+ }
+ }
+ if !found {
+ flds[i].Status = folder.StatusErrorConfig
+ }
+ }
+
+ // Update folders
+ f.Log.Infof("Loading initial folders config: %d folders found", len(flds))
+ for _, fc := range flds {
+ if _, err := f.createUpdate(fc, false, 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())
+}
+
+// Get returns the folder config or nil if not existing
+func (f *Folders) Get(id string) *folder.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() []folder.FolderConfig {
+ fcMutex.Lock()
+ defer fcMutex.Unlock()
+
+ return f.getConfigArrUnsafe()
+}
+
+// getConfigArrUnsafe Same as GetConfigArr without mutex protection
+func (f *Folders) getConfigArrUnsafe() []folder.FolderConfig {
+ var conf []folder.FolderConfig
+
+ for _, v := range f.folders {
+ conf = append(conf, (*v).GetConfig())
+ }
+ return conf
+}
+
+// Add adds a new folder
+func (f *Folders) Add(newF folder.FolderConfig) (*folder.FolderConfig, error) {
+ return f.createUpdate(newF, true, false)
+}
+
+// CreateUpdate creates or update a folder
+func (f *Folders) createUpdate(newF folder.FolderConfig, create bool, initial bool) (*folder.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 folder.IFOLDER
+ switch newF.Type {
+ // SYNCTHING
+ case folder.TypeCloudSync:
+ if f.SThg == nil {
+ return nil, fmt.Errorf("ClownSync type not supported (syncthing not initialized)")
+ }
+ fld = f.SThg.NewFolderST(f.Conf)
+ // PATH MAP
+ case folder.TypePathMap:
+ fld = folder.NewFolderPathMap(f.Conf)
+ default:
+ 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)
+
+ // Add new folder
+ newFolder, err := fld.Add(newF)
+ if err != nil {
+ newF.Status = folder.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) (folder.FolderConfig, error) {
+ var err error
+
+ fcMutex.Lock()
+ defer fcMutex.Unlock()
+
+ fld := folder.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
+}
+
+// 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)
+ 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 []folder.FolderConfig `xml:"folders"`
+}
+
+// foldersConfigRead reads folders config from disk
+func foldersConfigRead(file string, folders *[]folder.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 []folder.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/syncthing/folder-st.go b/lib/syncthing/folder-st.go
new file mode 100644
index 0000000..da27062
--- /dev/null
+++ b/lib/syncthing/folder-st.go
@@ -0,0 +1,170 @@
+package st
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "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"
+)
+
+// IFOLDER interface implementation for syncthing
+
+// STFolder .
+type STFolder struct {
+ 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
+func (s *SyncThing) NewFolderST(gc *xdsconfig.Config) *STFolder {
+ return &STFolder{
+ globalConfig: gc,
+ st: s,
+ }
+}
+
+// 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) {
+
+ // 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.globalConfig.FileConf.ShareRootDir
+ }
+
+ f.fConfig = cfg
+
+ f.fConfig.DataCloudSync.BuilderSThgID = f.st.MyID // FIXME - should be removed after local ST config rework
+
+ // Update Syncthing folder
+ // (expect if status is ErrorConfig)
+ // TODO: add cache to avoid multiple requests on startup
+ if f.fConfig.Status != folder.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 = folder.StatusErrorConfig
+ 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
+ }
+
+ return &f.fConfig, nil
+}
+
+// GetConfig Get public part of folder config
+func (f *STFolder) GetConfig() folder.FolderConfig {
+ return f.fConfig
+}
+
+// GetFullPath returns the full path
+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)
+}
+
+// Remove a folder
+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, "")
+}
+
+// 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 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 3380cda..5086994 100644
--- a/lib/syncthing/st.go
+++ b/lib/syncthing/st.go
@@ -27,11 +27,13 @@ import (
// SyncThing .
type SyncThing struct {
- BaseURL string
- APIKey string
- Home string
- STCmd *exec.Cmd
- STICmd *exec.Cmd
+ BaseURL string
+ APIKey string
+ Home string
+ STCmd *exec.Cmd
+ STICmd *exec.Cmd
+ MyID string
+ Connected bool
// Private fields
binDir string
@@ -41,6 +43,7 @@ type SyncThing struct {
conf *xdsconfig.Config
client *common.HTTPClient
log *logrus.Logger
+ Events *Events
}
// ExitChan Channel used for process exit
@@ -125,6 +128,9 @@ func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
conf: conf,
}
+ // Create Events monitoring
+ s.Events = s.NewEventListener()
+
return &s
}
@@ -211,13 +217,13 @@ func (s *SyncThing) Start() (*exec.Cmd, error) {
env := []string{
"STNODEFAULTFOLDER=1",
"STNOUPGRADE=1",
- "STNORESTART=1",
+ "STNORESTART=1", // FIXME SEB remove ?
}
s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
// Use autogenerated apikey if not set by config.json
- if s.APIKey == "" {
+ if err == nil && s.APIKey == "" {
if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
defer fd.Close()
if b, err := ioutil.ReadAll(fd); err == nil {
@@ -296,6 +302,7 @@ func (s *SyncThing) StopInotify() {
// Connect Establish HTTP connection with Syncthing
func (s *SyncThing) Connect() error {
var err error
+ s.Connected = false
s.client, err = common.HTTPNewClient(s.BaseURL,
common.HTTPClientConfig{
URLPrefix: "/rest",
@@ -312,9 +319,22 @@ func (s *SyncThing) Connect() error {
return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
}
- s.client.SetLogger(s.log)
+ // Redirect HTTP log into a file
+ s.client.SetLogLevel(s.conf.Log.Level.String())
+ s.client.LoggerPrefix = "SYNCTHING: "
+ s.client.LoggerOut = s.conf.LogVerboseOut
+
+ s.MyID, err = s.IDGet()
+ if err != nil {
+ return fmt.Errorf("ERROR: cannot retrieve ID")
+ }
+
+ s.Connected = true
+
+ // Start events monitoring
+ err = s.Events.Start()
- return nil
+ return err
}
// IDGet returns the Syncthing ID of Syncthing instance running locally
@@ -360,44 +380,3 @@ func (s *SyncThing) IsConfigInSync() (bool, error) {
}
return d.ConfigInSync, nil
}
-
-// FolderStatus Returns all information about the current
-func (s *SyncThing) FolderStatus(folderID string) (*FolderStatus, error) {
- var data []byte
- var res FolderStatus
- if folderID == "" {
- return nil, fmt.Errorf("folderID not set")
- }
- if err := s.client.HTTPGet("db/status?folder="+folderID, &data); err != nil {
- return nil, err
- }
- if err := json.Unmarshal(data, &res); err != nil {
- return nil, err
- }
- return &res, nil
-}
-
-// 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
-}
-
-// FolderScan Request immediate folder scan.
-// Scan all folders if folderID param is empty
-func (s *SyncThing) FolderScan(folderID string, subpath string) error {
- url := "db/scan"
- if folderID != "" {
- url += "?folder=" + folderID
-
- if subpath != "" {
- url += "&sub=" + subpath
- }
- }
- return s.client.HTTPPost(url, "")
-}
diff --git a/lib/syncthing/stEvent.go b/lib/syncthing/stEvent.go
new file mode 100644
index 0000000..9ca8b78
--- /dev/null
+++ b/lib/syncthing/stEvent.go
@@ -0,0 +1,265 @@
+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
+ cntErrConn := 0
+ cntErrRetry := 1
+ for {
+ select {
+ case <-e.stop:
+ e.log.Infof("Event monitoring exited")
+ return
+
+ case <-time.After(e.MonitorTime * time.Millisecond):
+
+ if !e.st.Connected {
+ cntErrConn++
+ time.Sleep(time.Second)
+ if cntErrConn > cntErrRetry {
+ e.log.Error("ST Event monitor: ST connection down")
+ cntErrConn = 0
+ cntErrRetry *= 2
+ if _, err := e.getEvents(since); err == nil {
+ e.st.Connected = true
+ cntErrRetry = 1
+ // XXX - should we reset since value ?
+ goto readEvent
+ }
+ }
+ continue
+ }
+
+ readEvent:
+ stEvArr, err := e.getEvents(since)
+ if err != nil {
+ e.log.Errorf("Syncthing Get Events: %v", err)
+ e.st.Connected = false
+ 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 661e19d..70ac70a 100644
--- a/lib/syncthing/stfolder.go
+++ b/lib/syncthing/stfolder.go
@@ -1,34 +1,77 @@
package st
import (
+ "encoding/json"
+ "fmt"
"path/filepath"
"strings"
- "github.com/iotbzh/xds-server/lib/xdsconfig"
+ "github.com/iotbzh/xds-server/lib/folder"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/protocol"
)
+// FolderLoadFromStConfig Load/Retrieve folder config from syncthing database
+func (s *SyncThing) FolderLoadFromStConfig(f *[]folder.FolderConfig) error {
+
+ defaultSdk := "" // cannot know which was the default sdk
+
+ stCfg, err := s.ConfigGet()
+ if err != nil {
+ return err
+ }
+ if len(stCfg.Devices) < 1 {
+ return fmt.Errorf("Cannot load syncthing config: no device defined")
+ }
+ devID := stCfg.Devices[0].DeviceID.String()
+ if devID == s.MyID {
+ if len(stCfg.Devices) < 2 {
+ return fmt.Errorf("Cannot load syncthing config: no valid device found")
+ }
+ devID = stCfg.Devices[1].DeviceID.String()
+ }
+
+ for _, stFld := range stCfg.Folders {
+ cliPath := strings.TrimPrefix(stFld.RawPath, s.conf.FileConf.ShareRootDir)
+ if cliPath == "" {
+ cliPath = stFld.RawPath
+ }
+ *f = append(*f, folder.FolderConfig{
+ ID: stFld.ID,
+ Label: stFld.Label,
+ ClientPath: strings.TrimRight(cliPath, "/"),
+ Type: folder.TypeCloudSync,
+ Status: folder.StatusDisable,
+ DefaultSdk: defaultSdk,
+ RootPath: s.conf.FileConf.ShareRootDir,
+ DataCloudSync: folder.CloudSyncConfig{SyncThingID: devID},
+ })
+ }
+
+ return nil
+}
+
// FolderChange is called when configuration has changed
-func (s *SyncThing) FolderChange(f xdsconfig.FolderConfig) error {
+func (s *SyncThing) FolderChange(f folder.FolderConfig) (string, error) {
// Get current config
stCfg, err := s.ConfigGet()
if err != nil {
s.log.Errorln(err)
- return err
+ return "", err
}
+ stClientID := f.DataCloudSync.SyncThingID
// Add new Device if needed
var devID protocol.DeviceID
- if err := devID.UnmarshalText([]byte(f.SyncThingID)); err != nil {
- s.log.Errorf("not a valid device id (err %v)\n", err)
- return err
+ if err := devID.UnmarshalText([]byte(stClientID)); err != nil {
+ s.log.Errorf("not a valid device id (err %v)", err)
+ return "", err
}
newDevice := config.DeviceConfiguration{
DeviceID: devID,
- Name: f.SyncThingID,
+ Name: stClientID,
Addresses: []string{"dynamic"},
}
@@ -49,13 +92,13 @@ func (s *SyncThing) FolderChange(f xdsconfig.FolderConfig) error {
label = strings.Split(id, "/")[0]
}
if id = f.ID; id == "" {
- id = f.SyncThingID[0:15] + "_" + label
+ id = stClientID[0:15] + "_" + label
}
folder := config.FolderConfiguration{
ID: id,
Label: label,
- RawPath: filepath.Join(s.conf.FileConf.ShareRootDir, f.RelativePath),
+ RawPath: filepath.Join(s.conf.FileConf.ShareRootDir, f.ClientPath),
}
if s.conf.FileConf.SThgConf.RescanIntervalS > 0 {
@@ -85,7 +128,7 @@ func (s *SyncThing) FolderChange(f xdsconfig.FolderConfig) error {
s.log.Errorln(err)
}
- return nil
+ return id, nil
}
// FolderDelete is called to delete a folder config
@@ -110,3 +153,61 @@ func (s *SyncThing) FolderDelete(id string) error {
return nil
}
+
+// FolderConfigGet Returns the configuration of a specific folder
+func (s *SyncThing) FolderConfigGet(folderID string) (config.FolderConfiguration, error) {
+ fc := config.FolderConfiguration{}
+ if folderID == "" {
+ return fc, fmt.Errorf("folderID not set")
+ }
+ cfg, err := s.ConfigGet()
+ if err != nil {
+ return fc, err
+ }
+ for _, f := range cfg.Folders {
+ if f.ID == folderID {
+ fc = f
+ return fc, nil
+ }
+ }
+ return fc, fmt.Errorf("id not found")
+}
+
+// FolderStatus Returns all information about the current
+func (s *SyncThing) FolderStatus(folderID string) (*FolderStatus, error) {
+ var data []byte
+ var res FolderStatus
+ if folderID == "" {
+ return nil, fmt.Errorf("folderID not set")
+ }
+ if err := s.client.HTTPGet("db/status?folder="+folderID, &data); err != nil {
+ return nil, err
+ }
+ if err := json.Unmarshal(data, &res); err != nil {
+ return nil, err
+ }
+ return &res, nil
+}
+
+// IsFolderInSync Returns true when folder is in sync
+func (s *SyncThing) IsFolderInSync(folderID string) (bool, error) {
+ sts, err := s.FolderStatus(folderID)
+ if err != nil {
+ return false, err
+ }
+ return sts.NeedBytes == 0 && sts.State == "idle", nil
+}
+
+// FolderScan Request immediate folder scan.
+// Scan all folders if folderID param is empty
+func (s *SyncThing) FolderScan(folderID string, subpath string) error {
+ url := "db/scan"
+ if folderID != "" {
+ url += "?folder=" + folderID
+
+ if subpath != "" {
+ url += "&sub=" + subpath
+ }
+ }
+ return s.client.HTTPPost(url, "")
+}
diff --git a/lib/webserver/server.go b/lib/webserver/server.go
index 8fd7e44..8639b66 100644
--- a/lib/webserver/server.go
+++ b/lib/webserver/server.go
@@ -2,6 +2,7 @@ package webserver
import (
"fmt"
+ "log"
"net/http"
"os"
@@ -26,7 +27,7 @@ type Server struct {
webApp *gin.RouterGroup
cfg *xdsconfig.Config
sessions *session.Sessions
- mfolder *model.Folder
+ mfolders *model.Folders
sdks *crosssdk.SDKs
log *logrus.Logger
stop chan struct{} // signals intentional stop
@@ -36,20 +37,21 @@ const indexFilename = "index.html"
const cookieMaxAge = "3600"
// New creates an instance of Server
-func New(cfg *xdsconfig.Config, mfolder *model.Folder, sdks *crosssdk.SDKs, log *logrus.Logger) *Server {
+func New(cfg *xdsconfig.Config, mfolders *model.Folders, sdks *crosssdk.SDKs, logr *logrus.Logger) *Server {
// Setup logging for gin router
- if log.Level == logrus.DebugLevel {
+ if logr.Level == logrus.DebugLevel {
gin.SetMode(gin.DebugMode)
} else {
gin.SetMode(gin.ReleaseMode)
}
- // TODO
- // - try to bind gin DefaultWriter & DefaultErrorWriter to logrus logger
- // - try to fix pb about isTerminal=false when out is in VSC Debug Console
- //gin.DefaultWriter = ??
- //gin.DefaultErrorWriter = ??
+ // Redirect gin logs into another logger (LogVerboseOut may be stderr or a file)
+ gin.DefaultWriter = cfg.LogVerboseOut
+ gin.DefaultErrorWriter = cfg.LogVerboseOut
+ log.SetOutput(cfg.LogVerboseOut)
+
+ // FIXME - fix pb about isTerminal=false when out is in VSC Debug Console
// Creates gin router
r := gin.New()
@@ -61,9 +63,9 @@ func New(cfg *xdsconfig.Config, mfolder *model.Folder, sdks *crosssdk.SDKs, log
webApp: nil,
cfg: cfg,
sessions: nil,
- mfolder: mfolder,
+ mfolders: mfolders,
sdks: sdks,
- log: log,
+ log: logr,
stop: make(chan struct{}),
}
@@ -84,7 +86,7 @@ func (s *Server) Serve() error {
s.sessions = session.NewClientSessions(s.router, s.log, cookieMaxAge)
// Create REST API
- s.api = apiv1.New(s.router, s.sessions, s.cfg, s.mfolder, s.sdks)
+ s.api = apiv1.New(s.router, s.sessions, s.cfg, s.mfolders, s.sdks)
// Websocket routes
s.sIOServer, err = socketio.NewServer(nil)
diff --git a/lib/xdsconfig/config.go b/lib/xdsconfig/config.go
index f2d0710..82ca97f 100644
--- a/lib/xdsconfig/config.go
+++ b/lib/xdsconfig/config.go
@@ -2,7 +2,7 @@ package xdsconfig
import (
"fmt"
-
+ "io"
"os"
"github.com/Sirupsen/logrus"
@@ -16,11 +16,20 @@ type Config struct {
APIVersion string `json:"apiVersion"`
VersionGitTag string `json:"gitTag"`
Builder BuilderConfig `json:"builder"`
- Folders FoldersConfig `json:"folders"`
// Private (un-exported fields in REST GET /config route)
- FileConf FileConfig `json:"-"`
- Log *logrus.Logger `json:"-"`
+ Options Options `json:"-"`
+ FileConf FileConfig `json:"-"`
+ Log *logrus.Logger `json:"-"`
+ LogVerboseOut io.Writer `json:"-"`
+}
+
+// Options set at the command line
+type Options struct {
+ ConfigFile string
+ LogLevel string
+ LogFile string
+ NoFolderConfig bool
}
// Config default values
@@ -41,7 +50,13 @@ func Init(cliCtx *cli.Context, log *logrus.Logger) (*Config, error) {
APIVersion: DefaultAPIVersion,
VersionGitTag: cliCtx.App.Metadata["git-tag"].(string),
Builder: BuilderConfig{},
- Folders: FoldersConfig{},
+
+ Options: Options{
+ ConfigFile: cliCtx.GlobalString("config"),
+ LogLevel: cliCtx.GlobalString("log"),
+ LogFile: cliCtx.GlobalString("logfile"),
+ NoFolderConfig: cliCtx.GlobalBool("no-folderconfig"),
+ },
FileConf: FileConfig{
WebAppDir: "webapp/dist",
ShareRootDir: DefaultShareDir,
@@ -52,7 +67,7 @@ func Init(cliCtx *cli.Context, log *logrus.Logger) (*Config, error) {
}
// config file settings overwrite default config
- err = updateConfigFromFile(&c, cliCtx.GlobalString("config"))
+ err = readGlobalConfig(&c, c.Options.ConfigFile)
if err != nil {
return nil, err
}
diff --git a/lib/xdsconfig/fileconfig.go b/lib/xdsconfig/fileconfig.go
index 90c1aad..2dbf884 100644
--- a/lib/xdsconfig/fileconfig.go
+++ b/lib/xdsconfig/fileconfig.go
@@ -11,6 +11,16 @@ import (
common "github.com/iotbzh/xds-common/golib"
)
+const (
+ // ConfigDir Directory in user HOME directory where xds config will be saved
+ ConfigDir = ".xds"
+ // GlobalConfigFilename Global config filename
+ GlobalConfigFilename = "config.json"
+ // FoldersConfigFilename Folders config filename
+ FoldersConfigFilename = "server-config_folders.xml"
+)
+
+// SyncThingConf definition
type SyncThingConf struct {
BinDir string `json:"binDir"`
Home string `json:"home"`
@@ -19,6 +29,7 @@ type SyncThingConf struct {
RescanIntervalS int `json:"rescanIntervalS"`
}
+// FileConfig is the JSON structure of xds-server config file (config.json)
type FileConfig struct {
WebAppDir string `json:"webAppDir"`
ShareRootDir string `json:"shareRootDir"`
@@ -28,21 +39,21 @@ type FileConfig struct {
LogsDir string `json:"logsDir"`
}
-// getConfigFromFile reads configuration from a config file.
+// readGlobalConfig reads configuration from a config file.
// Order to determine which config file is used:
// 1/ from command line option: "--config myConfig.json"
// 2/ $HOME/.xds/config.json file
// 3/ <current_dir>/config.json file
// 4/ <xds-server executable dir>/config.json file
-
-func updateConfigFromFile(c *Config, confFile string) error {
+func readGlobalConfig(c *Config, confFile string) error {
searchIn := make([]string, 0, 3)
if confFile != "" {
searchIn = append(searchIn, confFile)
}
if usr, err := user.Current(); err == nil {
- searchIn = append(searchIn, path.Join(usr.HomeDir, ".xds", "config.json"))
+ searchIn = append(searchIn, path.Join(usr.HomeDir, ConfigDir,
+ GlobalConfigFilename))
}
cwd, err := os.Getwd()
if err == nil {
@@ -70,7 +81,6 @@ func updateConfigFromFile(c *Config, confFile string) error {
// TODO move on viper package to support comments in JSON and also
// bind with flags (command line options)
// see https://github.com/spf13/viper#working-with-flags
-
fd, _ := os.Open(*cFile)
defer fd.Close()
fCfg := FileConfig{}
@@ -79,14 +89,15 @@ func updateConfigFromFile(c *Config, confFile string) error {
}
// Support environment variables (IOW ${MY_ENV_VAR} syntax) in config.json
- for _, field := range []*string{
+ vars := []*string{
&fCfg.WebAppDir,
&fCfg.ShareRootDir,
&fCfg.SdkRootDir,
- &fCfg.LogsDir,
- &fCfg.SThgConf.Home,
- &fCfg.SThgConf.BinDir} {
-
+ &fCfg.LogsDir}
+ if fCfg.SThgConf != nil {
+ vars = append(vars, &fCfg.SThgConf.Home, &fCfg.SThgConf.BinDir)
+ }
+ for _, field := range vars {
var err error
if *field, err = common.ResolveEnvVar(*field); err != nil {
return err
@@ -123,3 +134,12 @@ func updateConfigFromFile(c *Config, confFile string) error {
c.FileConf = fCfg
return nil
}
+
+// FoldersConfigFilenameGet
+func FoldersConfigFilenameGet() (string, error) {
+ usr, err := user.Current()
+ if err != nil {
+ return "", err
+ }
+ return path.Join(usr.HomeDir, ConfigDir, FoldersConfigFilename), nil
+}
diff --git a/lib/xdsconfig/folderconfig.go b/lib/xdsconfig/folderconfig.go
deleted file mode 100644
index bb2b56f..0000000
--- a/lib/xdsconfig/folderconfig.go
+++ /dev/null
@@ -1,85 +0,0 @@
-package xdsconfig
-
-import (
- "fmt"
- "log"
- "path/filepath"
-)
-
-// FolderType constances
-const (
- FolderTypeDocker = 0
- FolderTypeWindowsSubsystem = 1
- FolderTypeCloudSync = 2
-
- FolderStatusErrorConfig = "ErrorConfig"
- FolderStatusDisable = "Disable"
- FolderStatusEnable = "Enable"
-)
-
-// FolderType is the type of sharing folder
-type FolderType int
-
-// FolderConfig is the config for one folder
-type FolderConfig struct {
- ID string `json:"id" binding:"required"`
- Label string `json:"label"`
- RelativePath string `json:"path"`
- Type FolderType `json:"type"`
- SyncThingID string `json:"syncThingID"`
- BuilderSThgID string `json:"builderSThgID"`
- Status string `json:"status"`
- DefaultSdk string `json:"defaultSdk"`
-
- // Not exported fields
- RootPath string `json:"-"`
-}
-
-// NewFolderConfig creates a new folder object
-func NewFolderConfig(id, label, rootDir, path string, defaultSdk string) FolderConfig {
- return FolderConfig{
- ID: id,
- Label: label,
- RelativePath: path,
- Type: FolderTypeCloudSync,
- SyncThingID: "",
- Status: FolderStatusDisable,
- RootPath: rootDir,
- DefaultSdk: defaultSdk,
- }
-}
-
-// GetFullPath returns the full path
-func (c *FolderConfig) GetFullPath(dir string) string {
- if &dir == nil {
- dir = ""
- }
- if filepath.IsAbs(dir) {
- return filepath.Join(c.RootPath, dir)
- }
- return filepath.Join(c.RootPath, c.RelativePath, dir)
-}
-
-// Verify is called to verify that a configuration is valid
-func (c *FolderConfig) Verify() error {
- var err error
-
- if c.Type != FolderTypeCloudSync {
- err = fmt.Errorf("Unsupported folder type")
- }
-
- if c.SyncThingID == "" {
- err = fmt.Errorf("device id not set (SyncThingID field)")
- }
-
- if c.RootPath == "" {
- err = fmt.Errorf("RootPath must not be empty")
- }
-
- if err != nil {
- c.Status = FolderStatusErrorConfig
- log.Printf("ERROR Verify: %v\n", err)
- }
-
- return err
-}
diff --git a/lib/xdsconfig/foldersconfig.go b/lib/xdsconfig/foldersconfig.go
deleted file mode 100644
index 4ad16df..0000000
--- a/lib/xdsconfig/foldersconfig.go
+++ /dev/null
@@ -1,47 +0,0 @@
-package xdsconfig
-
-import (
- "fmt"
-)
-
-// FoldersConfig contains all the folder configurations
-type FoldersConfig []FolderConfig
-
-// GetIdx returns the index of the folder matching id in FoldersConfig array
-func (c FoldersConfig) GetIdx(id string) int {
- for i := range c {
- if id == c[i].ID {
- return i
- }
- }
- return -1
-}
-
-// Update is used to fully update or add a new FolderConfig
-func (c FoldersConfig) Update(newCfg FoldersConfig) FoldersConfig {
- for i := range newCfg {
- found := false
- for j := range c {
- if newCfg[i].ID == c[j].ID {
- c[j] = newCfg[i]
- found = true
- break
- }
- }
- if !found {
- c = append(c, newCfg[i])
- }
- }
- return c
-}
-
-// Delete is used to delete a folder matching id in FoldersConfig array
-func (c FoldersConfig) Delete(id string) (FoldersConfig, FolderConfig, error) {
- if idx := c.GetIdx(id); idx != -1 {
- f := c[idx]
- c = append(c[:idx], c[idx+1:]...)
- return c, f, nil
- }
-
- return c, FolderConfig{}, fmt.Errorf("invalid id")
-}
diff --git a/main.go b/main.go
index fd1480e..4fd49e9 100644
--- a/main.go
+++ b/main.go
@@ -7,7 +7,7 @@ import (
"os"
"os/exec"
"os/signal"
- "strings"
+ "path/filepath"
"syscall"
"time"
@@ -47,7 +47,7 @@ type Context struct {
SThg *st.SyncThing
SThgCmd *exec.Cmd
SThgInotCmd *exec.Cmd
- MFolder *model.Folder
+ MFolders *model.Folders
SDKs *crosssdk.SDKs
WWWServer *webserver.Server
Exit chan os.Signal
@@ -98,7 +98,7 @@ func handlerSigTerm(ctx *Context) {
ctx.Log.Infof("Stoping Web server...")
ctx.WWWServer.Stop()
}
- os.Exit(1)
+ os.Exit(0)
}
// XDS Server application main routine
@@ -111,33 +111,60 @@ func xdsApp(cliCtx *cli.Context) error {
// Load config
cfg, err := xdsconfig.Init(ctx.Cli, ctx.Log)
if err != nil {
- return cli.NewExitError(err, 2)
+ return cli.NewExitError(err, -2)
}
ctx.Config = cfg
- // TODO allow to redirect stdout/sterr into logs file
- //logFilename := filepath.Join(ctx.Config.FileConf.LogsDir + "xds-server.log")
-
- // FIXME - add a builder interface and support other builder type (eg. native)
- builderType := "syncthing"
+ // Logs redirected into a file when logsDir is set
+ logfilename := cliCtx.GlobalString("logfile")
+ ctx.Config.LogVerboseOut = os.Stderr
+ if ctx.Config.FileConf.LogsDir != "" {
+ if logfilename != "stdout" {
+ if logfilename == "" {
+ logfilename = "xds-server.log"
+ }
+ // is it an absolute path ?
+ logFile := logfilename
+ if logfilename[0] == '.' || logfilename[0] != '/' {
+ logFile = filepath.Join(ctx.Config.FileConf.LogsDir, logfilename)
+ }
+ fmt.Printf("Logging file: %s\n", 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 cli.NewExitError(msgErr, int(syscall.EPERM))
+ }
+ ctx.Log.Out = fdL
+ }
- switch builderType {
- case "syncthing":
+ logFileHTTPReq := filepath.Join(ctx.Config.FileConf.LogsDir, "xds-server-verbose.log")
+ fmt.Printf("Logging file for HTTP requests: %s\n", logFileHTTPReq)
+ 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 cli.NewExitError(msgErr, int(syscall.EPERM))
+ }
+ ctx.Config.LogVerboseOut = fdLH
+ }
- // Start local instance of Syncthing and Syncthing-notify
+ // 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 cli.NewExitError(err, 2)
+ return cli.NewExitError(err, -4)
}
fmt.Printf("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 cli.NewExitError(err, 2)
+ return cli.NewExitError(err, -4)
}
fmt.Printf("Syncthing-inotify started (PID %d)\n", ctx.SThgInotCmd.Process.Pid)
@@ -156,64 +183,37 @@ func xdsApp(cliCtx *cli.Context) error {
retry--
}
if err != nil || retry == 0 {
- return cli.NewExitError(err, 2)
- }
-
- // Retrieve Syncthing config
- id, err := ctx.SThg.IDGet()
- if err != nil {
- return cli.NewExitError(err, 2)
- }
-
- if ctx.Config.Builder, err = xdsconfig.NewBuilderConfig(id); err != nil {
- return cli.NewExitError(err, 2)
- }
-
- // Retrieve initial Syncthing config
-
- // FIXME: cannot retrieve default SDK, need to save on disk or somewhere
- // else all config to be able to restore it.
- defaultSdk := ""
- stCfg, err := ctx.SThg.ConfigGet()
- if err != nil {
- return cli.NewExitError(err, 2)
+ return cli.NewExitError(err, -4)
}
- for _, stFld := range stCfg.Folders {
- relativePath := strings.TrimPrefix(stFld.RawPath, ctx.Config.FileConf.ShareRootDir)
- if relativePath == "" {
- relativePath = stFld.RawPath
- }
- newFld := xdsconfig.NewFolderConfig(stFld.ID,
- stFld.Label,
- ctx.Config.FileConf.ShareRootDir,
- strings.TrimRight(relativePath, "/"),
- defaultSdk)
- ctx.Config.Folders = ctx.Config.Folders.Update(xdsconfig.FoldersConfig{newFld})
+ // FIXME: do we still need Builder notion ? if no cleanup
+ if ctx.Config.Builder, err = xdsconfig.NewBuilderConfig(ctx.SThg.MyID); err != nil {
+ return cli.NewExitError(err, -4)
}
+ }
- // Init model folder
- ctx.MFolder = model.NewFolder(ctx.Config, ctx.SThg)
+ // Init model folder
+ ctx.MFolders = model.FoldersNew(ctx.Config, ctx.SThg)
- default:
- err = fmt.Errorf("Unsupported builder type")
- return cli.NewExitError(err, 3)
+ // Load initial folders config from disk
+ if err := ctx.MFolders.LoadConfig(); err != nil {
+ return cli.NewExitError(err, -5)
}
// Init cross SDKs
ctx.SDKs, err = crosssdk.Init(ctx.Config, ctx.Log)
if err != nil {
- return cli.NewExitError(err, 2)
+ return cli.NewExitError(err, -6)
}
// Create and start Web Server
- ctx.WWWServer = webserver.New(ctx.Config, ctx.MFolder, ctx.SDKs, ctx.Log)
+ ctx.WWWServer = webserver.New(ctx.Config, ctx.MFolders, ctx.SDKs, ctx.Log)
if err = ctx.WWWServer.Serve(); err != nil {
ctx.Log.Println(err)
- return cli.NewExitError(err, 3)
+ return cli.NewExitError(err, -7)
}
- return cli.NewExitError("Program exited ", 4)
+ return cli.NewExitError("Program exited ", -99)
}
// main
@@ -247,6 +247,17 @@ func main() {
Usage: "logging level (supported levels: panic, fatal, error, warn, info, debug)\n\t",
EnvVar: "LOG_LEVEL",
},
+ cli.StringFlag{
+ Name: "logfile",
+ Value: "stdout",
+ Usage: "filename where logs will be redirected (default stdout)\n\t",
+ EnvVar: "LOG_FILENAME",
+ },
+ cli.BoolFlag{
+ Name: "no-folderconfig, nfc",
+ Usage: fmt.Sprintf("Do not read folder config file (%s)\n\t", xdsconfig.FoldersConfigFilename),
+ EnvVar: "NO_FOLDERCONFIG",
+ },
}
// only one action: Web Server
diff --git a/scripts/xds-server-start.sh b/scripts/xds-server-start.sh
index dc108fe..7985759 100755
--- a/scripts/xds-server-start.sh
+++ b/scripts/xds-server-start.sh
@@ -73,3 +73,5 @@ if [ "$1" != "-dryrun" ]; then
pid_xds=$(jobs -p)
echo "pid=${pid_xds}"
fi
+
+exit 0
diff --git a/scripts/xds-server-stop.sh b/scripts/xds-server-stop.sh
index 8a6bf5e..674ed25 100755
--- a/scripts/xds-server-stop.sh
+++ b/scripts/xds-server-stop.sh
@@ -16,3 +16,4 @@ if [ "$nbProc" != "0" ]; then
pkill -KILL syncthing-inotify
fi
+exit 0
diff --git a/scripts/xds-utils/install-agl-sdks.sh b/scripts/xds-utils/install-agl-sdks.sh
index 9fbacbb..f230569 100755
--- a/scripts/xds-utils/install-agl-sdks.sh
+++ b/scripts/xds-utils/install-agl-sdks.sh
@@ -75,7 +75,7 @@ if [ "$FILE" = "" ]; then
exit 1
fi
SDK_FILE=${XDT_SDK}/${FILE}
-elif [ ! -f $FILE ]; then
+elif [ ! -f "$FILE" ]; then
echo "SDK file not found: $FILE"
exit 1
else
@@ -108,14 +108,14 @@ cleanExit ()
}
# Get SDK installer
-if [ ! -f ${SDK_FILE} ]; then
+if [ ! -f "${SDK_FILE}" ]; then
do_cleanup=true
- wget "$SDK_BASEURL/$FILE" -O ${SDK_FILE} || exit 1
+ wget "$SDK_BASEURL/$FILE" -O "${SDK_FILE}" || exit 1
fi
# Retreive default install dir to extract version
-offset=$(grep -na -m1 "^MARKER:$" ${SDK_FILE} | cut -d':' -f1)
-eval $(head -n $offset ${SDK_FILE} | grep ^DEFAULT_INSTALL_DIR= )
+offset=$(grep -na -m1 "^MARKER:$" "${SDK_FILE}" | cut -d':' -f1)
+eval $(head -n $offset "${SDK_FILE}" | grep ^DEFAULT_INSTALL_DIR= )
VERSION=$(basename $DEFAULT_INSTALL_DIR)
[ "$PROFILE" = "" ] && { echo "PROFILE is not set"; exit 1; }
diff --git a/webapp/assets/images/iot-bzh-logo-small.png b/webapp/assets/images/iot-bzh-logo-small.png
new file mode 100644
index 0000000..2c3b2ae
--- /dev/null
+++ b/webapp/assets/images/iot-bzh-logo-small.png
Binary files differ
diff --git a/webapp/src/app/app.component.css b/webapp/src/app/app.component.css
index 0ec4936..a47ad13 100644
--- a/webapp/src/app/app.component.css
+++ b/webapp/src/app/app.component.css
@@ -1,13 +1,18 @@
-.navbar-inverse {
- background-color: #330066;
+.navbar {
+ background-color: whitesmoke;
}
.navbar-brand {
- background: #330066;
- color: white;
font-size: x-large;
+ font-variant: small-caps;
+ color: #5a28a1;
}
+a.navbar-brand {
+ margin-top: 5px;
+}
+
+
.navbar-nav ul li a {
color: #fff;
}
@@ -15,3 +20,12 @@
.menu-text {
color: #fff;
}
+
+#logo-iot {
+ padding: 0 2px;
+ height: 60px;
+}
+
+li>a {
+ color:#5a28a1;
+}
diff --git a/webapp/src/app/app.component.html b/webapp/src/app/app.component.html
index b02dbe2..a889b12 100644
--- a/webapp/src/app/app.component.html
+++ b/webapp/src/app/app.component.html
@@ -1,4 +1,5 @@
-<nav class="navbar navbar-fixed-top navbar-inverse">
+<nav class="navbar navbar-fixed-top">
+ <!-- navbar-inverse"> -->
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#myNavbar"
@@ -7,6 +8,8 @@
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
+
+ <img class="navbar-brand" id="logo-iot" src="assets/images/iot-bzh-logo-small.png">
<a class="navbar-brand" href="#">X(cross) Development System Dashboard</a>
</div>
@@ -24,4 +27,4 @@
<div style="margin:10px;">
<router-outlet></router-outlet>
-</div> \ No newline at end of file
+</div>
diff --git a/webapp/src/app/app.module.ts b/webapp/src/app/app.module.ts
index 4877f6e..10ff7a4 100644
--- a/webapp/src/app/app.module.ts
+++ b/webapp/src/app/app.module.ts
@@ -10,6 +10,7 @@ import { ModalModule } from 'ngx-bootstrap/modal';
import { AccordionModule } from 'ngx-bootstrap/accordion';
import { CarouselModule } from 'ngx-bootstrap/carousel';
import { PopoverModule } from 'ngx-bootstrap/popover';
+import { CollapseModule } from 'ngx-bootstrap/collapse';
import { BsDropdownModule } from 'ngx-bootstrap/dropdown';
// Import the application components and services.
@@ -21,9 +22,11 @@ import { DlXdsAgentComponent, CapitalizePipe } from "./config/downloadXdsAgent.c
import { ProjectCardComponent } from "./projects/projectCard.component";
import { ProjectReadableTypePipe } from "./projects/projectCard.component";
import { ProjectsListAccordionComponent } from "./projects/projectsListAccordion.component";
+import { ProjectAddModalComponent} from "./projects/projectAddModal.component";
import { SdkCardComponent } from "./sdks/sdkCard.component";
import { SdksListAccordionComponent } from "./sdks/sdksListAccordion.component";
import { SdkSelectDropdownComponent } from "./sdks/sdkSelectDropdown.component";
+import { SdkAddModalComponent} from "./sdks/sdkAddModal.component";
import { HomeComponent } from "./home/home.component";
import { DevelComponent } from "./devel/devel.component";
@@ -52,6 +55,7 @@ import { SdkService } from "./services/sdk.service";
AccordionModule.forRoot(),
CarouselModule.forRoot(),
PopoverModule.forRoot(),
+ CollapseModule.forRoot(),
BsDropdownModule.forRoot(),
],
declarations: [
@@ -67,9 +71,11 @@ import { SdkService } from "./services/sdk.service";
ProjectCardComponent,
ProjectReadableTypePipe,
ProjectsListAccordionComponent,
+ ProjectAddModalComponent,
SdkCardComponent,
SdksListAccordionComponent,
SdkSelectDropdownComponent,
+ SdkAddModalComponent,
],
providers: [
AppRoutingProviders,
@@ -88,4 +94,4 @@ import { SdkService } from "./services/sdk.service";
bootstrap: [AppComponent]
})
export class AppModule {
-} \ No newline at end of file
+}
diff --git a/webapp/src/app/config/config.component.css b/webapp/src/app/config/config.component.css
index f480857..6412f9a 100644
--- a/webapp/src/app/config/config.component.css
+++ b/webapp/src/app/config/config.component.css
@@ -1,3 +1,8 @@
+.fa-big {
+ font-size: 20px;
+ font-weight: bold;
+}
+
.fa-size-x2 {
font-size: 20px;
}
@@ -23,4 +28,8 @@ tr.info>th {
tr.info>td {
vertical-align: middle;
-} \ No newline at end of file
+}
+
+.panel-heading {
+ background: aliceblue;
+}
diff --git a/webapp/src/app/config/config.component.html b/webapp/src/app/config/config.component.html
index d9229d5..c36ba02 100644
--- a/webapp/src/app/config/config.component.html
+++ b/webapp/src/app/config/config.component.html
@@ -1,11 +1,17 @@
<div class="panel panel-default">
- <div class="panel-heading clearfix">
- <h2 class="panel-title pull-left">Global Configuration</h2>
- <div class="pull-right">
- <span class="fa fa-fw fa-exchange fa-size-x2" [style.color]="((serverStatus$ | async)?.WS_connected)?'green':'red'"></span>
- </div>
+ <div class="panel-heading">
+ <h2 class="panel-title" (click)="gConfigIsCollapsed = !gConfigIsCollapsed">
+ Global Configuration
+ <div class="pull-right">
+ <span class="fa fa-fw fa-exchange fa-size-x2" [style.color]="((serverStatus$ | async)?.WS_connected)?'green':'red'"></span>
+
+ <button class="btn btn-link" (click)="gConfigIsCollapsed = !gConfigIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': gConfigIsCollapsed, 'fa-angle-double-right': !gConfigIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
</div>
- <div class="panel-body">
+ <div class="panel-body" [collapse]="gConfigIsCollapsed && (agentStatus$ | async)?.connected">
<div class="row">
<div class="col-xs-12">
<table class="table table-condensed">
@@ -50,9 +56,18 @@
<div class="panel panel-default">
<div class="panel-heading">
- <h2 class="panel-title">Cross SDKs Configuration</h2>
+ <h2 class="panel-title" (click)="sdksIsCollapsed = !sdksIsCollapsed">
+ Cross SDKs
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="childSdkModal.show(); $event.stopPropagation()"><span class="fa fa-plus fa-size-x2"></span></button>
+
+ <button class="btn btn-link" (click)="sdksIsCollapsed = !sdksIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': sdksIsCollapsed, 'fa-angle-double-right': !sdksIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
</div>
- <div class="panel-body">
+ <div class="panel-body" [collapse]="sdksIsCollapsed">
<div class="row col-xs-12">
<sdks-list-accordion [sdks]="(sdks$ | async)"></sdks-list-accordion>
</div>
@@ -61,34 +76,31 @@
<div class="panel panel-default">
<div class="panel-heading">
- <h2 class="panel-title">Projects Configuration</h2>
- </div>
- <div class="panel-body">
- <form [formGroup]="addProjectForm" (ngSubmit)="onSubmit()">
- <div class="row ">
- <div class="col-xs-2">
- <button class="btn btn-primary" type="submit" [disabled]="!addProjectForm.valid"><i class="fa fa-plus"></i>&nbsp;Add Folder</button>
- </div>
+ <h2 class="panel-title" (click)="projectsIsCollapsed = !projectsIsCollapsed; $event.stopPropagation()">
+ Projects
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="childProjectModal.show(); $event.stopPropagation()"><span class="fa fa-plus fa-size-x2"></span></button>
- <div class="col-xs-6">
- <label>Folder Path </label>
- <input type="text" style="width:70%;" formControlName="path" placeholder="myProject">
- </div>
- <div class="col-xs-4">
- <label>Label </label>
- <input type="text" formControlName="label" (keyup)="onKeyLabel($event)">
- </div>
+ <button class="btn btn-link" (click)="projectsIsCollapsed = !projectsIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': projectsIsCollapsed, 'fa-angle-double-right': !projectsIsCollapsed}"></span>
+ </button>
</div>
- </form>
-
+ </h2>
+ </div>
+ <div class="panel-body" [collapse]="projectsIsCollapsed">
<div class="row col-xs-12">
<projects-list-accordion [projects]="(config$ | async).projects"></projects-list-accordion>
</div>
</div>
</div>
+<!-- Modals -->
+<project-add-modal #childProjectModal [title]="'Add a new project'">
+</project-add-modal>
+<sdk-add-modal #childSdkModal [title]="'Add a new SDK'">
+</sdk-add-modal>
<!-- only for debug -->
<div *ngIf="false" class="row">
{{config$ | async | json}}
-</div> \ No newline at end of file
+</div>
diff --git a/webapp/src/app/config/config.component.ts b/webapp/src/app/config/config.component.ts
index 7d9931e..b107e81 100644
--- a/webapp/src/app/config/config.component.ts
+++ b/webapp/src/app/config/config.component.ts
@@ -1,18 +1,16 @@
-import { Component, OnInit } from "@angular/core";
+import { Component, ViewChild, OnInit } from "@angular/core";
import { Observable } from 'rxjs/Observable';
import { FormControl, FormGroup, Validators, FormBuilder } from '@angular/forms';
+import { CollapseModule } from 'ngx-bootstrap/collapse';
-// Import RxJs required methods
-import 'rxjs/add/operator/map';
-import 'rxjs/add/operator/filter';
-import 'rxjs/add/operator/debounceTime';
-
-import { ConfigService, IConfig, IProject, ProjectType, IxdsAgentPackage } from "../services/config.service";
+import { ConfigService, IConfig, IxdsAgentPackage } from "../services/config.service";
import { XDSServerService, IServerStatus, IXDSAgentInfo } from "../services/xdsserver.service";
import { XDSAgentService, IAgentStatus } from "../services/xdsagent.service";
import { SyncthingService, ISyncThingStatus } from "../services/syncthing.service";
import { AlertService } from "../services/alert.service";
import { ISdk, SdkService } from "../services/sdk.service";
+import { ProjectAddModalComponent } from "../projects/projectAddModal.component";
+import { SdkAddModalComponent } from "../sdks/sdkAddModal.component";
@Component({
templateUrl: './app/config/config.component.html',
@@ -23,6 +21,8 @@ import { ISdk, SdkService } from "../services/sdk.service";
// and from http://plnkr.co/edit/vCdjZM?p=preview
export class ConfigComponent implements OnInit {
+ @ViewChild('childProjectModal') childProjectModal: ProjectAddModalComponent;
+ @ViewChild('childSdkModal') childSdkModal: SdkAddModalComponent;
config$: Observable<IConfig>;
sdks$: Observable<ISdk[]>;
@@ -34,20 +34,20 @@ export class ConfigComponent implements OnInit {
userEditedLabel: boolean = false;
xdsAgentPackages: IxdsAgentPackage[] = [];
+ gConfigIsCollapsed: boolean = true;
+ sdksIsCollapsed: boolean = true;
+ projectsIsCollapsed: boolean = false;
+
// TODO replace by reactive FormControl + add validation
syncToolUrl: string;
xdsAgentUrl: string;
xdsAgentRetry: string;
- projectsRootDir: string;
+ projectsRootDir: string; // FIXME: should be remove when projectAddModal will always return full path
showApplyBtn = { // Used to show/hide Apply buttons
"retry": false,
"rootDir": false,
};
- addProjectForm: FormGroup;
- pathCtrl = new FormControl("", Validators.required);
-
-
constructor(
private configSvr: ConfigService,
private xdsServerSvr: XDSServerService,
@@ -55,14 +55,7 @@ export class ConfigComponent implements OnInit {
private stSvr: SyncthingService,
private sdkSvr: SdkService,
private alert: AlertService,
- private fb: FormBuilder
) {
- // FIXME implement multi project support
- this.curProj = 0;
- this.addProjectForm = fb.group({
- path: this.pathCtrl,
- label: ["", Validators.nullValidator],
- });
}
ngOnInit() {
@@ -81,20 +74,6 @@ export class ConfigComponent implements OnInit {
this.xdsAgentPackages = cfg.xdsAgentPackages;
});
- // Auto create label name
- this.pathCtrl.valueChanges
- .debounceTime(100)
- .filter(n => n)
- .map(n => "Project_" + n.split('/')[0])
- .subscribe(value => {
- if (value && !this.userEditedLabel) {
- this.addProjectForm.patchValue({ label: value });
- }
- });
- }
-
- onKeyLabel(event: any) {
- this.userEditedLabel = (this.addProjectForm.value.label !== "");
}
submitGlobConf(field: string) {
@@ -118,21 +97,10 @@ export class ConfigComponent implements OnInit {
}
xdsAgentRestartConn() {
- let aurl = this.xdsAgentUrl;
+ let aUrl = this.xdsAgentUrl;
this.configSvr.syncToolURL = this.syncToolUrl;
- this.configSvr.xdsAgentUrl = aurl;
+ this.configSvr.xdsAgentUrl = aUrl;
this.configSvr.loadProjects();
}
- onSubmit() {
- let formVal = this.addProjectForm.value;
-
- this.configSvr.addProject({
- label: formVal['label'],
- path: formVal['path'],
- type: ProjectType.SYNCTHING,
- // FIXME: allow to set defaultSdkID from New Project config panel
- });
- }
-
-} \ No newline at end of file
+}
diff --git a/webapp/src/app/devel/build/build.component.css b/webapp/src/app/devel/build/build.component.css
index 92f953e..695a89b 100644
--- a/webapp/src/app/devel/build/build.component.css
+++ b/webapp/src/app/devel/build/build.component.css
@@ -33,8 +33,9 @@
width: 10em;
}
-.fa-size-x2 {
+.fa-big {
font-size: 18px;
+ font-weight: bold;
}
.textarea-scroll {
@@ -46,4 +47,8 @@ h2 {
font-family: sans-serif;
font-variant: small-caps;
font-size: x-large;
-} \ No newline at end of file
+}
+
+.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..2bcd2c7 100644
--- a/webapp/src/app/devel/build/build.component.html
+++ b/webapp/src/app/devel/build/build.component.html
@@ -1,8 +1,15 @@
<div class="panel panel-default">
<div class="panel-heading">
- <h2 class="panel-title">Build</h2>
+ <h2 class="panel-title" (click)="buildIsCollapsed = !buildIsCollapsed">
+ Build
+ <div class="pull-right">
+ <button class="btn btn-link" (click)="buildIsCollapsed = !buildIsCollapsed; $event.stopPropagation()">
+ <span class="fa fa-big" [ngClass]="{'fa-angle-double-down': buildIsCollapsed, 'fa-angle-double-right': !buildIsCollapsed}"></span>
+ </button>
+ </div>
+ </h2>
</div>
- <div class="panel-body">
+ <div class="panel-body" [collapse]="buildIsCollapsed">
<form [formGroup]="buildForm">
<div class="col-xs-12">
<table class="table table-borderless table-center">
@@ -18,7 +25,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>
@@ -26,43 +33,43 @@
</tr>
<tr>
<td colspan="2">
- <accordion>
- <accordion-group #group>
- <div accordion-heading>
- Advanced Settings
- <i class="pull-right float-xs-right fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
- </div>
+ <accordion>
+ <accordion-group #group>
+ <div accordion-heading>
+ Advanced Settings
+ <i class="pull-right float-xs-right fa" [ngClass]="{'fa-chevron-down': group.isOpen, 'fa-chevron-right': !group.isOpen}"></i>
+ </div>
- <table class="table table-borderless table-in-accordion">
- <tbody>
- <tr>
- <th>Clean Command</th>
- <td> <input type="text" style="width:99%;" formControlName="cmdClean"> </td>
- </tr>
- <tr>
- <th>Pre-Build Command</th>
- <td> <input type="text" style="width:99%;" formControlName="cmdPrebuild"> </td>
- </tr>
- <tr>
- <th>Build Command</th>
- <td> <input type="text" style="width:99%;" formControlName="cmdBuild"> </td>
- </tr>
- <tr>
- <th>Populate Command</th>
- <td> <input type="text" style="width:99%;" formControlName="cmdPopulate"> </td>
- </tr>
- <tr>
- <th>Env variables</th>
- <td> <input type="text" style="width:99%;" formControlName="envVars"> </td>
- </tr>
- <tr *ngIf="debugEnable">
- <th>Args variables</th>
- <td> <input type="text" style="width:99%;" formControlName="cmdArgs"> </td>
- </tr>
- </tbody>
- </table>
- </accordion-group>
- </accordion>
+ <table class="table table-borderless table-in-accordion">
+ <tbody>
+ <tr>
+ <th>Clean Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdClean"> </td>
+ </tr>
+ <tr>
+ <th>Pre-Build Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdPrebuild"> </td>
+ </tr>
+ <tr>
+ <th>Build Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdBuild"> </td>
+ </tr>
+ <tr>
+ <th>Populate Command</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdPopulate"> </td>
+ </tr>
+ <tr>
+ <th>Env variables</th>
+ <td> <input type="text" style="width:99%;" formControlName="envVars"> </td>
+ </tr>
+ <tr *ngIf="debugEnable">
+ <th>Args variables</th>
+ <td> <input type="text" style="width:99%;" formControlName="cmdArgs"> </td>
+ </tr>
+ </tbody>
+ </table>
+ </accordion-group>
+ </accordion>
</td>
</tr>
</tbody>
@@ -105,4 +112,4 @@
</div>
</div>
</div>
-</div> \ No newline at end of file
+</div>
diff --git a/webapp/src/app/devel/build/build.component.ts b/webapp/src/app/devel/build/build.component.ts
index 449c557..48a5824 100644
--- a/webapp/src/app/devel/build/build.component.ts
+++ b/webapp/src/app/devel/build/build.component.ts
@@ -23,10 +23,10 @@ export class BuildComponent implements OnInit, AfterViewChecked {
@Input() curProject: IProject;
- buildForm: FormGroup;
- subpathCtrl = new FormControl("", Validators.required);
- debugEnable: boolean = false;
-
+ public buildForm: FormGroup;
+ public subpathCtrl = new FormControl("", Validators.required);
+ public debugEnable: boolean = false;
+ public buildIsCollapsed: boolean = false;
public cmdOutput: string;
public cmdInfo: string;
@@ -67,7 +67,8 @@ export class BuildComponent implements OnInit, AfterViewChecked {
// Command output data tunneling
this.xdsSvr.CmdOutput$.subscribe(data => {
- this.cmdOutput += data.stdout + "\n";
+ this.cmdOutput += data.stdout;
+ this.cmdOutput += data.stderr;
});
// Command exit
@@ -219,4 +220,4 @@ export class BuildComponent implements OnInit, AfterViewChecked {
private _outputFooter(): string {
return "\n";
}
-} \ No newline at end of file
+}
diff --git a/webapp/src/app/devel/deploy/deploy.component.ts b/webapp/src/app/devel/deploy/deploy.component.ts
index 4dba256..e51b7f2 100644
--- a/webapp/src/app/devel/deploy/deploy.component.ts
+++ b/webapp/src/app/devel/deploy/deploy.component.ts
@@ -37,8 +37,8 @@ export class DeployComponent implements OnInit {
ngOnInit() {
this.deploying = false;
- if (this.curProject && this.curProject.path) {
- this.deployForm.patchValue({ wgtFile: this.curProject.path });
+ if (this.curProject && this.curProject.pathClient) {
+ this.deployForm.patchValue({ wgtFile: this.curProject.pathClient });
}
}
@@ -60,4 +60,4 @@ export class DeployComponent implements OnInit {
this.alert.error(msg);
});
}
-} \ No newline at end of file
+}
diff --git a/webapp/src/app/devel/devel.component.css b/webapp/src/app/devel/devel.component.css
index 40d6fec..4b03dcb 100644
--- a/webapp/src/app/devel/devel.component.css
+++ b/webapp/src/app/devel/devel.component.css
@@ -12,3 +12,8 @@
.table-borderless>thead>tr>th {
border: none;
}
+
+a.dropdown-item.disabled {
+ pointer-events:none;
+ opacity:0.4;
+}
diff --git a/webapp/src/app/devel/devel.component.html b/webapp/src/app/devel/devel.component.html
index feac413..8e71c58 100644
--- a/webapp/src/app/devel/devel.component.html
+++ b/webapp/src/app/devel/devel.component.html
@@ -7,11 +7,14 @@
<td>
<div class="btn-group" dropdown *ngIf="curPrj">
<button dropdownToggle type="button" class="btn btn-primary dropdown-toggle" style="width: 20em;">
- {{curPrj.label}} <span class="caret" style="float: right; margin-top: 8px;"></span>
- </button>
+ {{curPrj.label}}
+ <span class="caret" style="float: right; margin-top: 8px;"></span>
+ </button>
<ul *dropdownMenu class="dropdown-menu" role="menu">
- <li role="menuitem"><a class="dropdown-item" *ngFor="let prj of (config$ | async)?.projects" (click)="curPrj=prj">{{prj.label}}</a>
+ <li role="menuitem"><a class="dropdown-item" *ngFor="let prj of (config$ | async)?.projects" [class.disabled]="!prj.isUsable"
+ (click)="curPrj=prj">{{prj.label}}</a>
</li>
+
</ul>
</div>
<span *ngIf="!curPrj" style="color:red; font-style: italic;">
@@ -34,4 +37,4 @@
<panel-deploy [curProject]=curPrj></panel-deploy>
</div>
-->
-</div> \ No newline at end of file
+</div>
diff --git a/webapp/src/app/devel/devel.component.ts b/webapp/src/app/devel/devel.component.ts
index ff12127..f40f25f 100644
--- a/webapp/src/app/devel/devel.component.ts
+++ b/webapp/src/app/devel/devel.component.ts
@@ -22,11 +22,14 @@ export class DevelComponent {
ngOnInit() {
this.config$ = this.configSvr.conf;
this.config$.subscribe((cfg) => {
- if ("projects" in cfg) {
+ // Select project if no one is selected or no project exists
+ if (this.curPrj && "id" in this.curPrj) {
+ this.curPrj = cfg.projects.find(p => p.id === this.curPrj.id) || cfg.projects[0];
+ } else if (this.curPrj == null && "projects" in cfg) {
this.curPrj = cfg.projects[0];
} else {
this.curPrj = null;
}
});
}
-} \ No newline at end of file
+}
diff --git a/webapp/src/app/projects/projectAddModal.component.css b/webapp/src/app/projects/projectAddModal.component.css
new file mode 100644
index 0000000..77f73a5
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.css
@@ -0,0 +1,24 @@
+.table-borderless>tbody>tr>td,
+.table-borderless>tbody>tr>th,
+.table-borderless>tfoot>tr>td,
+.table-borderless>tfoot>tr>th,
+.table-borderless>thead>tr>td,
+.table-borderless>thead>tr>th {
+ border: none;
+}
+
+tr>th {
+ vertical-align: middle;
+}
+
+tr>td {
+ vertical-align: middle;
+}
+
+th label {
+ margin-bottom: 0;
+}
+
+td input {
+ width: 100%;
+}
diff --git a/webapp/src/app/projects/projectAddModal.component.html b/webapp/src/app/projects/projectAddModal.component.html
new file mode 100644
index 0000000..dc84985
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.html
@@ -0,0 +1,54 @@
+<div bsModal #childProjectModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel"
+ [config]="{backdrop: 'static'}" aria-hidden="true">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title pull-left">{{title}}</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="hide()">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+
+ <form [formGroup]="addProjectForm" (ngSubmit)="onSubmit()">
+ <div class="modal-body">
+ <div class="row ">
+ <div class="col-xs-12">
+ <table class="table table-borderless">
+ <tbody>
+ <tr>
+ <th><label>Sharing Type </label></th>
+ <td><select class="form-control" formControlName="type">
+ <option *ngFor="let t of projectTypes" [value]="t.value">{{t.display}}
+ </option>
+ </select>
+ </td>
+ </tr>
+ <tr>
+ <th><label for="select-local-path">Local Path </label></th>
+ <td><input type="text" id="select-local-path" formControlName="pathCli" placeholder="/tmp/myProject" (change)="onChangeLocalProject($event)"></td>
+ </tr>
+ <tr>
+ <th><label for="select-server-path">Server Path </label></th>
+ <td><input type="text" id="select-server-path" formControlName="pathSvr"></td>
+ </tr>
+ <tr>
+ <th><label for="select-label">Label </label></th>
+ <td><input type="text" formControlName="label" id="select-label" (keyup)="onKeyLabel($event)"></td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <div class="pull-left">
+ <button class="btn btn-default" (click)="cancelAction=true; hide()"> Cancel </button>
+ </div>
+ <div class="">
+ <button class="btn btn-primary" type="submit" [disabled]="!addProjectForm.valid">Add Folder</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/projects/projectAddModal.component.ts b/webapp/src/app/projects/projectAddModal.component.ts
new file mode 100644
index 0000000..7ef5b5e
--- /dev/null
+++ b/webapp/src/app/projects/projectAddModal.component.ts
@@ -0,0 +1,152 @@
+import { Component, Input, ViewChild, OnInit } from '@angular/core';
+import { Observable } from 'rxjs/Observable';
+import { ModalDirective } from 'ngx-bootstrap/modal';
+import { FormControl, FormGroup, Validators, FormBuilder, ValidatorFn, AbstractControl } from '@angular/forms';
+
+// Import RxJs required methods
+import 'rxjs/add/operator/map';
+import 'rxjs/add/operator/filter';
+import 'rxjs/add/operator/debounceTime';
+
+import { AlertService, IAlert } from "../services/alert.service";
+import {
+ ConfigService, IConfig, IProject, ProjectType, ProjectTypes,
+ IxdsAgentPackage
+} from "../services/config.service";
+
+
+@Component({
+ selector: 'project-add-modal',
+ templateUrl: './app/projects/projectAddModal.component.html',
+ styleUrls: ['./app/projects/projectAddModal.component.css']
+})
+export class ProjectAddModalComponent {
+ @ViewChild('childProjectModal') public childProjectModal: ModalDirective;
+ @Input() title?: string;
+
+ config$: Observable<IConfig>;
+
+ cancelAction: boolean = false;
+ userEditedLabel: boolean = false;
+ projectTypes = ProjectTypes;
+
+ addProjectForm: FormGroup;
+ typeCtrl: FormControl;
+ pathCliCtrl: FormControl;
+ pathSvrCtrl: FormControl;
+
+ constructor(
+ private alert: AlertService,
+ private configSvr: ConfigService,
+ private fb: FormBuilder
+ ) {
+ // Define types (first one is special/placeholder)
+ this.projectTypes.unshift({ value: -1, display: "--Select a type--" });
+
+ this.typeCtrl = new FormControl(this.projectTypes[0].value, Validators.pattern("[0-9]+"));
+ this.pathCliCtrl = new FormControl("", Validators.required);
+ this.pathSvrCtrl = new FormControl({ value: "", disabled: true }, [Validators.required, Validators.minLength(1)]);
+
+ this.addProjectForm = fb.group({
+ type: this.typeCtrl,
+ pathCli: this.pathCliCtrl,
+ pathSvr: this.pathSvrCtrl,
+ label: ["", Validators.nullValidator],
+ });
+ }
+
+ ngOnInit() {
+ this.config$ = this.configSvr.conf;
+
+ // Auto create label name
+ this.pathCliCtrl.valueChanges
+ .debounceTime(100)
+ .filter(n => n)
+ .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 });
+ }
+ });
+
+ // Handle disabling of Server path
+ this.typeCtrl.valueChanges
+ .debounceTime(500)
+ .subscribe(valType => {
+ let dis = (valType === String(ProjectType.SYNCTHING));
+ this.pathSvrCtrl.reset({ value: "", disabled: dis });
+ });
+ }
+
+ show() {
+ this.cancelAction = false;
+ this.childProjectModal.show();
+ }
+
+ hide() {
+ this.childProjectModal.hide();
+ }
+
+ onKeyLabel(event: any) {
+ this.userEditedLabel = (this.addProjectForm.value.label !== "");
+ }
+
+ /* FIXME: change input to file type
+ <td><input type="file" id="select-local-path" webkitdirectory
+ formControlName="pathCli" placeholder="myProject" (change)="onChangeLocalProject($event)"></td>
+
+ onChangeLocalProject(e) {
+ if e.target.files.length < 1 {
+ console.log('NO files');
+ }
+ let dir = e.target.files[0].webkitRelativePath;
+ console.log("files: " + dir);
+ let u = URL.createObjectURL(e.target.files[0]);
+ }
+ */
+ onChangeLocalProject(e) {
+ }
+
+ onSubmit() {
+ if (this.cancelAction) {
+ return;
+ }
+
+ let formVal = this.addProjectForm.value;
+
+ let type = formVal['type'].value;
+ let numType = Number(formVal['type']);
+ this.configSvr.addProject({
+ label: formVal['label'],
+ pathClient: formVal['pathCli'],
+ pathServer: formVal['pathSvr'],
+ type: numType,
+ // FIXME: allow to set defaultSdkID from New Project config panel
+ })
+ .subscribe(prj => {
+ this.alert.info("Project " + prj.label + " successfully created.");
+ this.hide();
+
+ // Reset Value for the next creation
+ this.addProjectForm.reset();
+ let selectedType = this.projectTypes[0].value;
+ this.addProjectForm.patchValue({ type: selectedType });
+
+ },
+ err => {
+ this.alert.error("Configuration ERROR: " + err, 60);
+ this.hide();
+ });
+ }
+
+}
diff --git a/webapp/src/app/projects/projectCard.component.ts b/webapp/src/app/projects/projectCard.component.ts
index 7a7fa21..a7ca9a3 100644
--- a/webapp/src/app/projects/projectCard.component.ts
+++ b/webapp/src/app/projects/projectCard.component.ts
@@ -1,5 +1,6 @@
import { Component, Input, Pipe, PipeTransform } from '@angular/core';
import { ConfigService, IProject, ProjectType } from "../services/config.service";
+import { AlertService } from "../services/alert.service";
@Component({
selector: 'project-card',
@@ -7,7 +8,9 @@ import { ConfigService, IProject, ProjectType } from "../services/config.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>
@@ -19,14 +22,25 @@ import { ConfigService, IProject, ProjectType } from "../services/config.service
<td>{{ project.id }}</td>
</tr>
<tr>
- <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Folder path</span></th>
- <td>{{ project.path}}</td>
+ <th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span>Sharing type</span></th>
+ <td>{{ project.type | readableType }}</td>
</tr>
<tr>
- <th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span>Synchronization type</span></th>
- <td>{{ project.type | readableType }}</td>
+ <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 && 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-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 >
`,
@@ -37,12 +51,26 @@ export class ProjectCardComponent {
@Input() project: IProject;
- constructor(private configSvr: ConfigService) {
+ constructor(
+ private alert: AlertService,
+ private configSvr: ConfigService
+ ) {
}
-
delete(prj: IProject) {
- this.configSvr.deleteProject(prj);
+ this.configSvr.deleteProject(prj)
+ .subscribe(res => {
+ }, err => {
+ this.alert.error("Delete local ERROR: " + err);
+ });
+ }
+
+ sync(prj: IProject) {
+ this.configSvr.syncProject(prj)
+ .subscribe(res => {
+ }, err => {
+ this.alert.error("ERROR: " + err);
+ });
}
}
@@ -53,11 +81,11 @@ export class ProjectCardComponent {
})
export class ProjectReadableTypePipe implements PipeTransform {
- transform(type: ProjectType): string {
- switch (+type) {
- case ProjectType.NATIVE: return "Native";
- case ProjectType.SYNCTHING: return "Cloud (Syncthing)";
- default: return String(type);
+ transform(type: ProjectType): string {
+ switch (type) {
+ case ProjectType.NATIVE_PATHMAP: return "Native (path mapping)";
+ case ProjectType.SYNCTHING: return "Cloud (Syncthing)";
+ default: return String(type);
+ }
}
- }
-} \ No newline at end of file
+}
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/sdks/sdkAddModal.component.html b/webapp/src/app/sdks/sdkAddModal.component.html
new file mode 100644
index 0000000..2c07fca
--- /dev/null
+++ b/webapp/src/app/sdks/sdkAddModal.component.html
@@ -0,0 +1,23 @@
+<div bsModal #sdkChildModal="bs-modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel"
+ aria-hidden="true">
+ <div class="modal-dialog modal-lg">
+ <div class="modal-content">
+ <div class="modal-header">
+ <h4 class="modal-title pull-left">{{title}}</h4>
+ <button type="button" class="close pull-right" aria-label="Close" (click)="hideChildModal()">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <ng-content select=".modal-body"> </ng-content>
+ <i>Not available for now.</i>
+ </div>
+
+ <div class="modal-footer">
+ <div class="pull-left">
+ <button class="btn btn-default" (click)="hide()"> Cancel </button>
+ </div>
+ </div>
+ </div>
+ </div>
+</div>
diff --git a/webapp/src/app/sdks/sdkAddModal.component.ts b/webapp/src/app/sdks/sdkAddModal.component.ts
new file mode 100644
index 0000000..b6c8eb2
--- /dev/null
+++ b/webapp/src/app/sdks/sdkAddModal.component.ts
@@ -0,0 +1,24 @@
+import { Component, Input, ViewChild } from '@angular/core';
+import { ModalDirective } from 'ngx-bootstrap/modal';
+
+@Component({
+ selector: 'sdk-add-modal',
+ templateUrl: './app/sdks/sdkAddModal.component.html',
+})
+export class SdkAddModalComponent {
+ @ViewChild('sdkChildModal') public sdkChildModal: ModalDirective;
+
+ @Input() title?: string;
+
+ // TODO
+ constructor() {
+ }
+
+ show() {
+ this.sdkChildModal.show();
+ }
+
+ hide() {
+ this.sdkChildModal.hide();
+ }
+}
diff --git a/webapp/src/app/services/alert.service.ts b/webapp/src/app/services/alert.service.ts
index 9dab36a..c3cae7a 100644
--- a/webapp/src/app/services/alert.service.ts
+++ b/webapp/src/app/services/alert.service.ts
@@ -30,8 +30,10 @@ export class AlertService {
this.uid = 0;
}
- public error(msg: string) {
- this.add({ type: "danger", msg: msg, dismissible: true });
+ public error(msg: string, dismissTime?: number) {
+ this.add({
+ type: "danger", msg: msg, dismissible: true, dismissTimeout: dismissTime
+ });
}
public warning(msg: string, dismissible?: boolean) {
diff --git a/webapp/src/app/services/config.service.ts b/webapp/src/app/services/config.service.ts
index 722c347..4501add 100644
--- a/webapp/src/app/services/config.service.ts
+++ b/webapp/src/app/services/config.service.ts
@@ -13,28 +13,40 @@ import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/mergeMap';
-import { XDSServerService, IXDSConfigProject } from "../services/xdsserver.service";
+import { XDSServerService, IXDSFolderConfig } from "../services/xdsserver.service";
import { XDSAgentService } from "../services/xdsagent.service";
import { SyncthingService, ISyncThingProject, ISyncThingStatus } from "../services/syncthing.service";
import { AlertService, IAlert } from "../services/alert.service";
import { UtilsService } from "../services/utils.service";
export enum ProjectType {
- NATIVE = 1,
+ NATIVE_PATHMAP = 1,
SYNCTHING = 2
}
-export interface INativeProject {
- // TODO
-}
+export var ProjectTypes = [
+ { value: ProjectType.NATIVE_PATHMAP, display: "Path mapping" },
+ { value: ProjectType.SYNCTHING, display: "Cloud Sync" }
+];
+
+export var ProjectStatus = {
+ ErrorConfig: "ErrorConfig",
+ Disable: "Disable",
+ Enable: "Enable",
+ Pause: "Pause",
+ Syncing: "Syncing"
+};
export interface IProject {
id?: string;
label: string;
- path: string;
+ pathClient: string;
+ pathServer?: string;
type: ProjectType;
- remotePrjDef?: INativeProject | ISyncThingProject;
- localPrjDef?: any;
+ status?: string;
+ isInSync?: boolean;
+ isUsable?: boolean;
+ serverPrjDef?: IXDSFolderConfig;
isExpanded?: boolean;
visible?: boolean;
defaultSdkID?: string;
@@ -133,6 +145,18 @@ 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.confStore.projects[i].isUsable = this._isUsableProject(prj);
+ this.confSubject.next(Object.assign({}, this.confStore));
+ }
+ });
}
// Save config into cookie
@@ -172,7 +196,7 @@ export class ConfigService {
let zurl = this.confStore.xdsAgentPackages && this.confStore.xdsAgentPackages.filter(elem => elem.os === os);
if (zurl && zurl.length) {
msg += " Download XDS-Agent tarball for " + zurl[0].os + " host OS ";
- msg += "<a class=\"fa fa-download\" href=\"" + zurl[0].url + "\" target=\"_blank\"></a>";
+ msg += "<a class=\"fa fa-download\" href=\"" + zurl[0].url + "\" target=\"_blank\"></a>";
}
msg += "</span>";
this.alert.error(msg);
@@ -209,16 +233,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,
- path: rPrj.path,
- type: ProjectType.SYNCTHING, // FIXME support other types
- 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));
@@ -270,100 +286,133 @@ export class ConfigService {
return id.slice(0, 15);
}
- addProject(prj: IProject) {
+ addProject(prj: IProject): Observable<IProject> {
// Substitute tilde with to user home path
- prj.path = prj.path.trim();
- if (prj.path.charAt(0) === '~') {
- prj.path = this.confStore.localSThg.tilde + prj.path.substring(1);
+ let pathCli = prj.pathClient.trim();
+ if (pathCli.charAt(0) === '~') {
+ pathCli = this.confStore.localSThg.tilde + pathCli.substring(1);
// Must be a full path (on Linux or Windows)
- } else if (!((prj.path.charAt(0) === '/') ||
- (prj.path.charAt(1) === ':' && (prj.path.charAt(2) === '\\' || prj.path.charAt(2) === '/')))) {
- prj.path = this.confStore.projectsRootDir + '/' + prj.path;
- }
-
- if (prj.id == null) {
- // FIXME - must be done on server side
- let prefix = this.getLabelRootName() || new Date().toISOString();
- let splath = prj.path.split('/');
- prj.id = prefix + "_" + splath[splath.length - 1];
- }
-
- if (this._getProjectIdx(prj.id) !== -1) {
- this.alert.warning("Project already exist (id=" + prj.id + ")", true);
- return;
- }
-
- // TODO - support others project types
- if (prj.type !== ProjectType.SYNCTHING) {
- this.alert.error('Project type not supported yet (type: ' + prj.type + ')');
- return;
+ } else if (!((pathCli.charAt(0) === '/') ||
+ (pathCli.charAt(1) === ':' && (pathCli.charAt(2) === '\\' || pathCli.charAt(2) === '/')))) {
+ pathCli = this.confStore.projectsRootDir + '/' + pathCli;
}
- let sdkPrj: IXDSConfigProject = {
- id: prj.id,
- label: prj.label,
- path: prj.path,
- hostSyncThingID: this.confStore.localSThg.ID,
+ let xdsPrj: IXDSFolderConfig = {
+ id: "",
+ label: prj.label || "",
+ path: pathCli,
+ type: prj.type,
defaultSdkID: prj.defaultSdkID,
+ dataPathMap: {
+ serverPath: prj.pathServer,
+ },
+ dataCloudSync: {
+ syncThingID: this.confStore.localSThg.ID,
+ }
};
-
// Send config to XDS server
let newPrj = prj;
- this.xdsServerSvr.addProject(sdkPrj)
- .subscribe(resStRemotePrj => {
- newPrj.remotePrjDef = resStRemotePrj;
-
- // FIXME REWORK local ST config
- // move logic to server side tunneling-back by WS
-
- // Now setup local config
- let stLocPrj: ISyncThingProject = {
- id: sdkPrj.id,
- label: sdkPrj.label,
- path: sdkPrj.path,
- remoteSyncThingID: resStRemotePrj.builderSThgID
- };
-
- // Set local Syncthing config
- this.stSvr.addProject(stLocPrj)
- .subscribe(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));
- },
- err => {
- this.alert.error("Configuration local ERROR: " + err);
- });
- },
- err => {
- this.alert.error("Configuration remote ERROR: " + err);
+ return this.xdsServerSvr.addProject(xdsPrj)
+ .flatMap(resStRemotePrj => {
+ xdsPrj = resStRemotePrj;
+ if (xdsPrj.type === ProjectType.SYNCTHING) {
+ // FIXME REWORK local ST config
+ // move logic to server side tunneling-back by WS
+ let stData = xdsPrj.dataCloudSync;
+
+ // Now setup local config
+ let stLocPrj: ISyncThingProject = {
+ id: xdsPrj.id,
+ label: xdsPrj.label,
+ path: xdsPrj.path,
+ serverSyncThingID: stData.builderSThgID
+ };
+
+ // Set local Syncthing config
+ return this.stSvr.addProject(stLocPrj);
+
+ } else {
+ return Observable.of(null);
+ }
+ })
+ .map(resStLocalPrj => {
+ this._addProject(xdsPrj);
+ return newPrj;
});
}
- deleteProject(prj: IProject) {
+ deleteProject(prj: IProject): Observable<IProject> {
let idx = this._getProjectIdx(prj.id);
+ let delPrj = prj;
if (idx === -1) {
throw new Error("Invalid project id (id=" + prj.id + ")");
}
- this.xdsServerSvr.deleteProject(prj.id)
- .subscribe(res => {
- this.stSvr.deleteProject(prj.id)
- .subscribe(res => {
- this.confStore.projects.splice(idx, 1);
- }, err => {
- this.alert.error("Delete local ERROR: " + err);
- });
- }, err => {
- this.alert.error("Delete remote ERROR: " + err);
+ return this.xdsServerSvr.deleteProject(prj.id)
+ .flatMap(res => {
+ if (prj.type === ProjectType.SYNCTHING) {
+ return this.stSvr.deleteProject(prj.id);
+ }
+ return Observable.of(null);
+ })
+ .map(res => {
+ this.confStore.projects.splice(idx, 1);
+ return delPrj;
});
}
+ 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 _isUsableProject(p) {
+ return p && p.isInSync &&
+ (p.status === ProjectStatus.Enable) &&
+ (p.status !== ProjectStatus.Syncing);
+ }
+
private _getProjectIdx(id: string): number {
return this.confStore.projects.findIndex((item) => item.id === id);
}
-} \ No newline at end of file
+ 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,
+ isUsable: this._isUsableProject(rPrj),
+ 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/syncthing.service.ts b/webapp/src/app/services/syncthing.service.ts
index 0e8c51c..aefb039 100644
--- a/webapp/src/app/services/syncthing.service.ts
+++ b/webapp/src/app/services/syncthing.service.ts
@@ -16,7 +16,7 @@ import 'rxjs/add/operator/retryWhen';
export interface ISyncThingProject {
id: string;
path: string;
- remoteSyncThingID: string;
+ serverSyncThingID: string;
label?: string;
}
@@ -180,7 +180,7 @@ export class SyncthingService {
return this.getID()
.flatMap(() => this._getConfig())
.flatMap((stCfg) => {
- let newDevID = prj.remoteSyncThingID;
+ let newDevID = prj.serverSyncThingID;
// Add new Device if needed
let dev = stCfg.devices.filter(item => item.deviceID === newDevID);
diff --git a/webapp/src/app/services/xdsserver.service.ts b/webapp/src/app/services/xdsserver.service.ts
index 4d20fa4..b69a196 100644
--- a/webapp/src/app/services/xdsserver.service.ts
+++ b/webapp/src/app/services/xdsserver.service.ts
@@ -20,7 +20,8 @@ import 'rxjs/add/operator/mergeMap';
export interface IXDSConfigProject {
id: string;
path: string;
- hostSyncThingID: string;
+ clientSyncThingID: string;
+ type: number;
label?: string;
defaultSdkID?: string;
}
@@ -31,15 +32,29 @@ interface IXDSBuilderConfig {
syncThingID: string;
}
-interface IXDSFolderConfig {
+export interface IXDSFolderConfig {
id: string;
label: string;
path: string;
type: number;
- syncThingID: string;
- builderSThgID?: string;
status?: string;
+ isInSync?: boolean;
defaultSdkID: string;
+
+ // FIXME better with union but tech pb with go code
+ //data?: IXDSPathMapConfig|IXDSCloudSyncConfig;
+ dataPathMap?: IXDSPathMapConfig;
+ dataCloudSync?: IXDSCloudSyncConfig;
+}
+
+export interface IXDSPathMapConfig {
+ // TODO
+ serverPath: string;
+}
+
+export interface IXDSCloudSyncConfig {
+ syncThingID: string;
+ builderSThgID?: string;
}
interface IXDSConfig {
@@ -92,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 };
@@ -113,6 +130,7 @@ export class XDSServerService {
} else {
this.wsUrl = 'ws://' + re[1];
this._handleIoSocket();
+ this._RegisterEvents();
}
}
@@ -158,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[]> {
@@ -172,22 +206,18 @@ export class XDSServerService {
return this._get('/folders');
}
- addProject(cfg: IXDSConfigProject): Observable<IXDSFolderConfig> {
- let folder: IXDSFolderConfig = {
- id: cfg.id || null,
- label: cfg.label || "",
- path: cfg.path,
- type: FOLDER_TYPE_CLOUDSYNC,
- syncThingID: cfg.hostSyncThingID,
- defaultSdkID: cfg.defaultSdkID || "",
- };
- return this._post('/folder', folder);
+ addProject(cfg: IXDSFolderConfig): Observable<IXDSFolderConfig> {
+ return this._post('/folder', cfg);
}
deleteProject(id: string): Observable<IXDSFolderConfig> {
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',
{
@@ -244,7 +274,13 @@ export class XDSServerService {
private _decodeError(err: any) {
let e: string;
- if (typeof err === "object") {
+ if (err instanceof Response) {
+ const body = err.json() || 'Server error';
+ e = body.error || JSON.stringify(body);
+ if (!e || e === "") {
+ e = `${err.status} - ${err.statusText || 'Unknown error'}`;
+ }
+ } else if (typeof err === "object") {
if (err.statusText) {
e = err.statusText;
} else if (err.error) {
@@ -253,7 +289,7 @@ export class XDSServerService {
e = JSON.stringify(err);
}
} else {
- e = err.json().error || 'Server error';
+ e = err.message ? err.message : err.toString();
}
return Observable.throw(e);
}
diff --git a/webapp/src/index.html b/webapp/src/index.html
index 33e5efd..290b4be 100644
--- a/webapp/src/index.html
+++ b/webapp/src/index.html
@@ -40,10 +40,11 @@
<body style="padding-top: 70px;"> <!-- padding needed due to fixed navbar -->
<app>
<div style="text-align:center; position:absolute; top:50%; width:100%; transform:translate(0,-50%);">
- Loading...
+ <img id="logo-iot" src="assets/images/iot-bzh-logo-small.png">
+ <br> Loading...
<i class="fa fa-spinner fa-spin fa-fw"></i>
</div>
</app>
</body>
-</html> \ No newline at end of file
+</html>
diff --git a/webapp/src/systemjs.config.js b/webapp/src/systemjs.config.js
index 19fe225..15c52ba 100644
--- a/webapp/src/systemjs.config.js
+++ b/webapp/src/systemjs.config.js
@@ -39,6 +39,7 @@
'ngx-bootstrap/carousel': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
'ngx-bootstrap/popover': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
'ngx-bootstrap/dropdown': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
+ 'ngx-bootstrap/collapse': 'npm:ngx-bootstrap/bundles/ngx-bootstrap.umd.min.js',
// other libraries
'socket.io-client': 'npm:socket.io-client/dist/socket.io.min.js'
},
@@ -65,4 +66,4 @@
}
}
});
-})(this); \ No newline at end of file
+})(this);