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