b0c198a3cfef5618f8e5e57775500f86b71807c5
[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/xdsconfig"
32         "github.com/iotbzh/xds-server/lib/xsapiv1"
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         var err error
220
221         fcMutex.Lock()
222         defer fcMutex.Unlock()
223
224         // Sanity check
225         if _, exist := f.folders[newF.ID]; create && exist {
226                 return nil, fmt.Errorf("ID already exists")
227         }
228         if newF.ClientPath == "" {
229                 return nil, fmt.Errorf("ClientPath must be set")
230         }
231
232         // Create a new folder object
233         var fld IFOLDER
234         switch newF.Type {
235         // SYNCTHING
236         case xsapiv1.TypeCloudSync:
237                 if f.SThg != nil {
238                         fld = NewFolderST(f.Context, f.SThg)
239                 } else {
240                         f.Log.Debugf("Disable project %v (syncthing not initialized)", newF.ID)
241                         fld = NewFolderSTDisable(f.Context)
242                 }
243
244         // PATH MAP
245         case xsapiv1.TypePathMap:
246                 fld = NewFolderPathMap(f.Context)
247         default:
248                 return nil, fmt.Errorf("Unsupported folder type")
249         }
250
251         // Allocate a new UUID
252         if create {
253                 newF.ID = fld.NewUID("")
254         }
255         if !create && newF.ID == "" {
256                 return nil, fmt.Errorf("Cannot update folder with null ID")
257         }
258
259         // Set default value if needed
260         if newF.Status == "" {
261                 newF.Status = xsapiv1.StatusDisable
262         }
263         if newF.Label == "" {
264                 newF.Label = filepath.Base(newF.ClientPath)
265                 if len(newF.ID) > 8 {
266                         newF.Label += "_" + newF.ID[0:8]
267                 }
268         }
269
270         // Normalize path (needed for Windows path including bashlashes)
271         newF.ClientPath = common.PathNormalize(newF.ClientPath)
272
273         var newFolder *xsapiv1.FolderConfig
274         if create {
275                 // Add folder
276                 if newFolder, err = fld.Add(newF); err != nil {
277                         newF.Status = xsapiv1.StatusErrorConfig
278                         log.Printf("ERROR Adding folder: %v\n", err)
279                         return newFolder, err
280                 }
281         } else {
282                 // Just update project config
283                 if newFolder, err = fld.Setup(newF); err != nil {
284                         newF.Status = xsapiv1.StatusErrorConfig
285                         log.Printf("ERROR Updating folder: %v\n", err)
286                         return newFolder, err
287                 }
288         }
289
290         // Add to folders list
291         f.folders[newF.ID] = &fld
292
293         // Save config on disk
294         if !initial {
295                 if err := f.SaveConfig(); err != nil {
296                         return newFolder, err
297                 }
298         }
299
300         // Force sync after creation
301         // (need to defer to be sure that WS events will arrive after HTTP creation reply)
302         go func() {
303                 time.Sleep(time.Millisecond * 500)
304                 fld.Sync()
305         }()
306
307         return newFolder, nil
308 }
309
310 // Delete deletes a specific folder
311 func (f *Folders) Delete(id string) (xsapiv1.FolderConfig, error) {
312         var err error
313
314         fcMutex.Lock()
315         defer fcMutex.Unlock()
316
317         fld := xsapiv1.FolderConfig{}
318         fc, exist := f.folders[id]
319         if !exist {
320                 return fld, fmt.Errorf("unknown id")
321         }
322
323         fld = (*fc).GetConfig()
324
325         if err = (*fc).Remove(); err != nil {
326                 return fld, err
327         }
328
329         delete(f.folders, id)
330
331         // Save config on disk
332         err = f.SaveConfig()
333
334         return fld, err
335 }
336
337 // Update Update a specific folder
338 func (f *Folders) Update(id string, cfg xsapiv1.FolderConfig) (*xsapiv1.FolderConfig, error) {
339         fcMutex.Lock()
340         defer fcMutex.Unlock()
341
342         fc, exist := f.folders[id]
343         if !exist {
344                 return nil, fmt.Errorf("unknown id")
345         }
346
347         // Copy current in a new object to change nothing in case of an error rises
348         newCfg := xsapiv1.FolderConfig{}
349         reflectme.Copy((*fc).GetConfig(), &newCfg)
350
351         // Only update some fields
352         dirty := false
353         for _, fieldName := range xsapiv1.FolderConfigUpdatableFields {
354                 valNew, err := reflectme.GetField(cfg, fieldName)
355                 if err == nil {
356                         valCur, err := reflectme.GetField(newCfg, fieldName)
357                         if err == nil && valNew != valCur {
358                                 err = reflectme.SetField(&newCfg, fieldName, valNew)
359                                 if err != nil {
360                                         return nil, err
361                                 }
362                                 dirty = true
363                         }
364                 }
365         }
366
367         if !dirty {
368                 return &newCfg, nil
369         }
370
371         fld, err := (*fc).Update(newCfg)
372         if err != nil {
373                 return fld, err
374         }
375
376         // Save config on disk
377         err = f.SaveConfig()
378
379         // Send event to notified changes
380         // TODO emit folder change event
381
382         return fld, err
383 }
384
385 // ForceSync Force the synchronization of a folder
386 func (f *Folders) ForceSync(id string) error {
387         fc := f.Get(id)
388         if fc == nil {
389                 return fmt.Errorf("Unknown id")
390         }
391         return (*fc).Sync()
392 }
393
394 // IsFolderInSync Returns true when folder is in sync
395 func (f *Folders) IsFolderInSync(id string) (bool, error) {
396         fc := f.Get(id)
397         if fc == nil {
398                 return false, fmt.Errorf("Unknown id")
399         }
400         return (*fc).IsInSync()
401 }
402
403 //*** Private functions ***
404
405 // Use XML format and not json to be able to save/load all fields including
406 // ones that are masked in json (IOW defined with `json:"-"`)
407 type xmlFolders struct {
408         XMLName xml.Name               `xml:"folders"`
409         Version string                 `xml:"version,attr"`
410         Folders []xsapiv1.FolderConfig `xml:"folders"`
411 }
412
413 // foldersConfigRead reads folders config from disk
414 func foldersConfigRead(file string, folders *[]xsapiv1.FolderConfig) error {
415         if !common.Exists(file) {
416                 return fmt.Errorf("No folder config file found (%s)", file)
417         }
418
419         ffMutex.Lock()
420         defer ffMutex.Unlock()
421
422         fd, err := os.Open(file)
423         defer fd.Close()
424         if err != nil {
425                 return err
426         }
427
428         data := xmlFolders{}
429         err = xml.NewDecoder(fd).Decode(&data)
430         if err == nil {
431                 // Decode old type encoding (number) for backward compatibility
432                 for i, d := range data.Folders {
433                         switch d.Type {
434                         case "1":
435                                 data.Folders[i].Type = xsapiv1.TypePathMap
436                         case "2":
437                                 data.Folders[i].Type = xsapiv1.TypeCloudSync
438                         case "3":
439                                 data.Folders[i].Type = xsapiv1.TypeCifsSmb
440                         }
441                 }
442
443                 *folders = data.Folders
444         }
445         return err
446 }
447
448 // foldersConfigWrite writes folders config on disk
449 func foldersConfigWrite(file string, folders []xsapiv1.FolderConfig) error {
450         ffMutex.Lock()
451         defer ffMutex.Unlock()
452
453         fd, err := os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0666)
454         defer fd.Close()
455         if err != nil {
456                 return err
457         }
458
459         data := &xmlFolders{
460                 Version: "1",
461                 Folders: folders,
462         }
463
464         enc := xml.NewEncoder(fd)
465         enc.Indent("", "  ")
466         return enc.Encode(data)
467 }