Add folder interface and support native pathmap folder type.
[src/xds/xds-server.git] / lib / syncthing / st.go
1 package st
2
3 import (
4         "encoding/json"
5         "os"
6         "os/exec"
7         "path"
8         "path/filepath"
9         "syscall"
10         "time"
11
12         "strings"
13
14         "fmt"
15
16         "io"
17
18         "io/ioutil"
19
20         "regexp"
21
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"
26 )
27
28 // SyncThing .
29 type SyncThing struct {
30         BaseURL string
31         APIKey  string
32         Home    string
33         STCmd   *exec.Cmd
34         STICmd  *exec.Cmd
35         MyID    string
36
37         // Private fields
38         binDir      string
39         logsDir     string
40         exitSTChan  chan ExitChan
41         exitSTIChan chan ExitChan
42         conf        *xdsconfig.Config
43         client      *common.HTTPClient
44         log         *logrus.Logger
45 }
46
47 // ExitChan Channel used for process exit
48 type ExitChan struct {
49         status int
50         err    error
51 }
52
53 // ConfigInSync Check whether if Syncthing configuration is in sync
54 type configInSync struct {
55         ConfigInSync bool `json:"configInSync"`
56 }
57
58 // FolderStatus Information about the current status of a folder.
59 type FolderStatus struct {
60         GlobalFiles       int   `json:"globalFiles"`
61         GlobalDirectories int   `json:"globalDirectories"`
62         GlobalSymlinks    int   `json:"globalSymlinks"`
63         GlobalDeleted     int   `json:"globalDeleted"`
64         GlobalBytes       int64 `json:"globalBytes"`
65
66         LocalFiles       int   `json:"localFiles"`
67         LocalDirectories int   `json:"localDirectories"`
68         LocalSymlinks    int   `json:"localSymlinks"`
69         LocalDeleted     int   `json:"localDeleted"`
70         LocalBytes       int64 `json:"localBytes"`
71
72         NeedFiles       int   `json:"needFiles"`
73         NeedDirectories int   `json:"needDirectories"`
74         NeedSymlinks    int   `json:"needSymlinks"`
75         NeedDeletes     int   `json:"needDeletes"`
76         NeedBytes       int64 `json:"needBytes"`
77
78         InSyncFiles int   `json:"inSyncFiles"`
79         InSyncBytes int64 `json:"inSyncBytes"`
80
81         State        string    `json:"state"`
82         StateChanged time.Time `json:"stateChanged"`
83
84         Sequence int64 `json:"sequence"`
85
86         IgnorePatterns bool `json:"ignorePatterns"`
87 }
88
89 // NewSyncThing creates a new instance of Syncthing
90 func NewSyncThing(conf *xdsconfig.Config, log *logrus.Logger) *SyncThing {
91         var url, apiKey, home, binDir string
92         var err error
93
94         stCfg := conf.FileConf.SThgConf
95         if stCfg != nil {
96                 url = stCfg.GuiAddress
97                 apiKey = stCfg.GuiAPIKey
98                 home = stCfg.Home
99                 binDir = stCfg.BinDir
100         }
101
102         if url == "" {
103                 url = "http://localhost:8384"
104         }
105         if url[0:7] != "http://" {
106                 url = "http://" + url
107         }
108
109         if home == "" {
110                 home = "/mnt/share"
111         }
112
113         if binDir == "" {
114                 if binDir, err = filepath.Abs(filepath.Dir(os.Args[0])); err != nil {
115                         binDir = "/usr/local/bin"
116                 }
117         }
118
119         s := SyncThing{
120                 BaseURL: url,
121                 APIKey:  apiKey,
122                 Home:    home,
123                 binDir:  binDir,
124                 logsDir: conf.FileConf.LogsDir,
125                 log:     log,
126                 conf:    conf,
127         }
128
129         return &s
130 }
131
132 // Start Starts syncthing process
133 func (s *SyncThing) startProc(exeName string, args []string, env []string, eChan *chan ExitChan) (*exec.Cmd, error) {
134
135         // Kill existing process (useful for debug ;-) )
136         if os.Getenv("DEBUG_MODE") != "" {
137                 exec.Command("bash", "-c", "pkill -9 "+exeName).Output()
138         }
139
140         path, err := exec.LookPath(path.Join(s.binDir, exeName))
141         if err != nil {
142                 return nil, fmt.Errorf("Cannot find %s executable in %s", exeName, s.binDir)
143         }
144         cmd := exec.Command(path, args...)
145         cmd.Env = os.Environ()
146         for _, ev := range env {
147                 cmd.Env = append(cmd.Env, ev)
148         }
149
150         // open log file
151         var outfile *os.File
152         logFilename := filepath.Join(s.logsDir, exeName+".log")
153         if s.logsDir != "" {
154                 outfile, err := os.Create(logFilename)
155                 if err != nil {
156                         return nil, fmt.Errorf("Cannot create log file %s", logFilename)
157                 }
158
159                 cmdOut, err := cmd.StdoutPipe()
160                 if err != nil {
161                         return nil, fmt.Errorf("Pipe stdout error for : %s", err)
162                 }
163
164                 go io.Copy(outfile, cmdOut)
165         }
166
167         err = cmd.Start()
168         if err != nil {
169                 return nil, err
170         }
171
172         *eChan = make(chan ExitChan, 1)
173         go func(c *exec.Cmd, oF *os.File) {
174                 status := 0
175                 sts, err := c.Process.Wait()
176                 if !sts.Success() {
177                         s := sts.Sys().(syscall.WaitStatus)
178                         status = s.ExitStatus()
179                 }
180                 if oF != nil {
181                         oF.Close()
182                 }
183                 s.log.Debugf("%s exited with status %d, err %v", exeName, status, err)
184
185                 *eChan <- ExitChan{status, err}
186         }(cmd, outfile)
187
188         return cmd, nil
189 }
190
191 // Start Starts syncthing process
192 func (s *SyncThing) Start() (*exec.Cmd, error) {
193         var err error
194
195         s.log.Infof(" ST home=%s", s.Home)
196         s.log.Infof(" ST  url=%s", s.BaseURL)
197
198         args := []string{
199                 "--home=" + s.Home,
200                 "-no-browser",
201                 "--gui-address=" + s.BaseURL,
202         }
203
204         if s.APIKey != "" {
205                 args = append(args, "-gui-apikey=\""+s.APIKey+"\"")
206                 s.log.Infof(" ST apikey=%s", s.APIKey)
207         }
208         if s.log.Level == logrus.DebugLevel {
209                 args = append(args, "-verbose")
210         }
211
212         env := []string{
213                 "STNODEFAULTFOLDER=1",
214                 "STNOUPGRADE=1",
215                 "STNORESTART=1", // FIXME SEB remove ?
216         }
217
218         s.STCmd, err = s.startProc("syncthing", args, env, &s.exitSTChan)
219
220         // Use autogenerated apikey if not set by config.json
221         if err == nil && s.APIKey == "" {
222                 if fd, err := os.Open(filepath.Join(s.Home, "config.xml")); err == nil {
223                         defer fd.Close()
224                         if b, err := ioutil.ReadAll(fd); err == nil {
225                                 re := regexp.MustCompile("<apikey>(.*)</apikey>")
226                                 key := re.FindStringSubmatch(string(b))
227                                 if len(key) >= 1 {
228                                         s.APIKey = key[1]
229                                 }
230                         }
231                 }
232         }
233
234         return s.STCmd, err
235 }
236
237 // StartInotify Starts syncthing-inotify process
238 func (s *SyncThing) StartInotify() (*exec.Cmd, error) {
239         var err error
240         exeName := "syncthing-inotify"
241
242         s.log.Infof(" STI  url=%s", s.BaseURL)
243
244         args := []string{
245                 "-target=" + s.BaseURL,
246         }
247         if s.APIKey != "" {
248                 args = append(args, "-api="+s.APIKey)
249                 s.log.Infof("%s uses apikey=%s", exeName, s.APIKey)
250         }
251         if s.log.Level == logrus.DebugLevel {
252                 args = append(args, "-verbosity=4")
253         }
254
255         env := []string{}
256
257         s.STICmd, err = s.startProc(exeName, args, env, &s.exitSTIChan)
258
259         return s.STICmd, err
260 }
261
262 func (s *SyncThing) stopProc(pname string, proc *os.Process, exit chan ExitChan) {
263         if err := proc.Signal(os.Interrupt); err != nil {
264                 s.log.Infof("Proc interrupt %s error: %s", pname, err.Error())
265
266                 select {
267                 case <-exit:
268                 case <-time.After(time.Second):
269                         // A bigger bonk on the head.
270                         if err := proc.Signal(os.Kill); err != nil {
271                                 s.log.Infof("Proc term %s error: %s", pname, err.Error())
272                         }
273                         <-exit
274                 }
275         }
276         s.log.Infof("%s stopped (PID %d)", pname, proc.Pid)
277 }
278
279 // Stop Stops syncthing process
280 func (s *SyncThing) Stop() {
281         if s.STCmd == nil {
282                 return
283         }
284         s.stopProc("syncthing", s.STCmd.Process, s.exitSTChan)
285         s.STCmd = nil
286 }
287
288 // StopInotify Stops syncthing process
289 func (s *SyncThing) StopInotify() {
290         if s.STICmd == nil {
291                 return
292         }
293         s.stopProc("syncthing-inotify", s.STICmd.Process, s.exitSTIChan)
294         s.STICmd = nil
295 }
296
297 // Connect Establish HTTP connection with Syncthing
298 func (s *SyncThing) Connect() error {
299         var err error
300         s.client, err = common.HTTPNewClient(s.BaseURL,
301                 common.HTTPClientConfig{
302                         URLPrefix:           "/rest",
303                         HeaderClientKeyName: "X-Syncthing-ID",
304                 })
305         if err != nil {
306                 msg := ": " + err.Error()
307                 if strings.Contains(err.Error(), "connection refused") {
308                         msg = fmt.Sprintf("(url: %s)", s.BaseURL)
309                 }
310                 return fmt.Errorf("ERROR: cannot connect to Syncthing %s", msg)
311         }
312         if s.client == nil {
313                 return fmt.Errorf("ERROR: cannot connect to Syncthing (null client)")
314         }
315
316         s.client.SetLogger(s.log)
317
318         s.MyID, err = s.IDGet()
319
320         return err
321 }
322
323 // IDGet returns the Syncthing ID of Syncthing instance running locally
324 func (s *SyncThing) IDGet() (string, error) {
325         var data []byte
326         if err := s.client.HTTPGet("system/status", &data); err != nil {
327                 return "", err
328         }
329         status := make(map[string]interface{})
330         json.Unmarshal(data, &status)
331         return status["myID"].(string), nil
332 }
333
334 // ConfigGet returns the current Syncthing configuration
335 func (s *SyncThing) ConfigGet() (config.Configuration, error) {
336         var data []byte
337         config := config.Configuration{}
338         if err := s.client.HTTPGet("system/config", &data); err != nil {
339                 return config, err
340         }
341         err := json.Unmarshal(data, &config)
342         return config, err
343 }
344
345 // ConfigSet set Syncthing configuration
346 func (s *SyncThing) ConfigSet(cfg config.Configuration) error {
347         body, err := json.Marshal(cfg)
348         if err != nil {
349                 return err
350         }
351         return s.client.HTTPPost("system/config", string(body))
352 }
353
354 // IsConfigInSync Returns true if configuration is in sync
355 func (s *SyncThing) IsConfigInSync() (bool, error) {
356         var data []byte
357         var d configInSync
358         if err := s.client.HTTPGet("system/config/insync", &data); err != nil {
359                 return false, err
360         }
361         if err := json.Unmarshal(data, &d); err != nil {
362                 return false, err
363         }
364         return d.ConfigInSync, nil
365 }