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