22 "github.com/Sirupsen/logrus"
23 common "github.com/iotbzh/xds-common/golib"
24 "github.com/iotbzh/xds-server/lib/xdsconfig"
25 "github.com/syncthing/syncthing/lib/config"
29 type SyncThing struct {
39 exitSTChan chan ExitChan
40 exitSTIChan chan ExitChan
41 conf *xdsconfig.Config
42 client *common.HTTPClient
46 // ExitChan Channel used for process exit
47 type ExitChan struct {
52 // ConfigInSync Check whether if Syncthing configuration is in sync
53 type configInSync struct {
54 ConfigInSync bool `json:"configInSync"`
57 // FolderStatus Information about the current status of a folder.
58 type FolderStatus struct {
59 GlobalFiles int `json:"globalFiles"`
60 GlobalDirectories int `json:"globalDirectories"`
61 GlobalSymlinks int `json:"globalSymlinks"`
62 GlobalDeleted int `json:"globalDeleted"`
63 GlobalBytes int64 `json:"globalBytes"`
65 LocalFiles int `json:"localFiles"`
66 LocalDirectories int `json:"localDirectories"`
67 LocalSymlinks int `json:"localSymlinks"`
68 LocalDeleted int `json:"localDeleted"`
69 LocalBytes int64 `json:"localBytes"`
71 NeedFiles int `json:"needFiles"`
72 NeedDirectories int `json:"needDirectories"`
73 NeedSymlinks int `json:"needSymlinks"`
74 NeedDeletes int `json:"needDeletes"`
75 NeedBytes int64 `json:"needBytes"`
77 InSyncFiles int `json:"inSyncFiles"`
78 InSyncBytes int64 `json:"inSyncBytes"`
80 State string `json:"state"`
81 StateChanged time.Time `json:"stateChanged"`
83 Sequence int64 `json:"sequence"`
85 IgnorePatterns bool `json:"ignorePatterns"`
88 // NewSyncThing creates a new instance of Syncthing
89 func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
90 var url, apiKey, home, binDir string
93 stCfg := conf.FileConf.SThgConf
95 url = stCfg.GuiAddress
96 apiKey = stCfg.GuiAPIKey
102 url = "http://localhost:8384"
104 if url[0:7] != "http://" {
105 url = "http://" + url
113 if binDir, err = filepath.Abs(filepath.Dir(os.Args[0])); err != nil {
114 binDir = "/usr/local/bin"
123 logsDir: conf.FileConf.LogsDir,
131 // Start Starts syncthing process
132 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
134 // Kill existing process (useful for debug ;-) )
135 if os.Getenv("DEBUG_MODE") != "" {
136 exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
139 path, err := exec.LookPath(path.Join(s.binDir, exeName))
141 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, s.binDir)
143 cmd := exec.Command(path, args...)
144 cmd.Env = os.Environ()
145 for _, ev := range env {
146 cmd.Env = append(cmd.Env, ev)
151 logFilename := filepath.Join(s.logsDir, exeName+".log")
153 outfile, err := os.Create(logFilename)
155 return nil, fmt.Errorf("Cannot create log file %s", logFilename)
158 cmdOut, err := cmd.StdoutPipe()
160 return nil, fmt.Errorf("Pipe stdout error for : %s", err)
163 go io.Copy(outfile, cmdOut)
171 *eChan = make(chan ExitChan, 1)
172 go func(c *exec.Cmd, oF *os.File) {
174 sts, err := c.Process.Wait()
176 s := sts.Sys().(syscall.WaitStatus)
177 status = s.ExitStatus()
182 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
184 *eChan <- ExitChan{status, err}
190 // Start Starts syncthing process
191 func (s *SyncThing) Start() (*exec.Cmd, error) {
194 s.log.Infof(" ST home=%s", s.Home)
195 s.log.Infof(" ST url=%s", s.BaseURL)
200 "--gui-address=" + s.BaseURL,
204 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
205 s.log.Infof(" ST apikey=%s", s.APIKey)
207 if s.log.Level == logrus.DebugLevel {
208 args = append(args, "-verbose")
212 "STNODEFAULTFOLDER=1",
217 s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
219 // Use autogenerated apikey if not set by config.json
221 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
223 if b, err := ioutil.ReadAll(fd); err == nil {
224 re := regexp.MustCompile("<apikey>(.*)</apikey>")
225 key := re.FindStringSubmatch(string(b))
236 // StartInotify Starts syncthing-inotify process
237 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
239 exeName := "syncthing-inotify"
241 s.log.Infof(" STI url=%s", s.BaseURL)
244 "-target=" + s.BaseURL,
247 args = append(args, "-api="+s.APIKey)
248 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
250 if s.log.Level == logrus.DebugLevel {
251 args = append(args, "-verbosity=4")
256 s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
261 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
262 if err := proc.Signal(os.Interrupt); err != nil {
263 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
267 case <-time.After(time.Second):
268 // A bigger bonk on the head.
269 if err := proc.Signal(os.Kill); err != nil {
270 s.log.Infof("Proc term %s error: %s", pname, err.Error())
275 s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
278 // Stop Stops syncthing process
279 func (s *SyncThing) Stop() {
283 s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
287 // StopInotify Stops syncthing process
288 func (s *SyncThing) StopInotify() {
292 s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
296 // Connect Establish HTTP connection with Syncthing
297 func (s *SyncThing) Connect() error {
299 s.client, err = common.HTTPNewClient(s.BaseURL,
300 common.HTTPClientConfig{
302 HeaderClientKeyName: "X-Syncthing-ID",
305 msg := ": " + err.Error()
306 if strings.Contains(err.Error(), "connection refused") {
307 msg = fmt.Sprintf("(url: %s)", s.BaseURL)
309 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
312 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
315 s.client.SetLogger(s.log)
320 // IDGet returns the Syncthing ID of Syncthing instance running locally
321 func (s *SyncThing) IDGet() (string, error) {
323 if err := s.client.HTTPGet("system/status", &data); err != nil {
326 status := make(map[string]interface{})
327 json.Unmarshal(data, &status)
328 return status["myID"].(string), nil
331 // ConfigGet returns the current Syncthing configuration
332 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
334 config := config.Configuration{}
335 if err := s.client.HTTPGet("system/config", &data); err != nil {
338 err := json.Unmarshal(data, &config)
342 // ConfigSet set Syncthing configuration
343 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
344 body, err := json.Marshal(cfg)
348 return s.client.HTTPPost("system/config", string(body))
351 // IsConfigInSync Returns true if configuration is in sync
352 func (s *SyncThing) IsConfigInSync() (bool, error) {
355 if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
358 if err := json.Unmarshal(data, &d); err != nil {
361 return d.ConfigInSync, nil
364 // FolderStatus Returns all information about the current
365 func (s *SyncThing) FolderStatus(folderID string) (*FolderStatus, error) {
369 return nil, fmt.Errorf("folderID not set")
371 if err := s.client.HTTPGet("db/status?folder="+folderID, &data); err != nil {
374 if err := json.Unmarshal(data, &res); err != nil {
380 // IsFolderInSync Returns true when folder is in sync
381 func (s *SyncThing) IsFolderInSync(folderID string) (bool, error) {
382 // FIXME better to detected FolderCompletion event (/rest/events)
383 // See https://docs.syncthing.net/dev/events.html
384 sts, err := s.FolderStatus(folderID)
388 return sts.NeedBytes == 0, nil
391 // FolderScan Request immediate folder scan.
392 // Scan all folders if folderID param is empty
393 func (s *SyncThing) FolderScan(folderID string, subpath string) error {
396 url += "?folder=" + folderID
399 url += "&sub=" + subpath
402 return s.client.HTTPPost(url, "")