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