From dd6f08b10b1597f44e3dc25509ac9a45336b0914 Mon Sep 17 00:00:00 2001
From: Sebastien Douheret <sebastien.douheret@iot.bzh>
Date: Thu, 10 Aug 2017 12:19:34 +0200
Subject: Add folder interface and support native pathmap folder type.

Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
---
 .vscode/settings.json                            |  17 +-
 Makefile                                         |   2 +-
 lib/apiv1/agent.go                               |   3 +
 lib/apiv1/apiv1.go                               |   6 +-
 lib/apiv1/config.go                              |   7 +-
 lib/apiv1/exec.go                                |  22 +-
 lib/apiv1/folders.go                             |  36 +--
 lib/apiv1/make.go                                |  12 +-
 lib/crosssdk/sdks.go                             |   3 +
 lib/folder/folder-interface.go                   |  59 ++++
 lib/folder/folder-pathmap.go                     |  88 ++++++
 lib/model/folder.go                              | 110 --------
 lib/model/folders.go                             | 333 +++++++++++++++++++++++
 lib/syncthing/folder-st.go                       |  97 +++++++
 lib/syncthing/st.go                              |  50 +---
 lib/syncthing/stfolder.go                        | 123 ++++++++-
 lib/webserver/server.go                          |   8 +-
 lib/xdsconfig/config.go                          |  21 +-
 lib/xdsconfig/fileconfig.go                      |  40 ++-
 lib/xdsconfig/folderconfig.go                    |  85 ------
 lib/xdsconfig/foldersconfig.go                   |  47 ----
 main.go                                          |  83 ++----
 webapp/src/app/config/config.component.html      |  17 +-
 webapp/src/app/config/config.component.ts        |  35 ++-
 webapp/src/app/devel/deploy/deploy.component.ts  |   6 +-
 webapp/src/app/projects/projectCard.component.ts |  33 ++-
 webapp/src/app/services/config.service.ts        |  80 +++---
 webapp/src/app/services/syncthing.service.ts     |   4 +-
 webapp/src/app/services/xdsserver.service.ts     |  44 +--
 29 files changed, 961 insertions(+), 510 deletions(-)
 create mode 100644 lib/folder/folder-interface.go
 create mode 100644 lib/folder/folder-pathmap.go
 delete mode 100644 lib/model/folder.go
 create mode 100644 lib/model/folders.go
 create mode 100644 lib/syncthing/folder-st.go
 delete mode 100644 lib/xdsconfig/folderconfig.go
 delete mode 100644 lib/xdsconfig/foldersconfig.go

diff --git a/.vscode/settings.json b/.vscode/settings.json
index 429cbbe..60fab57 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -12,6 +12,16 @@
         "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",
@@ -40,6 +50,11 @@
         "sdkid",
         "CLOUDSYNC",
         "xdsagent",
-        "eows"
+        "gdbserver",
+        "golib",
+        "eows",
+        "mfolders",
+        "IFOLDER",
+        "flds"
     ]
 }
diff --git a/Makefile b/Makefile
index 236a415..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
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 cde2526..f32e53b 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,
 	}
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/exec.go b/lib/apiv1/exec.go
index eb93af8..4a591be 100644
--- a/lib/apiv1/exec.go
+++ b/lib/apiv1/exec.go
@@ -113,11 +113,13 @@ 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()
 
 	// Build command line
 	cmd := []string{}
@@ -135,7 +137,7 @@ func (s *APIService) execCmd(c *gin.Context) {
 
 	// FIXME - SEB: exec prevents to use syntax:
 	//  xds-exec -l debug -c xds-config.env -- "cd build && cmake .."
-	cmd = append(cmd, "cd", prj.GetFullPath(args.RPath))
+	cmd = append(cmd, "cd", folder.GetFullPath(args.RPath))
 	cmd = append(cmd, "&&", "exec", args.Cmd)
 
 	// Process command arguments
@@ -163,7 +165,7 @@ func (s *APIService) execCmd(c *gin.Context) {
 	execWS.Log = s.log
 
 	// Append client project dir to environment
-	execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.RelativePath)
+	execWS.Env = append(args.Env, "CLIENT_PROJECT_DIR="+prj.ClientPath)
 
 	// Set command execution timeout
 	if args.CmdTimeout == 0 {
@@ -189,8 +191,8 @@ func (s *APIService) execCmd(c *gin.Context) {
 		// Set correct path
 		data := e.UserData
 		rootPath := (*data)["RootPath"].(string)
-		relaPath := (*data)["RelativePath"].(string)
-		stdin = strings.Replace(stdin, relaPath, rootPath+"/"+relaPath, -1)
+		clientPath := (*data)["ClientPath"].(string)
+		stdin = strings.Replace(stdin, clientPath, rootPath+"/"+clientPath, -1)
 
 		return stdin, nil
 	}
@@ -283,7 +285,7 @@ func (s *APIService) execCmd(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 {
@@ -291,8 +293,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)
 					}
@@ -326,7 +328,7 @@ func (s *APIService) execCmd(c *gin.Context) {
 	data := make(map[string]interface{})
 	data["ID"] = prj.ID
 	data["RootPath"] = prj.RootPath
-	data["RelativePath"] = prj.RelativePath
+	data["ClientPath"] = prj.ClientPath
 	data["ExitImmediate"] = args.ExitImmediate
 	if args.TTY && args.TTYGdbserverFix {
 		data["gdbServerTTY"] = "workaround"
diff --git a/lib/apiv1/folders.go b/lib/apiv1/folders.go
index 44bda24..f957c6d 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
@@ -56,19 +46,11 @@ func (s *APIService) addFolder(c *gin.Context) {
 // delFolder deletes folder from server config
 func (s *APIService) delFolder(c *gin.Context) {
 	id := c.Param("id")
-	if id == "" {
-		common.APIError(c, "Invalid id")
-		return
-	}
-
-	confMut.Lock()
-	defer confMut.Unlock()
 
 	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
 	}
diff --git a/lib/apiv1/make.go b/lib/apiv1/make.go
index 6ae840b..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 {
@@ -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)
 					}
@@ -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..b76b3f3
--- /dev/null
+++ b/lib/folder/folder-interface.go
@@ -0,0 +1,59 @@
+package folder
+
+// FolderType definition
+type FolderType int
+
+const (
+	TypePathMap   = 1
+	TypeCloudSync = 2
+	TypeCifsSmb   = 3
+)
+
+// Folder Status definition
+const (
+	StatusErrorConfig = "ErrorConfig"
+	StatusDisable     = "Disable"
+	StatusEnable      = "Enable"
+)
+
+// IFOLDER Folder interface
+type IFOLDER interface {
+	Add(cfg FolderConfig) (*FolderConfig, error) // Add a new folder
+	GetConfig() FolderConfig                     // Get folder public configuration
+	GetFullPath(dir string) string               // Get folder full path
+	Remove() error                               // Remove a folder
+	Sync() error                                 // Force folder files synchronization
+	IsInSync() (bool, error)                     // Check if folder files are in-sync
+}
+
+// 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"`
+	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..8711df2
--- /dev/null
+++ b/lib/folder/folder-pathmap.go
@@ -0,0 +1,88 @@
+package folder
+
+import (
+	"fmt"
+	"io/ioutil"
+	"os"
+	"path/filepath"
+
+	common "github.com/iotbzh/xds-common/golib"
+)
+
+// IFOLDER interface implementation for native/path mapping folders
+
+// PathMap .
+type PathMap struct {
+	config FolderConfig
+}
+
+// NewFolderPathMap Create a new instance of PathMap
+func NewFolderPathMap() *PathMap {
+	f := PathMap{}
+	return &f
+}
+
+// Add a new folder
+func (f *PathMap) Add(cfg FolderConfig) (*FolderConfig, error) {
+	if cfg.DataPathMap.ServerPath == "" {
+		return nil, fmt.Errorf("ServerPath must be set")
+	}
+
+	// Sanity check
+	dir := cfg.DataPathMap.ServerPath
+	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 = cfg.DataPathMap.ServerPath
+	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
+}
+
+// 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..3c2457c
--- /dev/null
+++ b/lib/model/folders.go
@@ -0,0 +1,333 @@
+package model
+
+import (
+	"encoding/xml"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+
+	"github.com/Sirupsen/logrus"
+	common "github.com/iotbzh/xds-common/golib"
+	"github.com/iotbzh/xds-server/lib/folder"
+	"github.com/iotbzh/xds-server/lib/syncthing"
+	"github.com/iotbzh/xds-server/lib/xdsconfig"
+	uuid "github.com/satori/go.uuid"
+	"github.com/syncthing/syncthing/lib/sync"
+)
+
+// 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
+}
+
+// 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),
+	}
+}
+
+// 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); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// 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)
+}
+
+// CreateUpdate creates or update a folder
+func (f *Folders) createUpdate(newF folder.FolderConfig, create 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")
+	}
+
+	// Allocate a new UUID
+	if create {
+		newF.ID = uuid.NewV1().String()
+	}
+	if !create && newF.ID == "" {
+		return nil, fmt.Errorf("Cannot update folder with null ID")
+	}
+
+	// Set default value if needed
+	if newF.Status == "" {
+		newF.Status = folder.StatusDisable
+	}
+
+	if newF.Label == "" {
+		newF.Label = filepath.Base(newF.ClientPath) + "_" + newF.ID[0:8]
+	}
+
+	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()
+	default:
+		return nil, fmt.Errorf("Unsupported folder type")
+	}
+
+	// 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
+	}
+
+	// Register folder object
+	f.folders[newF.ID] = &fld
+
+	// Save config on disk
+	err = f.SaveConfig()
+
+	return newFolder, err
+}
+
+// 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
+}
+
+// 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..ffcd284
--- /dev/null
+++ b/lib/syncthing/folder-st.go
@@ -0,0 +1,97 @@
+package st
+
+import (
+	"fmt"
+	"path/filepath"
+
+	"github.com/iotbzh/xds-server/lib/folder"
+	"github.com/iotbzh/xds-server/lib/xdsconfig"
+	"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
+}
+
+// NewFolderST Create a new instance of STFolder
+func (s *SyncThing) NewFolderST(gc *xdsconfig.Config) *STFolder {
+	return &STFolder{
+		globalConfig: gc,
+		st:           s,
+	}
+}
+
+// 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
+		}
+
+		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)
+}
+
+// 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) {
+	return f.st.IsFolderInSync(f.stfConfig.ID)
+}
diff --git a/lib/syncthing/st.go b/lib/syncthing/st.go
index 3380cda..9bdb48f 100644
--- a/lib/syncthing/st.go
+++ b/lib/syncthing/st.go
@@ -32,6 +32,7 @@ type SyncThing struct {
 	Home    string
 	STCmd   *exec.Cmd
 	STICmd  *exec.Cmd
+	MyID    string
 
 	// Private fields
 	binDir      string
@@ -211,13 +212,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 {
@@ -314,7 +315,9 @@ func (s *SyncThing) Connect() error {
 
 	s.client.SetLogger(s.log)
 
-	return nil
+	s.MyID, err = s.IDGet()
+
+	return err
 }
 
 // IDGet returns the Syncthing ID of Syncthing instance running locally
@@ -360,44 +363,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/stfolder.go b/lib/syncthing/stfolder.go
index 661e19d..bbdcc43 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,63 @@ 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) {
+	// 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/webserver/server.go b/lib/webserver/server.go
index 7649cce..5183208 100644
--- a/lib/webserver/server.go
+++ b/lib/webserver/server.go
@@ -27,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
@@ -37,7 +37,7 @@ const indexFilename = "index.html"
 const cookieMaxAge = "3600"
 
 // New creates an instance of Server
-func New(cfg *xdsconfig.Config, mfolder *model.Folder, sdks *crosssdk.SDKs, logr *logrus.Logger) *Server {
+func New(cfg *xdsconfig.Config, mfolders *model.Folders, sdks *crosssdk.SDKs, logr *logrus.Logger) *Server {
 
 	// Setup logging for gin router
 	if logr.Level == logrus.DebugLevel {
@@ -63,7 +63,7 @@ func New(cfg *xdsconfig.Config, mfolder *model.Folder, sdks *crosssdk.SDKs, logr
 		webApp:    nil,
 		cfg:       cfg,
 		sessions:  nil,
-		mfolder:   mfolder,
+		mfolders:  mfolders,
 		sdks:      sdks,
 		log:       logr,
 		stop:      make(chan struct{}),
@@ -86,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..a3e5a7e 100644
--- a/lib/xdsconfig/config.go
+++ b/lib/xdsconfig/config.go
@@ -2,7 +2,6 @@ package xdsconfig
 
 import (
 	"fmt"
-
 	"os"
 
 	"github.com/Sirupsen/logrus"
@@ -16,13 +15,21 @@ 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)
+	Options  Options        `json:"-"`
 	FileConf FileConfig     `json:"-"`
 	Log      *logrus.Logger `json:"-"`
 }
 
+// Options set at the command line
+type Options struct {
+	ConfigFile     string
+	LogLevel       string
+	LogFile        string
+	NoFolderConfig bool
+}
+
 // Config default values
 const (
 	DefaultAPIVersion = "1"
@@ -41,7 +48,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 +65,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 060a927..65ab7a0 100644
--- a/main.go
+++ b/main.go
@@ -8,7 +8,6 @@ import (
 	"os/exec"
 	"os/signal"
 	"path/filepath"
-	"strings"
 	"syscall"
 	"time"
 
@@ -48,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
@@ -99,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
@@ -112,7 +111,7 @@ 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
 
@@ -136,26 +135,24 @@ func xdsApp(cliCtx *cli.Context) error {
 		ctx.Log.Out = fdL
 	}
 
-	// FIXME - add a builder interface and support other builder type (eg. native)
-	builderType := "syncthing"
-
-	switch builderType {
-	case "syncthing":
-
-		// 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)
 
@@ -174,64 +171,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
@@ -271,6 +241,11 @@ func main() {
 			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/webapp/src/app/config/config.component.html b/webapp/src/app/config/config.component.html
index d9229d5..5211c2d 100644
--- a/webapp/src/app/config/config.component.html
+++ b/webapp/src/app/config/config.component.html
@@ -71,13 +71,24 @@
                 </div>
 
                 <div class="col-xs-6">
-                    <label>Folder Path </label>
-                    <input type="text" style="width:70%;" formControlName="path" placeholder="myProject">
+                    <label>Client/Local Path </label>
+                    <input type="text" style="width:70%;" formControlName="pathCli" placeholder="myProject">
+                </div>
+                <div class="col-xs-6">
+                    <label>Server Path </label>
+                    <input type="text" style="width:70%;" formControlName="pathSvr" placeholder="myProject">
                 </div>
                 <div class="col-xs-4">
                     <label>Label </label>
                     <input type="text" formControlName="label" (keyup)="onKeyLabel($event)">
                 </div>
+                <div class="col-xs-4">
+                    <label>Type </label>
+                    <select class="form-control" formControlName="type">
+                        <option *ngFor="let t of projectTypes" [value]="t.value">{{t.display}}
+                        </option>
+                    </select>
+                </div>
             </div>
         </form>
 
@@ -91,4 +102,4 @@
 <!-- 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..0df707b 100644
--- a/webapp/src/app/config/config.component.ts
+++ b/webapp/src/app/config/config.component.ts
@@ -7,7 +7,8 @@ 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, IProject, ProjectType, ProjectTypes,
+    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";
@@ -33,6 +34,7 @@ export class ConfigComponent implements OnInit {
     curProj: number;
     userEditedLabel: boolean = false;
     xdsAgentPackages: IxdsAgentPackage[] = [];
+    projectTypes = ProjectTypes;
 
     // TODO replace by reactive FormControl + add validation
     syncToolUrl: string;
@@ -45,8 +47,8 @@ export class ConfigComponent implements OnInit {
     };
 
     addProjectForm: FormGroup;
-    pathCtrl = new FormControl("", Validators.required);
-
+    pathCliCtrl = new FormControl("", Validators.required);
+    pathSvrCtrl = new FormControl("", Validators.required);
 
     constructor(
         private configSvr: ConfigService,
@@ -57,11 +59,16 @@ export class ConfigComponent implements OnInit {
         private alert: AlertService,
         private fb: FormBuilder
     ) {
-        // FIXME implement multi project support
+        // Define types (first one is special/placeholder)
+        this.projectTypes.unshift({value: -1, display: "--Select a type--"});
+        let selectedType = this.projectTypes[0].value;
+
         this.curProj = 0;
         this.addProjectForm = fb.group({
-            path: this.pathCtrl,
+            pathCli: this.pathCliCtrl,
+            pathSvr: this.pathSvrCtrl,
             label: ["", Validators.nullValidator],
+            type: [selectedType, Validators.pattern("[0-9]+")],
         });
     }
 
@@ -82,7 +89,7 @@ export class ConfigComponent implements OnInit {
         });
 
         // Auto create label name
-        this.pathCtrl.valueChanges
+        this.pathCliCtrl.valueChanges
             .debounceTime(100)
             .filter(n => n)
             .map(n => "Project_" + n.split('/')[0])
@@ -91,6 +98,9 @@ export class ConfigComponent implements OnInit {
                     this.addProjectForm.patchValue({ label: value });
                 }
             });
+
+        // Select 1 first type by default
+        // SEB this.typeCtrl.setValue({type: ProjectTypes[0].value});
     }
 
     onKeyLabel(event: any) {
@@ -118,21 +128,24 @@ 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;
 
+        let type = formVal['type'].value;
+        let numType = Number(formVal['type']);
         this.configSvr.addProject({
             label: formVal['label'],
-            path: formVal['path'],
-            type: ProjectType.SYNCTHING,
+            pathClient: formVal['pathCli'],
+            pathServer: formVal['pathSvr'],
+            type: numType,
             // FIXME: allow to set defaultSdkID from New Project config panel
         });
     }
 
-}
\ 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/projects/projectCard.component.ts b/webapp/src/app/projects/projectCard.component.ts
index 7a7fa21..23e10a6 100644
--- a/webapp/src/app/projects/projectCard.component.ts
+++ b/webapp/src/app/projects/projectCard.component.ts
@@ -19,14 +19,23 @@ 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 != ''">
+                <th><span class="fa fa-fw fa-folder-open-o"></span>&nbsp;<span>Server path</span></th>
+                <td>{{ project.pathServer }}</td>
+            </tr>
+            <!--
+            <tr>
+                <th><span class="fa fa-fw fa-status"></span>&nbsp;<span>Status</span></th>
+                <td>{{ project.remotePrjDef.status }}</td>
+            </tr>
+            -->
             </tbody>
         </table >
     `,
@@ -53,11 +62,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/services/config.service.ts b/webapp/src/app/services/config.service.ts
index 722c347..c65332f 100644
--- a/webapp/src/app/services/config.service.ts
+++ b/webapp/src/app/services/config.service.ts
@@ -13,17 +13,22 @@ 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 var ProjectTypes = [
+    { value: ProjectType.NATIVE_PATHMAP, display: "Path mapping" },
+    { value: ProjectType.SYNCTHING, display: "Cloud Sync" }
+];
+
 export interface INativeProject {
     // TODO
 }
@@ -31,7 +36,8 @@ export interface INativeProject {
 export interface IProject {
     id?: string;
     label: string;
-    path: string;
+    pathClient: string;
+    pathServer?: string;
     type: ProjectType;
     remotePrjDef?: INativeProject | ISyncThingProject;
     localPrjDef?: any;
@@ -172,7 +178,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);
@@ -213,8 +219,9 @@ export class ConfigService {
                             let pp: IProject = {
                                 id: rPrj.id,
                                 label: rPrj.label,
-                                path: rPrj.path,
-                                type: ProjectType.SYNCTHING,    // FIXME support other types
+                                pathClient: rPrj.path,
+                                pathServer: rPrj.dataPathMap.serverPath,
+                                type: rPrj.type,
                                 remotePrjDef: Object.assign({}, rPrj),
                                 localPrjDef: Object.assign({}, lPrj[0]),
                             };
@@ -272,57 +279,46 @@ export class ConfigService {
 
     addProject(prj: 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];
+        } else if (!((pathCli.charAt(0) === '/') ||
+            (pathCli.charAt(1) === ':' && (pathCli.charAt(2) === '\\' || pathCli.charAt(2) === '/')))) {
+            pathCli = this.confStore.projectsRootDir + '/' + pathCli;
         }
 
-        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;
-        }
-
-        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)
+        this.xdsServerSvr.addProject(xdsPrj)
             .subscribe(resStRemotePrj => {
                 newPrj.remotePrjDef = resStRemotePrj;
+                newPrj.id = resStRemotePrj.id;
 
                 // FIXME REWORK local ST config
                 //  move logic to server side tunneling-back by WS
+                let stData = resStRemotePrj.dataCloudSync;
 
                 // Now setup local config
                 let stLocPrj: ISyncThingProject = {
-                    id: sdkPrj.id,
-                    label: sdkPrj.label,
-                    path: sdkPrj.path,
-                    remoteSyncThingID: resStRemotePrj.builderSThgID
+                    id: resStRemotePrj.id,
+                    label: xdsPrj.label,
+                    path: xdsPrj.path,
+                    serverSyncThingID: stData.builderSThgID
                 };
 
                 // Set local Syncthing config
@@ -366,4 +362,4 @@ export class ConfigService {
         return this.confStore.projects.findIndex((item) => item.id === id);
     }
 
-}
\ No newline at end of file
+}
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..b11fe9f 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,28 @@ interface IXDSBuilderConfig {
     syncThingID: string;
 }
 
-interface IXDSFolderConfig {
+export interface IXDSFolderConfig {
     id: string;
     label: string;
     path: string;
     type: number;
-    syncThingID: string;
-    builderSThgID?: string;
     status?: string;
     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 {
@@ -172,16 +186,8 @@ 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> {
@@ -244,7 +250,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 +265,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);
     }
-- 
cgit