Defined structures for /exec and /signal response.
[src/xds/xds-server.git] / lib / model / folders.go
1 package model
2
3 import (
4         "encoding/xml"
5         "fmt"
6         "log"
7         "os"
8         "path/filepath"
9         "strings"
10         "time"
11
12         "github.com/Sirupsen/logrus"
13         "github.com/franciscocpg/reflectme"
14         common "github.com/iotbzh/xds-common/golib"
15         "github.com/iotbzh/xds-server/lib/folder"
16         "github.com/iotbzh/xds-server/lib/syncthing"
17         "github.com/iotbzh/xds-server/lib/xdsconfig"
18         "github.com/syncthing/syncthing/lib/sync"
19 )
20
21 // Folders Represent a an XDS folders
22 type Folders struct {
23         fileOnDisk string
24         Conf       *xdsconfig.Config
25         Log        *logrus.Logger
26         SThg       *st.SyncThing
27         folders    map[string]*folder.IFOLDER
28         registerCB []RegisteredCB
29 }
30
31 type RegisteredCB struct {
32         cb   *folder.EventCB
33         data *folder.EventCBData
34 }
35
36 // Mutex to make add/delete atomic
37 var fcMutex = sync.NewMutex()
38 var ffMutex = sync.NewMutex()
39
40 // FoldersNew Create a new instance of Model Folders
41 func FoldersNew(cfg *xdsconfig.Config, st *st.SyncThing) *Folders {
42         file, _ := xdsconfig.FoldersConfigFilenameGet()
43         return &Folders{
44                 fileOnDisk: file,
45                 Conf:       cfg,
46                 Log:        cfg.Log,
47                 SThg:       st,
48                 folders:    make(map[string]*folder.IFOLDER),
49                 registerCB: []RegisteredCB{},
50         }
51 }
52
53 // LoadConfig Load folders configuration from disk
54 func (f *Folders) LoadConfig() error {
55         var flds []folder.FolderConfig
56         var stFlds []folder.FolderConfig
57
58         // load from disk
59         if f.Conf.Options.NoFolderConfig {
60                 f.Log.Infof("Don't read folder config file (-no-folderconfig option is set)")
61         } else if f.fileOnDisk != "" {
62                 f.Log.Infof("Use folder config file: %s", f.fileOnDisk)
63                 err := foldersConfigRead(f.fileOnDisk, &flds)
64                 if err != nil {
65                         if strings.HasPrefix(err.Error(), "No folder config") {
66                                 f.Log.Warnf(err.Error())
67                         } else {
68                                 return err
69                         }
70                 }
71         } else {
72                 f.Log.Warnf("Folders config filename not set")
73         }
74
75         // Retrieve initial Syncthing config (just append don't overwrite existing ones)
76         if f.SThg != nil {
77                 f.Log.Infof("Retrieve syncthing folder config")
78                 if err := f.SThg.FolderLoadFromStConfig(&stFlds); err != nil {
79                         // Don't exit on such error, just log it
80                         f.Log.Errorf(err.Error())
81                 }
82         } else {
83                 f.Log.Infof("Syncthing support is disabled.")
84         }
85
86         // Merge syncthing folders into XDS folders
87         for _, stf := range stFlds {
88                 found := false
89                 for i, xf := range flds {
90                         if xf.ID == stf.ID {
91                                 found = true
92                                 // sanity check
93                                 if xf.Type != folder.TypeCloudSync {
94                                         flds[i].Status = folder.StatusErrorConfig
95                                 }
96                                 break
97                         }
98                 }
99                 // add it
100                 if !found {
101                         flds = append(flds, stf)
102                 }
103         }
104
105         // Detect ghost project
106         // (IOW existing in xds file config and not in syncthing database)
107         if f.SThg != nil {
108                 for i, xf := range flds {
109                         // only for syncthing project
110                         if xf.Type != folder.TypeCloudSync {
111                                 continue
112                         }
113                         found := false
114                         for _, stf := range stFlds {
115                                 if stf.ID == xf.ID {
116                                         found = true
117                                         break
118                                 }
119                         }
120                         if !found {
121                                 flds[i].Status = folder.StatusErrorConfig
122                         }
123                 }
124         }
125
126         // Update folders
127         f.Log.Infof("Loading initial folders config: %d folders found", len(flds))
128         for _, fc := range flds {
129                 if _, err := f.createUpdate(fc, false, true); err != nil {
130                         return err
131                 }
132         }
133
134         // Save config on disk
135         err := f.SaveConfig()
136
137         return err
138 }
139
140 // SaveConfig Save folders configuration to disk
141 func (f *Folders) SaveConfig() error {
142         if f.fileOnDisk == "" {
143                 return fmt.Errorf("Folders config filename not set")
144         }
145
146         // FIXME: buffered save or avoid to write on disk each time
147         return foldersConfigWrite(f.fileOnDisk, f.getConfigArrUnsafe())
148 }
149
150 // ResolveID Complete a Folder ID (helper for user that can use partial ID value)
151 func (f *Folders) ResolveID(id string) (string, error) {
152         if id == "" {
153                 return "", nil
154         }
155
156         match := []string{}
157         for iid := range f.folders {
158                 if strings.HasPrefix(iid, id) {
159                         match = append(match, iid)
160                 }
161         }
162
163         if len(match) == 1 {
164                 return match[0], nil
165         } else if len(match) == 0 {
166                 return id, fmt.Errorf("Unknown id")
167         }
168         return id, fmt.Errorf("Multiple IDs found with provided prefix: " + id)
169 }
170
171 // Get returns the folder config or nil if not existing
172 func (f *Folders) Get(id string) *folder.IFOLDER {
173         if id == "" {
174                 return nil
175         }
176         fc, exist := f.folders[id]
177         if !exist {
178                 return nil
179         }
180         return fc
181 }
182
183 // GetConfigArr returns the config of all folders as an array
184 func (f *Folders) GetConfigArr() []folder.FolderConfig {
185         fcMutex.Lock()
186         defer fcMutex.Unlock()
187
188         return f.getConfigArrUnsafe()
189 }
190
191 // getConfigArrUnsafe Same as GetConfigArr without mutex protection
192 func (f *Folders) getConfigArrUnsafe() []folder.FolderConfig {
193         conf := []folder.FolderConfig{}
194         for _, v := range f.folders {
195                 conf = append(conf, (*v).GetConfig())
196         }
197         return conf
198 }
199
200 // Add adds a new folder
201 func (f *Folders) Add(newF folder.FolderConfig) (*folder.FolderConfig, error) {
202         return f.createUpdate(newF, true, false)
203 }
204
205 // CreateUpdate creates or update a folder
206 func (f *Folders) createUpdate(newF folder.FolderConfig, create bool, initial bool) (*folder.FolderConfig, error) {
207
208         fcMutex.Lock()
209         defer fcMutex.Unlock()
210
211         // Sanity check
212         if _, exist := f.folders[newF.ID]; create && exist {
213                 return nil, fmt.Errorf("ID already exists")
214         }
215         if newF.ClientPath == "" {
216                 return nil, fmt.Errorf("ClientPath must be set")
217         }
218
219         // Create a new folder object
220         var fld folder.IFOLDER
221         switch newF.Type {
222         // SYNCTHING
223         case folder.TypeCloudSync:
224                 if f.SThg != nil {
225                         fld = f.SThg.NewFolderST(f.Conf)
226                 } else {
227                         f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID)
228                         fld = folder.NewFolderSTDisable(f.Conf)
229                 }
230
231         // PATH MAP
232         case folder.TypePathMap:
233                 fld = folder.NewFolderPathMap(f.Conf)
234         default:
235                 return nil, fmt.Errorf("Unsupported folder type")
236         }
237
238         // Allocate a new UUID
239         if create {
240                 newF.ID = fld.NewUID("")
241         }
242         if !create && newF.ID == "" {
243                 return nil, fmt.Errorf("Cannot update folder with null ID")
244         }
245
246         // Set default value if needed
247         if newF.Status == "" {
248                 newF.Status = folder.StatusDisable
249         }
250         if newF.Label == "" {
251                 newF.Label = filepath.Base(newF.ClientPath)
252                 if len(newF.ID) > 8 {
253                         newF.Label += "_" + newF.ID[0:8]
254                 }
255         }
256
257         // Normalize path (needed for Windows path including bashlashes)
258         newF.ClientPath = common.PathNormalize(newF.ClientPath)
259
260         // Add new folder
261         newFolder, err := fld.Add(newF)
262         if err != nil {
263                 newF.Status = folder.StatusErrorConfig
264                 log.Printf("ERROR Adding folder: %v\n", err)
265                 return newFolder, err
266         }
267
268         // Add to folders list
269         f.folders[newF.ID] = &fld
270
271         // Save config on disk
272         if !initial {
273                 if err := f.SaveConfig(); err != nil {
274                         return newFolder, err
275                 }
276         }
277
278         // Register event change callback
279         for _, rcb := range f.registerCB {
280                 if err := fld.RegisterEventChange(rcb.cb, rcb.data); err != nil {
281                         return newFolder, err
282                 }
283         }
284
285         // Force sync after creation
286         // (need to defer to be sure that WS events will arrive after HTTP creation reply)
287         go func() {
288                 time.Sleep(time.Millisecond * 500)
289                 fld.Sync()
290         }()
291
292         return newFolder, nil
293 }
294
295 // Delete deletes a specific folder
296 func (f *Folders) Delete(id string) (folder.FolderConfig, error) {
297         var err error
298
299         fcMutex.Lock()
300         defer fcMutex.Unlock()
301
302         fld := folder.FolderConfig{}
303         fc, exist := f.folders[id]
304         if !exist {
305                 return fld, fmt.Errorf("unknown id")
306         }
307
308         fld = (*fc).GetConfig()
309
310         if err = (*fc).Remove(); err != nil {
311                 return fld, err
312         }
313
314         delete(f.folders, id)
315
316         // Save config on disk
317         err = f.SaveConfig()
318
319         return fld, err
320 }
321
322 // Update Update a specific folder
323 func (f *Folders) Update(id string, cfg folder.FolderConfig) (*folder.FolderConfig, error) {
324         fcMutex.Lock()
325         defer fcMutex.Unlock()
326
327         fc, exist := f.folders[id]
328         if !exist {
329                 return nil, fmt.Errorf("unknown id")
330         }
331
332         // Copy current in a new object to change nothing in case of an error rises
333         newCfg := folder.FolderConfig{}
334         reflectme.Copy((*fc).GetConfig(), &newCfg)
335
336         // Only update some fields
337         dirty := false
338         for _, fieldName := range folder.FolderConfigUpdatableFields {
339                 valNew, err := reflectme.GetField(cfg, fieldName)
340                 if err == nil {
341                         valCur, err := reflectme.GetField(newCfg, fieldName)
342                         if err == nil && valNew != valCur {
343                                 err = reflectme.SetField(&newCfg, fieldName, valNew)
344                                 if err != nil {
345                                         return nil, err
346                                 }
347                                 dirty = true
348                         }
349                 }
350         }
351
352         if !dirty {
353                 return &newCfg, nil
354         }
355
356         fld, err := (*fc).Update(newCfg)
357         if err != nil {
358                 return fld, err
359         }
360
361         // Save config on disk
362         err = f.SaveConfig()
363
364         // Send event to notified changes
365         // TODO emit folder change event
366
367         return fld, err
368 }
369
370 // RegisterEventChange requests registration for folder event change
371 func (f *Folders) RegisterEventChange(id string, cb *folder.EventCB, data *folder.EventCBData) error {
372
373         flds := make(map[string]*folder.IFOLDER)
374         if id != "" {
375                 // Register to a specific folder
376                 flds[id] = f.Get(id)
377         } else {
378                 // Register to all folders
379                 flds = f.folders
380                 f.registerCB = append(f.registerCB, RegisteredCB{cb: cb, data: data})
381         }
382
383         for _, fld := range flds {
384                 err := (*fld).RegisterEventChange(cb, data)
385                 if err != nil {
386                         return err
387                 }
388         }
389
390         return nil
391 }
392
393 // ForceSync Force the synchronization of a folder
394 func (f *Folders) ForceSync(id string) error {
395         fc := f.Get(id)
396         if fc == nil {
397                 return fmt.Errorf("Unknown id")
398         }
399         return (*fc).Sync()
400 }
401
402 // IsFolderInSync Returns true when folder is in sync
403 func (f *Folders) IsFolderInSync(id string) (bool, error) {
404         fc := f.Get(id)
405         if fc == nil {
406                 return false, fmt.Errorf("Unknown id")
407         }
408         return (*fc).IsInSync()
409 }
410
411 //*** Private functions ***
412
413 // Use XML format and not json to be able to save/load all fields including
414 // ones that are masked in json (IOW defined with `json:"-"`)
415 type xmlFolders struct {
416         XMLName xml.Name              `xml:"folders"`
417         Version string                `xml:"version,attr"`
418         Folders []folder.FolderConfig `xml:"folders"`
419 }
420
421 // foldersConfigRead reads folders config from disk
422 func foldersConfigRead(file string, folders *[]folder.FolderConfig) error {
423         if !common.Exists(file) {
424                 return fmt.Errorf("No folder config file found (%s)", file)
425         }
426
427         ffMutex.Lock()
428         defer ffMutex.Unlock()
429
430         fd, err := os.Open(file)
431         defer fd.Close()
432         if err != nil {
433                 return err
434         }
435
436         data := xmlFolders{}
437         err = xml.NewDecoder(fd).Decode(&data)
438         if err == nil {
439                 *folders = data.Folders
440         }
441         return err
442 }
443
444 // foldersConfigWrite writes folders config on disk
445 func foldersConfigWrite(file string, folders []folder.FolderConfig) error {
446         ffMutex.Lock()
447         defer ffMutex.Unlock()
448
449         fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
450         defer fd.Close()
451         if err != nil {
452                 return err
453         }
454
455         data := &xmlFolders{
456                 Version: "1",
457                 Folders: folders,
458         }
459
460         enc := xml.NewEncoder(fd)
461         enc.Indent("", "  ")
462         return enc.Encode(data)
463 }